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