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