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