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