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