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