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