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