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