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 * jQuery selector string for GCN internal links. 14 * @const 15 * @type {string} 16 */ 17 var GCN_LINK_SELECTOR = 18 'a[data-gentics-aloha-repository="' + GCN_REPOSITORY_ID + '"]'; 19 20 /** 21 * @private 22 * @const 23 * @type {object.<string, boolean>} Default page settings. 24 */ 25 var DEFAULT_SETTINGS = { 26 // Load folder information 27 folder: true, 28 29 // Lock page when loading it 30 update: true, 31 32 // Have language variants be included in the page response. 33 langvars: true, 34 35 // Have page variants be included in the page response. 36 pagevars: true 37 }; 38 39 /** 40 * Checks whether the given tag is a magic link block. 41 * 42 * @param {TagAPI} tag A tag that must already have been fetched. 43 * @param {Object} constructs Set of constructs. 44 * @return {boolean} True if the given tag's constructId is equal to the 45 * `MAGIC_LINK' value. 46 */ 47 function isMagicLinkTag(tag, constructs) { 48 return !!(constructs[GCN.settings.MAGIC_LINK] 49 && (constructs[GCN.settings.MAGIC_LINK].constructId 50 === tag.prop('constructId'))); 51 } 52 53 /** 54 * Checks whether or not the given anchor element is an internal GCN link. 55 * 56 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link 57 * DOM element. 58 * @return {boolean} True if the anchor element holds an external url. 59 */ 60 function isExternalLink($link) { 61 return GCN_REPOSITORY_ID !== 62 $link.attr('data-gentics-aloha-repository'); 63 } 64 65 /** 66 * Given a list of editable objects, returns a jQuery set containing these 67 * editables' corresponding DOM elements that are attached to the document. 68 * 69 * @param {object<string, object>} editables A list of editable objects 70 * mapped to the id of their 71 * corresponding DOM element. 72 * @return {jQuery.<HTMLElement>} A jQuery set of editable DOM elements. 73 */ 74 function getEditables(page) { 75 var id; 76 var $editables = jQuery(); 77 var editables = page._editables; 78 for (id in editables) { 79 if (editables.hasOwnProperty(id)) { 80 $editables = $editables.add('#' + id); 81 } 82 } 83 return $editables; 84 } 85 86 /** 87 * Returns all editables associated with the given page that have been 88 * rendered in the document for editing. 89 * 90 * @param {PageAPI} page 91 * @return {object} A set of editable objects. 92 */ 93 function getEditedEditables(page) { 94 return page._editables; 95 } 96 97 /** 98 * Derives a list of the blocks that were rendered inside at least one of 99 * the given page's edit()ed editables. 100 * 101 * @param {object<string, object>} editables A list of editable objects 102 * mapped to the id of their 103 * corresponding DOM element. 104 * @return {Array.<object>} The set of blocks contained in any of the 105 * page's rendered editables. 106 */ 107 function getRenderedBlocks(page) { 108 var editables = getEditedEditables(page); 109 var editableId; 110 var renderedBlocks = []; 111 for (editableId in editables) { 112 if (editables.hasOwnProperty(editableId)) { 113 if (editables[editableId]._gcnContainedBlocks) { 114 renderedBlocks = renderedBlocks.concat( 115 editables[editableId]._gcnContainedBlocks 116 ); 117 } 118 } 119 } 120 return renderedBlocks; 121 } 122 123 /** 124 * Gets the DOM element associated with the given block. 125 * 126 * @param {object} block 127 * @return {?jQuery.<HTMLElement>} A jQuery unit set of the block's 128 * corresponding DOM element, or null if no 129 * element for the given block exists in 130 * the document. 131 */ 132 function getElement(block) { 133 var $element = jQuery('#' + block.element); 134 return $element.length ? $element : null; 135 } 136 137 /** 138 * Retrieves a jQuery set of all link elements that are contained in 139 * editables associated with the given page. 140 * 141 * @param {PageAPI} page 142 * @return {jQuery.<HTMLElement>} A jQuery set of link elements. 143 */ 144 function getEditableLinks(page) { 145 return getEditables(page).find('a'); 146 } 147 148 /** 149 * Synonym for getting contstruct system-wide. 150 * 151 * @param {function(object)} success A callback function that receives a 152 * list of constructs when they are 153 * successfully loaded. 154 * @param {error(GCNError):boolean=} error Optional custom error handler. 155 */ 156 function getAllConstructs(success, error) { 157 GCN.Admin.constructs(success, error); 158 } 159 160 /** 161 * Determines all blocks that no longer need their tags to be kept in the 162 * given page's tag list. 163 * 164 * @param {PageAPI} page 165 * @param {function(Array.<object>)} success A callback function that 166 * receives a list of obsolete 167 * blocks. 168 * @param {function(GCNError):boolean=} error Optional custom error 169 * handler. 170 */ 171 function getObsoleteBlocks(page, success, error) { 172 var blocks = getRenderedBlocks(page); 173 if (0 === blocks.length) { 174 success([]); 175 return; 176 } 177 var $links = getEditableLinks(page); 178 var numToProcess = blocks.length; 179 var obsolete = []; 180 var onSuccess = function () { 181 if ((0 === --numToProcess) && success) { 182 success(obsolete); 183 } 184 }; 185 var onError = function (GCNError) { 186 if (error) { 187 return error(GCNError); 188 } 189 }; 190 getAllConstructs(function (constructs) { 191 var processTag = function (block) { 192 page.tag(block.tagname, function (tag) { 193 if (isMagicLinkTag(tag, constructs)) { 194 var $element = getElement(block); 195 if (!$element) { 196 obsolete.push(block); 197 } else if (isExternalLink($element)) { 198 $element.removeAttr('id'); 199 obsolete.push(block); 200 } 201 } 202 onSuccess(); 203 }, onError); 204 }; 205 var i; 206 for (i = 0; i < blocks.length; i++) { 207 processTag(blocks[i]); 208 } 209 }); 210 } 211 212 /** 213 * Checks whether or not the given block has a corresponding element in the 214 * document DOM. 215 * 216 * @private 217 * @static 218 * @param {object} 219 * @return {boolean} True if an inline element for this block exists. 220 */ 221 function hasInlineElement(block) { 222 return !!getElement(block); 223 } 224 225 /** 226 * Matches the name "aloha-block" in a className string. 227 * 228 * @const 229 * @type {RegExp} 230 */ 231 var ALOHA_BLOCK_CLASS = /\baloha-block\b/g; 232 233 /** 234 * Strips unwanted names from the given className string. 235 * 236 * All class names beginning with "aloha-block*" will be removed. 237 * 238 * @param {string} classes Space seperated list of classes. 239 * @return {string} Sanitized classes string. 240 */ 241 function cleanBlockClasses(classes) { 242 return classes ? classes.replace(ALOHA_BLOCK_CLASS, '') : ''; 243 } 244 245 /** 246 * Determines the backend object that was set to the given link. 247 * 248 * @param {jQuery.<HTMLElement>} $link A link in an editable. 249 * @return {Object} An object containing the gtxalohapagelink part keyword 250 * and value. The keyword may be either be "url" or 251 * "fileurl" depending on the type of object linked to. 252 * The value may be a string url ("http://...") for 253 * external links or an integer for internal links. 254 */ 255 function getTagPartsFromLink($link) { 256 var linkData = $link.attr('data-gentics-aloha-object-id'); 257 var tagparts = { 258 text: $link.text(), 259 title: $link.attr('title'), 260 target: $link.attr('target'), 261 'class': cleanBlockClasses($link.attr('class')) 262 }; 263 264 if (!linkData) { 265 tagparts.url = $link.attr('href'); 266 return tagparts; 267 } 268 269 var idParts = linkData.split('.'); 270 271 if (2 !== idParts.length) { 272 tagparts.url = linkData; 273 } else if ('10007' === idParts[0]) { 274 tagparts.url = parseInt(idParts[1], 10); 275 tagparts.fileurl = 0; 276 } else if ('10008' === idParts[0] || '10011' === idParts[0]) { 277 tagparts.url = 0; 278 tagparts.fileUrl = parseInt(idParts[1], 10); 279 } else { 280 tagparts.extUrl = $link.attr('href'); 281 } 282 283 return tagparts; 284 } 285 286 /** 287 * Checks whether a page object has a corresponding tag for a given link 288 * DOM element. 289 * 290 * @param {PageAPI} page The page object in which to look for the link tag. 291 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link 292 * DOM element. 293 * @return {boolean} True if there is a tag on the page that corresponds with 294 * the givn link. 295 */ 296 function hasTagForLink(page, $link) { 297 var id = $link.attr('id'); 298 return !!(id && page._getBlockById(id)); 299 } 300 301 /** 302 * Checks whether or not the given part has a part type of the given 303 * name 304 * 305 * @param {TagAPI} tag 306 * @param {string} part Part name 307 * @return {boolean} True of part exists in the given tag. 308 */ 309 function hasTagPart(tag, part) { 310 return !!tag._data.properties[part]; 311 } 312 313 /** 314 * Updates the parts of a tag in the page object that corresponds to the 315 * given link DOM element. 316 * 317 * @param {PageAPI} page 318 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link 319 * DOM element. 320 */ 321 function updateLinkTag(page, $link) { 322 var block = page._getBlockById($link.attr('id')); 323 // ASSERT(block) 324 var tag = page.tag(block.tagname); 325 var parts = getTagPartsFromLink($link); 326 var part; 327 for (part in parts) { 328 if (parts.hasOwnProperty(part) && hasTagPart(tag, part)) { 329 tag.part(part, parts[part]); 330 } 331 } 332 } 333 334 /** 335 * Creates a new tag for the given link in the page. 336 * 337 * @param {PageAPI} page 338 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link 339 * element. 340 * @param {function} success Callback function that whill be called when 341 * the new tag is created. 342 * @param {function(GCNError):boolean=} error Optional custom error 343 * handler. 344 */ 345 function createLinkTag(page, $link, success, error) { 346 page.createTag({ 347 keyword: GCN.settings.MAGIC_LINK, 348 magicValue: $link.html() 349 }).edit(function (html, tag) { 350 // Copy over the rendered id value for the link so that we can bind 351 // the link in the DOM with the newly created block. 352 $link.attr('id', jQuery(html).attr('id')); 353 updateLinkTag(page, $link); 354 success(); 355 }, error); 356 } 357 358 /** 359 * Identifies internal GCN links in the given page's rendered editables, 360 * and updates their corresponding content tags, or create new tags for the 361 * if they are new links. 362 * 363 * @param {PageAPI} page 364 * @param {function} success 365 * @param {function} error 366 */ 367 function processGCNLinks(page, success, error) { 368 var $links = getEditableLinks(page); 369 var $gcnlinks = $links.filter(GCN_LINK_SELECTOR); 370 if (0 === $gcnlinks.length) { 371 if (success) { 372 success(); 373 } 374 return; 375 } 376 var numToProcess = $gcnlinks.length; 377 var onSuccess = function () { 378 if ((0 === --numToProcess) && success) { 379 success(); 380 } 381 }; 382 var onError = function (GCNError) { 383 if (error) { 384 return error(GCNError); 385 } 386 }; 387 var i; 388 for (i = 0; i < $gcnlinks.length; i++) { 389 if (hasTagForLink(page, $gcnlinks.eq(i))) { 390 updateLinkTag(page, $gcnlinks.eq(i)); 391 onSuccess(); 392 } else { 393 createLinkTag(page, $gcnlinks.eq(i), onSuccess, onError); 394 } 395 } 396 } 397 398 /** 399 * Adds the given blocks into the page's list of blocks that are to be 400 * deleted when the page is saved. 401 * 402 * @param {PageAPI} page 403 * @param {Array.<object>} blocks Blocks that are to be marked for deletion. 404 */ 405 function deleteBlocks(page, blocks) { 406 blocks = jQuery.isArray(blocks) ? blocks : [blocks]; 407 var i; 408 for (i = 0; i < blocks.length; i++) { 409 if (-1 === 410 jQuery.inArray(blocks[i].tagname, page._deletedBlocks)) { 411 page._deletedBlocks.push(blocks[i].tagname); 412 } 413 delete page._blocks[blocks[i].element]; 414 } 415 } 416 417 /** 418 * Removes all tags on the given page which belong to links that are no 419 * longer present in any of the page's rendered editables. 420 * 421 * @param {PageAPI} page 422 * @param {function} success Callback function that will be invoked when 423 * all obsolete tags have been successfully 424 * marked for deletion. 425 * @param {function(GCNError):boolean=} error Optional custom error 426 * handler. 427 */ 428 function deleteObsoleteLinkTags(page, success, error) { 429 getObsoleteBlocks(page, function (obsolete) { 430 deleteBlocks(page, obsolete); 431 if (success) { 432 success(); 433 } 434 }, error); 435 } 436 437 /** 438 * Searches for the an Aloha editable object of the given id. 439 * 440 * @TODO: Once Aloha.getEditableById() is patched to not cause an 441 * JavaScript exception if the element for the given ID is not found 442 * then we can deprecate this function and use Aloha's instead. 443 * 444 * @static 445 * @param {string} id Id of Aloha.Editable object to find. 446 * @return {Aloha.Editable=} The editable object, if wound; otherwise null. 447 */ 448 function getAlohaEditableById(id) { 449 var Aloha = (typeof window !== 'undefined') && window.Aloha; 450 if (!Aloha) { 451 return null; 452 } 453 454 // If the element is a textarea then route to the editable div. 455 var element = jQuery('#' + id); 456 if (element.length && 457 element[0].nodeName.toLowerCase() === 'textarea') { 458 id += '-aloha'; 459 } 460 461 var editables = Aloha.editables; 462 var j = editables.length; 463 while (j) { 464 if (editables[--j].getId() === id) { 465 return editables[j]; 466 } 467 } 468 469 return null; 470 } 471 472 /** 473 * For a given list of editables and a list of blocks, determines in which 474 * editable each block is contained. The result is a map of block sets. 475 * Each of these sets of blocks are mapped against the id of the editable 476 * in which they are rendered. 477 * 478 * @param {string} content The rendered content in which both the list of 479 * editables, and blocks are contained. 480 * @param {Array.<object>} editables A list of editables contained in the 481 * content. 482 * @param {Array.<object>} blocks A list of blocks containd in the content. 483 * @return {object<string, Array>} A object whose keys are editable ids, 484 * and whose values are arrays of blocks 485 * contained in the corresponding 486 * editable. 487 */ 488 function categorizeBlocksAgainstEditables(content, editables, blocks) { 489 var i; 490 var $content = jQuery('<div>' + content + '</div>'); 491 var sets = {}; 492 var editableId; 493 494 var editablesSelectors = []; 495 for (i = 0; i < editables.length; i++) { 496 editablesSelectors.push('#' + editables[i].element); 497 } 498 499 var markerClass = 'aloha-editable-tmp-marker__'; 500 var $editables = $content.find(editablesSelectors.join(',')); 501 $editables.addClass(markerClass); 502 503 var $block; 504 var $parent; 505 for (i = 0; i < blocks.length; i++) { 506 $block = $content.find('#' + blocks[i].element); 507 if ($block.length) { 508 $parent = $block.closest('.' + markerClass); 509 if ($parent.length) { 510 editableId = $parent.attr('id'); 511 if (editableId) { 512 if (!sets[editableId]) { 513 sets[editableId] = []; 514 } 515 sets[editableId].push(blocks[i]); 516 } 517 } 518 } 519 } 520 521 return sets; 522 } 523 524 /** 525 * Causes the given editables to be tracked, so that when the content 526 * object is saved, these editables will be processed. 527 * 528 * @private 529 * @param {PageAPI} page 530 * @param {Array.<object>} editables A set of object representing 531 * editable tags that have been 532 * rendered. 533 */ 534 function trackEditables(page, editables) { 535 if (!page.hasOwnProperty('_editables')) { 536 page._editables = {}; 537 } 538 var i; 539 for (i = 0; i < editables.length; i++) { 540 page._editables[editables[i].element] = editables[i]; 541 } 542 } 543 544 /** 545 * Causes the given blocks to be tracked so that when the content object is 546 * saved, these editables will be processed. 547 * 548 * @private 549 * @param {PageAPI} page 550 * @param {Array.<object>} blocks An set of object representing block 551 * tags that have been rendered. 552 */ 553 function trackBlocks(page, blocks) { 554 if (!page.hasOwnProperty('_blocks')) { 555 page._blocks = {}; 556 } 557 var i; 558 for (i = 0; i < blocks.length; i++) { 559 page._blocks[blocks[i].element] = blocks[i]; 560 } 561 } 562 563 /** 564 * Associates a list of blocks with an editable so that it can later be 565 * determined which blocks are contained inside which editable. 566 * 567 * @param object 568 */ 569 function associateBlocksWithEditable(editable, blocks) { 570 editable._gcnContainedBlocks = editable._gcnContainedBlocks 571 ? editable._gcnContainedBlocks.concat(blocks) 572 : blocks; 573 } 574 575 /** 576 * @private 577 * @const 578 * @type {number} 579 */ 580 //var TYPE_ID = 10007; 581 582 /** 583 * @private 584 * @const 585 * @type {Enum} 586 */ 587 var STATUS = { 588 589 // page was not found in the database 590 NOTFOUND: -1, 591 592 // page is locally modified and not yet (re-)published 593 MODIFIED: 0, 594 595 // page is marked to be published (dirty) 596 TOPUBLISH: 1, 597 598 // page is published and online 599 PUBLISHED: 2, 600 601 // Page is offline 602 OFFLINE: 3, 603 604 // Page is in the queue (publishing of the page needs to be affirmed) 605 QUEUE: 4, 606 607 // page is in timemanagement and outside of the defined timespan 608 // (currently offline) 609 TIMEMANAGEMENT: 5, 610 611 // page is to be published at a given time (not yet) 612 TOPUBLISH_AT: 6 613 }; 614 615 /** 616 * @class 617 * @name PageAPI 618 * @extends ContentObjectAPI 619 * @extends TagContainerAPI 620 * 621 * Page object information can be extended using the default REST-API. 622 * options: 623 * 624 * - update: true 625 * Whether the page should be locked in the backend when loading it. 626 * default: true 627 * 628 * - template: true 629 * Whether the template information should be embedded in the page object. 630 * default: true 631 * 632 * - folder: true, 633 * Whether the folder information should be embedded in the page object. 634 * default: true 635 * WARNING: do not turn this option off - it will leave the API in a broken 636 * state. 637 * 638 * - langvars: false, 639 * When the language variants shall be embedded in the page response. 640 * default: false 641 642 * - workflow: false, 643 * When the workflow information shall be embedded in the page response. 644 * default: false 645 646 * - pagevars: false, 647 * When the page variants shall be embedded in the page response. Page 648 * variants will contain folder information. 649 * default: false 650 * 651 * - translationstatus: false 652 * Will return information on the page's translation status. 653 * default: false 654 */ 655 var PageAPI = GCN.defineChainback({ 656 /** @lends PageAPI */ 657 658 __chainbacktype__: 'PageAPI', 659 _extends: [ GCN.ContentObjectAPI, GCN.TagContainerAPI ], 660 _type: 'page', 661 662 /** 663 * A hash set of block tags belonging to this page. This set grows as 664 * this page's tags are rendered. 665 * 666 * @private 667 * @type {Array.<object>} 668 */ 669 _blocks: {}, 670 671 /** 672 * A hash set of editable tags belonging to this page. This set grows 673 * as this page's tags are rendered. 674 * 675 * @private 676 * @type {Array.<object>} 677 */ 678 _editables: {}, 679 680 /** 681 * Writable properties for the page object. 682 * 683 * @type {Array.string} Writable properties for the page object. 684 * @const 685 */ 686 WRITEABLE_PROPS: ['cdate', 687 'description', 688 'fileName', 689 'folderId', // @TODO Check if moving a page is 690 // implemented correctly. 691 'name', 692 'priority', 693 'templateId'], 694 695 /** 696 * Gets all blocks that are associated with this page. 697 <<<<<<< HEAD 698 * 699 * It is important to note that the set of blocks in the returned array 700 * will only include those that are the returned by the server when 701 * calling edit() on a tag that belongs to this page. 702 * 703 ======= 704 * 705 * It is important to note that the set of blocks in the returned array 706 * will only include those that are the returned by the server when 707 * calling edit() on a tag that belongs to this page. 708 * 709 >>>>>>> c55e5259a789aa7d67b059a511259bb9651d9db8 710 * @return {Array.<object>} The set of blocks that have been 711 * initialized by calling edit() on one of 712 * this page's tags. 713 */ 714 '!blocks': function () { 715 return this._blocks; 716 }, 717 718 /** 719 * Retrieves a block with the given id among the blocks that are 720 * tracked by this page content object. 721 * 722 * @private 723 * @param {string} id The block's id. 724 * @return {?object} The block data object. 725 */ 726 '!_getBlockById': function (id) { 727 return this._blocks[id]; 728 }, 729 730 /** 731 * Extracts the editables and blocks that have been rendered from the 732 * REST API render call's response data, and stores them in the page. 733 * 734 * @override 735 */ 736 '!_processRenderedTags': function (data) { 737 var tags = this._getEditablesAndBlocks(data); 738 var containedBlocks = categorizeBlocksAgainstEditables( 739 data.content, 740 tags.editables, 741 tags.blocks 742 ); 743 744 trackEditables(this, tags.editables); 745 trackBlocks(this, tags.blocks); 746 747 var editableId; 748 for (editableId in containedBlocks) { 749 if (containedBlocks.hasOwnProperty(editableId) 750 && this._editables[editableId]) { 751 associateBlocksWithEditable(this._editables[editableId], 752 containedBlocks[editableId]); 753 } 754 } 755 756 return tags; 757 }, 758 759 /** 760 * Processes this page's tags in preparation for saving. 761 * 762 * The preparation process: 763 * 764 * 1. For all editables associated with this page, determine which of 765 * their blocks have been rendered into the DOM for editing so that 766 * changes to the DOM can be reflected in the corresponding data 767 * structures before pushing the tags to the server. 768 * 769 * 2. 770 * 771 * Processes rendered tags, and updates the `_blocks' and `_editables' 772 * arrays accordingly. This function is called during pre-saving to 773 * update this page's editable tags. 774 * 775 * @private 776 */ 777 '!_prepareTagsForSaving': function (success, error) { 778 if (!this.hasOwnProperty('_deletedBlocks')) { 779 this._deletedBlocks = []; 780 } 781 var page = this; 782 processGCNLinks(page, function () { 783 deleteObsoleteLinkTags(page, function () { 784 page._updateEditableBlocks(); 785 if (success) { 786 success(); 787 } 788 }, error); 789 }, error); 790 }, 791 792 /** 793 * Writes the contents of editables back into their corresponding tags. 794 * If a corresponding tag cannot be found for an editable, a new one 795 * will be created for it. 796 * 797 * A reference for each editable tag is then added to the `_shadow' 798 * object in order that the tag will be sent with the save request. 799 * 800 * @private 801 */ 802 '!_updateEditableBlocks': function () { 803 var $element; 804 var elementId; 805 var editable; 806 var editables = this._editables; 807 var tags = this._data.tags; 808 var tagname; 809 var html; 810 var alohaEditable; 811 var $cleanElement; 812 var customSerializer; 813 814 for (elementId in editables) { 815 if (editables.hasOwnProperty(elementId)) { 816 editable = editables[elementId]; 817 $element = jQuery('#' + elementId); 818 819 // If this editable has no element that was placed in the 820 // DOM, then do not attempt to update it. 821 if (0 === $element.length) { 822 continue; 823 } 824 825 tagname = editable.tagname; 826 827 if (!tags[tagname]) { 828 tags[tagname] = { 829 name : tagname, 830 active : true, 831 properties : {} 832 }; 833 } else { 834 // Make sure, that all tags (which relate to editables) 835 // are activated. 836 tags[tagname].active = true; 837 } 838 839 // If the editable element has been aloha()fied, then we 840 // need to use getContents() from is corresponding 841 // Aloha.Editable object in order to get clean HTML. 842 843 alohaEditable = getAlohaEditableById(elementId); 844 845 if (alohaEditable) { 846 // Avoid the unnecessary overhead of custom editable 847 // serialization by calling html ourselves. 848 $cleanElement = jQuery('<div>').append( 849 alohaEditable.getContents(true) 850 ); 851 alohaEditable.setUnmodified(); 852 // Apply the custom editable serialization as the last step. 853 customSerializer = window.Aloha.Editable.getContentSerializer(); 854 html = this.encode($cleanElement, customSerializer); 855 } else { 856 html = this.encode($element); 857 } 858 859 tags[tagname].properties[editable.partname] = { 860 stringValue: html, 861 type: 'RICHTEXT' 862 }; 863 864 this._update('tags.' + GCN.escapePropertyName(tagname), 865 tags[tagname]); 866 } 867 } 868 }, 869 870 /** 871 * @see ContentObjectAPI.!_loadParams 872 */ 873 '!_loadParams': function () { 874 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 875 }, 876 877 /** 878 * Get this page's template. 879 * 880 * @public 881 * @function 882 * @name template 883 * @memberOf PageAPI 884 * @param {funtion(TemplateAPI)=} success Optional callback to receive 885 * a {@link TemplateAPI} object 886 * as the only argument. 887 * @param {function(GCNError):boolean=} error Optional custom error 888 * handler. 889 * @return {TemplateAPI} This page's parent template. 890 */ 891 '!template': function (success, error) { 892 var id = this._fetched ? this.prop('templateId') : null; 893 return this._continue(GCN.TemplateAPI, id, success, error); 894 }, 895 896 /** 897 * @override 898 * @see ContentObjectAPI._save 899 */ 900 '!_save': function (settings, success, error) { 901 var that = this; 902 this._fulfill(function () { 903 that._read(function () { 904 var fork = that._fork(); 905 fork._prepareTagsForSaving(function () { 906 fork._persist(settings, function () { 907 if (success) { 908 that._invoke(success, [that]); 909 } 910 fork._merge(); 911 }, error); 912 }, error); 913 }, error); 914 }, error); 915 }, 916 917 //--------------------------------------------------------------------- 918 // Surface the tag container methods that are applicable for GCN page 919 // objects. 920 //--------------------------------------------------------------------- 921 922 /** 923 * Creates a tag of a given tagtype in this page. 924 * The first parameter should either be the construct keyword or ID, 925 * or an object containing exactly one of the following property sets:<br/> 926 * <ol> 927 * <li><i>keyword</i> to create a tag based on the construct with given keyword</li> 928 * <li><i>constructId</i> to create a tag based on the construct with given ID</li> 929 * <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li> 930 * </ol> 931 * 932 * Exmaple: 933 * <pre> 934 * createTag('link', onSuccess, onError); 935 * </pre> 936 * or 937 * <pre> 938 * createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError); 939 * </pre> 940 * or 941 * <pre> 942 * createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError); 943 * </pre> 944 * 945 * @public 946 * @function 947 * @name createTag 948 * @memberOf PageAPI 949 * @param {string|number|object} construct either the keyword of the construct, or the ID of the construct 950 * or an object with the following properties 951 * <ul> 952 * <li><i>keyword</i> keyword of the construct</li> 953 * <li><i>constructId</i> ID of the construct</li> 954 * <li><i>magicValue</i> magic value to be filled into the tag</li> 955 * <li><i>sourcePageId</i> source page id</li> 956 * <li><i>sourceTagname</i> source tag name</li> 957 * </ul> 958 * @param {function(TagAPI)=} success Optional callback that will 959 * receive the newly created tag as 960 * its only argument. 961 * @param {function(GCNError):boolean=} error Optional custom error 962 * handler. 963 * @return {TagAPI} The newly created tag. 964 * @throws INVALID_ARGUMENTS 965 */ 966 '!createTag': function () { 967 return this._createTag.apply(this, arguments); 968 }, 969 970 /** 971 * Deletes the specified tag from this page. 972 * 973 * @public 974 * @function 975 * @name removeTag 976 * @memberOf PageAPI 977 * @param {string} id The id of the tag to be deleted. 978 * @param {function(PageAPI)=} success Optional callback that receive 979 * this object as its only 980 * argument. 981 * @param {function(GCNError):boolean=} error Optional custom error 982 * handler. 983 */ 984 removeTag: function () { 985 this._removeTag.apply(this, arguments); 986 }, 987 988 /** 989 * Deletes a set of tags from this page. 990 * 991 * @public 992 * @function 993 * @name removeTags 994 * @memberOf PageAPI 995 * @param {Array.<string>} ids The ids of the set of tags to be 996 * deleted. 997 * @param {function(PageAPI)=} success Optional callback that receive 998 * this object as its only 999 * argument. 1000 * @param {function(GCNError):boolean=} error Optional custom error 1001 * handler. 1002 */ 1003 removeTags: function () { 1004 this._removeTags.apply(this, arguments); 1005 }, 1006 1007 /** 1008 * Marks the page as to be taken offline. This method will change the 1009 * state of the page object. 1010 * 1011 * @public 1012 * @function 1013 * @name takeOffline 1014 * @memberOf PageAPI 1015 * @param {funtion(PageAPI)=} success Optional callback to receive this 1016 * page object as the only argument. 1017 * @param {function(GCNError):boolean=} error Optional custom error 1018 * handler. 1019 */ 1020 takeOffline: function (success, error) { 1021 var that = this; 1022 1023 this._read(function () { 1024 that._update('status', STATUS.OFFLINE, error); 1025 if (success) { 1026 that._save(null, success, error); 1027 } 1028 }, error); 1029 }, 1030 1031 /** 1032 * Trigger publish process for the page. 1033 * 1034 * @public 1035 * @function 1036 * @name publish 1037 * @memberOf PageAPI 1038 * @param {funtion(PageAPI)=} success Optional callback to receive this 1039 * page object as the only argument. 1040 * @param {function(GCNError):boolean=} error Optional custom error 1041 * handler. 1042 */ 1043 publish: function (success, error) { 1044 var that = this; 1045 this._fulfill(function () { 1046 that._authAjax({ 1047 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1048 '/publish/' + that.id(), 1049 type: 'POST', 1050 json: {}, // There needs to be at least empty content 1051 // because of a bug in Jersey. 1052 success: function (response) { 1053 that._data.status = STATUS.PUBLISHED; 1054 if (success) { 1055 that._invoke(success, [that]); 1056 } 1057 }, 1058 error: error 1059 }); 1060 }); 1061 }, 1062 1063 /** 1064 * Renders a preview of the current page. 1065 * 1066 * @public 1067 * @function 1068 * @name preview 1069 * @memberOf PageAPI 1070 * @param {function(string, PageAPI)} success Callback to receive the 1071 * rendered page preview as 1072 * the first argument, and 1073 * this page object as the 1074 * second. 1075 * @param {function(GCNError):boolean=} error Optional custom error 1076 * handler. 1077 */ 1078 preview: function (success, error) { 1079 var that = this; 1080 1081 this._read(function () { 1082 that._authAjax({ 1083 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1084 '/preview/', 1085 json: { 1086 page: that._data // @FIXME Shouldn't this a be merge of 1087 // the `_shadow' object and the 1088 // `_data'. 1089 }, 1090 type: 'POST', 1091 error: error, 1092 success: function (response) { 1093 if (success) { 1094 GCN._handleContentRendered(response.preview, that, 1095 function (html) { 1096 that._invoke(success, [html, that]); 1097 }); 1098 } 1099 } 1100 }); 1101 }, error); 1102 }, 1103 1104 /** 1105 * Unlocks the page when finishing editing 1106 * 1107 * @public 1108 * @function 1109 * @name unlock 1110 * @memberOf PageAPI 1111 * @param {funtion(PageAPI)=} success Optional callback to receive this 1112 * page object as the only argument. 1113 * @param {function(GCNError):boolean=} error Optional custom error 1114 * handler. 1115 */ 1116 unlock: function (success, error) { 1117 var that = this; 1118 this._fulfill(function () { 1119 that._authAjax({ 1120 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1121 '/cancel/' + that.id(), 1122 type: 'POST', 1123 json: {}, // There needs to be at least empty content 1124 // because of a bug in Jersey. 1125 error: error, 1126 success: function (response) { 1127 if (success) { 1128 that._invoke(success, [that]); 1129 } 1130 } 1131 }); 1132 }); 1133 }, 1134 1135 /** 1136 * @see GCN.ContentObjectAPI._processResponse 1137 */ 1138 '!_processResponse': function (data) { 1139 jQuery.extend(this._data, data[this._type]); 1140 1141 // if data contains page variants turn them into page objects 1142 if (this._data.pageVariants) { 1143 var pagevars = []; 1144 var i; 1145 for (i = 0; i < this._data.pageVariants.length; i++) { 1146 pagevars.push(this._continue(GCN.PageAPI, 1147 this._data.pageVariants[i])); 1148 } 1149 this._data.pageVariants = pagevars; 1150 } 1151 }, 1152 1153 /** 1154 * @override 1155 */ 1156 '!_removeAssociatedTagData': function (tagid) { 1157 var block; 1158 for (block in this._blocks) { 1159 if (this._blocks.hasOwnProperty(block) && 1160 this._blocks[block].tagname === tagid) { 1161 delete this._blocks[block]; 1162 } 1163 } 1164 1165 var editable; 1166 for (editable in this._editables) { 1167 if (this._editables.hasOwnProperty(editable) && 1168 this._editables[editable].tagname === tagid) { 1169 delete this._editables[editable]; 1170 } 1171 } 1172 } 1173 1174 }); 1175 1176 GCN.page = GCN.exposeAPI(PageAPI); 1177 GCN.PageAPI = PageAPI; 1178 1179 }(GCN)); 1180