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