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