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