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 * @param {number|string} 623 * id of the page to be loaded 624 * @param {function(ContentObjectAPI))=} 625 * success Optional success callback that will receive this 626 * object as its only argument. 627 * @param {function(GCNError):boolean=} 628 * error Optional custom error handler. 629 * @param {object} 630 * settings additional settings for object creation. These 631 * correspond to options available from the REST-API and will 632 * extend or modify the PageAPI object. 633 * <dl> 634 * <dt>update: true</dt> 635 * <dd>Whether the page should be locked in the backend when 636 * loading it. default: true</dd> 637 * <dt>template: true</dt> 638 * <dd>Whether the template information should be embedded in 639 * the page object. default: true</dd> 640 * <dt>folder: true</dt> 641 * <dd>Whether the folder information should be embedded in the 642 * page object. default: true <b>WARNING</b>: do not turn this 643 * option off - it will leave the API in a broken state.</dd> 644 * <dt>langvars: false</dt> 645 * <dd>When the language variants shall be embedded in the page 646 * response. default: false</dd> 647 * <dt>workflow: false</dt> 648 * <dd>When the workflow information shall be embedded in the 649 * page response. default: false</dd> 650 * <dt>pagevars: false</dt> 651 * <dd>When the page variants shall be embedded in the page 652 * response. Page variants will contain folder information. 653 * default: false</dd> 654 * <dt>translationstatus: false</dt> 655 * <dd>Will return information on the page's translation status. 656 * default: false</dd> 657 * </dl> 658 */ 659 var PageAPI = GCN.defineChainback({ 660 /** @lends PageAPI */ 661 662 __chainbacktype__: 'PageAPI', 663 _extends: [ GCN.TagContainerAPI, GCN.ContentObjectAPI ], 664 _type: 'page', 665 666 /** 667 * A hash set of block tags belonging to this page. This set grows as 668 * this page's tags are rendered. 669 * 670 * @private 671 * @type {Array.<object>} 672 */ 673 _blocks: {}, 674 675 /** 676 * A hash set of editable tags belonging to this page. This set grows 677 * as this page's tags are rendered. 678 * 679 * @private 680 * @type {Array.<object>} 681 */ 682 _editables: {}, 683 684 /** 685 * Writable properties for the page object. Currently the following 686 * properties are writeable: cdate, description, fileName, folderId, 687 * name, priority, templateId. WARNING: changing the folderId might not 688 * work as expected. 689 * 690 * @type {Array.string} 691 * @const 692 */ 693 WRITEABLE_PROPS: ['cdate', 694 'description', 695 'fileName', 696 'folderId', // @TODO Check if moving a page is 697 // implemented correctly. 698 'name', 699 'priority', 700 'templateId'], 701 702 /** 703 * @type {object} Constraints for writeable props 704 * @const 705 * 706 */ 707 WRITEABLE_PROPS_CONSTRAINTS: { 708 'name': { 709 maxLength: 255 710 } 711 }, 712 713 /** 714 * Gets all blocks that are associated with this page. 715 * 716 * It is important to note that the set of blocks in the returned array 717 * will only include those that are the returned by the server when 718 * calling edit() on a tag that belongs to this page. 719 * 720 * @return {Array.<object>} The set of blocks that have been 721 * initialized by calling edit() on one of 722 * this page's tags. 723 */ 724 '!blocks': function () { 725 return this._blocks; 726 }, 727 728 /** 729 * Retrieves a block with the given id among the blocks that are 730 * tracked by this page content object. 731 * 732 * @private 733 * @param {string} id The block's id. 734 * @return {?object} The block data object. 735 */ 736 '!_getBlockById': function (id) { 737 return this._blocks[id]; 738 }, 739 740 /** 741 * Extracts the editables and blocks that have been rendered from the 742 * REST API render call's response data, and stores them in the page. 743 * 744 * @override 745 */ 746 '!_processRenderedTags': function (data) { 747 var tags = this._getEditablesAndBlocks(data); 748 var containedBlocks = categorizeBlocksAgainstEditables( 749 data.content, 750 tags.editables, 751 tags.blocks 752 ); 753 754 trackEditables(this, tags.editables); 755 trackBlocks(this, tags.blocks); 756 757 var editableId; 758 for (editableId in containedBlocks) { 759 if (containedBlocks.hasOwnProperty(editableId) 760 && this._editables[editableId]) { 761 associateBlocksWithEditable(this._editables[editableId], 762 containedBlocks[editableId]); 763 } 764 } 765 766 return tags; 767 }, 768 769 /** 770 * Processes this page's tags in preparation for saving. 771 * 772 * The preparation process: 773 * 774 * 1. For all editables associated with this page, determine which of 775 * their blocks have been rendered into the DOM for editing so that 776 * changes to the DOM can be reflected in the corresponding data 777 * structures before pushing the tags to the server. 778 * 779 * 2. 780 * 781 * Processes rendered tags, and updates the `_blocks' and `_editables' 782 * arrays accordingly. This function is called during pre-saving to 783 * update this page's editable tags. 784 * 785 * @private 786 */ 787 '!_prepareTagsForSaving': function (success, error) { 788 if (!this.hasOwnProperty('_deletedBlocks')) { 789 this._deletedBlocks = []; 790 } 791 var page = this; 792 processGCNLinks(page, function () { 793 deleteObsoleteLinkTags(page, function () { 794 page._updateEditableBlocks(); 795 if (success) { 796 success(); 797 } 798 }, error); 799 }, error); 800 }, 801 802 /** 803 * Writes the contents of editables back into their corresponding tags. 804 * If a corresponding tag cannot be found for an editable, a new one 805 * will be created for it. 806 * 807 * A reference for each editable tag is then added to the `_shadow' 808 * object in order that the tag will be sent with the save request. 809 * 810 * @private 811 */ 812 '!_updateEditableBlocks': function () { 813 var $element; 814 var elementId; 815 var editable; 816 var editables = this._editables; 817 var tags = this._data.tags; 818 var tagname; 819 var html; 820 var alohaEditable; 821 var $cleanElement; 822 var customSerializer; 823 824 for (elementId in editables) { 825 if (editables.hasOwnProperty(elementId)) { 826 editable = editables[elementId]; 827 $element = jQuery('#' + elementId); 828 829 // If this editable has no element that was placed in the 830 // DOM, then do not attempt to update it. 831 if (0 === $element.length) { 832 continue; 833 } 834 835 tagname = editable.tagname; 836 837 if (!tags[tagname]) { 838 tags[tagname] = { 839 name : tagname, 840 active : true, 841 properties : {} 842 }; 843 } else { 844 // Make sure, that all tags (which relate to editables) 845 // are activated. 846 tags[tagname].active = true; 847 } 848 849 // If the editable element has been aloha()fied, then we 850 // need to use getContents() from is corresponding 851 // Aloha.Editable object in order to get clean HTML. 852 853 alohaEditable = getAlohaEditableById(elementId); 854 855 if (alohaEditable) { 856 // Avoid the unnecessary overhead of custom editable 857 // serialization by calling html ourselves. 858 $cleanElement = jQuery('<div>').append( 859 alohaEditable.getContents(true) 860 ); 861 alohaEditable.setUnmodified(); 862 // Apply the custom editable serialization as the last step. 863 customSerializer = window.Aloha.Editable.getContentSerializer(); 864 html = this.encode($cleanElement, customSerializer); 865 } else { 866 html = this.encode($element); 867 } 868 869 tags[tagname].properties[editable.partname] = { 870 stringValue: html, 871 type: 'RICHTEXT' 872 }; 873 874 this._update('tags.' + GCN.escapePropertyName(tagname), 875 tags[tagname]); 876 } 877 } 878 }, 879 880 /** 881 * @see ContentObjectAPI.!_loadParams 882 */ 883 '!_loadParams': function () { 884 return jQuery.extend(DEFAULT_SETTINGS, this._settings); 885 }, 886 887 /** 888 * Get this page's template. 889 * 890 * @public 891 * @function 892 * @name template 893 * @memberOf PageAPI 894 * @param {funtion(TemplateAPI)=} success Optional callback to receive 895 * a {@link TemplateAPI} object 896 * as the only argument. 897 * @param {function(GCNError):boolean=} error Optional custom error 898 * handler. 899 * @return {TemplateAPI} This page's parent template. 900 */ 901 '!template': function (success, error) { 902 var id = this._fetched ? this.prop('templateId') : null; 903 return this._continue(GCN.TemplateAPI, id, success, error); 904 }, 905 906 /** 907 * @override 908 * @see ContentObjectAPI._save 909 */ 910 '!_save': function (settings, success, error) { 911 var that = this; 912 this._fulfill(function () { 913 that._read(function () { 914 var fork = that._fork(); 915 fork._prepareTagsForSaving(function () { 916 fork._persist(settings, function () { 917 if (success) { 918 that._invoke(success, [that]); 919 } 920 fork._merge(); 921 }, error); 922 }, error); 923 }, error); 924 }, error); 925 }, 926 927 //--------------------------------------------------------------------- 928 // Surface the tag container methods that are applicable for GCN page 929 // objects. 930 //--------------------------------------------------------------------- 931 932 /** 933 * Creates a tag of a given tagtype in this page. 934 * The first parameter should either be the construct keyword or ID, 935 * or an object containing exactly one of the following property sets:<br/> 936 * <ol> 937 * <li><i>keyword</i> to create a tag based on the construct with given keyword</li> 938 * <li><i>constructId</i> to create a tag based on the construct with given ID</li> 939 * <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li> 940 * </ol> 941 * 942 * Exmaple: 943 * <pre> 944 * createTag('link', onSuccess, onError); 945 * </pre> 946 * or 947 * <pre> 948 * createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError); 949 * </pre> 950 * or 951 * <pre> 952 * createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError); 953 * </pre> 954 * 955 * @public 956 * @function 957 * @name createTag 958 * @memberOf PageAPI 959 * @param {string|number|object} construct either the keyword of the 960 * construct, or the ID of the construct 961 * or an object with the following 962 * properties 963 * <ul> 964 * <li><i>keyword</i> keyword of the construct</li> 965 * <li><i>constructId</i> ID of the construct</li> 966 * <li><i>magicValue</i> magic value to be filled into the tag</li> 967 * <li><i>sourcePageId</i> source page id</li> 968 * <li><i>sourceTagname</i> source tag name</li> 969 * </ul> 970 * @param {function(TagAPI)=} success Optional callback that will 971 * receive the newly created tag as 972 * its only argument. 973 * @param {function(GCNError):boolean=} error Optional custom error 974 * handler. 975 * @return {TagAPI} The newly created tag. 976 * @throws INVALID_ARGUMENTS 977 */ 978 '!createTag': function () { 979 return this._createTag.apply(this, arguments); 980 }, 981 982 /** 983 * Deletes the specified tag from this page. 984 * 985 * @public 986 * @function 987 * @memberOf PageAPI 988 * @param {string} 989 * id The id of the tag to be deleted. 990 * @param {function(PageAPI)=} 991 * success Optional callback that receive this object as its 992 * only argument. 993 * @param {function(GCNError):boolean=} 994 * error Optional custom error handler. 995 */ 996 removeTag: function () { 997 this._removeTag.apply(this, arguments); 998 }, 999 1000 /** 1001 * Deletes a set of tags from this page. 1002 * 1003 * @public 1004 * @function 1005 * @memberOf PageAPI 1006 * @param {Array. 1007 * <string>} ids The ids of the set of tags to be deleted. 1008 * @param {function(PageAPI)=} 1009 * success Optional callback that receive this object as its 1010 * only argument. 1011 * @param {function(GCNError):boolean=} 1012 * error Optional custom error handler. 1013 */ 1014 removeTags: function () { 1015 this._removeTags.apply(this, arguments); 1016 }, 1017 1018 /** 1019 * Marks the page as to be taken offline. This method will change the 1020 * state of the page object. 1021 * 1022 * @public 1023 * @function 1024 * @memberOf PageAPI 1025 * @param {funtion(PageAPI)=} success Optional callback to receive this 1026 * page object as the only argument. 1027 * @param {function(GCNError):boolean=} error Optional custom error 1028 * handler. 1029 */ 1030 takeOffline: function (success, error) { 1031 var that = this; 1032 1033 this._read(function () { 1034 that._update('status', STATUS.OFFLINE, error); 1035 if (success) { 1036 that._save(null, success, error); 1037 } 1038 }, error); 1039 }, 1040 1041 /** 1042 * Trigger publish process for the page. 1043 * 1044 * @public 1045 * @function 1046 * @memberOf PageAPI 1047 * @param {funtion(PageAPI)=} success Optional callback to receive this 1048 * page object as the only argument. 1049 * @param {function(GCNError):boolean=} error Optional custom error 1050 * handler. 1051 */ 1052 publish: function (success, error) { 1053 var that = this; 1054 this._fulfill(function () { 1055 that._authAjax({ 1056 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1057 '/publish/' + that.id(), 1058 type: 'POST', 1059 json: {}, // There needs to be at least empty content 1060 // because of a bug in Jersey. 1061 success: function (response) { 1062 that._data.status = STATUS.PUBLISHED; 1063 if (success) { 1064 that._invoke(success, [that]); 1065 } 1066 }, 1067 error: error 1068 }); 1069 }); 1070 }, 1071 1072 /** 1073 * Renders a preview of the current page. 1074 * 1075 * @public 1076 * @function 1077 * @memberOf PageAPI 1078 * @param {function(string, 1079 * PageAPI)} success Callback to receive the rendered page 1080 * preview as the first argument, and this page object as the 1081 * second. 1082 * @param {function(GCNError):boolean=} 1083 * error Optional custom error handler. 1084 */ 1085 preview: function (success, error) { 1086 var that = this; 1087 1088 this._read(function () { 1089 that._authAjax({ 1090 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1091 '/preview/', 1092 json: { 1093 page: that._data // @FIXME Shouldn't this a be merge of 1094 // the `_shadow' object and the 1095 // `_data'. 1096 }, 1097 type: 'POST', 1098 error: error, 1099 success: function (response) { 1100 if (success) { 1101 GCN._handleContentRendered(response.preview, that, 1102 function (html) { 1103 that._invoke(success, [html, that]); 1104 }); 1105 } 1106 } 1107 }); 1108 }, error); 1109 }, 1110 1111 /** 1112 * Unlocks the page when finishing editing 1113 * 1114 * @public 1115 * @function 1116 * @memberOf PageAPI 1117 * @param {funtion(PageAPI)=} 1118 * success Optional callback to receive this page object as 1119 * the only argument. 1120 * @param {function(GCNError):boolean=} 1121 * error Optional custom error handler. 1122 */ 1123 unlock: function (success, error) { 1124 var that = this; 1125 this._fulfill(function () { 1126 that._authAjax({ 1127 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1128 '/cancel/' + that.id(), 1129 type: 'POST', 1130 json: {}, // There needs to be at least empty content 1131 // because of a bug in Jersey. 1132 error: error, 1133 success: function (response) { 1134 if (success) { 1135 that._invoke(success, [that]); 1136 } 1137 } 1138 }); 1139 }); 1140 }, 1141 1142 /** 1143 * @see GCN.ContentObjectAPI._processResponse 1144 */ 1145 '!_processResponse': function (data) { 1146 jQuery.extend(this._data, data[this._type]); 1147 1148 // if data contains page variants turn them into page objects 1149 if (this._data.pageVariants) { 1150 var pagevars = []; 1151 var i; 1152 for (i = 0; i < this._data.pageVariants.length; i++) { 1153 pagevars.push(this._continue(GCN.PageAPI, 1154 this._data.pageVariants[i])); 1155 } 1156 this._data.pageVariants = pagevars; 1157 } 1158 }, 1159 1160 /** 1161 * @override 1162 */ 1163 '!_removeAssociatedTagData': function (tagid) { 1164 var block; 1165 for (block in this._blocks) { 1166 if (this._blocks.hasOwnProperty(block) && 1167 this._blocks[block].tagname === tagid) { 1168 delete this._blocks[block]; 1169 } 1170 } 1171 1172 var editable; 1173 for (editable in this._editables) { 1174 if (this._editables.hasOwnProperty(editable) && 1175 this._editables[editable].tagname === tagid) { 1176 delete this._editables[editable]; 1177 } 1178 } 1179 } 1180 1181 }); 1182 1183 /** 1184 * Creates a new instance of PageAPI. See the {@link PageAPI} constructor for detailed information. 1185 * 1186 * @function 1187 * @name page 1188 * @memberOf GCN 1189 * @see PageAPI 1190 */ 1191 GCN.page = GCN.exposeAPI(PageAPI); 1192 GCN.PageAPI = PageAPI; 1193 1194 }(GCN)); 1195