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