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 * Initializes the contents that have been rendered into a given container 189 * element 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 597 tag._read(function () { 598 // Because no further operations are allowed on this tag until 599 // we the rendering process finished is completed on its parent 600 // content object. 601 tag._procure(); 602 603 var template = ($element && $element.length) 604 ? tag._makeTemplate($element) 605 : '<node ' + tag._name + '>'; 606 607 var contentObj = tag.parent(); 608 609 contentObj._renderTemplate(template, mode, function (data) { 610 611 // Because the parent content object needs to track any 612 // blocks or editables that have been rendered in this tag. 613 var tags = contentObj._processRenderedTags(data); 614 615 GCN._handleContentRendered(data.content, tag, 616 function (html) { 617 if ($element && $element.length) { 618 GCN.renderOnto($element, html); 619 GCN.pub('content-inserted', [$element, html]); 620 } 621 622 var frontendEditing = function (callback) { 623 if ('edit' === mode) { 624 initializeFrontendEditing(tags.editables, 625 tags.blocks, 626 contentObj.id(), 627 callback); 628 } else if (callback) { 629 callback(); 630 } 631 }; 632 633 // Because the caller of edit() my wish to do things 634 // in addition to, or instead of, our front end 635 // initialization. 636 if (success) { 637 tag._invoke(success, 638 [html, tag, data, frontendEditing]); 639 } else { 640 frontendEditing(); 641 } 642 643 // Because now, both the tag, and its content object 644 // are stable can on the tag object that were queued 645 // during the rendering process can now be 646 // initiatated. 647 tag._vacate(); 648 }); 649 }, function () { 650 tag._vacate(); 651 }); 652 }, error); 653 }, 654 655 /** 656 * <p> 657 * Render the tag based on its settings on the server. Can be called 658 * with the following arguments:<(p> 659 * 660 * <pre> 661 * // Render tag contents into div whose id is "content-div" 662 * render('#content-div') or render(jQuery('#content-div')) 663 * </pre> 664 * 665 * <pre> 666 * // Pass the html rendering of the tag in the given callback 667 * render(function(html, tag) { 668 * // implementation! 669 * }) 670 * </pre> 671 * 672 * Whenever a 2nd argument is provided, it will be taken as as custom 673 * error handler. Invoking render() without any arguments will yield no 674 * results. 675 * 676 * @function 677 * @name render 678 * @memberOf TagAPI 679 * @param {string|jQuery.HTMLElement} 680 * selector jQuery selector or jQuery target element to be 681 * used as render destination 682 * @param {function(string, 683 * GCN.TagAPI)} success success function that will receive 684 * the rendered html as well as the TagAPI object 685 */ 686 render: function () { 687 var that = this; 688 var args = arguments; 689 // Wait until DOM is ready 690 jQuery(function () { 691 args = getRenderOptions(args); 692 if (args.element || args.success) { 693 that._render('view', args.element, args.success, 694 args.error); 695 } 696 }); 697 }, 698 699 /** 700 * <p> 701 * Renders this tag for editing. 702 * </p> 703 * 704 * <p> 705 * Differs from the render() method in that it calls this tag to be 706 * rendered in "edit" mode via the REST API so that it is rendered with 707 * any additional content that is appropriate for when this tag is used 708 * in edit mode. 709 * </p> 710 * 711 * <p> 712 * The GCN JS API library will also start keeping track of various 713 * aspects of this tag and its rendered content. 714 * </p> 715 * 716 * @function 717 * @name edit 718 * @memberOf TagAPI 719 * @param {(string|jQuery.HTMLElement)=} element 720 * The element into which this tag is to be rendered. 721 * @param {function(string, 722 * TagAPI)=} success A function that will be called once the tag is 723 * rendered. 724 * @param {function(GCNError):boolean=} error 725 * A custom error handler. 726 */ 727 edit: function () { 728 var tag = this; 729 var args = getRenderOptions(arguments); 730 731 if (args.data) { 732 var parent = tag.parent(); 733 var tags = parent._processRenderedTags(args.data); 734 735 initializeFrontendEditing( 736 tags.editables, 737 tags.blocks, 738 parent.id(), 739 function () { 740 if (args.success) { 741 tag._invoke(args.success, 742 [args.content, tag, args.data]); 743 } 744 } 745 ); 746 } else { 747 748 // Because we need to wait until the DOM is ready before we can 749 // interact with DOM elements. 750 jQuery(function () { 751 if (args.element || args.success) { 752 tag._render( 753 'edit', 754 args.element, 755 args.success, 756 args.error 757 ); 758 } 759 }); 760 } 761 }, 762 763 /** 764 * Persists the changes to this tag on its container object. 765 * 766 * @function 767 * @name save 768 * @memberOf TagAPI 769 * @param {function(TagAPI)} success Callback to be invoked when this 770 * operation completes normally. 771 * @param {function(GCNError):boolean} error Custom error handler. 772 */ 773 save: function (success, error) { 774 var that = this; 775 this.parent().save(function () { 776 if (success) { 777 that._invoke(success, [that]); 778 } 779 }, error); 780 } 781 782 }); 783 784 // Unlike content objects, tags do not have unique ids and so we uniquely I 785 // dentify tags by their name, and their parent's id. 786 TagAPI._needsChainedHash = true; 787 788 GCN.tag = GCN.exposeAPI(TagAPI); 789 GCN.TagAPI = TagAPI; 790 791 }(GCN)); 792