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