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