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