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 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 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 this._data = id; 627 this._fetched = true; 628 629 if (success) { 630 success(this); 631 } 632 } else { 633 // Ensure that each object has its very own `_data' and 634 // `_shadow' objects. 635 if (!this._fetched) { 636 this._data = {}; 637 this._shadow = {}; 638 this._data.id = id; 639 } 640 641 if (success) { 642 this._read(success, error); 643 } 644 } 645 }, 646 647 /** 648 * Replaces tag blocks with appropriate "<node *>" notation in a given 649 * string. 650 * 651 * Given an element whose innerHTML is: 652 * <pre> 653 * "<span id="GENTICS_BLOCK_123">My Tag</span>", 654 * </pre> 655 * `encode()' will return: 656 * <pre> 657 * "<node 123>". 658 * </pre> 659 * 660 * @name encode 661 * @function 662 * @memberOf ContentObjectAPI 663 * @param {!jQuery} 664 * An element whose contents are to be encoded. 665 * @param {?function(!Element): string} 666 * A function that returns the serialized contents of the 667 * given element as a HTML string, excluding the start and end 668 * tag of the element. If not provided, jQuery.html() will 669 * be used. 670 * @return {string} The encoded HTML string. 671 */ 672 '!encode': function ($element, serializeFn) { 673 var that = this; 674 var clone = $element.clone(); 675 // Empty all content blocks of their innerHTML. 676 var id; 677 var $block; 678 var html; 679 for (id in this._blocks) { 680 if (this._blocks.hasOwnProperty(id)) { 681 $block = clone.find('#' + this._blocks[id].element); 682 if ($block.length) { 683 $block.html('').attr('id', BLOCK_ENCODING_PREFIX + 684 this._blocks[id].tagname); 685 } 686 } 687 } 688 serializeFn = serializeFn || function (element) { 689 return jQuery(element).html(); 690 }; 691 html = serializeFn(clone[0]); 692 return html.replace(contentBlockRegExp, function (substr, match) { 693 return '<node ' + match + '>'; 694 }); 695 }, 696 697 /** 698 * For a given string, replace all occurances of "<node>" with 699 * appropriate HTML markup, allowing notated tags to be rendered within 700 * the surrounding HTML content. 701 * 702 * The `success()' handler will receives a string containing the 703 * contents of the `str' string with references to "<node>" having been 704 * inflated into their appropriate tag rendering. 705 * 706 * @name decode 707 * @function 708 * @memberOf ContentObjectAPI 709 * @param {string} str The content string, in which "<node *>" tags 710 * will be inflated with their HTML rendering. 711 * @param {function(ContentObjectAPI))} success Success callback that 712 * will receive the 713 * decoded string. 714 * @param {function(GCNError):boolean=} error Optional custom error 715 * handler. 716 */ 717 '!decode': function (str, success, error) { 718 if (!success) { 719 return; 720 } 721 722 var prefix = 'gcn-tag-placeholder-'; 723 var toRender = []; 724 var html = replaceNodeTags(str, function (name, offset, str) { 725 toRender.push('<node ', name, '>'); 726 return '<div id="' + prefix + name + '"></div>'; 727 }); 728 729 if (!toRender.length) { 730 success(html); 731 return; 732 } 733 734 // Instead of rendering each tag individually, we render them 735 // together in one string, and map the results back into our 736 // original html string. This allows us to perform one request to 737 // the server for any number of node tags found. 738 739 var parsed = jQuery('<div>' + html + '</div>'); 740 var template = toRender.join(''); 741 var that = this; 742 743 this._renderTemplate(template, 'edit', function (data) { 744 var content = data.content; 745 var tag; 746 var tags = data.tags; 747 var j = tags.length; 748 var rendered = jQuery('<div>' + content + '</div>'); 749 750 var replaceTag = (function (numTags) { 751 return function (tag) { 752 parsed.find('#' + prefix + tag.prop('name')) 753 .replaceWith( 754 rendered.find('#' + tag.prop('id')) 755 ); 756 757 if (0 === --numTags) { 758 success(parsed.html()); 759 } 760 }; 761 }(j)); 762 763 while (j) { 764 that.tag(tags[--j], replaceTag); 765 } 766 }, error); 767 }, 768 769 /** 770 * Clears this object from its constructor's cache so that the next 771 * attempt to access this object will result in a brand new instance 772 * being initialized and placed in the cache. 773 * 774 * @name clear 775 * @function 776 * @memberOf ContentObjectAPI 777 */ 778 '!clear': function () { 779 // Do not clear the id from the _data. 780 var id = this._data.id; 781 this._data = {}; 782 this._data.id = id; 783 this._shadow = {}; 784 this._fetched = false; 785 this._clearCache(); 786 }, 787 788 /** 789 * Retreives this objects parent folder. 790 * 791 * @param {function(ContentObjectAPI)=} success Callback that will 792 * receive the requested 793 * object. 794 * @param {function(GCNError):boolean=} error Custom error handler. 795 * @return {ContentObjectAPI)} API object for the retrieved GCN folder. 796 */ 797 '!folder': function (success, error) { 798 return this._continue(GCN.FolderAPI, this._data.folderId, success, 799 error); 800 }, 801 802 /** 803 * Saves changes made to this content object to the backend. 804 * 805 * @param {object=} settings Optional settings to pass on to the ajax 806 * function. 807 * @param {function(ContentObjectAPI)=} success Optional callback that 808 * receives this object as 809 * its only argument. 810 * @param {function(GCNError):boolean=} error Optional customer error 811 * handler. 812 */ 813 save: function () { 814 var settings; 815 var success; 816 var error; 817 var args = Array.prototype.slice.call(arguments); 818 var len = args.length; 819 var i; 820 821 for (i = 0; i < len; ++i) { 822 switch (jQuery.type(args[i])) { 823 case 'object': 824 if (!settings) { 825 settings = args[i]; 826 } 827 break; 828 case 'function': 829 if (!success) { 830 success = args[i]; 831 } else { 832 error = args[i]; 833 } 834 break; 835 case 'undefined': 836 break; 837 default: 838 var err = GCN.createError('UNKNOWN_ARGUMENT', 839 'Don\'t know what to do with arguments[' + i + '] ' + 840 'value: "' + args[i] + '"', args); 841 GCN.handleError(err, error); 842 return; 843 } 844 } 845 846 this._save(settings, success, error); 847 }, 848 849 /** 850 * Persists this object's local data onto the server. If the object 851 * has not yet been fetched we need to get it first so we can update 852 * its internals properly... 853 * 854 * @private 855 * @param {object} settings Object which will extend the basic 856 * settings of the ajax call 857 * @param {function(ContentObjectAPI)=} success Optional callback that 858 * receives this object as 859 * its only argument. 860 * @param {function(GCNError):boolean=} error Optional customer error 861 * handler. 862 */ 863 '!_save': function (settings, success, error) { 864 var that = this; 865 this._continueWith(function () { 866 that._persist(settings, success, error); 867 }, error); 868 }, 869 870 '!json': function () { 871 var json = {}; 872 873 if (this._deletedTags.length) { 874 json['delete'] = this._deletedTags; 875 } 876 877 if (this._deletedBlocks.length) { 878 json['delete'] = json['delete'] 879 ? json['delete'].concat(this._deletedBlocks) 880 : this._deletedBlocks; 881 } 882 883 json[this._type] = this._shadow; 884 json[this._type].id = this._data.id; 885 return json; 886 }, 887 888 /** 889 * Sends the current state of this content object to be stored on the 890 * server. 891 * 892 * @private 893 * @param {function(ContentObjectAPI)=} success Optional callback that 894 * receives this object as 895 * its only argument. 896 * @param {function(GCNError):boolean=} error Optional customer error 897 * handler. 898 * @throws HTTP_ERROR 899 */ 900 '!_persist': function (settings, success, error) { 901 var that = this; 902 903 if (!this._fetched) { 904 that._read(function () { 905 that._persist(settings, success, error); 906 }, error); 907 908 return; 909 } 910 911 var json = this.json(); 912 jQuery.extend(json, settings); 913 var tags = json[this._type].tags; 914 var tagname; 915 916 for (tagname in tags) { 917 if (tags.hasOwnProperty(tagname)) { 918 tags[tagname].active = true; 919 } 920 } 921 922 this._authAjax({ 923 url : GCN.settings.BACKEND_PATH + '/rest/' + that._type + 924 '/save/' + that.id(), 925 type : 'POST', 926 error : error, 927 json : json, 928 success : function (response) { 929 // We must not overwrite the `_data.tags' object with this 930 // one. 931 delete that._shadow.tags; 932 933 // Everything else in `_shadow' should be written over to 934 // `_data' before resetting the `_shadow' object. 935 jQuery.extend(that._data, that._shadow); 936 that._shadow = {}; 937 938 that._deletedTags = []; 939 that._deletedBlocks = []; 940 941 if (success) { 942 success(that); 943 } 944 } 945 }); 946 }, 947 948 /** 949 * Deletes this content object from its containing parent. 950 * 951 * @param {function(ContentObjectAPI)=} success Optional callback that 952 * receives this object as 953 * its only argument. 954 * @param {function(GCNError):boolean=} error Optional customer error 955 * handler. 956 */ 957 remove: function (success, error) { 958 this._remove(success, error); 959 }, 960 961 /** 962 * Performs a REST API request to delete this object from the server. 963 * 964 * @private 965 * @param {function(ContentObjectAPI)=} success Optional callback that 966 * will be invoked once 967 * this object has been 968 * removed. 969 * @param {function(GCNError):boolean=} error Optional customer error 970 * handler. 971 */ 972 '!_remove': function (success, error) { 973 var that = this; 974 this._authAjax({ 975 url : GCN.settings.BACKEND_PATH + '/rest/' + that._type + 976 '/delete/' + that.id(), 977 type : 'POST', 978 error : error, 979 success : function (response) { 980 // Clean cache & reset object to make sure it can't be used 981 // properly any more. 982 that._clearCache(); 983 that._data = {}; 984 that._shadow = {}; 985 986 // Don't forward the object to the success handler since 987 // it's been deleted. 988 if (success) { 989 success(); 990 } 991 } 992 }); 993 }, 994 995 /** 996 * Removes any additionaly data stored on this objec which pertains to 997 * a tag matching the given tagname. This function will be called when 998 * a tag is being removed in order to bring the content object to a 999 * consistant state. 1000 * Should be overriden by subclasses. 1001 * 1002 * @param {string} tagid The Id of the tag whose associated data we 1003 * want we want to remove. 1004 */ 1005 '!_removeAssociatedTagData': function (tagname) {} 1006 1007 }); 1008 1009 /** 1010 * Generates a factory method for chainback classes. The method signature 1011 * used with this factory function will match that of the target class' 1012 * constructor. Therefore this function is expected to be invoked with the 1013 * follow combination of arguments ... 1014 * 1015 * Examples for GCN.pages api: 1016 * 1017 * To get an array containing 1 page: 1018 * pages(1) 1019 * pages(1, function () {}) 1020 * 1021 * To get an array containing 2 pages: 1022 * pages([1, 2]) 1023 * pages([1, 2], function () {}) 1024 * 1025 * To get an array containing any and all pages: 1026 * pages() 1027 * pages(function () {}) 1028 * 1029 * To get an array containing no pages: 1030 * pages([]) 1031 * pages([], function () {}); 1032 * 1033 * @param {Chainback} clazz The Chainback class we want to expose. 1034 * @throws UNKNOWN_ARGUMENT 1035 */ 1036 GCN.exposeAPI = function (clazz) { 1037 return function () { 1038 // Convert arguments into an array 1039 // https://developer.mozilla.org/en/JavaScript/Reference/... 1040 // ...Functions_and_function_scope/arguments 1041 var args = Array.prototype.slice.call(arguments); 1042 var id; 1043 var ids; 1044 var success; 1045 var error; 1046 var settings; 1047 1048 // iterate over arguments to find id || ids, succes, error and 1049 // settings 1050 jQuery.each(args, function (i, arg) { 1051 switch (jQuery.type(arg)) { 1052 // set id 1053 case 'string': 1054 case 'number': 1055 if (!id && !ids) { 1056 id = arg; 1057 } else { 1058 GCN.error('UNKNOWN_ARGUMENT', 1059 'id is already set. Don\'t know what to do with ' + 1060 'arguments[' + i + '] value: "' + arg + '"'); 1061 } 1062 break; 1063 // set ids 1064 case 'array': 1065 if (!id && !ids) { 1066 ids = args[0]; 1067 } else { 1068 GCN.error('UNKNOWN_ARGUMENT', 1069 'ids is already set. Don\'t know what to do with' + 1070 ' arguments[' + i + '] value: "' + arg + '"'); 1071 } 1072 break; 1073 // success and error handlers 1074 case 'function': 1075 if (!success) { 1076 success = arg; 1077 } else if (success && !error) { 1078 error = arg; 1079 } else { 1080 GCN.error('UNKNOWN_ARGUMENT', 1081 'success and error handler already set. Don\'t ' + 1082 'know what to do with arguments[' + i + ']'); 1083 } 1084 break; 1085 // settings 1086 case 'object': 1087 if (!id && !ids) { 1088 id = arg; 1089 } else if (!settings) { 1090 settings = arg; 1091 } else { 1092 GCN.error('UNKNOWN_ARGUMENT', 1093 'settings are already present. Don\'t know what ' + 1094 'to do with arguments[' + i + '] value:' + ' "' + 1095 arg + '"'); 1096 } 1097 break; 1098 default: 1099 GCN.error('UNKNOWN_ARGUMENT', 1100 'Don\'t know what to do with arguments[' + i + 1101 '] value: "' + arg + '"'); 1102 } 1103 }); 1104 1105 // Prepare a new set of arguments to pass on during initialzation 1106 // of callee object. 1107 args = []; 1108 1109 // settings should always be an object, even if it's just empty 1110 if (!settings) { 1111 settings = {}; 1112 } 1113 1114 args[0] = (typeof id !== 'undefined') ? id : ids; 1115 args[1] = success || settings.success || null; 1116 args[2] = error || settings.error || null; 1117 args[3] = settings; 1118 1119 var hash = (id || ids) 1120 ? clazz._makeHash(ids ? ids.sort().join(',') : id) 1121 : null; 1122 1123 return GCN.getChainback(clazz, hash, null, args); 1124 }; 1125 1126 }; 1127 1128 }(GCN)); 1129