1 /*global window: true, GCN: true, jQuery: true*/ 2 (function (GCN) { 3 4 'use strict'; 5 6 /** 7 * Searches for the an Aloha editable object of the given id. 8 * 9 * @TODO: Once Aloha.getEditableById() is patched to not cause an 10 * JavaScript exception if the element for the given ID is not found 11 * then we can deprecate this function and use Aloha's instead. 12 * 13 * @static 14 * @param {string} id Id of Aloha.Editable object to find. 15 * @return {Aloha.Editable=} The editable object, if wound; otherwise null. 16 */ 17 function getAlohaEditableById(id) { 18 var Aloha = (typeof window !== 'undefined') && window.Aloha; 19 if (!Aloha) { 20 return null; 21 } 22 23 // If the element is a textarea then route to the editable div. 24 var element = jQuery('#' + id); 25 if (element.length && 26 element[0].nodeName.toLowerCase() === 'textarea') { 27 id += '-aloha'; 28 } 29 30 var editables = Aloha.editables; 31 var j = editables.length; 32 while (j) { 33 if (editables[--j].getId() === id) { 34 return editables[j]; 35 } 36 } 37 38 return null; 39 } 40 41 /** 42 * Helper function to normalize the arguments that can be passed to the 43 * `edit()' and `render()' methods. 44 * 45 * @private 46 * @static 47 * @param {arguments} args A list of arguments. 48 * @return {object} Object containing an the properties `element', 49 * `success' and `error', and `data'. 50 */ 51 function getRenderOptions(args) { 52 var argv = Array.prototype.slice.call(args); 53 var argc = args.length; 54 var arg; 55 var i; 56 57 var element; 58 var success; 59 var error; 60 var prerenderedData = false; 61 62 for (i = 0; i < argc; ++i) { 63 arg = argv[i]; 64 65 switch (jQuery.type(arg)) { 66 case 'string': 67 element = jQuery(arg); 68 break; 69 case 'object': 70 if (element) { 71 prerenderedData = arg; 72 } else { 73 element = arg; 74 } 75 break; 76 case 'function': 77 if (success) { 78 error = arg; 79 } else { 80 success = arg; 81 } 82 break; 83 // Descarding all other types of arguments... 84 } 85 } 86 87 return { 88 element : element, 89 success : success, 90 error : error, 91 data : prerenderedData 92 }; 93 } 94 95 /** 96 * Exposes an API to operate on a Content.Node tag. 97 * 98 * @class 99 * @name TagAPI 100 */ 101 var TagAPI = GCN.defineChainback({ 102 103 __chainbacktype__: 'TagAPI', 104 105 /** 106 * Type of the object 107 * 108 * @type {string} 109 */ 110 _type: 'tag', 111 112 /** 113 * A reference to the object in which this tag is contained. This value 114 * is set during initialization. 115 * 116 * @type {GCN.ContentObject} 117 */ 118 _parent: null, 119 120 /** 121 * Name of this tag. 122 * 123 * @type {string} 124 */ 125 _name: null, 126 127 /** 128 * Gets this tag's information from the object that contains it. 129 * 130 * @param {function(TagAPI)} success Callback to be invoked when this 131 * operation completes normally. 132 * @param {function(GCNError):boolean} error Custom error handler. 133 */ 134 '!_read': function (success, error) { 135 var parent = this.parent(); 136 // Because tags always retrieve their data from a parent object, 137 // this tag is only completely fetched if it's parent is also fetch. 138 // The parent could have been cleared of all it's data using 139 // _clearCache() while this tag was left in a _fetched state, so we 140 // need to check. 141 if (this._fetched && parent._fetched) { 142 if (success) { 143 this._invoke(success, [this]); 144 } 145 return; 146 } 147 148 // Because when loading folders via folder(1).folders() will 149 // fetch them without any tag data. We therefore have to refetch 150 // them wit their tag data. 151 if (parent._fetched && !parent._data.tags) { 152 parent._data.tags = {}; 153 parent.fetch(function (response) { 154 if (GCN.getResponseCode(response) !== 'OK') { 155 GCN.handleResponseError(response); 156 return; 157 } 158 var newTags = {}; 159 jQuery.each( 160 response[parent._type].tags, 161 function (name, data) { 162 if (!GCN.TagContainerAPI.hasTagData(parent, name)) { 163 newTags[name] = data; 164 } 165 } 166 ); 167 GCN.TagContainerAPI.extendTags(parent, newTags); 168 parent._read(success, error); 169 }); 170 return; 171 } 172 173 var that = this; 174 175 // Take the data for this tag from it's container. 176 parent._read(function () { 177 that._data = parent._getTagData(that._name); 178 179 if (!that._data) { 180 var err = GCN.createError('TAG_NOT_FOUND', 181 'Could not find tag "' + that._name + '" in ' + 182 parent._type + " " + parent._data.id, that); 183 GCN.handleError(err, error); 184 return; 185 } 186 187 that._fetched = true; 188 189 if (success) { 190 that._invoke(success, [that]); 191 } 192 }, error); 193 }, 194 195 /** 196 * Retrieve the object in which this tag is contained. It does so by 197 * getting this chainback's "chainlink ancestor" object. 198 * 199 * @function 200 * @name parent 201 * @memberOf TagAPI 202 * @return {GCN.AbstractTagContainer} 203 */ 204 '!parent': function () { 205 return this._ancestor(); 206 }, 207 208 /** 209 * Initialize a tag object. Unlike other chainback objects, tags will 210 * always have a parent. If its parent have been loaded, we will 211 * immediately copy the this tag's data from the parent's `_data' object 212 * to the tag's `_data' object. 213 * 214 * @param {string|object} 215 * settings 216 * @param {function(TagAPI)} 217 * success Callback to be invoked when this operation 218 * completes normally. 219 * @param {function(GCNError):boolean} 220 * error Custom error handler. 221 */ 222 _init: function (settings, success, error) { 223 if (jQuery.type(settings) === 'object') { 224 this._name = settings.name; 225 this._data = settings; 226 this._data.id = settings.id; 227 this._fetched = true; 228 } else { 229 // We don't want to reinitalize the data object when it 230 // has not been fetched yet. 231 if (!this._fetched) { 232 this._data = {}; 233 this._data.id = this._name = settings; 234 } 235 } 236 237 if (success) { 238 var that = this; 239 240 this._read(function (container) { 241 that._read(success, error); 242 }, error); 243 244 // Even if not success callback is given, read this tag's data from 245 // is container, if that container has the data available. 246 // If we are initializing a placeholder tag object (in the process 247 // of creating brand new tag, for example), then its parent 248 // container will not have any data for this tag yet. We know that 249 // we are working with a placeholder tag if no `_data.id' or `_name' 250 // property is set. 251 } else if (!this._fetched && this._name && 252 this.parent()._fetched) { 253 this._data = this.parent()._getTagData(this._name); 254 this._fetched = !!this._data; 255 256 // We are propably initializing a placholder object, we will assign 257 // it its own `_data' and `_fetched' properties so that it is not 258 // accessing the prototype values. 259 } else if (!this._fetched) { 260 this._data = {}; 261 this._data.id = this._name = settings; 262 this._fetched = false; 263 } 264 }, 265 266 /** 267 * Gets or sets a property of this tags. Note that tags do not have a 268 * `_shadow' object, and we update the `_data' object directly. 269 * 270 * @function 271 * @name prop 272 * @memberOf TagAPI 273 * @param {string} 274 * name Name of tag part. 275 * @param {*=} 276 * set Optional value. If provided, the tag part will be 277 * replaced with this value. 278 * @return {*} The value of the accessed tag part. 279 * @throws UNFETCHED_OBJECT_ACCESS 280 */ 281 '!prop': function (name, value) { 282 var parent = this.parent(); 283 284 if (!this._fetched) { 285 GCN.error('UNFETCHED_OBJECT_ACCESS', 286 'Calling method `prop()\' on an unfetched object: ' + 287 parent._type + " " + parent._data.id, this); 288 289 return; 290 } 291 292 if (jQuery.type(value) !== 'undefined') { 293 this._data[name] = value; 294 parent._update('tags.' + GCN.escapePropertyName(this.prop('name')), 295 this._data); 296 } 297 298 return this._data[name]; 299 }, 300 301 /** 302 * <p> 303 * Gets or sets a part of a tag. 304 * 305 * <p> 306 * There exists different types of tag parts, and the possible value of 307 * each kind of tag part may differ. 308 * 309 * <p> 310 * Below is a list of possible kinds of tag parts, and references to 311 * what the possible range their values can take: 312 * 313 * <pre> 314 * STRING : {@link TagParts.STRING} 315 * RICHTEXT : {@link TagParts.RICHTEXT} 316 * BOOLEAN : {@link TagParts.BOOLEAN} 317 * IMAGE : {@link TagParts.IMAGE} 318 * FILE : {@link TagParts.FILE} 319 * FOLDER : {@link TagParts.FOLDER} 320 * PAGE : {@link TagParts.PAGE} 321 * OVERVIEW : {@link TagParts.OVERVIEW} 322 * PAGETAG : {@link TagParts.PAGETAG} 323 * TEMPLATETAG : {@link TagParts.TEMPLATETAG} 324 * SELECT : {@link TagParts.SELECT} 325 * MULTISELECT : {@link TagParts.MULTISELECT} 326 * </pre> 327 * 328 * @function 329 * @name part 330 * @memberOf TagAPI 331 * 332 * @param {string} name Name of tag opart. 333 * @param {*=} value (optional) 334 * If provided, the tag part will be update with this 335 * value. How this happens differs between different type 336 * of tag parts. 337 * @return {*} The value of the accessed tag part. Null if the part 338 * does not exist. 339 * @throws UNFETCHED_OBJECT_ACCESS 340 */ 341 '!part': function (name, value) { 342 if (!this._fetched) { 343 var parent = this.parent(); 344 345 GCN.error( 346 'UNFETCHED_OBJECT_ACCESS', 347 'Calling method `prop()\' on an unfetched object: ' 348 + parent._type + " " + parent._data.id, 349 this 350 ); 351 352 return null; 353 } 354 355 var part = this._data.properties[name]; 356 357 if (!part) { 358 return null; 359 } 360 361 if (jQuery.type(value) === 'undefined') { 362 return GCN.TagParts.get(part); 363 } 364 365 var partValue = GCN.TagParts.set(part, value); 366 367 // Each time we perform a write operation on a tag, we will update 368 // the tag in the tag container's `_shadow' object as well. 369 this.parent()._update( 370 'tags.' + GCN.escapePropertyName(this._name), 371 this._data 372 ); 373 374 return partValue; 375 }, 376 377 /** 378 * Returns a list of all of this tag's parts. 379 * 380 * @function 381 * @memberOf TagAPI 382 * @name parts 383 * @param {string} name 384 * @return {Array.<string>} 385 */ 386 '!parts': function (name) { 387 var parts = []; 388 jQuery.each(this._data.properties, function (key) { 389 parts.push(key); 390 }); 391 return parts; 392 }, 393 394 /** 395 * Remove this tag from its containing object (it's parent). 396 * 397 * @function 398 * @memberOf TagAPI 399 * @name remove 400 * @param {function} callback A function that receive this tag's parent 401 * object as its only arguments. 402 */ 403 remove: function (success, error) { 404 var parent = this.parent(); 405 406 if (!parent.hasOwnProperty('_deletedTags')) { 407 parent._deletedTags = []; 408 } 409 410 GCN.pub('tag.before-deleted', {tag: this}); 411 412 parent._deletedTags.push(this._name); 413 414 if (parent._data.tags && 415 parent._data.tags[this._name]) { 416 delete parent._data.tags[this._name]; 417 } 418 419 if (parent._shadow.tags && 420 parent._shadow.tags[this._name]) { 421 delete parent._shadow.tags[this._name]; 422 } 423 424 parent._removeAssociatedTagData(this._name); 425 426 this._clearCache(); 427 428 if (success) { 429 parent._persist(null, success, error); 430 } 431 }, 432 433 /** 434 * Given a DOM element, will generate a template which represents this 435 * tag as it would be if rendered in the element. 436 * 437 * @param {jQuery.<HTMLElement>} $element DOM element with which to 438 * generate the template. 439 * @return {string} Template string. 440 */ 441 '!_makeTemplate': function ($element) { 442 if (0 === $element.length) { 443 return '<node ' + this._name + '>'; 444 } 445 var placeholder = 446 '-{(' + this.parent().id() + ':' + this._name + ')}-'; 447 var template = jQuery.trim( 448 $element.clone().html(placeholder)[0].outerHTML 449 ); 450 return template.replace(placeholder, '<node ' + this._name + '>'); 451 }, 452 453 /** 454 * Will render this tag in the given render `mode'. If an element is 455 * provided, the content will be placed in that element. If the `mode' 456 * is "edit", any rendered editables will be initialized for Aloha 457 * Editor. Any editable that are rendered into an element will also be 458 * added to the tag's parent object's `_editables' array so that they 459 * can have their changed contents copied back into their corresponding 460 * tags during saving. 461 * 462 * @param {string} mode The rendering mode. Valid values are "view", 463 * and "edit". 464 * @param {jQuery.<HTMLElement>} element DOM element into which the 465 * the rendered content should be 466 * placed. 467 * @param {function(string, TagAPI, object)} Optional success handler. 468 * @param {function(GCNError):boolean} Optional custom error handler. 469 */ 470 '!_render': function (mode, $element, success, error) { 471 var tag = this._fork(); 472 tag._read(function () { 473 var template = ($element && $element.length) 474 ? tag._makeTemplate($element) 475 : '<node ' + tag._name + '>'; 476 477 var obj = tag.parent(); 478 479 obj._renderTemplate(template, mode, function (data) { 480 481 // Because the parent content object needs to track any 482 // blocks or editables that have been rendered in this tag. 483 obj._processRenderedTags(data); 484 485 GCN._handleContentRendered(data.content, tag, 486 function (html) { 487 if ($element && $element.length) { 488 GCN.renderOnto($element, html); 489 // Because 'content-inserted' is deprecated by 490 // 'tag.inserted'. 491 GCN.pub('content-inserted', [$element, html]); 492 GCN.pub('tag.inserted', [$element, html]); 493 } 494 495 var frontendEditing = function (callback) { 496 if ('edit' === mode) { 497 // Because 'rendered-for-editing' is deprecated by 498 // 'tag.rendered-for-editing'. 499 GCN.pub('rendered-for-editing', { 500 tag: tag, 501 data: data, 502 callback: callback 503 }); 504 GCN.pub('tag.rendered-for-editing', { 505 tag: tag, 506 data: data, 507 callback: callback 508 }); 509 } else if (callback) { 510 callback(); 511 } 512 }; 513 514 // Because the caller of edit() my wish to do things 515 // in addition to, or instead of, our frontend 516 // initialization. 517 if (success) { 518 tag._invoke( 519 success, 520 [html, tag, data, frontendEditing] 521 ); 522 } else { 523 frontendEditing(); 524 } 525 526 tag._merge(); 527 }); 528 }, function () { 529 tag._merge(); 530 }); 531 }, error); 532 }, 533 534 /** 535 * <p> 536 * Render the tag based on its settings on the server. Can be called 537 * with the following arguments:<(p> 538 * 539 * <pre> 540 * // Render tag contents into div whose id is "content-div" 541 * render('#content-div') or render(jQuery('#content-div')) 542 * </pre> 543 * 544 * <pre> 545 * // Pass the html rendering of the tag in the given callback 546 * render(function(html, tag) { 547 * // implementation! 548 * }) 549 * </pre> 550 * 551 * Whenever a 2nd argument is provided, it will be taken as as custom 552 * error handler. Invoking render() without any arguments will yield no 553 * results. 554 * 555 * @function 556 * @name render 557 * @memberOf TagAPI 558 * @param {string|jQuery.HTMLElement} 559 * selector jQuery selector or jQuery target element to be 560 * used as render destination 561 * @param {function(string, 562 * GCN.TagAPI)} success success function that will receive 563 * the rendered html as well as the TagAPI object 564 */ 565 render: function () { 566 var tag = this; 567 var args = arguments; 568 jQuery(function () { 569 args = getRenderOptions(args); 570 if (args.element || args.success) { 571 tag._render( 572 'view', 573 args.element, 574 args.success, 575 args.error 576 ); 577 } 578 }); 579 }, 580 581 /** 582 * <p> 583 * Renders this tag for editing. 584 * </p> 585 * 586 * <p> 587 * Differs from the render() method in that it calls this tag to be 588 * rendered in "edit" mode via the REST API so that it is rendered with 589 * any additional content that is appropriate for when this tag is used 590 * in edit mode. 591 * </p> 592 * 593 * <p> 594 * The GCN JS API library will also start keeping track of various 595 * aspects of this tag and its rendered content. 596 * </p> 597 * 598 * <p> 599 * When a jQuery selector is passed to this method, the contents of the 600 * rendered tag will overwrite the element identified by that selector. 601 * All rendered blocks and editables will be automatically placed into 602 * the DOM and initialize for editing. 603 * </p> 604 * 605 * <p> 606 * The behavior is different when this method is called with a function 607 * as its first argument. In this case the rendered contents of the tag 608 * will not be autmatically placed into the DOM, but will be passed onto 609 * the callback function as argmuments. It is then up to the caller to 610 * place the content into the DOM and initialize all rendered blocks and 611 * editables appropriately. 612 * </p> 613 * 614 * @function 615 * @name edit 616 * @memberOf TagAPI 617 * @param {(string|jQuery.HTMLElement)=} element 618 * The element into which this tag is to be rendered. 619 * @param {function(string,TagAPI)=} success 620 * A function that will be called once the tag is rendered. 621 * @param {function(GCNError):boolean=} error 622 * A custom error handler. 623 */ 624 edit: function () { 625 var tag = this; 626 var args = getRenderOptions(arguments); 627 if (args.data) { 628 629 // Because the parent content object needs to track any 630 // blocks or editables that have been rendered in this tag. 631 tag.parent()._processRenderedTags(args.data); 632 633 // Because 'rendered-for-editing' is deprecated in favor of 634 // 'tag.rendered-for-editing' 635 GCN.pub('rendered-for-editing', { 636 tag: tag, 637 data: args.data, 638 callback: function () { 639 if (args.success) { 640 tag._invoke( 641 args.success, 642 [args.content, tag, args.data] 643 ); 644 } 645 } 646 }); 647 GCN.pub('tag.rendered-for-editing', { 648 tag: tag, 649 data: args.data, 650 callback: function () { 651 if (args.success) { 652 tag._invoke( 653 args.success, 654 [args.content, tag, args.data] 655 ); 656 } 657 } 658 }); 659 } else { 660 jQuery(function () { 661 if (args.element || args.success) { 662 tag._render( 663 'edit', 664 args.element, 665 args.success, 666 args.error 667 ); 668 } 669 }); 670 } 671 }, 672 673 /** 674 * Persists the changes to this tag on its container object. Will only 675 * save this one tag and not affect the container object itself. 676 * Important: be careful when dealing with editable contents - these 677 * will be reloaded from Aloha Editor editables when a page is saved 678 * and thus overwrite changes you made to an editable tag. 679 * 680 * @function 681 * @name save 682 * @memberOf TagAPI 683 * @param {object=} settings Optional settings to pass on to the ajax 684 * function. 685 * @param {function(TagAPI)} success Callback to be invoked when this 686 * operation completes normally. 687 * @param {function(GCNError):boolean} error Custom error handler. 688 */ 689 save: function (settings, success, error) { 690 var tag = this; 691 var parent = tag.parent(); 692 var type = parent._type; 693 // to support the optional setting object as first argument we need 694 // to shift the arguments when it is not an object 695 if (jQuery.type(settings) !== 'object') { 696 error = success; 697 success = settings; 698 settings = null; 699 } 700 var json = settings || {}; 701 // create a mockup object to be able to save only one tag 702 // id is needed - REST API won't accept objects without id 703 json[type] = { id: parent.id(), tags: {} }; 704 json[type].tags[tag._name] = tag._data; 705 706 parent._authAjax({ 707 url : GCN.settings.BACKEND_PATH + '/rest/' + type + '/save/' 708 + parent.id() + GCN._getChannelParameter(parent), 709 type : 'POST', 710 error : error, 711 json : json, 712 success : function onTagSaveSuccess(response) { 713 if (GCN.getResponseCode(response) === 'OK') { 714 tag._invoke(success, [tag]); 715 } else { 716 tag._die(GCN.getResponseCode(response)); 717 GCN.handleResponseError(response, error); 718 } 719 } 720 }); 721 }, 722 723 /** 724 * TagAPI Objects are not cached themselves. Their _data object 725 * always references a tag in the _data of their parent, so that 726 * changes made to the TagAPI object will also change the tag in the 727 * _data of the parent. 728 * If the parent is reloaded and the _data refreshed, this would not 729 * clear or refresh the cache of the TagAPI objects. This would lead 730 * to a "broken" references and changes made to the cached TagAPI object 731 * would no longer change the tag in the parent. 732 * 733 * @private 734 * @return {Chainback} This Chainback. 735 */ 736 _addToCache: function () { 737 return this; 738 } 739 }); 740 741 // Unlike content objects, tags do not have unique ids and so we uniquely I 742 // dentify tags by their name, and their parent's id. 743 TagAPI._needsChainedHash = true; 744 745 GCN.tag = GCN.exposeAPI(TagAPI); 746 GCN.TagAPI = TagAPI; 747 748 }(GCN)); 749