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 * Initializes the contents that have been rendered into a given container 43 * element for frontend editing. 44 * 45 * @TODO: This function should be moved out of Gentics Content.Node 46 * JavaScript API. We should publish a message instead, and pass 47 * these arguments in the message. 48 * 49 * @private 50 * @static 51 * @param {Array.<object>} editables Editables to be `aloha()'fied. 52 * @param {Array.<object>} blocks Blocks to receive tagfill buttons. 53 * @param {number|string} pageId id of the page the tag belongs to. 54 * @param {function} callback 55 */ 56 function initializeFrontendEditing(editables, blocks, pageId, callback) { 57 var Aloha = (typeof window !== 'undefined') && window.Aloha; 58 if (!Aloha) { 59 return; 60 } 61 if (Aloha.GCN) { 62 Aloha.GCN.page = GCN.page(pageId); 63 Aloha.GCN.setupConstructsButton(pageId); 64 } 65 var editable; 66 var unmodified = []; 67 var j = editables && editables.length; 68 while (j--) { 69 Aloha.jQuery('#' + editables[j].element).aloha(); 70 editable = getAlohaEditableById(editables[j].element); 71 if (editable) { 72 unmodified.push(editable); 73 if (editables[j].readonly) { 74 editable.disable(); 75 } 76 } 77 } 78 if (Aloha.GCN) { 79 j = Aloha.editables.length; 80 while (j--) { 81 if (!Aloha.editables[j].isModified()) { 82 unmodified.push(Aloha.editables[j]); 83 } 84 } 85 Aloha.GCN.alohaBlocks(blocks, pageId, function () { 86 var j = unmodified.length; 87 while (j--) { 88 unmodified[j].setUnmodified(); 89 } 90 if (callback) { 91 callback(); 92 } 93 }); 94 } else if (callback) { 95 callback(); 96 } 97 } 98 99 /** 100 * Helper function to normalize the arguments that can be passed to the 101 * `edit()' and `render()' methods. 102 * 103 * @private 104 * @static 105 * @param {arguments} args A list of arguments. 106 * @return {object} Object containing an the properties `element', 107 * `success' and `error', and `data'. 108 */ 109 function getRenderOptions(args) { 110 var argv = Array.prototype.slice.call(args); 111 var argc = args.length; 112 var arg; 113 var i; 114 115 var element; 116 var success; 117 var error; 118 var prerenderedData = false; 119 120 for (i = 0; i < argc; ++i) { 121 arg = argv[i]; 122 123 switch (jQuery.type(arg)) { 124 case 'string': 125 element = jQuery(arg); 126 break; 127 case 'object': 128 if (element) { 129 prerenderedData = arg; 130 } else { 131 element = arg; 132 } 133 break; 134 case 'function': 135 if (success) { 136 error = arg; 137 } else { 138 success = arg; 139 } 140 break; 141 // Descarding all other types of arguments... 142 } 143 } 144 145 return { 146 element : element, 147 success : success, 148 error : error, 149 data : prerenderedData 150 }; 151 } 152 153 /** 154 * Exposes an API to operate on a Content.Node tag. 155 * 156 * @class 157 * @name TagAPI 158 */ 159 var TagAPI = GCN.defineChainback({ 160 161 __chainbacktype__: 'TagAPI', 162 163 /** 164 * A reference to the object in which this tag is contained. This value 165 * is set during initialization. 166 * 167 * @type {GCN.ContentObject} 168 */ 169 _parent: null, 170 171 /** 172 * Name of this tag. 173 * 174 * @type {string} 175 */ 176 _name: null, 177 178 /** 179 * Gets this tag's information from the object that contains it. 180 * 181 * @param {function(TagAPI)} success Callback to be invoked when this 182 * operation completes normally. 183 * @param {function(GCNError):boolean} error Custom error handler. 184 */ 185 '!_read': function (success, error) { 186 if (this._fetched) { 187 if (success) { 188 this._invoke(success, [this]); 189 } 190 return; 191 } 192 193 var that = this; 194 var parent = this.parent(); 195 196 // assert(parent) 197 198 // Take the data for this tag from it's container. 199 parent._read(function () { 200 that._data = parent._getTagData(that._name); 201 202 if (!that._data) { 203 var err = GCN.createError('TAG_NOT_FOUND', 204 'Could not find tag "' + that._name + '" in ' + 205 parent._type + " " + parent._data.id, that); 206 GCN.handleError(err, error); 207 return; 208 } 209 210 that._fetched = true; 211 212 if (success) { 213 that._invoke(success, [that]); 214 } 215 }, error); 216 }, 217 218 /** 219 * Retrieve the object in which this tag is contained. It does so by 220 * getting this chainback's "chainlink ancestor" object. 221 * 222 * @function 223 * @name parent 224 * @memberOf TagAPI 225 * @return {GCN.AbstractTagContainer} 226 */ 227 '!parent': function () { 228 return this._ancestor(); 229 }, 230 231 /** 232 * Initialize a tag object. Unlike other chainback objects, tags will 233 * always have a parent. If its parent have been loaded, we will 234 * immediately copy the this tag's data from the parent's `_data' object 235 * to the tag's `_data' object. 236 * 237 * @param {string|object} 238 * settings 239 * @param {function(TagAPI)} 240 * success Callback to be invoked when this operation 241 * completes normally. 242 * @param {function(GCNError):boolean} 243 * error Custom error handler. 244 */ 245 _init: function (settings, success, error) { 246 if (jQuery.type(settings) === 'object') { 247 this._name = settings.name; 248 this._data = settings; 249 this._data.id = settings.id; 250 this._fetched = true; 251 } else { 252 // We don't want to reinitalize the data object when it 253 // has not been fetched yet. 254 if (!this._fetched) { 255 this._data = {}; 256 this._data.id = this._name = settings; 257 } 258 } 259 260 if (success) { 261 var that = this; 262 263 this._read(function (container) { 264 that._read(success, error); 265 }, error); 266 267 // Even if not success callback is given, read this tag's data from 268 // is container, it that container has the data available. 269 // If we are initializing a placeholder tag object (in the process 270 // of creating brand new tag, for example), then its parent 271 // container will not have any data for this tag yet. We know that 272 // we are working with a placeholder tag if no `_data.id' or `_name' 273 // property is set. 274 } else if (!this._fetched && this._name && 275 this.parent()._fetched) { 276 this._data = this.parent()._getTagData(this._name); 277 this._fetched = !!this._data; 278 279 // We are propably initializing a placholder object, we will assign 280 // it its own `_data' and `_fetched' properties so that it is not 281 // accessing the prototype values. 282 } else if (!this._fetched) { 283 this._data = {}; 284 this._data.id = this._name = settings; 285 this._fetched = false; 286 } 287 }, 288 289 /** 290 * Gets or sets a property of this tags. Note that tags do not have a 291 * `_shadow' object, and we update the `_data' object directly. 292 * 293 * @function 294 * @name prop 295 * @memberOf TagAPI 296 * @param {string} 297 * name Name of tag part. 298 * @param {*=} 299 * set Optional value. If provided, the tag part will be 300 * replaced with this value. 301 * @return {*} The value of the accessed tag part. 302 * @throws UNFETCHED_OBJECT_ACCESS 303 */ 304 '!prop': function (name, value) { 305 var parent = this.parent(); 306 307 if (!this._fetched) { 308 GCN.error('UNFETCHED_OBJECT_ACCESS', 309 'Calling method `prop()\' on an unfetched object: ' + 310 parent._type + " " + parent._data.id, this); 311 312 return; 313 } 314 315 if (jQuery.type(value) !== 'undefined') { 316 this._data[name] = value; 317 parent._update('tags.' + GCN.escapePropertyName(name), 318 this._data); 319 } 320 321 return this._data[name]; 322 }, 323 324 /** 325 * <p> 326 * Gets or sets a part of a tag. 327 * 328 * <p> 329 * There exists different types of tag parts, and the possible value of 330 * each kind of tag part may differ. 331 * 332 * <p> 333 * Below is a list of possible kinds of tag parts, and references to 334 * what the possible range their values can take: 335 * 336 * <pre> 337 * STRING : {@link TagParts.STRING} 338 * RICHTEXT : {@link TagParts.RICHTEXT} 339 * BOOLEAN : {@link TagParts.BOOLEAN} 340 * IMAGE : {@link TagParts.IMAGE} 341 * FILE : {@link TagParts.FILE} 342 * FOLDER : {@link TagParts.FOLDER} 343 * PAGE : {@link TagParts.PAGE} 344 * OVERVIEW : {@link TagParts.OVERVIEW} 345 * PAGETAG : {@link TagParts.PAGETAG} 346 * TEMPLATETAG : {@link TagParts.TEMPLATETAG} 347 * SELECT : {@link TagParts.SELECT} 348 * MULTISELECT : {@link TagParts.MULTISELECT} 349 * </pre> 350 * 351 * @function 352 * @name part 353 * @memberOf TagAPI 354 * 355 * @param {string} 356 * name Name of tag opart. 357 * @param {*=} 358 * set Optional value. If provided, the tag part will be 359 * update with this value. How this happends differs between 360 * different type of tag parts. 361 * @return {*} The value of the accessed tag part. 362 * @throws UNFETCHED_OBJECT_ACCESS 363 * @throws PART_NOT_FOUND 364 */ 365 '!part': function (name, value) { 366 var parent; 367 368 if (!this._fetched) { 369 parent = this.parent(); 370 371 GCN.error('UNFETCHED_OBJECT_ACCESS', 372 'Calling method `prop()\' on an unfetched object: ' + 373 parent._type + " " + parent._data.id, this); 374 375 return null; 376 } 377 378 var part = this._data.properties[name]; 379 380 if (!part) { 381 parent = this.parent(); 382 383 GCN.error('PART_NOT_FOUND', 'Tag "' + this._name + 384 '" of ' + parent._type + ' ' + parent._data.id + 385 ' does not have a part "' + name + '"', this); 386 387 return null; 388 } 389 390 if (jQuery.type(value) === 'undefined') { 391 return GCN.TagParts.get(part); 392 } 393 394 var partValue = GCN.TagParts.set(part, value); 395 396 // Each time we perform a write operation on a tag, we will update 397 // the tag in the tag container's `_shadow' object as well. 398 this.parent()._update('tags.' + GCN.escapePropertyName(this._name), 399 this._data); 400 401 return partValue; 402 }, 403 404 /** 405 * Remove this tag from its containing object (it's parent). 406 * 407 * @function 408 * @memberOf TagAPI 409 * @name remove 410 * @param {function} callback A function that receive this tag's parent 411 * object as its only arguments. 412 */ 413 remove: function (success, error) { 414 var parent = this.parent(); 415 416 if (!parent.hasOwnProperty('_deletedTags')) { 417 parent._deletedTags = []; 418 } 419 420 parent._deletedTags.push(this._name); 421 422 if (parent._data.tags && 423 parent._data.tags[this._name]) { 424 delete parent._data.tags[this._name]; 425 } 426 427 if (parent._shadow.tags && 428 parent._shadow.tags[this._name]) { 429 delete parent._shadow.tags[this._name]; 430 } 431 432 parent._removeAssociatedTagData(this._name); 433 434 if (success) { 435 parent._persist(success, error); 436 } 437 }, 438 439 /** 440 * Given a DOM element, will generate a template which represents this 441 * tag as it would be if rendered in the element. 442 * 443 * @param {jQuery.<HTMLElement>} $element DOM element with which to 444 * generate the template. 445 * @return {string} Template string. 446 */ 447 '!_makeTemplate': function ($element) { 448 if (0 === $element.length) { 449 return '<node ' + this._name + '>'; 450 } 451 var placeholder = 452 '-{(' + this.parent().id() + ':' + this._name + ')}-'; 453 var template = jQuery.trim( 454 $element.clone().html(placeholder)[0].outerHTML 455 ); 456 return template.replace(placeholder, '<node ' + this._name + '>'); 457 }, 458 459 /** 460 * Will render this tag in the given render `mode'. If an element is 461 * provided, the content will be placed in that element. If the `mode' 462 * is "edit", any rendered editables will be initialized for Aloha 463 * Editor. Any editable that are rendered into an element will also be 464 * added to the tag's parent object's `_editables' array so that they 465 * can have their changed contents copied back into their corresponding 466 * tags during saving. 467 * 468 * @param {string} mode The rendering mode. Valid values are "view", 469 * and "edit". 470 * @param {jQuery.<HTMLElement>} element DOM element into which the 471 * the rendered content should be 472 * placed. 473 * @param {function(string, TagAPI, object)} Optional success handler. 474 * @param {function(GCNError):boolean} Optional custom error handler. 475 */ 476 '!_render': function (mode, $element, success, error) { 477 var tag = this; 478 479 tag._read(function () { 480 // Because no further operations are allowed on this tag until 481 // we the rendering process finished is completed on its parent 482 // content object. 483 tag._procure(); 484 485 var template = ($element && $element.length) 486 ? tag._makeTemplate($element) 487 : '<node ' + tag._name + '>'; 488 489 var contentObj = tag.parent(); 490 491 contentObj._renderTemplate(template, mode, function (data) { 492 493 // Because the parent content object needs to track any 494 // blocks or editables that have been rendered in this tag. 495 var tags = contentObj._processRenderedTags(data); 496 497 GCN._handleContentRendered(data.content, tag, 498 function (html) { 499 if ($element && $element.length) { 500 GCN.renderOnto($element, html); 501 GCN.pub('content-inserted', [$element, html]); 502 } 503 504 var frontendEditing = function (callback) { 505 if ('edit' === mode) { 506 initializeFrontendEditing(tags.editables, 507 tags.blocks, 508 contentObj.id(), 509 callback); 510 } else if (callback) { 511 callback(); 512 } 513 }; 514 515 // Because the caller of edit() my wish to do things 516 // in addition to, or instead of, our front end 517 // initialization. 518 if (success) { 519 tag._invoke(success, 520 [html, tag, data, frontendEditing]); 521 } else { 522 frontendEditing(); 523 } 524 525 // Because now, both the tag, and its content object 526 // are stable can on the tag object that were queued 527 // during the rendering process can now be 528 // initiatated. 529 tag._vacate(); 530 }); 531 }, function () { 532 tag._vacate(); 533 }); 534 }, error); 535 }, 536 537 /** 538 * <p> 539 * Render the tag based on its settings on the server. Can be called 540 * with the following arguments:<(p> 541 * 542 * <pre> 543 * // Render tag contents into div whose id is "content-div" 544 * render('#content-div') or render(jQuery('#content-div')) 545 * </pre> 546 * 547 * <pre> 548 * // Pass the html rendering of the tag in the given callback 549 * render(function(html, tag) { 550 * // implementation! 551 * }) 552 * </pre> 553 * 554 * Whenever a 2nd argument is provided, it will be taken as as custom 555 * error handler. Invoking render() without any arguments will yield no 556 * results. 557 * 558 * @function 559 * @name render 560 * @memberOf TagAPI 561 * @param {string|jQuery.HTMLElement} 562 * selector jQuery selector or jQuery target element to be 563 * used as render destination 564 * @param {function(string, 565 * GCN.TagAPI)} success success function that will receive 566 * the rendered html as well as the TagAPI object 567 */ 568 render: function () { 569 var that = this; 570 var args = arguments; 571 // Wait until DOM is ready 572 jQuery(function () { 573 args = getRenderOptions(args); 574 if (args.element || args.success) { 575 that._render('view', args.element, args.success, 576 args.error); 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 * @function 599 * @name edit 600 * @memberOf TagAPI 601 * @param {(string|jQuery.HTMLElement)=} element 602 * The element into which this tag is to be rendered. 603 * @param {function(string, 604 * TagAPI)=} success A function that will be called once the tag is 605 * rendered. 606 * @param {function(GCNError):boolean=} error 607 * A custom error handler. 608 */ 609 edit: function () { 610 var tag = this; 611 var args = getRenderOptions(arguments); 612 613 if (args.data) { 614 var parent = tag.parent(); 615 var tags = parent._processRenderedTags(args.data); 616 617 initializeFrontendEditing( 618 tags.editables, 619 tags.blocks, 620 parent.id(), 621 function () { 622 if (args.success) { 623 tag._invoke(args.success, 624 [args.content, tag, args.data]); 625 } 626 } 627 ); 628 } else { 629 630 // Because we need to wait until the DOM is ready before we can 631 // interact with DOM elements. 632 jQuery(function () { 633 if (args.element || args.success) { 634 tag._render( 635 'edit', 636 args.element, 637 args.success, 638 args.error 639 ); 640 } 641 }); 642 } 643 }, 644 645 /** 646 * Persists the changes to this tag on its container object. 647 * 648 * @function 649 * @name save 650 * @memberOf TagAPI 651 * @param {function(TagAPI)} success Callback to be invoked when this 652 * operation completes normally. 653 * @param {function(GCNError):boolean} error Custom error handler. 654 */ 655 save: function (success, error) { 656 var that = this; 657 this.parent().save(function () { 658 if (success) { 659 that._invoke(success, [that]); 660 } 661 }, error); 662 } 663 664 }); 665 666 // Unlike content objects, tags do not have unique ids and so we uniquely I 667 // dentify tags by their name, and their parent's id. 668 TagAPI._needsChainedHash = true; 669 670 GCN.tag = GCN.exposeAPI(TagAPI); 671 GCN.TagAPI = TagAPI; 672 673 }(GCN)); 674