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