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