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 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 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 that 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 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. 303 * @param {*} value The value we wish to set the property to. 304 * @param {function=} error Custom error handler. 305 * @param {boolean=} force If true, no error will be thrown if `path' 306 * cannot be fully resolved against the 307 * internal `_data' object, instead, the path 308 * will be created on the shadow object. 309 */ 310 '!_update': function (pathStr, value, error, force) { 311 var path = pathStr.split('.'); 312 var shadow = this._shadow; 313 var actual = this._data; 314 var i = 0; 315 var j = path.length; 316 var pathNode; 317 // Whether or not the traversal path in `_data' and `_shadow' are 318 // at the same position in the respective objects. 319 var areMirrored = true; 320 321 while (true) { 322 pathNode = path[i++]; 323 324 if (areMirrored) { 325 actual = actual[pathNode]; 326 areMirrored = jQuery.type(actual) !== 'undefined'; 327 } 328 329 if (i === j) { 330 break; 331 } 332 333 if (shadow[pathNode]) { 334 shadow = shadow[pathNode]; 335 } else if (areMirrored || force) { 336 shadow = (shadow[pathNode] = {}); 337 } else { 338 break; // goto error 339 } 340 } 341 342 if (i === j && (areMirrored || force)) { 343 shadow[pathNode] = value; 344 } else { 345 var err = GCN.createError('TYPE_ERROR', 'Object "' + 346 path.slice(0, i).join('.') + '" does not exist', 347 actual); 348 349 GCN.handleError(err, error); 350 } 351 }, 352 353 /** 354 * Receives the response from a REST API request, and stores it in the 355 * internal `_data' object. 356 * 357 * @private 358 * @param {object} data Parsed JSON response data. 359 */ 360 '!_processResponse': function (data) { 361 jQuery.extend(this._data, data[this._type]); 362 }, 363 364 /** 365 * Specifies a list of parameters that will be added to the url when 366 * loading the content object from the server. 367 * 368 * @private 369 * @return {object} object With parameters to be appended to the load 370 * request 371 */ 372 '!_loadParams': function () {}, 373 374 /** 375 * Reads the proporty `property' of this content object if this 376 * property is among those in the WRITEABLE_PROPS array. If a send 377 * argument is provided, them the property is updated with that value. 378 * 379 * @name prop 380 * @function 381 * @memberOf ContentObjectAPI 382 * @param {String} property Name of the property to be read or updated. 383 * @param {String} value Value to be set property to. 384 * @return {?*} Meta attribute. 385 * @throws UNFETCHED_OBJECT_ACCESS 386 * @throws READONLY_ATTRIBUTE 387 */ 388 '!prop': function (property, value) { 389 if (!this._fetched) { 390 GCN.error('UNFETCHED_OBJECT_ACCESS', 391 'Object not fetched yet.'); 392 393 return; 394 } 395 396 if (value) { 397 if (jQuery.inArray(property, this.WRITEABLE_PROPS) >= 0) { 398 this._update(property, value); 399 } else { 400 GCN.error('READONLY_ATTRIBUTE', 401 'Attribute "' + property + '" of ' + this._type + 402 ' is read-only. Writeable properties are: ' + 403 this.WRITEABLE_PROPS); 404 } 405 } 406 407 return ((jQuery.type(this._shadow[property]) !== 'undefined' 408 ? this._shadow : this._data)[property]); 409 }, 410 411 /** 412 * Sends the a template string to the Aloha Servlet for rendering. 413 * 414 * @TODO: Consider making this function public. At least one developer 415 * has had need to render a custom template for a content 416 * object. 417 * 418 * @private 419 * @param {string} template Template which will be rendered. 420 * @param {string} mode The rendering mode. Valid values are "view", 421 * "edit", "pub." 422 * @param {function(object)} success A callback the receives the render 423 * response. 424 * @param {function(GCNError):boolean} error Error handler. 425 */ 426 '!_renderTemplate' : function (template, mode, success, error) { 427 var url = GCN.settings.BACKEND_PATH + '/rest/' + this._type + 428 '/render/' + this.id(); 429 430 url += '?edit=' + ('edit' === mode) 431 + '&template=' + encodeURIComponent(template); 432 433 this._authAjax({ 434 url : url, 435 error : error, 436 success : success 437 }); 438 }, 439 440 /** 441 * Wrapper for internal chainback _ajax method. 442 * 443 * @private 444 * @param {object<string, *>} settings Settings for the ajax request. 445 * The settings object is identical 446 * to that of the `GCN.ajax' 447 * method, which handles the actual 448 * ajax transportation. 449 * @throws AJAX_ERROR 450 */ 451 '!_ajax': function (settings) { 452 var that = this; 453 454 // force no cache for all API calls 455 settings.cache = false; 456 settings.success = (function (onSuccess, onError) { 457 return function (data) { 458 // Ajax calls that do not target the REST API servlet do 459 // not response data with a `responseInfo' object. 460 // "/CNPortletapp/alohatag" is an example. So we cannot 461 // just assume that it exists. 462 if (data.responseInfo) { 463 switch (data.responseInfo.responseCode) { 464 case 'OK': 465 break; 466 case 'AUTHREQUIRED': 467 GCN.clearSession(); 468 that._authAjax(settings); 469 return; 470 default: 471 GCN.handleResponseError(data, onError); 472 return; 473 } 474 } 475 476 if (onSuccess) { 477 onSuccess(data); 478 } 479 }; 480 }(settings.success, settings.error, settings.url)); 481 482 this._queueAjax(settings); 483 }, 484 485 /** 486 * Similar to `_ajax', except that it prefixes the ajax url with the 487 * current session's `sid', and will trigger an 488 * `authentication-required' event if the session is not authenticated. 489 * 490 * @TODO(petro): Consider simplifiying this function signature to read: 491 * `_auth( url, success, error )' 492 * 493 * @private 494 * @param {object<string, *>} settings Settings for the ajax request. 495 * @throws AUTHENTICATION_FAILED 496 */ 497 _authAjax: function (settings) { 498 var that = this; 499 500 if (GCN.isAuthenticating) { 501 GCN.afterNextAuthentication(function () { 502 that._authAjax(settings); 503 }); 504 505 return; 506 } 507 508 if (!GCN.sid) { 509 var cancel; 510 511 if (settings.error) { 512 cancel = function (error) { 513 GCN.handleError( 514 error || GCN.createError('AUTHENTICATION_FAILED'), 515 settings.error 516 ); 517 }; 518 } else { 519 cancel = function (error) { 520 if (error) { 521 GCN.error(error.code, error.message, error.data); 522 } else { 523 GCN.error('AUTHENTICATION_FAILED'); 524 } 525 }; 526 } 527 528 GCN.afterNextAuthentication(function () { 529 that._authAjax(settings); 530 }); 531 532 if (GCN.usingSSO) { 533 // First, try to automatically authenticate via 534 // Single-SignOn 535 GCN.loginWithSSO(GCN.onAuthenticated, function () { 536 // ... if SSO fails, then fallback to requesting user 537 // credentials: broadcast `authentication-required' 538 // message. 539 GCN.authenticate(cancel); 540 }); 541 } else { 542 // Trigger the `authentication-required' event to request 543 // user credentials. 544 GCN.authenticate(cancel); 545 } 546 547 return; 548 } 549 550 // Append "?sid=..." or "&sid=..." if needed. 551 552 var urlFragment = settings.url.substr( 553 GCN.settings.BACKEND_PATH.length 554 ); 555 var isSidInUrl = /[\?\&]sid=/.test(urlFragment); 556 if (!isSidInUrl) { 557 var isFirstParam = (jQuery.inArray('?', 558 urlFragment.split('')) === -1); 559 560 settings.url += (isFirstParam ? '?' : '&') + 'sid=' 561 + (GCN.sid || ''); 562 } 563 564 this._ajax(settings); 565 }, 566 567 /** 568 * Recursively call `_continueWith()'. 569 * 570 * @private 571 * @override 572 */ 573 '!_onContinue': function (success, error) { 574 var that = this; 575 this._continueWith(function () { 576 that._read(success, error); 577 }, error); 578 }, 579 580 /** 581 * Initializes this content object. If a `success' callback is 582 * provided, it will cause this object's data to be fetched and passed 583 * to the callback. This object's data will be fetched from the cache 584 * if is available, otherwise it will be fetched from the server. If 585 * this content object API contains parent chainbacks, it will get its 586 * parent to fetch its own data first. 587 * 588 * You might also provide an object for initialization, to directly 589 * instantiate the object's data without loading it from the server. 590 * To do so just pass in a data object as received from the server 591 * instead of an id--just make sure this object has an `id' property. 592 * 593 * If an `error' handler is provided, as the third parameter, it will 594 * catch any errors that have occured since the invocation of this 595 * call. It allows the global error handler to be intercepted before 596 * stopping the error or allowing it to propagate on to the global 597 * handler. 598 * 599 * @private 600 * @param {number|string|object} id 601 * @param {function(ContentObjectAPI))=} success Optional success 602 * callback that will 603 * receive this 604 * object as its only 605 * argument. 606 * @param {function(GCNError):boolean=} error Optional custom error 607 * handler. 608 * @param {object} settings Basic settings for this object. 609 * @throws INVALID_DATA If no id is found when providing an object for 610 * initialization. 611 */ 612 _init: function (id, success, error, settings) { 613 this._settings = settings; 614 615 if (jQuery.type(id) === 'object') { 616 if (!id.id) { 617 var err = GCN.createError('INVALID_DATA', 618 'Data not sufficient for initalization: id is missing', 619 id); 620 621 GCN.handleError(err, error); 622 623 return; 624 } 625 626 // Clear the old data and populate the _data field using the given object data 627 this._data = {}; 628 this._data = id; 629 this._fetched = true; 630 631 if (success) { 632 success(this); 633 } 634 } else { 635 // Ensure that each object has its very own `_data' and 636 // `_shadow' objects. 637 if (!this._fetched) { 638 this._data = {}; 639 this._shadow = {}; 640 this._data.id = id; 641 } 642 643 if (success) { 644 this._read(success, error); 645 } 646 } 647 }, 648 649 /** 650 * Replaces tag blocks with appropriate "<node *>" notation in a given 651 * string. 652 * 653 * Given an element whose innerHTML is: 654 * <pre> 655 * "<span id="GENTICS_BLOCK_123">My Tag</span>", 656 * </pre> 657 * `encode()' will return: 658 * <pre> 659 * "<node 123>". 660 * </pre> 661 * 662 * @name encode 663 * @function 664 * @memberOf ContentObjectAPI 665 * @param {string} HTML string to be encoded. 666 * @return {string} The encoded HTML string. 667 */ 668 '!encode': function (html) { 669 var that = this; 670 var clone = jQuery('<div>' + html + '</div>'); 671 // Empty all content blocks of their innerHTML. 672 var id; 673 var $block; 674 for (id in this._blocks) { 675 if (this._blocks.hasOwnProperty(id)) { 676 $block = clone.find('#' + this._blocks[id].element); 677 if ($block.length) { 678 $block.html('').attr('id', BLOCK_ENCODING_PREFIX + 679 this._blocks[id].tagname); 680 } 681 } 682 } 683 684 return clone.html().replace(contentBlockRegExp, 685 function (substr, match) { 686 return '<node ' + match + '>'; 687 }); 688 }, 689 690 /** 691 * For a given string, replace all occurances of "<node>" with 692 * appropriate HTML markup, allowing notated tags to be rendered within 693 * the surrounding HTML content. 694 * 695 * The `success()' handler will receives a string containing the 696 * contents of the `str' string with references to "<node>" having been 697 * inflated into their appropriate tag rendering. 698 * 699 * @name decode 700 * @function 701 * @memberOf ContentObjectAPI 702 * @param {string} str The content string, in which "<node *>" tags 703 * will be inflated with their HTML rendering. 704 * @param {function(ContentObjectAPI))} success Success callback that 705 * will receive the 706 * decoded string. 707 * @param {function(GCNError):boolean=} error Optional custom error 708 * handler. 709 */ 710 '!decode': function (str, success, error) { 711 if (!success) { 712 return; 713 } 714 715 var prefix = 'gcn-tag-placeholder-'; 716 var toRender = []; 717 var html = replaceNodeTags(str, function (name, offset, str) { 718 toRender.push('<node ', name, '>'); 719 return '<div id="' + prefix + name + '"></div>'; 720 }); 721 722 if (!toRender.length) { 723 success(html); 724 return; 725 } 726 727 // Instead of rendering each tag individually, we render them 728 // together in one string, and map the results back into our 729 // original html string. This allows us to perform one request to 730 // the server for any number of node tags found. 731 732 var parsed = jQuery('<div>' + html + '</div>'); 733 var template = toRender.join(''); 734 var that = this; 735 736 this._renderTemplate(template, 'edit', function (data) { 737 var content = data.content; 738 var tag; 739 var tags = data.tags; 740 var j = tags.length; 741 var rendered = jQuery('<div>' + content + '</div>'); 742 743 var replaceTag = (function (numTags) { 744 return function (tag) { 745 parsed.find('#' + prefix + tag.prop('name')) 746 .replaceWith( 747 rendered.find('#' + tag.prop('id')) 748 ); 749 750 if (0 === --numTags) { 751 success(parsed.html()); 752 } 753 }; 754 }(j)); 755 756 while (j) { 757 that.tag(tags[--j], replaceTag); 758 } 759 }, error); 760 }, 761 762 /** 763 * Clears this object from its constructor's cache so that the next 764 * attempt to access this object will result in a brand new instance 765 * being initialized and placed in the cache. 766 * 767 * @name clear 768 * @function 769 * @memberOf ContentObjectAPI 770 */ 771 '!clear': function () { 772 // Do not clear the id from the _data. 773 var id = this._data.id; 774 this._data = {}; 775 this._data.id = id; 776 this._shadow = {}; 777 this._fetched = false; 778 this._clearCache(); 779 }, 780 781 /** 782 * Retreives this objects parent folder. 783 * 784 * @param {function(ContentObjectAPI)=} success Callback that will 785 * receive the requested 786 * object. 787 * @param {function(GCNError):boolean=} error Custom error handler. 788 * @return {ContentObjectAPI)} API object for the retrieved GCN folder. 789 */ 790 '!folder': function (success, error) { 791 return this._continue(GCN.FolderAPI, this._data.folderId, success, 792 error); 793 }, 794 795 /** 796 * Saves changes made to this content object to the backend. 797 * 798 * @param {object=} settings Optional settings to pass on to the ajax 799 * function. 800 * @param {function(ContentObjectAPI)=} success Optional callback that 801 * receives this object as 802 * its only argument. 803 * @param {function(GCNError):boolean=} error Optional customer error 804 * handler. 805 */ 806 save: function () { 807 var settings; 808 var success; 809 var error; 810 var args = Array.prototype.slice.call(arguments); 811 var len = args.length; 812 var i; 813 814 for (i = 0; i < len; ++i) { 815 switch (jQuery.type(args[i])) { 816 case 'object': 817 if (!settings) { 818 settings = args[i]; 819 } 820 break; 821 case 'function': 822 if (!success) { 823 success = args[i]; 824 } else { 825 error = args[i]; 826 } 827 break; 828 case 'undefined': 829 break; 830 default: 831 var err = GCN.createError('UNKNOWN_ARGUMENT', 832 'Don\'t know what to do with arguments[' + i + '] ' + 833 'value: "' + args[i] + '"', args); 834 GCN.handleError(err, error); 835 return; 836 } 837 } 838 839 this._save(settings, success, error); 840 }, 841 842 /** 843 * Persists this object's local data onto the server. If the object 844 * has not yet been fetched we need to get it first so we can update 845 * its internals properly... 846 * 847 * @private 848 * @param {object} settings Object which will extend the basic 849 * settings of the ajax call 850 * @param {function(ContentObjectAPI)=} success Optional callback that 851 * receives this object as 852 * its only argument. 853 * @param {function(GCNError):boolean=} error Optional customer error 854 * handler. 855 */ 856 '!_save': function (settings, success, error) { 857 var that = this; 858 this._continueWith(function () { 859 that._persist(settings, success, error); 860 }, error); 861 }, 862 863 /** 864 * Sends the current state of this content object to be stored on the 865 * server. 866 * 867 * @private 868 * @param {function(ContentObjectAPI)=} success Optional callback that 869 * receives this object as 870 * its only argument. 871 * @param {function(GCNError):boolean=} error Optional customer error 872 * handler. 873 * @throws HTTP_ERROR 874 */ 875 '!_persist': function (settings, success, error) { 876 var that = this; 877 878 if (!this._fetched) { 879 that._read(function () { 880 that._persist(settings, success, error); 881 }, error); 882 883 return; 884 } 885 886 var json = {}; 887 888 if (this._deletedTags.length) { 889 json['delete'] = this._deletedTags; 890 } 891 892 if (this._deletedBlocks.length) { 893 json['delete'] = json['delete'] 894 ? json['delete'].concat(this._deletedBlocks) 895 : this._deletedBlocks; 896 } 897 898 json[this._type] = this._shadow; 899 json[this._type].id = this._data.id; 900 901 jQuery.extend(json, settings); 902 903 var tags = json[this._type].tags; 904 var tagname; 905 for (tagname in tags) { 906 if (tags.hasOwnProperty(tagname)) { 907 tags[tagname].active = true; 908 } 909 } 910 911 this._authAjax({ 912 url : GCN.settings.BACKEND_PATH + '/rest/' + that._type + 913 '/save/' + that.id(), 914 type : 'POST', 915 error : error, 916 json : json, 917 success : function (response) { 918 // We must not overwrite the `_data.tags' object with this 919 // one. 920 delete that._shadow.tags; 921 922 // Everything else in `_shadow' should be written over to 923 // `_data' before resetting the `_shadow' object. 924 jQuery.extend(that._data, that._shadow); 925 that._shadow = {}; 926 927 that._deletedTags = []; 928 that._deletedBlocks = []; 929 930 if (success) { 931 success(that); 932 } 933 } 934 }); 935 }, 936 937 /** 938 * Deletes this content object from its containing parent. 939 * 940 * @param {function(ContentObjectAPI)=} success Optional callback that 941 * receives this object as 942 * its only argument. 943 * @param {function(GCNError):boolean=} error Optional customer error 944 * handler. 945 */ 946 remove: function (success, error) { 947 this._remove(success, error); 948 }, 949 950 /** 951 * Performs a REST API request to delete this object from the server. 952 * 953 * @private 954 * @param {function(ContentObjectAPI)=} success Optional callback that 955 * will be invoked once 956 * this object has been 957 * removed. 958 * @param {function(GCNError):boolean=} error Optional customer error 959 * handler. 960 */ 961 '!_remove': function (success, error) { 962 var that = this; 963 this._authAjax({ 964 url : GCN.settings.BACKEND_PATH + '/rest/' + that._type + 965 '/delete/' + that.id(), 966 type : 'POST', 967 error : error, 968 success : function (response) { 969 // Clean cache & reset object to make sure it can't be used 970 // properly any more. 971 that._clearCache(); 972 that._data = {}; 973 that._shadow = {}; 974 975 // Don't forward the object to the success handler since 976 // it's been deleted. 977 if (success) { 978 success(); 979 } 980 } 981 }); 982 }, 983 984 /** 985 * Removes any additionaly data stored on this objec which pertains to 986 * a tag matching the given tagname. This function will be called when 987 * a tag is being removed in order to bring the content object to a 988 * consistant state. 989 * Should be overriden by subclasses. 990 * 991 * @param {string} tagid The Id of the tag whose associated data we 992 * want we want to remove. 993 */ 994 '!_removeAssociatedTagData': function (tagname) {} 995 996 }); 997 998 /** 999 * Generates a factory method for chainback classes. The method signature 1000 * used with this factory function will match that of the target class' 1001 * constructor. Therefore this function is expected to be invoked with the 1002 * follow combination of arguments ... 1003 * 1004 * Examples for GCN.pages api: 1005 * 1006 * To get an array containing 1 page: 1007 * pages(1) 1008 * pages(1, function () {}) 1009 * 1010 * To get an array containing 2 pages: 1011 * pages([1, 2]) 1012 * pages([1, 2], function () {}) 1013 * 1014 * To get an array containing any and all pages: 1015 * pages() 1016 * pages(function () {}) 1017 * 1018 * To get an array containing no pages: 1019 * pages([]) 1020 * pages([], function () {}); 1021 * 1022 * @param {Chainback} clazz The Chainback class we want to expose. 1023 * @throws UNKNOWN_ARGUMENT 1024 */ 1025 GCN.exposeAPI = function (clazz) { 1026 return function () { 1027 // Convert arguments into an array 1028 // https://developer.mozilla.org/en/JavaScript/Reference/... 1029 // ...Functions_and_function_scope/arguments 1030 var args = Array.prototype.slice.call(arguments); 1031 var id; 1032 var ids; 1033 var success; 1034 var error; 1035 var settings; 1036 1037 // iterate over arguments to find id || ids, succes, error and 1038 // settings 1039 jQuery.each(args, function (i, arg) { 1040 switch (jQuery.type(arg)) { 1041 // set id 1042 case 'string': 1043 case 'number': 1044 if (!id && !ids) { 1045 id = arg; 1046 } else { 1047 GCN.error('UNKNOWN_ARGUMENT', 1048 'id is already set. Don\'t know what to do with ' + 1049 'arguments[' + i + '] value: "' + arg + '"'); 1050 } 1051 break; 1052 // set ids 1053 case 'array': 1054 if (!id && !ids) { 1055 ids = args[0]; 1056 } else { 1057 GCN.error('UNKNOWN_ARGUMENT', 1058 'ids is already set. Don\'t know what to do with' + 1059 ' arguments[' + i + '] value: "' + arg + '"'); 1060 } 1061 break; 1062 // success and error handlers 1063 case 'function': 1064 if (!success) { 1065 success = arg; 1066 } else if (success && !error) { 1067 error = arg; 1068 } else { 1069 GCN.error('UNKNOWN_ARGUMENT', 1070 'success and error handler already set. Don\'t ' + 1071 'know what to do with arguments[' + i + ']'); 1072 } 1073 break; 1074 // settings 1075 case 'object': 1076 if (!id && !ids) { 1077 id = arg; 1078 } else if (!settings) { 1079 settings = arg; 1080 } else { 1081 GCN.error('UNKNOWN_ARGUMENT', 1082 'settings are already present. Don\'t know what ' + 1083 'to do with arguments[' + i + '] value:' + ' "' + 1084 arg + '"'); 1085 } 1086 break; 1087 default: 1088 GCN.error('UNKNOWN_ARGUMENT', 1089 'Don\'t know what to do with arguments[' + i + 1090 '] value: "' + arg + '"'); 1091 } 1092 }); 1093 1094 // Prepare a new set of arguments to pass on during initialzation 1095 // of callee object. 1096 args = []; 1097 1098 // settings should always be an object, even if it's just empty 1099 if (!settings) { 1100 settings = {}; 1101 } 1102 1103 args[0] = (typeof id !== 'undefined') ? id : ids; 1104 args[1] = success || settings.success || null; 1105 args[2] = error || settings.error || null; 1106 args[3] = settings; 1107 1108 var hash = (id || ids) 1109 ? clazz._makeHash(ids ? ids.sort().join(',') : id) 1110 : null; 1111 1112 return GCN.getChainback(clazz, hash, null, args); 1113 }; 1114 1115 }; 1116 1117 }(GCN)); 1118