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