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