1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * Updates the internal data of the given content object. 7 * 8 * This function extends and overwrites properties of the instances's 9 * internal data structure. No property is deleted on account of being 10 * absent from the given `props' object. 11 * 12 * @param {ContentObjectAPI} obj An instance whose internal data is to be 13 * reset. 14 * @param {object} props The properties with which to replace the internal 15 * data of the given chainback instance. 16 */ 17 function update(obj, props) { 18 jQuery.extend(obj._data, props); 19 } 20 21 /** 22 * The prefix that will be temporarily applied to block tags during an 23 * encode() process. 24 * 25 * @type {string} 26 * @const 27 */ 28 var BLOCK_ENCODING_PREFIX = 'GCN_BLOCK_TMP__'; 29 30 /** 31 * Will match <span id="GENTICS_block_123"></span>" but not "<node abc123>" 32 * tags. The first backreference contains the tagname of the tag 33 * corresponding to this block. 34 * 35 * Limitation: Will not work with unicode characters. 36 * 37 * @type {RexExp} 38 * @const 39 */ 40 var CONTENT_BLOCK = new RegExp( 41 // "<span" or "<div" but not "<node" 42 '<(?!node)[a-z]+' + 43 // "class=... data-*..." 44 '(?:\\s+[^/<>\\s=]+(?:=(?:"[^"]*"|\'[^\']*\'|[^>/\\s]+))?)*?' + 45 // " id = " 46 '\\s+id\\s*=\\s*["\']?' + 47 // "GCN_BLOCK_TMP__" 48 BLOCK_ENCODING_PREFIX + 49 // "_abc-123" 50 '([^"\'/<>\\s=]*)["\']?' + 51 // class=... data-*... 52 '(?:\\s+[^/<>\\s=]+(?:=(?:"[^"]*"|\'[^\']*\'|[^>/\\s]+))?)*' + 53 // "' ...></span>" or "</div>" 54 '\\s*></[a-z]+>', 55 'gi' 56 ); 57 58 /** 59 * Will match <node foo> or <node bar_123> or <node foo-bar> but not 60 * <node "blah">. 61 * 62 * @type {RegExp} 63 * @const 64 */ 65 var NODE_NOTATION = /<node ([a-z0-9_\-]+?)>/gim; 66 67 /** 68 * Examines a string for "<node>" tags, and for each occurance of this 69 * notation, the given callback will be invoked to manipulate the string. 70 * 71 * @private 72 * @static 73 * @param {string} str The string that will be examined for "<node>" tags. 74 * @param {function} onMatchFound Callback function that should receive the 75 * following three parameters: 76 * 77 * name:string The name of the tag being notated by the 78 * node substring. If the `str' arguments 79 * is "<node myTag>", then the `name' value 80 * will be "myTag". 81 * offset:number The offset where the node substring was 82 * found within the examined string. 83 * str:string The string in which the "<node *>" 84 * substring occured. 85 * 86 * The return value of the function will 87 * replace the entire "<node>" substring 88 * that was passed to it within the examined 89 * string. 90 */ 91 function replaceNodeTags(str, onMatchFound) { 92 var parsed = str.replace(NODE_NOTATION, function (substr, tagname, 93 offset, examined) { 94 return onMatchFound(tagname, offset, examined); 95 }); 96 return parsed; 97 } 98 99 /* 100 * have a look at _init 101 */ 102 GCN.ContentObjectAPI = GCN.defineChainback({ 103 /** @lends ContentObjectAPI */ 104 105 /** 106 * @private 107 * @type {string} A string denoting a content node type. This value is 108 * used to compose the correct REST API ajax urls. The 109 * following are valid values: "node", "folder", 110 * "template", "page", "file", "image". 111 */ 112 _type: null, 113 114 /** 115 * @private 116 * @type {object<string,*>} An internal object to store data that we 117 * get from the server. 118 */ 119 _data: {}, 120 121 /** 122 * @private 123 * @type {object<string,*>} An internal object to store updates to 124 * the content object. Should reflect the 125 * structural typography of the `_data' 126 * object. 127 */ 128 _shadow: {}, 129 130 /** 131 * @type {boolean} Flags whether or not data for this content object have 132 * been fetched from the server. 133 */ 134 _fetched: false, 135 136 /** 137 * @private 138 * @type {object} will contain an objects internal settings 139 */ 140 _settings: null, 141 142 /** 143 * An array of all properties of an object that can be changed by the 144 * user. Writeable properties for all content objects. 145 * 146 * @public 147 * @type {Array.string} 148 */ 149 WRITEABLE_PROPS: [], 150 151 /** 152 * <p>This object can contain various contrains for writeable props. 153 * Those contrains will be checked when the user tries to set/save a 154 * property. Currently only maxLength is beeing handled.</p> 155 * 156 * <p>Example:</p> 157 * <pre>WRITEABLE_PROPS_CONSTRAINTS: { 158 * 'name': { 159 * maxLength: 255 160 * } 161 * }</pre> 162 * @type {object} 163 * @const 164 * 165 */ 166 WRITEABLE_PROPS_CONSTRAINTS: {}, 167 168 /** 169 * Fetches this content object's data from the backend. 170 * 171 * @ignore 172 * @param {function(object)} success A function to receive the server 173 * response. 174 * @param {function(GCNError):boolean} error Optional custrom error 175 * handler. 176 */ 177 '!fetch': function (success, error, stack) { 178 var obj = this; 179 var ajax = function () { 180 obj._authAjax({ 181 url: GCN.settings.BACKEND_PATH + '/rest/' + obj._type + 182 '/load/' + obj.id() + GCN._getChannelParameter(obj), 183 data: obj._loadParams(), 184 error: error, 185 success: success 186 }); 187 }; 188 189 // If this chainback object has an ancestor, then invoke that 190 // parent's `_read()' method before fetching the data for this 191 // chainback object. 192 if (obj._chain) { 193 var circularReference = 194 stack && -1 < jQuery.inArray(obj._chain, stack); 195 if (!circularReference) { 196 stack = stack || []; 197 stack.push(obj._chain); 198 obj._chain._read(ajax, error, stack); 199 return; 200 } 201 } 202 203 ajax(); 204 }, 205 206 /** 207 * Internal method, to fetch this object's data from the server. 208 * 209 * @ignore 210 * @private 211 * @param {function(ContentObjectAPI)=} success Optional callback that 212 * receives this object as 213 * its only argument. 214 * @param {function(GCNError):boolean=} error Optional customer error 215 * handler. 216 */ 217 '!_read': function (success, error, stack) { 218 var obj = this; 219 if (obj._fetched) { 220 if (success) { 221 obj._invoke(success, [obj]); 222 } 223 return; 224 } 225 226 if (obj.multichannelling) { 227 obj.multichannelling.read(obj, success, error); 228 return; 229 } 230 231 var id = obj.id(); 232 233 if (null === id || undefined === id) { 234 obj._getIdFromParent(function () { 235 obj._read(success, error, stack); 236 }, error, stack); 237 return; 238 } 239 240 obj.fetch(function (response) { 241 obj._processResponse(response); 242 obj._fetched = true; 243 if (success) { 244 obj._invoke(success, [obj]); 245 } 246 }, error, stack); 247 }, 248 249 /** 250 * Retrieves this object's id from its parent. This function is used 251 * in order for this object to be able to fetch its data from the 252 * backend. 253 * 254 * @ignore 255 * @private 256 * @param {function(ContentObjectAPI)=} success Optional callback that 257 * receives this object as 258 * its only argument. 259 * @param {function(GCNError):boolean=} error Optional customer error 260 * handler. 261 * @throws CANNOT_GET_OBJECT_ID 262 */ 263 '!_getIdFromParent': function (success, error, stack) { 264 var parent = this._ancestor(); 265 266 if (!parent) { 267 var err = GCN.createError('CANNOT_GET_OBJECT_ID', 268 'Cannot get an id for object', this); 269 GCN.handleError(err, error); 270 return; 271 } 272 273 var that = this; 274 275 parent._read(function () { 276 if ('folder' === that._type) { 277 // There are 3 possible property names that an object can 278 // use to hold the id of the folder that it is related to: 279 // 280 // "folderId": for pages, templates, files, and images. 281 // "motherId": for folders 282 // "nodeId": for nodes 283 // 284 // We need to see which of this properties is set, the 285 // first one we find will be our folder's id. 286 var props = ['folderId', 'motherId', 'nodeId']; 287 var prop = props.pop(); 288 var id; 289 290 while (prop) { 291 id = parent.prop(prop); 292 if (typeof id !== 'undefined') { 293 break; 294 } 295 prop = props.pop(); 296 } 297 298 that._data.id = id; 299 } else { 300 that._data.id = parent.prop(that._type + 'Id'); 301 } 302 303 if (that._data.id === null || typeof that._data.id === 'undefined') { 304 var err = GCN.createError('CANNOT_GET_OBJECT_ID', 305 'Cannot get an id for object', this); 306 GCN.handleError(err, error); 307 return; 308 } 309 310 that._setHash(that._data.id)._addToCache(); 311 312 if (success) { 313 success(); 314 } 315 }, error, stack); 316 }, 317 318 /** 319 * Gets this object's node id. If used in a multichannelling is enabled 320 * it will return the channel id or 0 if no channel was set. 321 * 322 * @public 323 * @function 324 * @name nodeId 325 * @memberOf ContentObjectAPI 326 * @return {number} The channel to which this object is set. 0 if no 327 * channel is set. 328 */ 329 '!nodeId': function () { 330 return this._channel || 0; 331 }, 332 333 /** 334 * Gets this object's id. We'll return the id of the object when it has 335 * been loaded - this can only be a localid. Otherwise we'll return the 336 * id which was provided by the user. This can either be a localid or a 337 * globalid. 338 * 339 * @name id 340 * @function 341 * @memberOf ContentObjectAPI 342 * @public 343 * @return {number} 344 */ 345 '!id': function () { 346 return this._data.id; 347 }, 348 349 /** 350 * Alias for {@link ContentObjectAPI#id} 351 * 352 * @name localId 353 * @function 354 * @memberOf ContentObjectAPI 355 * @private 356 * @return {number} 357 * @decprecated 358 */ 359 '!localId': function () { 360 return this.id(); 361 }, 362 363 /** 364 * Update the `_shadow' object that maintains changes to properties 365 * that reflected the internal `_data' object. This shadow object is 366 * used to persist differential changes to a REST API object. 367 * 368 * @ignore 369 * @private 370 * @param {string} path The path through the object to the property we 371 * want to modify if a node in the path contains 372 * dots, then these dots should be escaped. This 373 * can be done using the GCN.escapePropertyName() 374 * convenience function. 375 * @param {*} value The value we wish to set the property to. 376 * @param {function=} error Custom error handler. 377 * @param {boolean=} force If true, no error will be thrown if `path' 378 * cannot be fully resolved against the 379 * internal `_data' object, instead, the path 380 * will be created on the shadow object. 381 */ 382 '!_update': function (pathStr, value, error, force) { 383 var boundary = Math.random().toString(8).substring(2); 384 var path = pathStr.replace(/\./g, boundary) 385 .replace(new RegExp('\\\\' + boundary, 'g'), '.') 386 .split(boundary); 387 var shadow = this._shadow; 388 var actual = this._data; 389 var i = 0; 390 var numPathNodes = path.length; 391 var pathNode; 392 // Whether or not the traversal path in `_data' and `_shadow' are 393 // at the same position in the respective objects. 394 var areMirrored = true; 395 396 while (true) { 397 pathNode = path[i++]; 398 399 if (areMirrored) { 400 actual = actual[pathNode]; 401 areMirrored = jQuery.type(actual) !== 'undefined'; 402 } 403 404 if (i === numPathNodes) { 405 break; 406 } 407 408 if (shadow[pathNode]) { 409 shadow = shadow[pathNode]; 410 } else if (areMirrored || force) { 411 shadow = (shadow[pathNode] = {}); 412 } else { 413 break; // goto error 414 } 415 } 416 417 if (i === numPathNodes && (areMirrored || force)) { 418 shadow[pathNode] = value; 419 } else { 420 var err = GCN.createError('TYPE_ERROR', 'Object "' + 421 path.slice(0, i).join('.') + '" does not exist', 422 actual); 423 GCN.handleError(err, error); 424 } 425 }, 426 427 /** 428 * Receives the response from a REST API request, and stores it in the 429 * internal `_data' object. 430 * 431 * @private 432 * @param {object} data Parsed JSON response data. 433 */ 434 '!_processResponse': function (data) { 435 jQuery.extend(this._data, data[this._type]); 436 }, 437 438 /** 439 * Specifies a list of parameters that will be added to the url when 440 * loading the content object from the server. 441 * 442 * @private 443 * @return {object} object With parameters to be appended to the load 444 * request 445 */ 446 '!_loadParams': function () {}, 447 448 /** 449 * Reads the property `property' of this content object if this 450 * property is among those in the WRITEABLE_PROPS array. If a second 451 * argument is provided, them the property is updated with that value. 452 * 453 * @name prop 454 * @function 455 * @memberOf ContentObjectAPI 456 * @param {String} property Name of the property to be read or updated. 457 * @param {String} value Optional value to set property to. If omitted the property will just be read. 458 * @param {function(GCNError):boolean=} error Custom error handler to 459 * stop error propagation for this 460 * synchronous call. 461 * @return {?*} Meta attribute. 462 * @throws UNFETCHED_OBJECT_ACCESS if the object has not been fetched from the server yet 463 * @throws READONLY_ATTRIBUTE whenever trying to write to an attribute that's readonly 464 */ 465 '!prop': function (property, value, error) { 466 if (!this._fetched) { 467 GCN.handleError(GCN.createError( 468 'UNFETCHED_OBJECT_ACCESS', 469 'Object not fetched yet.' 470 ), error); 471 return; 472 } 473 474 if (typeof value !== 'undefined') { 475 // Check whether the property is writable 476 if (jQuery.inArray(property, this.WRITEABLE_PROPS) >= 0) { 477 // Check wether the property has a constraint and verify it 478 var constraint = this.WRITEABLE_PROPS_CONSTRAINTS[property]; 479 if (constraint) { 480 // verify maxLength 481 if (constraint.maxLength && value.length >= constraint.maxLength) { 482 var data = { name: property, value: value, maxLength: constraint.maxLength }; 483 var constraintError = GCN.createError('ATTRIBUTE_CONSTRAINT_VIOLATION', 484 'Attribute "' + property + '" of ' + this._type + 485 ' is too long. The \'maxLength\' was set to {' + constraint.maxLength + '} ', data); 486 GCN.handleError(constraintError, error); 487 return; 488 } 489 } 490 this._update(GCN.escapePropertyName(property), value); 491 } else { 492 GCN.handleError(GCN.createError('READONLY_ATTRIBUTE', 493 'Attribute "' + property + '" of ' + this._type + 494 ' is read-only. Writeable properties are: ' + 495 this.WRITEABLE_PROPS, this.WRITEABLE_PROPS), error); 496 return; 497 } 498 } 499 500 return ( 501 (jQuery.type(this._shadow[property]) !== 'undefined' 502 ? this._shadow 503 : this._data)[property] 504 ); 505 }, 506 507 /** 508 * Sends the a template string to the Aloha Servlet for rendering. 509 * 510 * @ignore 511 * @TODO: Consider making this function public. At least one developer 512 * has had need to render a custom template for a content 513 * object. 514 * 515 * @private 516 * @param {string} template Template which will be rendered. 517 * @param {string} mode The rendering mode. Valid values are "view", 518 * "edit", "pub." 519 * @param {function(object)} success A callback the receives the render 520 * response. 521 * @param {function(GCNError):boolean} error Error handler. 522 */ 523 '!_renderTemplate' : function (template, mode, success, error) { 524 var channelParam = GCN._getChannelParameter(this); 525 var url = GCN.settings.BACKEND_PATH 526 + '/rest/' + this._type 527 + '/render/' + this.id() 528 + channelParam 529 + (channelParam ? '&' : '?') 530 + 'edit=' + ('edit' === mode) 531 + '&template=' + encodeURIComponent(template); 532 this._authAjax({ 533 url: url, 534 error: error, 535 success: success 536 }); 537 }, 538 539 /** 540 * Wrapper for internal chainback _ajax method. 541 * 542 * @ignore 543 * @private 544 * @param {object<string, *>} settings Settings for the ajax request. 545 * The settings object is identical 546 * to that of the `GCN.ajax' 547 * method, which handles the actual 548 * ajax transportation. 549 * @throws AJAX_ERROR 550 */ 551 '!_ajax': function (settings) { 552 var that = this; 553 554 // force no cache for all API calls 555 settings.cache = false; 556 settings.success = (function (onSuccess, onError) { 557 return function (data) { 558 // Ajax calls that do not target the REST API servlet do 559 // not response data with a `responseInfo' object. 560 // "/CNPortletapp/alohatag" is an example. So we cannot 561 // just assume that it exists. 562 if (data.responseInfo) { 563 switch (data.responseInfo.responseCode) { 564 case 'OK': 565 break; 566 case 'AUTHREQUIRED': 567 GCN.clearSession(); 568 that._authAjax(settings); 569 return; 570 default: 571 GCN.handleResponseError(data, onError); 572 return; 573 } 574 } 575 576 if (onSuccess) { 577 onSuccess(data); 578 } 579 }; 580 }(settings.success, settings.error, settings.url)); 581 582 this._queueAjax(settings); 583 }, 584 585 /** 586 * Concrete implementatation of _fulfill(). 587 * 588 * Resolves all promises made by this content object while ensuring 589 * that circularReferences, (which are completely possible, and valid) 590 * do not result in infinit recursion. 591 * 592 * @override 593 */ 594 '!_fulfill': function (success, error, stack) { 595 var obj = this; 596 if (obj._chain) { 597 var circularReference = 598 stack && -1 < jQuery.inArray(obj._chain, stack); 599 if (!circularReference) { 600 stack = stack || []; 601 stack.push(obj._chain); 602 obj._fulfill(function () { 603 obj._read(success, error); 604 }, error, stack); 605 return; 606 } 607 } 608 obj._read(success, error); 609 }, 610 611 /** 612 * Similar to `_ajax', except that it prefixes the ajax url with the 613 * current session's `sid', and will trigger an 614 * `authentication-required' event if the session is not authenticated. 615 * 616 * @ignore 617 * @TODO(petro): Consider simplifiying this function signature to read: 618 * `_auth( url, success, error )' 619 * 620 * @private 621 * @param {object<string, *>} settings Settings for the ajax request. 622 * @throws AUTHENTICATION_FAILED 623 */ 624 _authAjax: function (settings) { 625 var that = this; 626 627 if (GCN.isAuthenticating) { 628 GCN.afterNextAuthentication(function () { 629 that._authAjax(settings); 630 }); 631 return; 632 } 633 634 if (!GCN.sid) { 635 var cancel; 636 637 if (settings.error) { 638 /** 639 * @ignore 640 */ 641 cancel = function (error) { 642 GCN.handleError( 643 error || GCN.createError('AUTHENTICATION_FAILED'), 644 settings.error 645 ); 646 }; 647 } else { 648 /** 649 * @ignore 650 */ 651 cancel = function (error) { 652 if (error) { 653 GCN.error(error.code, error.message, error.data); 654 } else { 655 GCN.error('AUTHENTICATION_FAILED'); 656 } 657 }; 658 } 659 660 GCN.afterNextAuthentication(function () { 661 that._authAjax(settings); 662 }); 663 664 if (GCN.usingSSO) { 665 // First, try to automatically authenticate via 666 // Single-SignOn 667 GCN.loginWithSSO(GCN.onAuthenticated, function () { 668 // ... if SSO fails, then fallback to requesting user 669 // credentials: broadcast `authentication-required' 670 // message. 671 GCN.authenticate(cancel); 672 }); 673 } else { 674 // Trigger the `authentication-required' event to request 675 // user credentials. 676 GCN.authenticate(cancel); 677 } 678 679 return; 680 } 681 682 // Append "?sid=..." or "&sid=..." if needed. 683 684 var urlFragment = settings.url.substr( 685 GCN.settings.BACKEND_PATH.length 686 ); 687 var isSidInUrl = /[\?\&]sid=/.test(urlFragment); 688 if (!isSidInUrl) { 689 var isFirstParam = (jQuery.inArray('?', 690 urlFragment.split('')) === -1); 691 settings.url += (isFirstParam ? '?' : '&') + 'sid=' 692 + (GCN.sid || ''); 693 } 694 695 this._ajax(settings); 696 }, 697 698 /** 699 * Recursively call `_continueWith()'. 700 * 701 * @ignore 702 * @private 703 * @override 704 */ 705 '!_onContinue': function (success, error) { 706 var that = this; 707 this._continueWith(function () { 708 that._read(success, error); 709 }, error); 710 }, 711 712 /** 713 * Initializes this content object. If a `success' callback is 714 * provided, it will cause this object's data to be fetched and passed 715 * to the callback. This object's data will be fetched from the cache 716 * if is available, otherwise it will be fetched from the server. If 717 * this content object API contains parent chainbacks, it will get its 718 * parent to fetch its own data first. 719 * 720 * <p> 721 * Basic content object implementation which all other content objects 722 * will inherit from. 723 * </p> 724 * 725 * <p> 726 * If a `success' callback is provided, 727 * it will cause this object's data to be fetched and passed to the 728 * callback. This object's data will be fetched from the cache if is 729 * available, otherwise it will be fetched from the server. If this 730 * content object API contains parent chainbacks, it will get its parent 731 * to fetch its own data first. 732 * </p> 733 * 734 * <p> 735 * You might also provide an object for initialization, to directly 736 * instantiate the object's data without loading it from the server. To 737 * do so just pass in a data object as received from the server instead 738 * of an id--just make sure this object has an `id' property. 739 * </p> 740 * 741 * <p> 742 * If an `error' handler is provided, as the third parameter, it will 743 * catch any errors that have occured since the invocation of this call. 744 * It allows the global error handler to be intercepted before stopping 745 * the error or allowing it to propagate on to the global handler. 746 * </p> 747 * 748 * @class 749 * @name ContentObjectAPI 750 * @param {number|string|object} 751 * id 752 * @param {function(ContentObjectAPI))=} 753 * success Optional success callback that will receive this 754 * object as its only argument. 755 * @param {function(GCNError):boolean=} 756 * error Optional custom error handler. 757 * @param {object} 758 * settings Basic settings for this object - depends on the 759 * ContentObjetAPI Object used. 760 * @throws INVALID_DATA 761 * If no id is found when providing an object for 762 * initialization. 763 */ 764 _init: function (data, success, error, settings) { 765 this._settings = settings; 766 var id; 767 768 if (jQuery.type(data) === 'object') { 769 if (data.multichannelling) { 770 this.multichannelling = data; 771 // Remove the inherited object from the chain. 772 if (this._chain) { 773 this._chain = this._chain._chain; 774 } 775 id = this.multichannelling.derivedFrom.id(); 776 } else { 777 if (!data.id) { 778 var err = GCN.createError( 779 'INVALID_DATA', 780 'Data not sufficient for initalization: id is missing', 781 data 782 ); 783 GCN.handleError(err, error); 784 return; 785 } 786 this._data = data; 787 this._fetched = true; 788 if (success) { 789 this._invoke(success, [this]); 790 } 791 return; 792 } 793 } else { 794 id = data; 795 } 796 797 // Ensure that each object has its very own `_data' and `_shadow' 798 // objects. 799 if (!this._fetched) { 800 this._data = {}; 801 this._shadow = {}; 802 this._data.id = id; 803 } 804 if (success) { 805 this._read(success, error); 806 } 807 }, 808 809 /** 810 * <p>Replaces tag blocks with appropriate "<node *>" notation in a given 811 * string. Given an element whose innerHTML is:<p> 812 * <pre> 813 * <span id="GENTICS_BLOCK_123">My Tag</span> 814 * </pre> 815 * <p>encode() will return:</p> 816 * <pre> 817 * <node 123> 818 * </pre> 819 * 820 * @name encode 821 * @function 822 * @memberOf ContentObjectAPI 823 * @param {!jQuery} $element 824 * An element whose contents are to be encoded. 825 * @param {?function(!Element): string} serializeFn 826 * A function that returns the serialized contents of the 827 * given element as a HTML string, excluding the start and end 828 * tag of the element. If not provided, jQuery.html() will 829 * be used. 830 * @return {string} The encoded HTML string. 831 */ 832 '!encode': function ($element, serializeFn) { 833 var $clone = $element.clone(); 834 var id; 835 var $block; 836 for (id in this._blocks) { 837 if (this._blocks.hasOwnProperty(id)) { 838 $block = $clone.find('#' + this._blocks[id].element); 839 if ($block.length) { 840 // Empty all content blocks of their innerHTML. 841 $block.html('').attr('id', BLOCK_ENCODING_PREFIX + 842 this._blocks[id].tagname); 843 } 844 } 845 } 846 serializeFn = serializeFn || function ($element) { 847 return jQuery($element).html(); 848 }; 849 var html = serializeFn($clone[0]); 850 return html.replace(CONTENT_BLOCK, function (substr, match) { 851 return '<node ' + match + '>'; 852 }); 853 }, 854 855 /** 856 * For a given string, replace all occurances of "<node>" with 857 * appropriate HTML markup, allowing notated tags to be rendered within 858 * the surrounding HTML content. 859 * 860 * The success() handler will receives a string containing the contents 861 * of the `str' string with references to "<node>" having been inflated 862 * into their appropriate tag rendering. 863 * 864 * @name decode 865 * @function 866 * @memberOf ContentObjectAPI 867 * @param {string} str The content string, in which "<node *>" tags 868 * will be inflated with their HTML rendering. 869 * @param {function(ContentObjectAPI))} success Success callback that 870 * will receive the 871 * decoded string. 872 * @param {function(GCNError):boolean=} error Optional custom error 873 * handler. 874 */ 875 '!decode': function (str, success, error) { 876 if (!success) { 877 return; 878 } 879 880 var prefix = 'gcn-tag-placeholder-'; 881 var toRender = []; 882 var html = replaceNodeTags(str, function (name, offset, str) { 883 toRender.push('<node ', name, '>'); 884 return '<div id="' + prefix + name + '"></div>'; 885 }); 886 887 if (!toRender.length) { 888 success(html); 889 return; 890 } 891 892 // Instead of rendering each tag individually, we render them 893 // together in one string, and map the results back into our 894 // original html string. This allows us to perform one request to 895 // the server for any number of node tags found. 896 897 var parsed = jQuery('<div>' + html + '</div>'); 898 var template = toRender.join(''); 899 var that = this; 900 901 this._renderTemplate(template, 'edit', function (data) { 902 var content = data.content; 903 var tag; 904 var tags = data.tags; 905 var j = tags.length; 906 var rendered = jQuery('<div>' + content + '</div>'); 907 908 var replaceTag = (function (numTags) { 909 return function (tag) { 910 parsed.find('#' + prefix + tag.prop('name')) 911 .replaceWith( 912 rendered.find('#' + tag.prop('id')) 913 ); 914 915 if (0 === --numTags) { 916 success(parsed.html()); 917 } 918 }; 919 }(j)); 920 921 while (j) { 922 that.tag(tags[--j], replaceTag); 923 } 924 }, error); 925 }, 926 927 /** 928 * Clears this object from its constructor's cache so that the next 929 * attempt to access this object will result in a brand new instance 930 * being initialized and placed in the cache. 931 * 932 * @name clear 933 * @function 934 * @memberOf ContentObjectAPI 935 */ 936 '!clear': function () { 937 // Do not clear the id from the _data. 938 var id = this._data.id; 939 this._data = {}; 940 this._data.id = id; 941 this._shadow = {}; 942 this._fetched = false; 943 this._clearCache(); 944 }, 945 946 /** 947 * Retrieves this objects parent folder. 948 * 949 * @name folder 950 * @function 951 * @memberOf ContentObjectAPI 952 * @param {function(FolderAPI)=} 953 * success Callback that will receive the requested object. 954 * @param {function(GCNError):boolean=} 955 * error Custom error handler. 956 * @return {FolderAPI} API object for the retrieved GCN folder. 957 */ 958 '!folder': function (success, error) { 959 return this._continue(GCN.FolderAPI, this._data.folderId, success, 960 error); 961 }, 962 963 /** 964 * Saves changes made to this content object to the backend. 965 * 966 * @param {object=} 967 * settings Optional settings to pass on to the ajax 968 * function. 969 * @param {function(ContentObjectAPI)=} 970 * success Optional callback that receives this object as its 971 * only argument. 972 * @param {function(GCNError):boolean=} 973 * error Optional customer error handler. 974 */ 975 save: function () { 976 var settings; 977 var success; 978 var error; 979 var args = Array.prototype.slice.call(arguments); 980 var len = args.length; 981 var i; 982 983 for (i = 0; i < len; ++i) { 984 switch (jQuery.type(args[i])) { 985 case 'object': 986 if (!settings) { 987 settings = args[i]; 988 } 989 break; 990 case 'function': 991 if (!success) { 992 success = args[i]; 993 } else { 994 error = args[i]; 995 } 996 break; 997 case 'undefined': 998 break; 999 default: 1000 var err = GCN.createError('UNKNOWN_ARGUMENT', 1001 'Don\'t know what to do with arguments[' + i + '] ' + 1002 'value: "' + args[i] + '"', args); 1003 GCN.handleError(err, error); 1004 return; 1005 } 1006 } 1007 1008 this._save(settings, success, error); 1009 }, 1010 1011 /** 1012 * Persists this object's local data onto the server. If the object 1013 * has not yet been fetched we need to get it first so we can update 1014 * its internals properly... 1015 * 1016 * @private 1017 * @param {object} settings Object which will extend the basic 1018 * settings of the ajax call 1019 * @param {function(ContentObjectAPI)=} success Optional callback that 1020 * receives this object as 1021 * its only argument. 1022 * @param {function(GCNError):boolean=} error Optional customer error 1023 * handler. 1024 */ 1025 '!_save': function (settings, success, error) { 1026 var that = this; 1027 this._fulfill(function () { 1028 that._persist(settings, success, error); 1029 }, error); 1030 }, 1031 1032 /** 1033 * Returns the bare data structure of this content object. 1034 * To be used for creating the save POST body data. 1035 * 1036 * @param {object<string, *>} Plain old object representation of this 1037 * content object. 1038 */ 1039 '!json': function () { 1040 var json = {}; 1041 1042 if (this._deletedTags.length) { 1043 json['delete'] = this._deletedTags; 1044 } 1045 1046 if (this._deletedBlocks.length) { 1047 json['delete'] = json['delete'] 1048 ? json['delete'].concat(this._deletedBlocks) 1049 : this._deletedBlocks; 1050 } 1051 1052 json[this._type] = this._shadow; 1053 json[this._type].id = this._data.id; 1054 return json; 1055 }, 1056 1057 /** 1058 * Sends the current state of this content object to be stored on the 1059 * server. 1060 * 1061 * @private 1062 * @param {function(ContentObjectAPI)=} success Optional callback that 1063 * receives this object as 1064 * its only argument. 1065 * @param {function(GCNError):boolean=} error Optional customer error 1066 * handler. 1067 * @throws HTTP_ERROR 1068 */ 1069 '!_persist': function (settings, success, error) { 1070 var that = this; 1071 1072 if (!this._fetched) { 1073 this._read(function () { 1074 that._persist(settings, success, error); 1075 }, error); 1076 return; 1077 } 1078 1079 var json = this.json(); 1080 jQuery.extend(json, settings); 1081 var tags = json[this._type].tags; 1082 var tagname; 1083 for (tagname in tags) { 1084 if (tags.hasOwnProperty(tagname)) { 1085 tags[tagname].active = true; 1086 } 1087 } 1088 1089 this._authAjax({ 1090 url : GCN.settings.BACKEND_PATH + '/rest/' 1091 + this._type + '/save/' + this.id() 1092 + GCN._getChannelParameter(this), 1093 type : 'POST', 1094 error : error, 1095 json : json, 1096 success : function (response) { 1097 // We must not overwrite the `_data.tags' object with this 1098 // one. 1099 delete that._shadow.tags; 1100 1101 // Everything else in `_shadow' should be written over to 1102 // `_data' before resetting the `_shadow' object. 1103 jQuery.extend(that._data, that._shadow); 1104 that._shadow = {}; 1105 that._deletedTags = []; 1106 that._deletedBlocks = []; 1107 1108 if (success) { 1109 that._invoke(success, [that]); 1110 } 1111 } 1112 }); 1113 }, 1114 1115 /** 1116 * Deletes this content object from its containing parent. 1117 * 1118 * @param {function(ContentObjectAPI)=} 1119 * success Optional callback that receives this object as its 1120 * only argument. 1121 * @param {function(GCNError):boolean=} 1122 * error Optional customer error handler. 1123 */ 1124 remove: function (success, error) { 1125 this._remove(success, error); 1126 }, 1127 1128 /** 1129 * Get a channel-local copy of this content object. 1130 * 1131 * @public 1132 * @function 1133 * @name localize 1134 * @memberOf ContentObjectAPI 1135 * @param {funtion(ContentObjectAPI)=} success Optional callback to 1136 * receive this content 1137 * object as the only 1138 * argument. 1139 * @param {function(GCNError):boolean=} error Optional custom error 1140 * handler. 1141 */ 1142 '!localize': function (success, error) { 1143 if (!this._channel && !GCN.channel()) { 1144 var err = GCN.createError( 1145 'NO_CHANNEL_ID_SET', 1146 'No channel is set in which to get the localized object', 1147 GCN 1148 ); 1149 GCN.handleError(err, error); 1150 return false; 1151 } 1152 var local = this._continue( 1153 this._constructor, 1154 { 1155 derivedFrom: this, 1156 multichannelling: true, 1157 read: GCN.multichannelling.localize 1158 }, 1159 success, 1160 error 1161 ); 1162 return local; 1163 }, 1164 1165 /** 1166 * Remove this channel-local object, and delete its local copy in the 1167 * backend. 1168 * 1169 * @public 1170 * @function 1171 * @name unlocalize 1172 * @memberOf ContentObjectAPI 1173 * @param {funtion(ContentObjectAPI)=} success Optional callback to 1174 * receive this content 1175 * object as the only 1176 * argument. 1177 * @param {function(GCNError):boolean=} error Optional custom error 1178 * handler. 1179 */ 1180 '!unlocalize': function (success, error) { 1181 if (!this._channel && !GCN.channel()) { 1182 var err = GCN.createError( 1183 'NO_CHANNEL_ID_SET', 1184 'No channel is set in which to get the unlocalized object', 1185 GCN 1186 ); 1187 GCN.handleError(err, error); 1188 return false; 1189 } 1190 var placeholder = { 1191 multichannelling: { 1192 derivedFrom: this 1193 } 1194 }; 1195 var that = this; 1196 GCN.multichannelling.unlocalize(placeholder, function () { 1197 // TODO: This should be done inside of 1198 // multichannelling.unlocalize() and not in this callback. 1199 // Clean cache & reset object to make sure it can't be used 1200 // properly any more. 1201 that._clearCache(); 1202 that._data = {}; 1203 that._shadow = {}; 1204 if (success) { 1205 success(); 1206 } 1207 }, error); 1208 }, 1209 1210 /** 1211 * Performs a REST API request to delete this object from the server. 1212 * 1213 * @private 1214 * @param {function()=} success Optional callback that 1215 * will be invoked once 1216 * this object has been 1217 * removed. 1218 * @param {function(GCNError):boolean=} error Optional customer error 1219 * handler. 1220 */ 1221 '!_remove': function (success, error) { 1222 var that = this; 1223 this._authAjax({ 1224 url : GCN.settings.BACKEND_PATH + '/rest/' 1225 + this._type + '/delete/' + this.id() 1226 + GCN._getChannelParameter(that), 1227 type : 'POST', 1228 error : error, 1229 success : function (response) { 1230 // Clean cache & reset object to make sure it can't be used 1231 // properly any more. 1232 that._clearCache(); 1233 that._data = {}; 1234 that._shadow = {}; 1235 1236 // Don't forward the object to the success handler since 1237 // it's been deleted. 1238 if (success) { 1239 that._invoke(success); 1240 } 1241 } 1242 }); 1243 }, 1244 1245 /** 1246 * Removes any additionaly data stored on this objec which pertains to 1247 * a tag matching the given tagname. This function will be called when 1248 * a tag is being removed in order to bring the content object to a 1249 * consistant state. 1250 * Should be overriden by subclasses. 1251 * 1252 * @param {string} tagid The Id of the tag whose associated data we 1253 * want we want to remove. 1254 */ 1255 '!_removeAssociatedTagData': function (tagname) {} 1256 1257 }); 1258 1259 GCN.ContentObjectAPI.update = update; 1260 1261 /** 1262 * Generates a factory method for chainback classes. The method signature 1263 * used with this factory function will match that of the target class' 1264 * constructor. Therefore this function is expected to be invoked with the 1265 * follow combination of arguments ... 1266 * 1267 * Examples for GCN.pages api: 1268 * 1269 * To get an array containing 1 page: 1270 * pages(1) 1271 * pages(1, function () {}) 1272 * 1273 * To get an array containing 2 pages: 1274 * pages([1, 2]) 1275 * pages([1, 2], function () {}) 1276 * 1277 * To get an array containing any and all pages: 1278 * pages() 1279 * pages(function () {}) 1280 * 1281 * To get an array containing no pages: 1282 * pages([]) 1283 * pages([], function () {}); 1284 * 1285 * @param {Chainback} ctor The Chainback constructor we want to expose. 1286 * @throws UNKNOWN_ARGUMENT 1287 */ 1288 GCN.exposeAPI = function (ctor) { 1289 return function () { 1290 // Convert arguments into an array 1291 // https://developer.mozilla.org/en/JavaScript/Reference/... 1292 // ...Functions_and_function_scope/arguments 1293 var args = Array.prototype.slice.call(arguments); 1294 var id; 1295 var ids; 1296 var success; 1297 var error; 1298 var settings; 1299 1300 // iterate over arguments to find id || ids, succes, error and 1301 // settings 1302 jQuery.each(args, function (i, arg) { 1303 switch (jQuery.type(arg)) { 1304 // set id 1305 case 'string': 1306 case 'number': 1307 if (!id && !ids) { 1308 id = arg; 1309 } else { 1310 GCN.error('UNKNOWN_ARGUMENT', 1311 'id is already set. Don\'t know what to do with ' + 1312 'arguments[' + i + '] value: "' + arg + '"'); 1313 } 1314 break; 1315 // set ids 1316 case 'array': 1317 if (!id && !ids) { 1318 ids = args[0]; 1319 } else { 1320 GCN.error('UNKNOWN_ARGUMENT', 1321 'ids is already set. Don\'t know what to do with' + 1322 ' arguments[' + i + '] value: "' + arg + '"'); 1323 } 1324 break; 1325 // success and error handlers 1326 case 'function': 1327 if (!success) { 1328 success = arg; 1329 } else if (success && !error) { 1330 error = arg; 1331 } else { 1332 GCN.error('UNKNOWN_ARGUMENT', 1333 'success and error handler already set. Don\'t ' + 1334 'know what to do with arguments[' + i + ']'); 1335 } 1336 break; 1337 // settings 1338 case 'object': 1339 if (!id && !ids) { 1340 id = arg; 1341 } else if (!settings) { 1342 settings = arg; 1343 } else { 1344 GCN.error('UNKNOWN_ARGUMENT', 1345 'settings are already present. Don\'t know what ' + 1346 'to do with arguments[' + i + '] value:' + ' "' + 1347 arg + '"'); 1348 } 1349 break; 1350 default: 1351 GCN.error('UNKNOWN_ARGUMENT', 1352 'Don\'t know what to do with arguments[' + i + 1353 '] value: "' + arg + '"'); 1354 } 1355 }); 1356 1357 // Prepare a new set of arguments to pass on during initialzation 1358 // of callee object. 1359 args = []; 1360 1361 // settings should always be an object, even if it's just empty 1362 if (!settings) { 1363 settings = {}; 1364 } 1365 1366 args[0] = (typeof id !== 'undefined') ? id : ids; 1367 args[1] = success || settings.success || null; 1368 args[2] = error || settings.error || null; 1369 args[3] = settings; 1370 1371 // We either add 0 (no channel) or the channelid to the hash 1372 var channel = GCN.settings.channel; 1373 1374 // Check if the value is false, and set it to 0 in this case 1375 if (!channel) { 1376 channel = 0; 1377 } 1378 1379 var hash = (id || ids) 1380 ? ctor._makeHash(channel + '/' + (ids ? ids.sort().join(',') : id)) 1381 : null; 1382 1383 return GCN.getChainback(ctor, hash, null, args); 1384 }; 1385 1386 }; 1387 1388 }(GCN)); 1389