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