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 * Retrieve the list of constructs of the tag that are used in this 935 * page. 936 * 937 * Note that tags that have been created on this page locally, but have 938 * yet to be persisted to the server (unsaved tags), will not have their 939 * constructs included in the list unless their constructs are used by 940 * other saved tags. 941 */ 942 '!constructs': function (success, error) { 943 var page = this; 944 if (page._constructs) { 945 return success(page._constructs); 946 } 947 page._authAjax({ 948 url: GCN.settings.BACKEND_PATH 949 + '/rest/construct/list.json?pageId=' + page.id(), 950 type: 'GET', 951 error: function (xhr, status, msg) { 952 GCN.handleHttpError(xhr, msg, error); 953 }, 954 success: function (response) { 955 if (GCN.getResponseCode(response) === 'OK') { 956 page._constructs = GCN.mapConstructs(response.constructs); 957 page._invoke(success, [page._constructs]); 958 } else { 959 GCN.handleResponseError(response, error); 960 } 961 } 962 }); 963 }, 964 965 /** 966 * @override 967 * @see ContentObjectAPI._save 968 */ 969 '!_save': function (settings, success, error) { 970 var page = this; 971 this._fulfill(function () { 972 page._read(function () { 973 var fork = page._fork(); 974 fork._prepareTagsForSaving(function () { 975 GCN.pub('page.before-saved', fork); 976 fork._persist(settings, function () { 977 if (success) { 978 page._constructs = null; 979 fork._merge(false); 980 page._invoke(success, [page]); 981 page._vacate(); 982 } else { 983 fork._merge(); 984 } 985 }, error); 986 }, error); 987 }, error); 988 }, error); 989 }, 990 991 //--------------------------------------------------------------------- 992 // Surface the tag container methods that are applicable for GCN page 993 // objects. 994 //--------------------------------------------------------------------- 995 996 /** 997 * Creates a tag of a given tagtype in this page. 998 * The first parameter should either be the construct keyword or ID, 999 * or an object containing exactly one of the following property sets:<br/> 1000 * <ol> 1001 * <li><i>keyword</i> to create a tag based on the construct with given keyword</li> 1002 * <li><i>constructId</i> to create a tag based on the construct with given ID</li> 1003 * <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li> 1004 * </ol> 1005 * 1006 * Exmaple: 1007 * <pre> 1008 * createTag('link', onSuccess, onError); 1009 * </pre> 1010 * or 1011 * <pre> 1012 * createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError); 1013 * </pre> 1014 * or 1015 * <pre> 1016 * createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError); 1017 * </pre> 1018 * 1019 * @public 1020 * @function 1021 * @name createTag 1022 * @memberOf PageAPI 1023 * @param {string|number|object} construct either the keyword of the 1024 * construct, or the ID of the construct 1025 * or an object with the following 1026 * properties 1027 * <ul> 1028 * <li><i>keyword</i> keyword of the construct</li> 1029 * <li><i>constructId</i> ID of the construct</li> 1030 * <li><i>magicValue</i> magic value to be filled into the tag</li> 1031 * <li><i>sourcePageId</i> source page id</li> 1032 * <li><i>sourceTagname</i> source tag name</li> 1033 * </ul> 1034 * @param {function(TagAPI)=} success Optional callback that will 1035 * receive the newly created tag as 1036 * its only argument. 1037 * @param {function(GCNError):boolean=} error Optional custom error 1038 * handler. 1039 * @return {TagAPI} The newly created tag. 1040 * @throws INVALID_ARGUMENTS 1041 */ 1042 '!createTag': function () { 1043 return this._createTag.apply(this, arguments); 1044 }, 1045 1046 /** 1047 * Deletes the specified tag from this page. 1048 * You should pass a keyword here not an Id. 1049 * 1050 * Note: Due to how the underlying RestAPI layer works, 1051 * the success callback will also be called if the specified tag 1052 * does not exist. 1053 * 1054 * @public 1055 * @function 1056 * @memberOf PageAPI 1057 * @param {string} 1058 * keyword The keyword of the tag to be deleted. 1059 * @param {function(PageAPI)=} 1060 * success Optional callback that receive this object as its 1061 * only argument. 1062 * @param {function(GCNError):boolean=} 1063 * error Optional custom error handler. 1064 */ 1065 removeTag: function () { 1066 this._removeTag.apply(this, arguments); 1067 }, 1068 1069 /** 1070 * Deletes a set of tags from this page. 1071 * 1072 * @public 1073 * @function 1074 * @memberOf PageAPI 1075 * @param {Array. 1076 * <string>} keywords The keywords of the tags 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 removeTags: function () { 1084 this._removeTags.apply(this, arguments); 1085 }, 1086 1087 /** 1088 * Takes the page offline. 1089 * If instant publishing is enabled, this will take the page offline 1090 * immediately. Otherwise it will be taken offline during the next 1091 * publish run. 1092 * 1093 * @public 1094 * @function 1095 * @memberOf PageAPI 1096 * @param {funtion(PageAPI)=} success Optional callback to receive this 1097 * page object as the only argument. 1098 * @param {function(GCNError):boolean=} error Optional custom error 1099 * handler. 1100 */ 1101 takeOffline: function (success, error) { 1102 var page = this; 1103 page._fulfill(function () { 1104 page._authAjax({ 1105 url: GCN.settings.BACKEND_PATH + '/rest/' + page._type + 1106 '/takeOffline/' + page.id(), 1107 type: 'POST', 1108 json: {}, // There needs to be at least empty content 1109 // because of a bug in Jersey. 1110 error: error, 1111 success: function (response) { 1112 if (success) { 1113 page._invoke(success, [page]); 1114 } 1115 } 1116 }); 1117 }); 1118 }, 1119 1120 /** 1121 * Trigger publish process for the page. 1122 * 1123 * @public 1124 * @function 1125 * @memberOf PageAPI 1126 * @param {funtion(PageAPI)=} success Optional callback to receive this 1127 * page object as the only argument. 1128 * @param {function(GCNError):boolean=} error Optional custom error 1129 * handler. 1130 */ 1131 publish: function (success, error) { 1132 var page = this; 1133 GCN.pub('page.before-publish', page); 1134 this._fulfill(function () { 1135 page._authAjax({ 1136 url: GCN.settings.BACKEND_PATH + '/rest/' + page._type + 1137 '/publish/' + page.id() + GCN._getChannelParameter(page), 1138 type: 'POST', 1139 json: {}, // There needs to be at least empty content 1140 // because of a bug in Jersey. 1141 success: function (response) { 1142 page._data.status = STATUS.PUBLISHED; 1143 if (success) { 1144 page._invoke(success, [page]); 1145 } 1146 }, 1147 error: error 1148 }); 1149 }); 1150 }, 1151 1152 /** 1153 * Renders a preview of the current page. 1154 * 1155 * @public 1156 * @function 1157 * @memberOf PageAPI 1158 * @param {function(string, 1159 * PageAPI)} success Callback to receive the rendered page 1160 * preview as the first argument, and this page object as the 1161 * second. 1162 * @param {function(GCNError):boolean=} 1163 * error Optional custom error handler. 1164 */ 1165 preview: function (success, error) { 1166 var that = this; 1167 1168 this._read(function () { 1169 that._authAjax({ 1170 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1171 '/preview/' + GCN._getChannelParameter(that), 1172 json: { 1173 page: that._data, // @FIXME Shouldn't this a be merge of 1174 // the `_shadow' object and the 1175 // `_data'. 1176 nodeId: that.nodeId() 1177 }, 1178 type: 'POST', 1179 error: error, 1180 success: function (response) { 1181 if (success) { 1182 GCN._handleContentRendered(response.preview, that, 1183 function (html) { 1184 that._invoke(success, [html, that]); 1185 }); 1186 } 1187 } 1188 }); 1189 }, error); 1190 }, 1191 1192 /** 1193 * Unlocks the page when finishing editing 1194 * 1195 * @public 1196 * @function 1197 * @memberOf PageAPI 1198 * @param {funtion(PageAPI)=} 1199 * success Optional callback to receive this page object as 1200 * the only argument. 1201 * @param {function(GCNError):boolean=} 1202 * error Optional custom error handler. 1203 */ 1204 unlock: function (success, error) { 1205 var that = this; 1206 this._fulfill(function () { 1207 that._authAjax({ 1208 url: GCN.settings.BACKEND_PATH + '/rest/' + that._type + 1209 '/cancel/' + that.id() + GCN._getChannelParameter(that), 1210 type: 'POST', 1211 json: {}, // There needs to be at least empty content 1212 // because of a bug in Jersey. 1213 error: error, 1214 success: function (response) { 1215 if (success) { 1216 that._invoke(success, [that]); 1217 } 1218 } 1219 }); 1220 }); 1221 }, 1222 1223 /** 1224 * @see GCN.ContentObjectAPI._processResponse 1225 */ 1226 '!_processResponse': function (data) { 1227 jQuery.extend(this._data, data[this._type]); 1228 1229 // if data contains page variants turn them into page objects 1230 if (this._data.pageVariants) { 1231 var pagevars = []; 1232 var i; 1233 for (i = 0; i < this._data.pageVariants.length; i++) { 1234 pagevars.push(this._continue(GCN.PageAPI, 1235 this._data.pageVariants[i])); 1236 } 1237 this._data.pageVariants = pagevars; 1238 } 1239 }, 1240 1241 /** 1242 * @override 1243 */ 1244 '!_removeAssociatedTagData': function (tagid) { 1245 var block; 1246 for (block in this._blocks) { 1247 if (this._blocks.hasOwnProperty(block) && 1248 this._blocks[block].tagname === tagid) { 1249 delete this._blocks[block]; 1250 } 1251 } 1252 1253 var editable, containedBlocks, i; 1254 for (editable in this._editables) { 1255 if (this._editables.hasOwnProperty(editable)) { 1256 if (this._editables[editable].tagname === tagid) { 1257 delete this._editables[editable]; 1258 } else { 1259 containedBlocks = this._editables[editable]._gcnContainedBlocks; 1260 if (jQuery.isArray(containedBlocks)) { 1261 for (i = containedBlocks.length -1; i >= 0; i--) { 1262 if (containedBlocks[i].tagname === tagid) { 1263 containedBlocks.splice(i, 1); 1264 } 1265 } 1266 } 1267 } 1268 } 1269 } 1270 } 1271 1272 }); 1273 1274 /** 1275 * Creates a new instance of PageAPI. 1276 * See the {@link PageAPI} constructor for detailed information. 1277 * 1278 * @function 1279 * @name page 1280 * @memberOf GCN 1281 * @see PageAPI 1282 */ 1283 GCN.page = GCN.exposeAPI(PageAPI); 1284 GCN.PageAPI = PageAPI; 1285 1286 GCN.PageAPI.trackRenderedTags = trackRenderedTags; 1287 1288 }(GCN)); 1289