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 {jQuery.<HTMLElement>} container The element that wraps the 201 * incoming tag contents. 202 */ 203 function initializeFrontendEditing(editables, blocks, pageId, container) { 204 var Aloha = (typeof window !== 'undefined') && window.Aloha; 205 if (!Aloha) { 206 return; 207 } 208 Aloha.ready(function () { 209 if (Aloha.GCN) { 210 Aloha.GCN.page = GCN.page(pageId); 211 Aloha.GCN.setupConstructsButton(pageId); 212 } 213 var j = editables && editables.length; 214 var editable; 215 var unmodified = []; 216 while (j--) { 217 Aloha.jQuery('#' + editables[j].element).aloha(); 218 editable = getAlohaEditableById(editables[j].element); 219 if (editable) { 220 unmodified.push(editable); 221 if (editables[j].readonly) { 222 editable.disable(); 223 } 224 } 225 } 226 if (Aloha.GCN) { 227 j = Aloha.editables.length; 228 while (j--) { 229 if (!Aloha.editables[j].isModified()) { 230 unmodified.push(Aloha.editables[j]); 231 } 232 } 233 Aloha.GCN.alohaBlocks(blocks, pageId, function () { 234 var j = unmodified.length; 235 while (j--) { 236 unmodified[j].setUnmodified(); 237 } 238 }); 239 } 240 }); 241 } 242 243 /** 244 * Helper function to normalize the arguments that can be passed to the 245 * `edit()' and `render()' methods. 246 * 247 * @private 248 * @static 249 * @param {arguments} args A list of arguments. 250 * @return {object} Object containing an the properties `element', 251 * `success' and `error', and `data'. 252 */ 253 function getRenderOptions(args) { 254 var argv = Array.prototype.slice.call(args); 255 var argc = args.length; 256 var arg; 257 var i; 258 259 var element; 260 var success; 261 var error; 262 var prerenderedData = false; 263 264 for (i = 0; i < argc; ++i) { 265 arg = argv[i]; 266 267 switch (jQuery.type(arg)) { 268 case 'string': 269 element = jQuery(arg); 270 break; 271 case 'object': 272 if (element) { 273 prerenderedData = arg; 274 } else { 275 element = arg; 276 } 277 break; 278 case 'function': 279 if (success) { 280 error = arg; 281 } else { 282 success = arg; 283 } 284 break; 285 // Descarding all other types of arguments... 286 } 287 } 288 289 return { 290 element : element, 291 success : success, 292 error : error, 293 data : prerenderedData 294 }; 295 } 296 297 /** 298 * Exposes an API to operate on a Content.Node tag. 299 * 300 * @class 301 * @name TagAPI 302 */ 303 var TagAPI = GCN.defineChainback({ 304 305 __chainbacktype__: 'TagAPI', 306 307 /** 308 * @type {GCN.ContentObject} A reference to the object in which this 309 * tag is contained. This value is set 310 * during initialization. 311 */ 312 _parent: null, 313 314 /** 315 * @type {string} Name of this tag. 316 */ 317 _name: null, 318 319 /** 320 * Gets this tag's information from the object that contains it. 321 * 322 * @param {function(TagAPI)} success Callback to be invoked when this 323 * operation completes normally. 324 * @param {function(GCNError):boolean} error Custom error handler. 325 */ 326 '!_read': function (success, error) { 327 if (this._fetched) { 328 if (success) { 329 this._invoke(success, [this]); 330 } 331 return; 332 } 333 334 var that = this; 335 var parent = this.parent(); 336 337 // assert(parent) 338 339 // Take the data for this tag from it's container. 340 parent._read(function () { 341 that._data = parent._getTagData(that._name); 342 343 if (!that._data) { 344 var err = GCN.createError('TAG_NOT_FOUND', 345 'Could not find tag "' + that._name + '" in ' + 346 parent._type + " " + parent._data.id, that); 347 GCN.handleError(err, error); 348 return; 349 } 350 351 that._fetched = true; 352 353 if (success) { 354 that._invoke(success, [that]); 355 } 356 }, error); 357 }, 358 359 /** 360 * Retrieve the object in which this tag is contained. It does so by 361 * getting this chainback's "chainlink ancestor" object. 362 * 363 * @return {GCN.AbstractTagContainer} 364 */ 365 '!parent': function () { 366 return this._ancestor(); 367 }, 368 369 /** 370 * Initialize a tag object. Unlike other chainback objects, tags will 371 * always have a parent. If its parent have been loaded, we will 372 * immediately copy the this tag's data from the parent's `_data' 373 * object to the tag's `_data' object. 374 * 375 * @param {string|object} settings 376 * @param {function(TagAPI)} success Callback to be invoked when this 377 * operation completes normally. 378 * @param {function(GCNError):boolean} error Custom error handler. 379 */ 380 _init: function (settings, success, error) { 381 if (jQuery.type(settings) === 'object') { 382 this._name = settings.name; 383 this._data = settings; 384 this._data.id = settings.id; 385 this._fetched = true; 386 } else { 387 // We don't want to reinitalize the data object when it 388 // has not been fetched yet. 389 if (!this._fetched) { 390 this._data = {}; 391 this._data.id = this._name = settings; 392 } 393 } 394 395 if (success) { 396 var that = this; 397 398 this._read(function (container) { 399 that._read(success, error); 400 }, error); 401 402 // Even if not success callback is given, read this tag's data from 403 // is container, it that container has the data available. 404 // If we are initializing a placeholder tag object (in the process 405 // of creating brand new tag, for example), then its parent 406 // container will not have any data for this tag yet. We know that 407 // we are working with a placeholder tag if no `_data.id' or `_name' 408 // property is set. 409 } else if (!this._fetched && this._name && 410 this.parent()._fetched) { 411 this._data = this.parent()._getTagData(this._name); 412 this._fetched = !!this._data; 413 414 // We are propably initializing a placholder object, we will assign 415 // it its own `_data' and `_fetched' properties so that it is not 416 // accessing the prototype values. 417 } else if (!this._fetched) { 418 this._data = {}; 419 this._data.id = this._name = settings; 420 this._fetched = false; 421 } 422 }, 423 424 /** 425 * Get or set a property of this tags. 426 * Note that tags do not have a `_shadow' object, and we update the 427 * `_data' object directly. 428 * 429 * @param {string} name Name of tag part. 430 * @param {*=} set Optional value. If provided, the tag part will be 431 * replaced with this value. 432 * @return {*} The value of the accessed tag part. 433 * @throws UNFETCHED_OBJECT_ACCESS 434 */ 435 '!prop': function (name, value) { 436 var parent = this.parent(); 437 438 if (!this._fetched) { 439 GCN.error('UNFETCHED_OBJECT_ACCESS', 440 'Calling method `prop()\' on an unfetched object: ' + 441 parent._type + " " + parent._data.id, this); 442 443 return; 444 } 445 446 if (jQuery.type(value) !== 'undefined') { 447 this._data[name] = value; 448 parent._update('tags.' + GCN.escapePropertyName(name), 449 this._data); 450 } 451 452 return this._data[name]; 453 }, 454 455 /** 456 * Get or set a part of this tags. 457 * 458 * @param {string} name Name of tag opart. 459 * @param {*=} set Optional value. If provided, the tag part will be 460 * replaced with this value. 461 * @return {*} The value of the accessed tag part. 462 * @throws UNFETCHED_OBJECT_ACCESS 463 * @throws PART_NOT_FOUND 464 */ 465 '!part': function (name, value) { 466 var parent; 467 468 if (!this._fetched) { 469 parent = this.parent(); 470 471 GCN.error('UNFETCHED_OBJECT_ACCESS', 472 'Calling method `prop()\' on an unfetched object: ' + 473 parent._type + " " + parent._data.id, this); 474 475 return null; 476 } 477 478 var part = this._data.properties[name]; 479 480 if (!part) { 481 parent = this.parent(); 482 483 GCN.error('PART_NOT_FOUND', 'Tag "' + this._name + 484 '" of ' + parent._type + ' ' + parent._data.id + 485 ' does not have a part "' + name + '"', this); 486 487 return null; 488 } 489 490 if (jQuery.type(value) === 'undefined') { 491 return getPartValue(part); 492 } 493 494 setPartValue(part, value); 495 496 // Each time we perform a write operation on a tag, we will update 497 // the tag in the tag container's `_shadow' object as well. 498 this.parent()._update('tags.' + GCN.escapePropertyName(this._name), 499 this._data); 500 501 return value; 502 }, 503 504 /** 505 * Remove this tag from its containing object (it's parent). 506 * 507 * @param {function} callback A function that receive this tag's parent 508 * object as its only arguments. 509 */ 510 remove: function (success, error) { 511 var parent = this.parent(); 512 513 if (!parent.hasOwnProperty('_deletedTags')) { 514 parent._deletedTags = []; 515 } 516 517 parent._deletedTags.push(this._name); 518 519 if (parent._data.tags && 520 parent._data.tags[this._name]) { 521 delete parent._data.tags[this._name]; 522 } 523 524 if (parent._shadow.tags && 525 parent._shadow.tags[this._name]) { 526 delete parent._shadow.tags[this._name]; 527 } 528 529 parent._removeAssociatedTagData(this._name); 530 531 if (success) { 532 parent._persist(success, error); 533 } 534 }, 535 536 /** 537 * Given a DOM element, will generate a template which represents this 538 * tag as it would be if rendered in the element. 539 * 540 * @param {jQuery.<HTMLElement>} $element DOM element with which to 541 * generate the template. 542 * @return {string} Template string. 543 */ 544 '!_makeTemplate': function ($element) { 545 if (0 === $element.length) { 546 return '<node ' + this._name + '>'; 547 } 548 var placeholder = '-{(' + this.parent().id() + ':' + this._name 549 + ')}-'; 550 var template = $element.clone().html(placeholder)[0].outerHTML; 551 return template.replace(placeholder, '<node ' + this._name + '>'); 552 }, 553 554 /** 555 * Will render this tag in the given render `mode'. If an element is 556 * provided, the content will be placed in that element. If the `mode' 557 * is "edit", any rendered editables will be initialized for Aloha 558 * Editor. Any editable that are rendered into an element will also be 559 * added to the tag's parent object's `_editables' array so that they 560 * can have their changed contents copied back into their corresponding 561 * tags during saving. 562 * 563 * @param {string} mode The rendering mode. Valid values are "view", 564 * and "edit". 565 * @param {jQuery.<HTMLElement>} element DOM element into which the 566 * the rendered content should be 567 * placed. 568 * @param {function(string, TagAPI, object)} Optional success handler. 569 * @param {function(GCNError):boolean} Optional custom error handler. 570 */ 571 '!_render': function (mode, $element, success, error) { 572 var that = this; 573 var parent = this.parent(); 574 this._read(function () { 575 that._procure(); 576 var template; 577 if ($element && $element.length) { 578 template = that._makeTemplate($element); 579 } else { 580 template = '<node ' + that._name + '>'; 581 } 582 parent._renderTemplate(template, mode, function (data) { 583 var tags = parent._processRenderedTags(data); 584 GCN._handleContentRendered(data.content, that, 585 function (html) { 586 if ($element) { 587 GCN.insertInnerHTMLWithScriptTags($element, html); 588 GCN.pub('content-inserted', [$element, html]); 589 } 590 if (success) { 591 that._invoke(success, [html, that, data]); 592 } 593 594 if (mode === 'edit') { 595 initializeFrontendEditing(tags.editables, 596 tags.blocks, parent.id(), $element); 597 } 598 that._vacate(); 599 }); 600 }, function () { 601 that._vacate(); 602 }); 603 }, error); 604 }, 605 606 /** 607 * Render the tag based on its settings on the server. 608 * Can be called with the following arguments: 609 * 610 * Do nothing: 611 * render() 612 * 613 * Render tag contents into div whose id is "content-div": 614 * @param {string|jQuery.<HTMLElement>} 615 * render('#content-div') or render(jQuery('#content-div')) 616 * 617 * Pass the html rendering of the tag in the given callback: 618 * @param {function(string, GCN.TagAPI)} 619 * render(function (html, tag) {}) 620 * 621 * Whenever a 2nd argument is provided, it will be taken as as custom 622 * error handler. 623 */ 624 render: function () { 625 var that = this; 626 var args = arguments; 627 // Wait until DOM is ready 628 jQuery(function () { 629 args = getRenderOptions(args); 630 if (args.element || args.success) { 631 that._render('view', args.element, args.success, 632 args.error); 633 } 634 }); 635 }, 636 637 /** 638 * Renders this tag for editing. 639 * 640 * Differs from the render() method in that it calls this tag to be 641 * rendered in "edit" mode via the REST API so that it is rendered with 642 * any additional content that is appropriate for when this tag is used 643 * in edit mode. 644 * 645 * The GCN JS API library will also start keeping track of various 646 * aspects of this tag and its rendered content. 647 * 648 * @param {(string|jQuery.<HTMLElement>)=} The element into which this 649 * tag is to be rendered. 650 * @param {function(string, TagAPI)=} A function that will be called 651 * once the tag is rendered. 652 * @param {function(GCNError):boolean=} A custom error handler. 653 */ 654 edit: function () { 655 var args = getRenderOptions(arguments); 656 if (args.data) { 657 var parent = this.parent(); 658 var tags = parent._processRenderedTags(args.data); 659 if (args.success) { 660 this._invoke(args.success, 661 [args.content, this, args.data]); 662 } 663 initializeFrontendEditing(tags.editables, tags.blocks, 664 parent.id()); 665 } else { 666 // Wait until DOM is ready 667 var that = this; 668 jQuery(function () { 669 if (args.element || args.success) { 670 that._render('edit', args.element, args.success, 671 args.error); 672 } 673 }); 674 } 675 }, 676 677 /** 678 * Persists the changes to this tag on its container object. 679 * 680 * @param {function(TagAPI)} success Callback to be invoked when this 681 * operation completes normally. 682 * @param {function(GCNError):boolean} error Custom error handler. 683 */ 684 save: function (success, error) { 685 var that = this; 686 this.parent().save(function () { 687 if (success) { 688 that._invoke(success, [that]); 689 } 690 }, error); 691 } 692 693 }); 694 695 // Unlike content objects, tags do not have unique ids and so we uniquely I 696 // dentify tags by their name, and their parent's id. 697 TagAPI._needsChainedHash = true; 698 699 GCN.tag = GCN.exposeAPI(TagAPI); 700 GCN.TagAPI = TagAPI; 701 702 }(GCN)); 703