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 that.node().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 685 for (elementId in editables) { 686 if (editables.hasOwnProperty(elementId)) { 687 editable = editables[elementId]; 688 element = jQuery('#' + elementId); 689 690 // If this editable has no element that was placed in the 691 // DOM, then do not attempt to update it. 692 if (0 === element.length) { 693 continue; 694 } 695 696 tagname = editable.tagname; 697 698 if (!tags[tagname]) { 699 tags[tagname] = { 700 name : tagname, 701 active : true, 702 properties : {} 703 }; 704 } else { 705 // make sure, that all tags (which relate to editables) 706 // are activated 707 tags[tagname].active = true; 708 } 709 710 // If the editable element has been `aloha()'fied, then we 711 // need to use `getContents()' from is corresponding 712 // Aloha.Editable object in order to get clean HTML. 713 714 alohaEditable = getAlohaEditableById(elementId); 715 716 if (alohaEditable) { 717 html = alohaEditable.getContents(); 718 alohaEditable.setUnmodified(); 719 } else { 720 html = element.html(); 721 } 722 723 tags[tagname].properties[editable.partname] = { 724 stringValue: this.encode(html), 725 type: 'RICHTEXT' 726 }; 727 728 this._update('tags.' + tagname, tags[tagname]); 729 } 730 } 731 }, 732 733 /** 734 * @see ContentObjectAPI.!_loadParams 735 */ 736 '!_loadParams': function () { 737 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 738 }, 739 740 /** 741 * Get this page's template. 742 * 743 * @public 744 * @function 745 * @name template 746 * @memberOf PageAPI 747 * @param {funtion(TemplateAPI)=} success Optional callback to receive 748 * a {@link TemplateAPI} object 749 * as the only argument. 750 * @param {function(GCNError):boolean=} error Optional custom error 751 * handler. 752 * @return {TemplateAPI} This page's parent template. 753 */ 754 '!template': function (success, error) { 755 var id = this._fetched ? this.prop('templateId') : null; 756 return this._continue(GCN.TemplateAPI, id, success, error); 757 }, 758 759 /** 760 * @override 761 * @see ContentObjectAPI._save 762 */ 763 '!_save': function (settings, success, error) { 764 var that = this; 765 this._continueWith(function () { 766 var fork = that._fork(); 767 fork._prepareTagsForSaving(function () { 768 fork._persist(settings, function () { 769 if (success) { 770 success(that); 771 } 772 fork._merge(); 773 }, error); 774 }, error); 775 }, error); 776 }, 777 778 //--------------------------------------------------------------------- 779 // Surface the tag container methods that are applicable for GCN page 780 // objects. 781 //--------------------------------------------------------------------- 782 783 /** 784 * Creates a tag of a given tagtype in this page. 785 * 786 * Exmaple: 787 * <pre> 788 * createTag('link', 'http://www.gentics.com', onSuccess, onError); 789 * </pre> 790 * or 791 * <pre> 792 * createTag('link', onSuccess, onError); 793 * </pre> 794 * 795 * @public 796 * @function 797 * @name createTag 798 * @memberOf PageAPI 799 * @param {string|number} construct The name of the construct on which 800 * the tag to be created should be 801 * derived from. Or the id of that 802 * @param {string=} magicValue Optional property that will override the 803 * default values of this tag type. 804 * @param {function(TagAPI)=} success Optional callback that will 805 * receive the newly created tag as 806 * its only argument. 807 * @param {function(GCNError):boolean=} error Optional custom error 808 * handler. 809 * @return {TagAPI} The newly created tag. 810 * @throws INVALID_ARGUMENTS 811 */ 812 '!createTag': function () { 813 return this._createTag.apply(this, arguments); 814 }, 815 816 /** 817 * Deletes the specified tag from this page. 818 * 819 * @public 820 * @function 821 * @name removeTag 822 * @memberOf PageAPI 823 * @param {string} id The id of the tag to be deleted. 824 * @param {function(PageAPI)=} success Optional callback that receive 825 * this object as its only 826 * argument. 827 * @param {function(GCNError):boolean=} error Optional custom error 828 * handler. 829 */ 830 removeTag: function () { 831 this._removeTag.apply(this, arguments); 832 }, 833 834 /** 835 * Deletes a set of tags from this page. 836 * 837 * @public 838 * @function 839 * @name removeTags 840 * @memberOf PageAPI 841 * @param {Array.<string>} ids The ids of the set of tags to be 842 * deleted. 843 * @param {function(PageAPI)=} success Optional callback that receive 844 * this object as its only 845 * argument. 846 * @param {function(GCNError):boolean=} error Optional custom error 847 * handler. 848 */ 849 removeTags: function () { 850 this._removeTags.apply(this, arguments); 851 }, 852 853 /** 854 * Marks the page as to be taken offline. This method will change the 855 * state of the page object. 856 * 857 * @public 858 * @function 859 * @name takeOffline 860 * @memberOf PageAPI 861 * @param {funtion(PageAPI)=} success Optional callback to receive this 862 * page object as the only argument. 863 * @param {function(GCNError):boolean=} error Optional custom error 864 * handler. 865 */ 866 takeOffline: function (success, error) { 867 var that = this; 868 869 this._read(function () { 870 that._update('status', STATUS.OFFLINE, error); 871 if (success) { 872 that._save(null, success, error); 873 } 874 }, error); 875 }, 876 877 /** 878 * Trigger publish process for the page. 879 * 880 * @public 881 * @function 882 * @name publish 883 * @memberOf PageAPI 884 * @param {funtion(PageAPI)=} success Optional callback to receive this 885 * page object as the only argument. 886 * @param {function(GCNError):boolean=} error Optional custom error 887 * handler. 888 */ 889 publish: function (success, error) { 890 var that = this; 891 this._continueWith(function () { 892 that._authAjax({ 893 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 894 '/publish/' + that.id(), 895 type: 'POST', 896 json: {}, // There needs to be at least empty content 897 // because of a bug in Jersey. 898 success: function (response) { 899 that._data.status = STATUS.PUBLISHED; 900 if (success) { 901 success(that); 902 } 903 }, 904 error: error 905 }); 906 }); 907 }, 908 909 /** 910 * Renders a preview of the current page. 911 * 912 * @public 913 * @function 914 * @name preview 915 * @memberOf PageAPI 916 * @param {function(string, PageAPI)} success Callback to receive the 917 * rendered page preview as 918 * the first argument, and 919 * this page object as the 920 * second. 921 * @param {function(GCNError):boolean=} error Optional custom error 922 * handler. 923 */ 924 preview: function (success, error) { 925 var that = this; 926 927 this._read(function () { 928 that._authAjax({ 929 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 930 '/preview/', 931 json: { 932 page: that._data // @FIXME Shouldn't this a be merge of 933 // the `_shadow' object and the 934 // `_data'. 935 }, 936 type: 'POST', 937 error: error, 938 success: function (response) { 939 if (success) { 940 GCN._handleContentRendered(response.preview, 941 function (html) { 942 success(html, that); 943 }); 944 } 945 } 946 }); 947 }, error); 948 }, 949 950 /** 951 * Unlocks the page when finishing editing 952 * 953 * @public 954 * @function 955 * @name unlock 956 * @memberOf PageAPI 957 * @param {funtion(PageAPI)=} success Optional callback to receive this 958 * page object as the only argument. 959 * @param {function(GCNError):boolean=} error Optional custom error 960 * handler. 961 */ 962 unlock: function (success, error) { 963 var that = this; 964 this._continueWith(function () { 965 that._authAjax({ 966 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 967 '/cancel/' + that.id(), 968 type: 'POST', 969 json: {}, // There needs to be at least empty content 970 // because of a bug in Jersey. 971 error: error, 972 success: function (response) { 973 if (success) { 974 success(that); 975 } 976 } 977 }); 978 }); 979 }, 980 981 /** 982 * @see GCN.ContentObjectAPI._processResponse 983 */ 984 '!_processResponse': function (data) { 985 jQuery.extend(this._data, data[this._type]); 986 987 // if data contains page variants turn them into page objects 988 if (this._data.pageVariants) { 989 var pagevars = []; 990 var i; 991 for (i = 0; i < this._data.pageVariants.length; i++) { 992 pagevars.push(this._continue(GCN.PageAPI, 993 this._data.pageVariants[i])); 994 } 995 this._data.pageVariants = pagevars; 996 } 997 }, 998 999 /** 1000 * @override 1001 */ 1002 '!_removeAssociatedTagData': function (tagid) { 1003 var block; 1004 for (block in this._blocks) { 1005 if (this._blocks.hasOwnProperty(block) && 1006 this._blocks[block].tagname === tagid) { 1007 delete this._blocks[block]; 1008 } 1009 } 1010 1011 var editable; 1012 for (editable in this._editables) { 1013 if (this._editables.hasOwnProperty(editable) && 1014 this._editables[editable].tagname === tagid) { 1015 delete this._editables[editable]; 1016 } 1017 } 1018 } 1019 1020 }); 1021 1022 GCN.page = GCN.exposeAPI(PageAPI); 1023 GCN.PageAPI = PageAPI; 1024 1025 }(GCN)); 1026