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