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