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