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 extended 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 if (success) { 437 success(); 438 } 439 }, error); 440 }, error); 441 }, error); 442 }, error); 443 }, 444 445 /** 446 * Removes any link blocks that existed in rendered tags, but have 447 * since been removed by the user while editing. 448 * 449 * @private 450 * @param {Array.<object>} blocks An array of blocks belonging to this 451 * page. 452 * @param {object} constrcts A set of constructs. 453 * @param {function} success 454 * @param {function(GCNError):boolean=} error Optional custom error 455 * handler. 456 */ 457 '!_removeUnusedLinkBlocks': function (blocks, constructs, success, 458 error) { 459 if (0 === blocks.length) { 460 if (success) { 461 success(); 462 } 463 return; 464 } 465 466 var j = blocks.length; 467 var numToProcess = j; 468 var that = this; 469 470 var onProcess = function () { 471 if (0 === --numToProcess) { 472 if (success) { 473 success(); 474 } 475 } 476 }; 477 478 var onError = function (GCNError) { 479 if (error) { 480 error(GCNError); 481 } 482 return; 483 }; 484 485 var createBlockTagProcessor = function (block) { 486 return function (tag) { 487 if (isMagicLinkTag(tag, constructs) && 488 !hasInlineElement(block)) { 489 that._deletedBlocks.push(block.tagname); 490 } 491 onProcess(); 492 }; 493 }; 494 495 while (j--) { 496 this.tag(blocks[j].tagname, 497 createBlockTagProcessor(blocks[j]), onError); 498 } 499 }, 500 501 /** 502 * Adds any newly created link blocks into this page object. This is 503 * done by looking for all link blocks that do not have corresponding 504 * tag in this object's `_blocks' list. For each anchor tag we find, 505 * create a tag for it and, add it in the list of tags. 506 * 507 * @private 508 * @param {function} success Function to invoke if this function 509 * successeds. 510 * @param {function(GCNError):boolean=} error Optional custom error 511 * handler. 512 */ 513 '!_addNewLinkBlocks': function (success, error) { 514 // Limit the search for links to be done only withing rendered 515 // editables. 516 var id; 517 var $editables = jQuery(); 518 for (id in this._editables) { 519 if (this._editables.hasOwnProperty(id)) { 520 $editables = $editables.add('#' + id); 521 } 522 } 523 524 var selector = [ 525 'a[data-gentics-aloha-repository="com.gentics.aloha.GCN.Page"]', 526 'a[data-GENTICS-aloha-repository="com.gentics.aloha.GCN.Page"]', 527 'a[data-gentics-gcn-url]' 528 ].join(','); 529 530 var links = $editables.find(selector); 531 if (0 === links.length) { 532 if (success) { 533 success(); 534 } 535 return; 536 } 537 538 var link; 539 var j = links.length; 540 var numToProcess = j; 541 var that = this; 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({keyword: GCN.settings.MAGIC_LINK, magicValue: 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 var that = this; 610 611 var onProcess = function () { 612 if (0 === --numToProcess) { 613 if (success) { 614 success(); 615 } 616 } 617 }; 618 619 var onError = function (GCNError) { 620 if (error) { 621 error(GCNError); 622 } 623 return; 624 }; 625 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 // ASSERT(tag) 688 689 for (elementId in editables) { 690 if (editables.hasOwnProperty(elementId)) { 691 editable = editables[elementId]; 692 element = jQuery('#' + elementId); 693 694 // If this editable has no element that was placed in the 695 // DOM, then do not attempt to update it. 696 if (0 === element.length) { 697 continue; 698 } 699 700 tagname = editable.tagname; 701 702 if (!tags[tagname]) { 703 tags[tagname] = { 704 name : tagname, 705 active : true, 706 properties : {} 707 }; 708 } else { 709 // make sure, that all tags (which relate to editables) 710 // are activated 711 tags[tagname].active = true; 712 } 713 714 // If the editable element has been `aloha()'fied, then we 715 // need to use `getContents()' from is corresponding 716 // Aloha.Editable object in order to get clean HTML. 717 718 alohaEditable = getAlohaEditableById(elementId); 719 720 if (alohaEditable) { 721 // Avoid the unnecessary overhead of custom editable 722 // serialization by calling html ourselves. 723 cleanElement = jQuery('<div>') 724 .append(alohaEditable.getContents(true)); 725 alohaEditable.setUnmodified(); 726 // Apply the custom editable serialization as the last step. 727 customSerializer = window.Aloha.Editable.getContentSerializer(); 728 html = this.encode(cleanElement, customSerializer); 729 } else { 730 html = this.encode(element); 731 } 732 733 tags[tagname].properties[editable.partname] = { 734 stringValue: html, 735 type: 'RICHTEXT' 736 }; 737 738 this._update('tags.' + GCN.escapePropertyName(tagname), 739 tags[tagname]); 740 } 741 } 742 }, 743 744 /** 745 * @see ContentObjectAPI.!_loadParams 746 */ 747 '!_loadParams': function () { 748 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 749 }, 750 751 /** 752 * Get this page's template. 753 * 754 * @public 755 * @function 756 * @name template 757 * @memberOf PageAPI 758 * @param {funtion(TemplateAPI)=} success Optional callback to receive 759 * a {@link TemplateAPI} object 760 * as the only argument. 761 * @param {function(GCNError):boolean=} error Optional custom error 762 * handler. 763 * @return {TemplateAPI} This page's parent template. 764 */ 765 '!template': function (success, error) { 766 var id = this._fetched ? this.prop('templateId') : null; 767 return this._continue(GCN.TemplateAPI, id, success, error); 768 }, 769 770 /** 771 * @override 772 * @see ContentObjectAPI._save 773 */ 774 '!_save': function (settings, success, error) { 775 var that = this; 776 this._fulfill(function () { 777 that._read(function () { 778 var fork = that._fork(); 779 fork._prepareTagsForSaving(function () { 780 fork._persist(settings, function () { 781 if (success) { 782 that._invoke(success, [that]); 783 } 784 fork._merge(); 785 }, error); 786 }, error); 787 }, error); 788 }, error); 789 }, 790 791 //--------------------------------------------------------------------- 792 // Surface the tag container methods that are applicable for GCN page 793 // objects. 794 //--------------------------------------------------------------------- 795 796 /** 797 * Creates a tag of a given tagtype in this page. 798 * The first parameter should either be the construct keyword or ID, 799 * or an object containing exactly one of the following property sets:<br/> 800 * <ol> 801 * <li><i>keyword</i> to create a tag based on the construct with given keyword</li> 802 * <li><i>constructId</i> to create a tag based on the construct with given ID</li> 803 * <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li> 804 * </ol> 805 * 806 * Exmaple: 807 * <pre> 808 * createTag('link', onSuccess, onError); 809 * </pre> 810 * or 811 * <pre> 812 * createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError); 813 * </pre> 814 * or 815 * <pre> 816 * createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError); 817 * </pre> 818 * 819 * @public 820 * @function 821 * @name createTag 822 * @memberOf PageAPI 823 * @param {string|number|object} construct either the keyword of the construct, or the ID of the construct 824 * or an object with the following properties 825 * <ul> 826 * <li><i>keyword</i> keyword of the construct</li> 827 * <li><i>constructId</i> ID of the construct</li> 828 * <li><i>magicValue</i> magic value to be filled into the tag</li> 829 * <li><i>sourcePageId</i> source page id</li> 830 * <li><i>sourceTagname</i> source tag name</li> 831 * </ul> 832 * @param {function(TagAPI)=} success Optional callback that will 833 * receive the newly created tag as 834 * its only argument. 835 * @param {function(GCNError):boolean=} error Optional custom error 836 * handler. 837 * @return {TagAPI} The newly created tag. 838 * @throws INVALID_ARGUMENTS 839 */ 840 '!createTag': function () { 841 return this._createTag.apply(this, arguments); 842 }, 843 844 /** 845 * Deletes the specified tag from this page. 846 * 847 * @public 848 * @function 849 * @name removeTag 850 * @memberOf PageAPI 851 * @param {string} id The id of the tag to be deleted. 852 * @param {function(PageAPI)=} success Optional callback that receive 853 * this object as its only 854 * argument. 855 * @param {function(GCNError):boolean=} error Optional custom error 856 * handler. 857 */ 858 removeTag: function () { 859 this._removeTag.apply(this, arguments); 860 }, 861 862 /** 863 * Deletes a set of tags from this page. 864 * 865 * @public 866 * @function 867 * @name removeTags 868 * @memberOf PageAPI 869 * @param {Array.<string>} ids The ids of the set of tags to be 870 * deleted. 871 * @param {function(PageAPI)=} success Optional callback that receive 872 * this object as its only 873 * argument. 874 * @param {function(GCNError):boolean=} error Optional custom error 875 * handler. 876 */ 877 removeTags: function () { 878 this._removeTags.apply(this, arguments); 879 }, 880 881 /** 882 * Marks the page as to be taken offline. This method will change the 883 * state of the page object. 884 * 885 * @public 886 * @function 887 * @name takeOffline 888 * @memberOf PageAPI 889 * @param {funtion(PageAPI)=} success Optional callback to receive this 890 * page object as the only argument. 891 * @param {function(GCNError):boolean=} error Optional custom error 892 * handler. 893 */ 894 takeOffline: function (success, error) { 895 var that = this; 896 897 this._read(function () { 898 that._update('status', STATUS.OFFLINE, error); 899 if (success) { 900 that._save(null, success, error); 901 } 902 }, error); 903 }, 904 905 /** 906 * Trigger publish process for the page. 907 * 908 * @public 909 * @function 910 * @name publish 911 * @memberOf PageAPI 912 * @param {funtion(PageAPI)=} success Optional callback to receive this 913 * page object as the only argument. 914 * @param {function(GCNError):boolean=} error Optional custom error 915 * handler. 916 */ 917 publish: function (success, error) { 918 var that = this; 919 this._fulfill(function () { 920 that._authAjax({ 921 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 922 '/publish/' + that.id(), 923 type: 'POST', 924 json: {}, // There needs to be at least empty content 925 // because of a bug in Jersey. 926 success: function (response) { 927 that._data.status = STATUS.PUBLISHED; 928 if (success) { 929 that._invoke(success, [that]); 930 } 931 }, 932 error: error 933 }); 934 }); 935 }, 936 937 /** 938 * Renders a preview of the current page. 939 * 940 * @public 941 * @function 942 * @name preview 943 * @memberOf PageAPI 944 * @param {function(string, PageAPI)} success Callback to receive the 945 * rendered page preview as 946 * the first argument, and 947 * this page object as the 948 * second. 949 * @param {function(GCNError):boolean=} error Optional custom error 950 * handler. 951 */ 952 preview: function (success, error) { 953 var that = this; 954 955 this._read(function () { 956 that._authAjax({ 957 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 958 '/preview/', 959 json: { 960 page: that._data // @FIXME Shouldn't this a be merge of 961 // the `_shadow' object and the 962 // `_data'. 963 }, 964 type: 'POST', 965 error: error, 966 success: function (response) { 967 if (success) { 968 GCN._handleContentRendered(response.preview, that, 969 function (html) { 970 that._invoke(success, [html, that]); 971 }); 972 } 973 } 974 }); 975 }, error); 976 }, 977 978 /** 979 * Unlocks the page when finishing editing 980 * 981 * @public 982 * @function 983 * @name unlock 984 * @memberOf PageAPI 985 * @param {funtion(PageAPI)=} success Optional callback to receive this 986 * page object as the only argument. 987 * @param {function(GCNError):boolean=} error Optional custom error 988 * handler. 989 */ 990 unlock: function (success, error) { 991 var that = this; 992 this._fulfill(function () { 993 that._authAjax({ 994 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 995 '/cancel/' + that.id(), 996 type: 'POST', 997 json: {}, // There needs to be at least empty content 998 // because of a bug in Jersey. 999 error: error, 1000 success: function (response) { 1001 if (success) { 1002 that._invoke(success, [that]); 1003 } 1004 } 1005 }); 1006 }); 1007 }, 1008 1009 /** 1010 * @see GCN.ContentObjectAPI._processResponse 1011 */ 1012 '!_processResponse': function (data) { 1013 jQuery.extend(this._data, data[this._type]); 1014 1015 // if data contains page variants turn them into page objects 1016 if (this._data.pageVariants) { 1017 var pagevars = []; 1018 var i; 1019 for (i = 0; i < this._data.pageVariants.length; i++) { 1020 pagevars.push(this._continue(GCN.PageAPI, 1021 this._data.pageVariants[i])); 1022 } 1023 this._data.pageVariants = pagevars; 1024 } 1025 }, 1026 1027 /** 1028 * @override 1029 */ 1030 '!_removeAssociatedTagData': function (tagid) { 1031 var block; 1032 for (block in this._blocks) { 1033 if (this._blocks.hasOwnProperty(block) && 1034 this._blocks[block].tagname === tagid) { 1035 delete this._blocks[block]; 1036 } 1037 } 1038 1039 var editable; 1040 for (editable in this._editables) { 1041 if (this._editables.hasOwnProperty(editable) && 1042 this._editables[editable].tagname === tagid) { 1043 delete this._editables[editable]; 1044 } 1045 } 1046 } 1047 1048 }); 1049 1050 GCN.page = GCN.exposeAPI(PageAPI); 1051 GCN.PageAPI = PageAPI; 1052 1053 }(GCN)); 1054