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