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