1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * @private 7 * @const 8 * @type {string} 9 */ 10 var GCN_REPOSITORY_ID = 'com.gentics.aloha.GCN.Page'; 11 12 /** 13 * @private 14 * @const 15 * @type {object.<string, boolean>} Default page settings. 16 */ 17 var DEFAULT_SETTINGS = { 18 // Load folder information 19 folder: true, 20 21 // Lock page when loading it 22 update: true, 23 24 // Have language variants be included in the page response. 25 langvars: true, 26 27 // Have page variants be included in the page response. 28 pagevars: true 29 }; 30 31 /** 32 * Searches for the an Aloha editable object of the given id. 33 * 34 * @TODO: Once Aloha.getEditableById() is patched to not cause an 35 * JavaScript exception if the element for the given ID is not found 36 * then we can deprecate this function and use Aloha's instead. 37 * 38 * @static 39 * @param {string} id Id of Aloha.Editable object to find. 40 * @return {Aloha.Editable=} The editable object, if wound; otherwise null. 41 */ 42 function getAlohaEditableById(id) { 43 var Aloha = (typeof window !== 'undefined') && window.Aloha; 44 if (!Aloha) { 45 return null; 46 } 47 48 // If the element is a textarea then route to the editable div. 49 var element = jQuery('#' + id); 50 if (element.length && 51 element[0].nodeName.toLowerCase() === 'textarea') { 52 id += '-aloha'; 53 } 54 55 var editables = Aloha.editables; 56 var j = editables.length; 57 while (j) { 58 if (editables[--j].getId() === id) { 59 return editables[j]; 60 } 61 } 62 63 return null; 64 } 65 66 /** 67 * Checks whether the given tag is a magic link block. 68 * 69 * @private 70 * @static 71 * @param {GCN.Tag} tag Must be a tag that has already been fetched. 72 * @param {object} constructs Set of constructs. 73 * @return {boolean} True if the given tag has the magic link constructId. 74 */ 75 function isMagicLinkTag(tag, constructs) { 76 return !!(constructs[GCN.settings.MAGIC_LINK] && 77 (constructs[GCN.settings.MAGIC_LINK].constructId === 78 tag.prop('constructId'))); 79 } 80 81 /** 82 * Checks whether or not the given block has a corresponding element in the 83 * document DOM. 84 * 85 * @private 86 * @static 87 * @param {object} 88 * @return {boolean} True if an inline element for this block exists. 89 */ 90 function hasInlineElement(block) { 91 return 0 < jQuery('#' + block.element).length; 92 } 93 94 /** 95 * For a given list of editables and a list of blocks, determine in which 96 * editable each block was rendered. The result is map with a list of 97 * blocks mapped against the id of the rendered editable element. 98 * 99 * @param {string} content The rendered content in which the list of 100 * editables, and blocks are contained. 101 * @param {Array.<object>} editables A list of editables contained in the 102 * rendered content. 103 * @param {Array.<object>} blocks A list of blocks containd in content 104 * string string. 105 * @return {object<string, <Array>} A object whose keys are editable ids, 106 * and whose values are Array of block 107 * contained in the corresponding 108 * editable. 109 */ 110 function getMapOfBlocksToEditables(content, editables, blocks) { 111 var i; 112 var $content = jQuery('<div>' + content + '</div>'); 113 var map = {}; 114 var editableElementId; 115 116 var selection = []; 117 for (i = 0; i < editables.length; i++) { 118 selection.push('#' + editables[i].element); 119 } 120 121 var markerClass = 'aloha-editable-tmp-marker__'; 122 var $editables = $content.find(selection.join(',')); 123 $editables.addClass(markerClass); 124 125 var $block; 126 var $parent; 127 for (i = 0; i < blocks.length; i++) { 128 $block = $content.find('#' + blocks[i].element); 129 if ($block.length) { 130 $parent = $block.parents('.' + markerClass); 131 if ($parent.length) { 132 editableElementId = $parent.attr('id'); 133 if (editableElementId) { 134 if (!map[editableElementId]) { 135 map[editableElementId] = []; 136 } 137 map[editableElementId].push(blocks[i]); 138 } 139 } 140 } 141 } 142 143 return map; 144 } 145 146 /** 147 * @private 148 * @const 149 * @type {number} 150 */ 151 //var TYPE_ID = 10007; 152 153 /** 154 * @private 155 * @const 156 * @type {Enum} 157 */ 158 var STATUS = { 159 160 // page was not found in the database 161 NOTFOUND: -1, 162 163 // page is locally modified and not yet (re-)published 164 MODIFIED: 0, 165 166 // page is marked to be published (dirty) 167 TOPUBLISH: 1, 168 169 // page is published and online 170 PUBLISHED: 2, 171 172 // Page is offline 173 OFFLINE: 3, 174 175 // Page is in the queue (publishing of the page needs to be affirmed) 176 QUEUE: 4, 177 178 // page is in timemanagement and outside of the defined timespan 179 // (currently offline) 180 TIMEMANAGEMENT: 5, 181 182 // page is to be published at a given time (not yet) 183 TOPUBLISH_AT: 6 184 }; 185 186 /** 187 * Given a link, will read the data-gentics-aloha-object-id attribute and 188 * from it, will determine the backend object that was selected by the 189 * repository browser. 190 * 191 * @param {jQuery.<HTMLElement>} link A link in an editable. 192 * @return {Object} 193 * A map containing the gtxalohapagelink part keyword and 194 * value. The keyword may be either be 'url' or 'fileurl' 195 * depending on the type of object linked to. The value may 196 * be a string url ('http://...') for external links or an 197 * integer for internal links. 198 */ 199 function getRepositoryLinkAsPart(link) { 200 var data = link.attr('data-gentics-aloha-object-id'); 201 var value; 202 var keyword; 203 if (data) { 204 var parts = data.split('.'); 205 if (parts.length === 2) { 206 if (parts[0] === '10008' || parts[0] === '10011') { 207 keyword = 'fileurl'; 208 } else { // assumes 10007 for page links 209 keyword = 'url'; 210 } 211 value = parts[1]; 212 } else { 213 keyword = 'url'; 214 value = data; 215 } 216 if (null !== value && typeof value !== 'undefined') { 217 value = parseInt(value, 10); 218 } 219 } else { 220 keyword = 'url'; 221 value = link.attr('data-gentics-gcn-url'); 222 } 223 return { 224 keyword: keyword, 225 value: value 226 }; 227 } 228 229 /** 230 * @class 231 * @name PageAPI 232 * @extends ContentObjectAPI 233 * @extends TagContainerAPI 234 * 235 * Page object information can be extened using the default REST-API. 236 * options: 237 * 238 * - update: true 239 * Whether the page should be locked in the backend when loading it. 240 * default: true 241 * 242 * - template: true 243 * Whether the template information should be embedded in the page object. 244 * default: true 245 * 246 * - folder: true, 247 * Whether the folder information should be embedded in the page object. 248 * default: true 249 * WARNING: do not turn this option off - it will leave the API in a broken 250 * state. 251 * 252 * - langvars: false, 253 * When the language variants shall be embedded in the page response. 254 * default: false 255 256 * - workflow: false, 257 * When the workflow information shall be embedded in the page response. 258 * default: false 259 260 * - pagevars: false, 261 * When the page variants shall be embedded in the page response. Page 262 * variants will contain folder information. 263 * default: false 264 * 265 * - translationstatus: false 266 * Will return information on the page's translation status. 267 * default: false 268 */ 269 var PageAPI = GCN.defineChainback({ 270 /** @lends PageAPI */ 271 272 __chainbacktype__: 'PageAPI', 273 _extends: [ GCN.ContentObjectAPI, GCN.TagContainerAPI ], 274 _type: 'page', 275 276 /** 277 * @private 278 * @type {Array.<object>} A hash set of block tags belonging to this 279 * content object. This set is added to when 280 * this page's tags are rendered. 281 */ 282 _blocks: {}, 283 284 /** 285 * @private 286 * @type {Array.<object>} A hash set of editable tags belonging to this 287 * content object. This set is added to when 288 * this page's tags are rendered. 289 */ 290 _editables: {}, 291 292 /** 293 * @type {Array.string} Writable properties for the page object. 294 */ 295 WRITEABLE_PROPS: ['cdate', 296 'description', 297 'fileName', 298 'folderId', // @TODO Check if moving a page is 299 // implemented correctly. 300 'name', 301 'priority', 302 'templateId'], 303 304 /** 305 * Gets all blocks in this page. Will return an array of all block 306 * objects found in the page AFTER they have been rendered using an 307 * `edit()' call for a contenttag. 308 * NOTE: If you have just loaded the page and not used the `edit()' 309 * method for any tag the array will be empty. Only those blocks 310 * that have been initialized using `edit()' will be available. 311 * 312 * @return {Array.<object>} Array of block objects. 313 */ 314 '!blocks': function () { 315 return this._blocks; 316 }, 317 318 /** 319 * Looks for a block with the given id in the `_blocks' array. 320 * 321 * @private 322 * @param {string} id The block's id. 323 * @return {?object} The block data object. 324 */ 325 '!_getBlockById': function (id) { 326 return this._blocks[id]; 327 }, 328 329 /** 330 * Maps the received editables into this content object's `_editable' 331 * hash. 332 * 333 * @private 334 * @param {Array.<object>} editables An set of object representing 335 * editable tags that have been 336 * rendered. 337 */ 338 '!_storeRenderedEditables': function (editables) { 339 if (!this.hasOwnProperty('_editables')) { 340 this._editables = {}; 341 } 342 343 var j = editables && editables.length; 344 345 while (j) { 346 this._editables[editables[--j].element] = editables[j]; 347 } 348 }, 349 350 /** 351 * Maps received blocks of this content object into the `_blocks' hash. 352 * 353 * @private 354 * @param {Array.<object>} blocks An set of object representing 355 * block tags that have been rendered. 356 */ 357 '!_storeRenderedBlocks': function (blocks) { 358 if (!this.hasOwnProperty('_blocks')) { 359 this._blocks = {}; 360 } 361 362 var j = blocks && blocks.length; 363 364 while (j) { 365 this._blocks[blocks[--j].element] = blocks[j]; 366 } 367 }, 368 369 /** 370 * @override 371 */ 372 '!_processRenderedTags': function (data) { 373 var tags = this._getEditablesAndBlocks(data); 374 var map = getMapOfBlocksToEditables(data.content, tags.editables, 375 tags.blocks); 376 377 this._storeRenderedEditables(tags.editables); 378 this._storeRenderedBlocks(tags.blocks); 379 380 var editableId; 381 for (editableId in map) { 382 if (map.hasOwnProperty(editableId) && 383 this._editables[editableId]) { 384 if (!this._editables[editableId]._gcnContainedBlocks) { 385 this._editables[editableId]._gcnContainedBlocks = 386 map[editableId]; 387 } else { 388 this._editables[editableId]._gcnContainedBlocks = 389 this._editables[editableId]._gcnContainedBlocks 390 .concat(map[editableId]); 391 } 392 } 393 } 394 395 return tags; 396 }, 397 398 /** 399 * Processes rendered tags, and update the `_blocks' and `_editables' 400 * array accordingly. This function is called during pre-saving to 401 * update this page's editable tags. 402 * 403 * @private 404 */ 405 '!_prepareTagsForSaving': function (success, error) { 406 if (!this.hasOwnProperty('_deletedBlocks')) { 407 this._deletedBlocks = []; 408 } 409 410 var editableId; 411 var containedBlocks = []; 412 for (editableId in this._editables) { 413 if (this._editables.hasOwnProperty(editableId)) { 414 if (this._editables[editableId]._gcnContainedBlocks) { 415 containedBlocks = containedBlocks.concat( 416 this._editables[editableId]._gcnContainedBlocks 417 ); 418 } 419 } 420 } 421 422 var blocks = []; 423 var j = containedBlocks.length; 424 while (j) { 425 if (this._blocks[containedBlocks[--j]]) { 426 blocks.push(containedBlocks[j]); 427 } 428 } 429 430 var that = this; 431 this._addNewLinkBlocks(function () { 432 GCN.Admin.constructs(function (constructs) { 433 that._removeOldLinkBlocks(blocks, constructs, function () { 434 that._removeUnusedLinkBlocks(blocks, constructs, function () { 435 that._updateEditableBlocks(); 436 that._invoke(success); 437 }, error); 438 }, error); 439 }, error); 440 }, error); 441 }, 442 443 /** 444 * Removes any link blocks that existed in rendered tags, but have 445 * since been removed by the user while editing. 446 * 447 * @private 448 * @param {Array.<object>} blocks An array of blocks belonging to this 449 * page. 450 * @param {object} constrcts A set of constructs. 451 * @param {function} success 452 * @param {function(GCNError):boolean=} error Optional custom error 453 * handler. 454 */ 455 '!_removeUnusedLinkBlocks': function (blocks, constructs, success, 456 error) { 457 if (0 === blocks.length) { 458 if (success) { 459 this._invoke(success); 460 } 461 return; 462 } 463 464 var j = blocks.length; 465 var numToProcess = j; 466 var that = this; 467 468 var onProcess = function () { 469 if (0 === --numToProcess) { 470 if (success) { 471 that._invoke(success); 472 } 473 } 474 }; 475 476 var onError = function (GCNError) { 477 if (error) { 478 that._invoke(error, [GCNError]); 479 } 480 return; 481 }; 482 483 var createBlockTagProcessor = function (block) { 484 return function (tag) { 485 if (isMagicLinkTag(tag, constructs) && 486 !hasInlineElement(block)) { 487 that._deletedBlocks.push(block.tagname); 488 } 489 490 onProcess(); 491 }; 492 }; 493 494 while (j) { 495 this.tag(blocks[--j].tagname, 496 createBlockTagProcessor(blocks[j]), onError); 497 } 498 }, 499 500 /** 501 * Adds any newly created link blocks into this page object. This is 502 * done by looking for all link blocks that do not have corresponding 503 * tag in this object's `_blocks' list. For each anchor tag we find, 504 * create a tag for it and, add it in the list of tags. 505 * 506 * @private 507 * @param {function} success Function to invoke if this function 508 * successeds. 509 * @param {function(GCNError):boolean=} error Optional custom error 510 * handler. 511 */ 512 '!_addNewLinkBlocks': function (success, error) { 513 // Limit the search for links to be done only withing rendered 514 // editables. 515 var id; 516 var $editables = jQuery(); 517 for (id in this._editables) { 518 if (this._editables.hasOwnProperty(id)) { 519 $editables = $editables.add('#' + id); 520 } 521 } 522 523 var selector = [ 524 'a[data-gentics-aloha-repository="com.gentics.aloha.GCN.Page"]', 525 'a[data-GENTICS-aloha-repository="com.gentics.aloha.GCN.Page"]', 526 'a[data-gentics-gcn-url]' 527 ].join(','); 528 529 var links = $editables.find(selector); 530 531 if (0 === links.length) { 532 if (success) { 533 this._invoke(success); 534 } 535 536 return; 537 } 538 539 var link; 540 var j = links.length; 541 var numToProcess = j; 542 var that = this; 543 544 var onProcessed = function () { 545 if (0 === --numToProcess) { 546 that._invoke(success); 547 } 548 }; 549 550 var onError = function (GCNError) { 551 if (error) { 552 that._invoke(error, [GCNError]); 553 } 554 return; 555 }; 556 557 var createOnEditHandler = function (link) { 558 return function (html, tag) { 559 link.attr('id', jQuery(html).attr('id')); 560 var part = getRepositoryLinkAsPart(link); 561 tag.part(part.keyword, part.value); 562 onProcessed(); 563 }; 564 }; 565 566 var tag; 567 568 while (j) { 569 link = links.eq(--j); 570 if (link.attr('data-gcnignore') === true) { 571 onProcessed(); 572 } else if (this._getBlockById(link.attr('id'))) { 573 tag = this.tag(this._getBlockById(link.attr('id')).tagname); 574 tag.part('text', link.html()); 575 var part = getRepositoryLinkAsPart(link); 576 tag.part(part.keyword, part.value); 577 onProcessed(); 578 } else { 579 this.createTag(GCN.settings.MAGIC_LINK, link.html()) 580 .edit(createOnEditHandler(link), onError); 581 } 582 } 583 }, 584 585 /** 586 * Any links that change from internal GCN links to external links will 587 * have their corresponding blocks added to the '_deletedBlocks' list 588 * since they these links no longer need to be tracked. Any tags in 589 * this list will be removed during saving. 590 * 591 * @private 592 * @param {Array.<object>} blocks An array of blocks belonging to this 593 * page. 594 * @param {object} constrcts A set of constructs. 595 * @param {function} success 596 * @param {function(GCNError):boolean=} error Optional custom error 597 * handler. 598 */ 599 '!_removeOldLinkBlocks': function (blocks, constructs, success, error) { 600 if (0 === blocks.length) { 601 if (success) { 602 this._invoke(success); 603 } 604 return; 605 } 606 607 var $links = jQuery('a'); 608 var j = blocks.length; 609 var numToProcess = j; 610 var that = this; 611 612 var onProcess = function () { 613 if (0 === --numToProcess) { 614 if (success) { 615 that._invoke(success); 616 } 617 } 618 }; 619 620 var onError = function (GCNError) { 621 if (error) { 622 that._invoke(error, [GCNError]); 623 } 624 return; 625 }; 626 627 var createBlockTagProcessor = function (block) { 628 return function (tag) { 629 if (!isMagicLinkTag(tag, constructs)) { 630 onProcess(); 631 return; 632 } 633 634 var a = $links.filter('#' + block.element); 635 636 if (a.length) { 637 var isExternal = (GCN_REPOSITORY_ID !== 638 a.attr('data-gentics-aloha-repository')) && 639 !a.attr('data-gentics-gcn-url'); 640 641 // An external tag was found. Stop tracking it and 642 // remove it from the list of blocks. 643 if (isExternal) { 644 a.removeAttr('id'); 645 that._deletedBlocks.push(block.tagname); 646 delete that._blocks[block.element]; 647 } 648 649 // No anchor tag was found for this block. Add it to the 650 // "delete" list. 651 } else { 652 that._deletedBlocks.push(block.tagname); 653 delete that._blocks[block.element]; 654 } 655 656 onProcess(); 657 }; 658 }; 659 660 while (j) { 661 this.tag(blocks[--j].tagname, 662 createBlockTagProcessor(blocks[j]), onError); 663 } 664 }, 665 666 /** 667 * Writes the contents of editables back into their corresponding tags. 668 * If a corresponding tag cannot be found for an editable, a new one 669 * will be created for it. 670 * 671 * A reference for each editable tag is then added to the `_shadow' 672 * object in order that the tag will be sent with the save request. 673 * 674 * @private 675 */ 676 '!_updateEditableBlocks': function () { 677 var element; 678 var elementId; 679 var editable; 680 var editables = this._editables; 681 var tags = this._data.tags; 682 var tagname; 683 var html; 684 var alohaEditable; 685 var cleanElement; 686 var customSerializer; 687 688 for (elementId in editables) { 689 if (editables.hasOwnProperty(elementId)) { 690 editable = editables[elementId]; 691 element = jQuery('#' + elementId); 692 693 // If this editable has no element that was placed in the 694 // DOM, then do not attempt to update it. 695 if (0 === element.length) { 696 continue; 697 } 698 699 tagname = editable.tagname; 700 701 if (!tags[tagname]) { 702 tags[tagname] = { 703 name : tagname, 704 active : true, 705 properties : {} 706 }; 707 } else { 708 // make sure, that all tags (which relate to editables) 709 // are activated 710 tags[tagname].active = true; 711 } 712 713 // If the editable element has been `aloha()'fied, then we 714 // need to use `getContents()' from is corresponding 715 // Aloha.Editable object in order to get clean HTML. 716 717 alohaEditable = getAlohaEditableById(elementId); 718 719 if (alohaEditable) { 720 // Avoid the unnecessary overhead of custom editable 721 // serialization by calling html ourselves. 722 cleanElement = jQuery('<div>') 723 .append(alohaEditable.getContents(true)); 724 alohaEditable.setUnmodified(); 725 // Apply the custom editable serialization as the last step. 726 customSerializer = window.Aloha.Editable.getContentSerializer(); 727 html = this.encode(cleanElement, customSerializer); 728 } else { 729 html = this.encode(element); 730 } 731 732 tags[tagname].properties[editable.partname] = { 733 stringValue: html, 734 type: 'RICHTEXT' 735 }; 736 737 this._update('tags.' + GCN.escapePropertyName(tagname), 738 tags[tagname]); 739 } 740 } 741 }, 742 743 /** 744 * @see ContentObjectAPI.!_loadParams 745 */ 746 '!_loadParams': function () { 747 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 748 }, 749 750 /** 751 * Get this page's template. 752 * 753 * @public 754 * @function 755 * @name template 756 * @memberOf PageAPI 757 * @param {funtion(TemplateAPI)=} success Optional callback to receive 758 * a {@link TemplateAPI} object 759 * as the only argument. 760 * @param {function(GCNError):boolean=} error Optional custom error 761 * handler. 762 * @return {TemplateAPI} This page's parent template. 763 */ 764 '!template': function (success, error) { 765 var id = this._fetched ? this.prop('templateId') : null; 766 return this._continue(GCN.TemplateAPI, id, success, error); 767 }, 768 769 /** 770 * @override 771 * @see ContentObjectAPI._save 772 */ 773 '!_save': function (settings, success, error) { 774 var that = this; 775 this._continueWith(function () { 776 var fork = that._fork(); 777 fork._prepareTagsForSaving(function () { 778 fork._persist(settings, function () { 779 if (success) { 780 that._invoke(success, [that]); 781 } 782 fork._merge(); 783 }, error); 784 }, error); 785 }, error); 786 }, 787 788 //--------------------------------------------------------------------- 789 // Surface the tag container methods that are applicable for GCN page 790 // objects. 791 //--------------------------------------------------------------------- 792 793 /** 794 * Creates a tag of a given tagtype in this page. 795 * 796 * Exmaple: 797 * <pre> 798 * createTag('link', 'http://www.gentics.com', onSuccess, onError); 799 * </pre> 800 * or 801 * <pre> 802 * createTag('link', onSuccess, onError); 803 * </pre> 804 * 805 * @public 806 * @function 807 * @name createTag 808 * @memberOf PageAPI 809 * @param {string|number} construct The name of the construct on which 810 * the tag to be created should be 811 * derived from. Or the id of that 812 * @param {string=} magicValue Optional property that will override the 813 * default values of this tag type. 814 * @param {function(TagAPI)=} success Optional callback that will 815 * receive the newly created tag as 816 * its only argument. 817 * @param {function(GCNError):boolean=} error Optional custom error 818 * handler. 819 * @return {TagAPI} The newly created tag. 820 * @throws INVALID_ARGUMENTS 821 */ 822 '!createTag': function () { 823 return this._createTag.apply(this, arguments); 824 }, 825 826 /** 827 * Deletes the specified tag from this page. 828 * 829 * @public 830 * @function 831 * @name removeTag 832 * @memberOf PageAPI 833 * @param {string} id The id of the tag to be deleted. 834 * @param {function(PageAPI)=} success Optional callback that receive 835 * this object as its only 836 * argument. 837 * @param {function(GCNError):boolean=} error Optional custom error 838 * handler. 839 */ 840 removeTag: function () { 841 this._removeTag.apply(this, arguments); 842 }, 843 844 /** 845 * Deletes a set of tags from this page. 846 * 847 * @public 848 * @function 849 * @name removeTags 850 * @memberOf PageAPI 851 * @param {Array.<string>} ids The ids of the set of tags to be 852 * deleted. 853 * @param {function(PageAPI)=} success Optional callback that receive 854 * this object as its only 855 * argument. 856 * @param {function(GCNError):boolean=} error Optional custom error 857 * handler. 858 */ 859 removeTags: function () { 860 this._removeTags.apply(this, arguments); 861 }, 862 863 /** 864 * Marks the page as to be taken offline. This method will change the 865 * state of the page object. 866 * 867 * @public 868 * @function 869 * @name takeOffline 870 * @memberOf PageAPI 871 * @param {funtion(PageAPI)=} success Optional callback to receive this 872 * page object as the only argument. 873 * @param {function(GCNError):boolean=} error Optional custom error 874 * handler. 875 */ 876 takeOffline: function (success, error) { 877 var that = this; 878 879 this._read(function () { 880 that._update('status', STATUS.OFFLINE, error); 881 if (success) { 882 that._save(null, success, error); 883 } 884 }, error); 885 }, 886 887 /** 888 * Trigger publish process for the page. 889 * 890 * @public 891 * @function 892 * @name publish 893 * @memberOf PageAPI 894 * @param {funtion(PageAPI)=} success Optional callback to receive this 895 * page object as the only argument. 896 * @param {function(GCNError):boolean=} error Optional custom error 897 * handler. 898 */ 899 publish: function (success, error) { 900 var that = this; 901 this._continueWith(function () { 902 that._authAjax({ 903 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 904 '/publish/' + that.id(), 905 type: 'POST', 906 json: {}, // There needs to be at least empty content 907 // because of a bug in Jersey. 908 success: function (response) { 909 that._data.status = STATUS.PUBLISHED; 910 if (success) { 911 that._invoke(success, [that]); 912 } 913 }, 914 error: error 915 }); 916 }); 917 }, 918 919 /** 920 * Renders a preview of the current page. 921 * 922 * @public 923 * @function 924 * @name preview 925 * @memberOf PageAPI 926 * @param {function(string, PageAPI)} success Callback to receive the 927 * rendered page preview as 928 * the first argument, and 929 * this page object as the 930 * second. 931 * @param {function(GCNError):boolean=} error Optional custom error 932 * handler. 933 */ 934 preview: function (success, error) { 935 var that = this; 936 937 this._read(function () { 938 that._authAjax({ 939 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 940 '/preview/', 941 json: { 942 page: that._data // @FIXME Shouldn't this a be merge of 943 // the `_shadow' object and the 944 // `_data'. 945 }, 946 type: 'POST', 947 error: error, 948 success: function (response) { 949 if (success) { 950 GCN._handleContentRendered(response.preview, that, 951 function (html) { 952 that._invoke(success, [html, that]); 953 }); 954 } 955 } 956 }); 957 }, error); 958 }, 959 960 /** 961 * Unlocks the page when finishing editing 962 * 963 * @public 964 * @function 965 * @name unlock 966 * @memberOf PageAPI 967 * @param {funtion(PageAPI)=} success Optional callback to receive this 968 * page object as the only argument. 969 * @param {function(GCNError):boolean=} error Optional custom error 970 * handler. 971 */ 972 unlock: function (success, error) { 973 var that = this; 974 this._continueWith(function () { 975 that._authAjax({ 976 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 977 '/cancel/' + that.id(), 978 type: 'POST', 979 json: {}, // There needs to be at least empty content 980 // because of a bug in Jersey. 981 error: error, 982 success: function (response) { 983 if (success) { 984 that._invoke(success, [that]); 985 } 986 } 987 }); 988 }); 989 }, 990 991 /** 992 * @see GCN.ContentObjectAPI._processResponse 993 */ 994 '!_processResponse': function (data) { 995 jQuery.extend(this._data, data[this._type]); 996 997 // if data contains page variants turn them into page objects 998 if (this._data.pageVariants) { 999 var pagevars = []; 1000 var i; 1001 for (i = 0; i < this._data.pageVariants.length; i++) { 1002 pagevars.push(this._continue(GCN.PageAPI, 1003 this._data.pageVariants[i])); 1004 } 1005 this._data.pageVariants = pagevars; 1006 } 1007 }, 1008 1009 /** 1010 * @override 1011 */ 1012 '!_removeAssociatedTagData': function (tagid) { 1013 var block; 1014 for (block in this._blocks) { 1015 if (this._blocks.hasOwnProperty(block) && 1016 this._blocks[block].tagname === tagid) { 1017 delete this._blocks[block]; 1018 } 1019 } 1020 1021 var editable; 1022 for (editable in this._editables) { 1023 if (this._editables.hasOwnProperty(editable) && 1024 this._editables[editable].tagname === tagid) { 1025 delete this._editables[editable]; 1026 } 1027 } 1028 } 1029 1030 }); 1031 1032 GCN.page = GCN.exposeAPI(PageAPI); 1033 GCN.PageAPI = PageAPI; 1034 1035 }(GCN)); 1036