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