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