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