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 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 success(); 460 } 461 return; 462 } 463 464 var j = blocks.length; 465 var numToProcess = j; 466 467 var onProcess = function () { 468 if (0 === --numToProcess) { 469 if (success) { 470 success(); 471 } 472 } 473 }; 474 475 var onError = function (GCNError) { 476 if (error) { 477 error(GCNError); 478 } 479 return; 480 }; 481 482 var that = this; 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 success(); 534 } 535 536 return; 537 } 538 539 var link; 540 var j = links.length; 541 var numToProcess = j; 542 543 var onProcessed = function () { 544 if (0 === --numToProcess) { 545 success(); 546 } 547 }; 548 549 var onError = function (GCNError) { 550 if (error) { 551 error(GCNError); 552 } 553 return; 554 }; 555 556 var createOnEditHandler = function (link) { 557 return function (html, tag) { 558 link.attr('id', jQuery(html).attr('id')); 559 var part = getRepositoryLinkAsPart(link); 560 tag.part(part.keyword, part.value); 561 onProcessed(); 562 }; 563 }; 564 565 var tag; 566 567 while (j) { 568 link = links.eq(--j); 569 if (link.attr('data-gcnignore') === true) { 570 onProcessed(); 571 } else if (this._getBlockById(link.attr('id'))) { 572 tag = this.tag(this._getBlockById(link.attr('id')).tagname); 573 tag.part('text', link.html()); 574 var part = getRepositoryLinkAsPart(link); 575 tag.part(part.keyword, part.value); 576 onProcessed(); 577 } else { 578 this.createTag(GCN.settings.MAGIC_LINK, link.html()) 579 .edit(createOnEditHandler(link), onError); 580 } 581 } 582 }, 583 584 /** 585 * Any links that change from internal GCN links to external links will 586 * have their corresponding blocks added to the '_deletedBlocks' list 587 * since they these links no longer need to be tracked. Any tags in 588 * this list will be removed during saving. 589 * 590 * @private 591 * @param {Array.<object>} blocks An array of blocks belonging to this 592 * page. 593 * @param {object} constrcts A set of constructs. 594 * @param {function} success 595 * @param {function(GCNError):boolean=} error Optional custom error 596 * handler. 597 */ 598 '!_removeOldLinkBlocks': function (blocks, constructs, success, error) { 599 if (0 === blocks.length) { 600 if (success) { 601 success(); 602 } 603 return; 604 } 605 606 var $links = jQuery('a'); 607 var j = blocks.length; 608 var numToProcess = j; 609 610 var onProcess = function () { 611 if (0 === --numToProcess) { 612 if (success) { 613 success(); 614 } 615 } 616 }; 617 618 var onError = function (GCNError) { 619 if (error) { 620 error(GCNError); 621 } 622 return; 623 }; 624 625 var that = this; 626 var createBlockTagProcessor = function (block) { 627 return function (tag) { 628 if (!isMagicLinkTag(tag, constructs)) { 629 onProcess(); 630 return; 631 } 632 633 var a = $links.filter('#' + block.element); 634 635 if (a.length) { 636 var isExternal = (GCN_REPOSITORY_ID !== 637 a.attr('data-gentics-aloha-repository')) && 638 !a.attr('data-gentics-gcn-url'); 639 640 // An external tag was found. Stop tracking it and 641 // remove it from the list of blocks. 642 if (isExternal) { 643 a.removeAttr('id'); 644 that._deletedBlocks.push(block.tagname); 645 delete that._blocks[block.element]; 646 } 647 648 // No anchor tag was found for this block. Add it to the 649 // "delete" list. 650 } else { 651 that._deletedBlocks.push(block.tagname); 652 delete that._blocks[block.element]; 653 } 654 655 onProcess(); 656 }; 657 }; 658 659 while (j) { 660 this.tag(blocks[--j].tagname, 661 createBlockTagProcessor(blocks[j]), onError); 662 } 663 }, 664 665 /** 666 * Writes the contents of editables back into their corresponding tags. 667 * If a corresponding tag cannot be found for an editable, a new one 668 * will be created for it. 669 * 670 * A reference for each editable tag is then added to the `_shadow' 671 * object in order that the tag will be sent with the save request. 672 * 673 * @private 674 */ 675 '!_updateEditableBlocks': function () { 676 var element; 677 var elementId; 678 var editable; 679 var editables = this._editables; 680 var tags = this._data.tags; 681 var tagname; 682 var html; 683 var alohaEditable; 684 var cleanElement; 685 var customSerializer; 686 687 for (elementId in editables) { 688 if (editables.hasOwnProperty(elementId)) { 689 editable = editables[elementId]; 690 element = jQuery('#' + elementId); 691 692 // If this editable has no element that was placed in the 693 // DOM, then do not attempt to update it. 694 if (0 === element.length) { 695 continue; 696 } 697 698 tagname = editable.tagname; 699 700 if (!tags[tagname]) { 701 tags[tagname] = { 702 name : tagname, 703 active : true, 704 properties : {} 705 }; 706 } else { 707 // make sure, that all tags (which relate to editables) 708 // are activated 709 tags[tagname].active = true; 710 } 711 712 // If the editable element has been `aloha()'fied, then we 713 // need to use `getContents()' from is corresponding 714 // Aloha.Editable object in order to get clean HTML. 715 716 alohaEditable = getAlohaEditableById(elementId); 717 718 if (alohaEditable) { 719 // Avoid the unnecessary overhead of custom editable 720 // serialization by calling html ourselves. 721 cleanElement = jQuery('<div>') 722 .append(alohaEditable.getContents(true)); 723 alohaEditable.setUnmodified(); 724 // Apply the custom editable serialization as the last step. 725 customSerializer = window.Aloha.Editable.getContentSerializer(); 726 html = this.encode(cleanElement, customSerializer); 727 } else { 728 html = this.encode(element); 729 } 730 731 tags[tagname].properties[editable.partname] = { 732 stringValue: html, 733 type: 'RICHTEXT' 734 }; 735 736 this._update('tags.' + tagname, tags[tagname]); 737 } 738 } 739 }, 740 741 /** 742 * @see ContentObjectAPI.!_loadParams 743 */ 744 '!_loadParams': function () { 745 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 746 }, 747 748 /** 749 * Get this page's template. 750 * 751 * @public 752 * @function 753 * @name template 754 * @memberOf PageAPI 755 * @param {funtion(TemplateAPI)=} success Optional callback to receive 756 * a {@link TemplateAPI} object 757 * as the only argument. 758 * @param {function(GCNError):boolean=} error Optional custom error 759 * handler. 760 * @return {TemplateAPI} This page's parent template. 761 */ 762 '!template': function (success, error) { 763 var id = this._fetched ? this.prop('templateId') : null; 764 return this._continue(GCN.TemplateAPI, id, success, error); 765 }, 766 767 /** 768 * @override 769 * @see ContentObjectAPI._save 770 */ 771 '!_save': function (settings, success, error) { 772 var that = this; 773 this._continueWith(function () { 774 var fork = that._fork(); 775 fork._prepareTagsForSaving(function () { 776 fork._persist(settings, function () { 777 if (success) { 778 success(that); 779 } 780 fork._merge(); 781 }, error); 782 }, error); 783 }, error); 784 }, 785 786 //--------------------------------------------------------------------- 787 // Surface the tag container methods that are applicable for GCN page 788 // objects. 789 //--------------------------------------------------------------------- 790 791 /** 792 * Creates a tag of a given tagtype in this page. 793 * 794 * Exmaple: 795 * <pre> 796 * createTag('link', 'http://www.gentics.com', onSuccess, onError); 797 * </pre> 798 * or 799 * <pre> 800 * createTag('link', onSuccess, onError); 801 * </pre> 802 * 803 * @public 804 * @function 805 * @name createTag 806 * @memberOf PageAPI 807 * @param {string|number} construct The name of the construct on which 808 * the tag to be created should be 809 * derived from. Or the id of that 810 * @param {string=} magicValue Optional property that will override the 811 * default values of this tag type. 812 * @param {function(TagAPI)=} success Optional callback that will 813 * receive the newly created tag as 814 * its only argument. 815 * @param {function(GCNError):boolean=} error Optional custom error 816 * handler. 817 * @return {TagAPI} The newly created tag. 818 * @throws INVALID_ARGUMENTS 819 */ 820 '!createTag': function () { 821 return this._createTag.apply(this, arguments); 822 }, 823 824 /** 825 * Deletes the specified tag from this page. 826 * 827 * @public 828 * @function 829 * @name removeTag 830 * @memberOf PageAPI 831 * @param {string} id The id of the tag to be deleted. 832 * @param {function(PageAPI)=} success Optional callback that receive 833 * this object as its only 834 * argument. 835 * @param {function(GCNError):boolean=} error Optional custom error 836 * handler. 837 */ 838 removeTag: function () { 839 this._removeTag.apply(this, arguments); 840 }, 841 842 /** 843 * Deletes a set of tags from this page. 844 * 845 * @public 846 * @function 847 * @name removeTags 848 * @memberOf PageAPI 849 * @param {Array.<string>} ids The ids of the set of tags to be 850 * deleted. 851 * @param {function(PageAPI)=} success Optional callback that receive 852 * this object as its only 853 * argument. 854 * @param {function(GCNError):boolean=} error Optional custom error 855 * handler. 856 */ 857 removeTags: function () { 858 this._removeTags.apply(this, arguments); 859 }, 860 861 /** 862 * Marks the page as to be taken offline. This method will change the 863 * state of the page object. 864 * 865 * @public 866 * @function 867 * @name takeOffline 868 * @memberOf PageAPI 869 * @param {funtion(PageAPI)=} success Optional callback to receive this 870 * page object as the only argument. 871 * @param {function(GCNError):boolean=} error Optional custom error 872 * handler. 873 */ 874 takeOffline: function (success, error) { 875 var that = this; 876 877 this._read(function () { 878 that._update('status', STATUS.OFFLINE, error); 879 if (success) { 880 that._save(null, success, error); 881 } 882 }, error); 883 }, 884 885 /** 886 * Trigger publish process for the page. 887 * 888 * @public 889 * @function 890 * @name publish 891 * @memberOf PageAPI 892 * @param {funtion(PageAPI)=} success Optional callback to receive this 893 * page object as the only argument. 894 * @param {function(GCNError):boolean=} error Optional custom error 895 * handler. 896 */ 897 publish: function (success, error) { 898 var that = this; 899 this._continueWith(function () { 900 that._authAjax({ 901 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 902 '/publish/' + that.id(), 903 type: 'POST', 904 json: {}, // There needs to be at least empty content 905 // because of a bug in Jersey. 906 success: function (response) { 907 that._data.status = STATUS.PUBLISHED; 908 if (success) { 909 success(that); 910 } 911 }, 912 error: error 913 }); 914 }); 915 }, 916 917 /** 918 * Renders a preview of the current page. 919 * 920 * @public 921 * @function 922 * @name preview 923 * @memberOf PageAPI 924 * @param {function(string, PageAPI)} success Callback to receive the 925 * rendered page preview as 926 * the first argument, and 927 * this page object as the 928 * second. 929 * @param {function(GCNError):boolean=} error Optional custom error 930 * handler. 931 */ 932 preview: function (success, error) { 933 var that = this; 934 935 this._read(function () { 936 that._authAjax({ 937 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 938 '/preview/', 939 json: { 940 page: that._data // @FIXME Shouldn't this a be merge of 941 // the `_shadow' object and the 942 // `_data'. 943 }, 944 type: 'POST', 945 error: error, 946 success: function (response) { 947 if (success) { 948 GCN._handleContentRendered(response.preview, that, 949 function (html) { 950 success(html, that); 951 }); 952 } 953 } 954 }); 955 }, error); 956 }, 957 958 /** 959 * Unlocks the page when finishing editing 960 * 961 * @public 962 * @function 963 * @name unlock 964 * @memberOf PageAPI 965 * @param {funtion(PageAPI)=} success Optional callback to receive this 966 * page object as the only argument. 967 * @param {function(GCNError):boolean=} error Optional custom error 968 * handler. 969 */ 970 unlock: function (success, error) { 971 var that = this; 972 this._continueWith(function () { 973 that._authAjax({ 974 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 975 '/cancel/' + that.id(), 976 type: 'POST', 977 json: {}, // There needs to be at least empty content 978 // because of a bug in Jersey. 979 error: error, 980 success: function (response) { 981 if (success) { 982 success(that); 983 } 984 } 985 }); 986 }); 987 }, 988 989 /** 990 * @see GCN.ContentObjectAPI._processResponse 991 */ 992 '!_processResponse': function (data) { 993 jQuery.extend(this._data, data[this._type]); 994 995 // if data contains page variants turn them into page objects 996 if (this._data.pageVariants) { 997 var pagevars = []; 998 var i; 999 for (i = 0; i < this._data.pageVariants.length; i++) { 1000 pagevars.push(this._continue(GCN.PageAPI, 1001 this._data.pageVariants[i])); 1002 } 1003 this._data.pageVariants = pagevars; 1004 } 1005 }, 1006 1007 /** 1008 * @override 1009 */ 1010 '!_removeAssociatedTagData': function (tagid) { 1011 var block; 1012 for (block in this._blocks) { 1013 if (this._blocks.hasOwnProperty(block) && 1014 this._blocks[block].tagname === tagid) { 1015 delete this._blocks[block]; 1016 } 1017 } 1018 1019 var editable; 1020 for (editable in this._editables) { 1021 if (this._editables.hasOwnProperty(editable) && 1022 this._editables[editable].tagname === tagid) { 1023 delete this._editables[editable]; 1024 } 1025 } 1026 } 1027 1028 }); 1029 1030 GCN.page = GCN.exposeAPI(PageAPI); 1031 GCN.PageAPI = PageAPI; 1032 1033 }(GCN)); 1034