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