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