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