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