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