1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * @class 7 * @name TagContainerAPI 8 */ 9 GCN.TagContainerAPI = GCN.defineChainback({ 10 /** @lends TagContainerAPI */ 11 12 /** 13 * @private 14 * @type {object<number, string>} Hash, mapping tag ids to their 15 * corresponding names. 16 */ 17 _tagIdToNameMap: null, 18 19 /** 20 * @private 21 * @type {object<number, string>} Hash, mapping tag ids to their 22 * corresponding names for newly created 23 * tags. 24 */ 25 _createdTagIdToNameMap: {}, 26 27 /** 28 * @private 29 * @type {Array.<object>} A set of blocks that are are to be removed 30 * from this content object when saving it. 31 * This array is populated during the save 32 * process. It get filled just before 33 * persisting the data to the server, and gets 34 * emptied as soon as the save operation 35 * succeeds. 36 */ 37 _deletedBlocks: [], 38 39 /** 40 * @private 41 * @type {Array.<object>} A set of tags that are are to be removed from 42 * from this content object when it is saved. 43 */ 44 _deletedTags: [], 45 46 /** 47 * Searching for a tag of a given id from the object structure that is 48 * returned by the REST API would require O(N) time. This function, 49 * builds a hash that maps the tag id with its corresponding name, so 50 * that it can be mapped in O(1) time instead. 51 * 52 * @private 53 * @return {object<number,string>} A hash map where the key is the tag 54 * id, and the value is the tag name. 55 */ 56 '!_mapTagIdsToNames': function () { 57 var name; 58 var map = {}; 59 var tags = this._data.tags; 60 for (name in tags) { 61 if (tags.hasOwnProperty(name)) { 62 map[tags[name].id] = name; 63 } 64 } 65 66 return map; 67 }, 68 69 /** 70 * Retrieves data for a tag from the internal data object. 71 * 72 * @private 73 * @param {string} name The name of the tag. 74 * @return {!object} The tag data, or null if a there if no tag 75 * matching the given name. 76 */ 77 '!_getTagData': function (name) { 78 return (this._data.tags && this._data.tags[name]) || 79 (this._shadow.tags && this._shadow.tags[name]); 80 }, 81 82 /** 83 * Get the tag whose id is `id'. 84 * Builds the `_tagIdToNameMap' hash map if it doesn't already exist. 85 * 86 * @todo: Should we deprecate this? 87 * @private 88 * @param {number} id Id of tag to retrieve. 89 * @return {object} The tag's data. 90 */ 91 '!_getTagDataById': function (id) { 92 if (!this._tagIdToNameMap) { 93 this._tagIdToNameMap = this._mapTagIdsToNames(); 94 } 95 96 return this._getTagData(this._tagIdToNameMap[id] || 97 this._createdTagIdToNameMap[id]); 98 }, 99 100 /** 101 * Extracts the editables and blocks that have been rendered. 102 * 103 * @param {object} data The response object received from the 104 * renderTemplate() call. 105 * @return {object} An object containing two properties: an array of 106 * blocks, and an array of editables. 107 */ 108 '!_processRenderedTags': function (data) { 109 var tags = this._getEditablesAndBlocks(data); 110 this._storeRenderedEditables(tags.editables); 111 this._storeRenderedBlocks(tags.blocks); 112 return tags; 113 }, 114 115 /** 116 * Get this content object's node. 117 * 118 * @public 119 * @function 120 * @name node 121 * @memberOf ContentObjectAPI 122 * @param {funtion(NodeAPI)=} success Optional callback to receive a 123 * {@link NodeAPI} object as the 124 * only argument. 125 * @param {function(GCNError):boolean=} error Optional custom error 126 * handler. 127 * @return {NodeAPI} This object's node. 128 */ 129 '!node': function (success, error) { 130 return this.folder().node(); 131 }, 132 133 /** 134 * Get this content object's parent folder. 135 * 136 * @public 137 * @function 138 * @name folder 139 * @memberOf ContentObjectAPI 140 * @param {funtion(FolderAPI)=} success Optional callback to receive a 141 * {@link FolderAPI} object as the 142 * only argument. 143 * @param {function(GCNError):boolean=} error Optional custom error 144 * handler. 145 * @return {FolderAPI} This object's parent folder. 146 */ 147 '!folder': function (success, error) { 148 var id = this._fetched ? this.prop('folderId') : null; 149 return this._continue(GCN.FolderAPI, id, success, error); 150 }, 151 152 /** 153 * Gets a tag of the specified id, contained in this content object. 154 * 155 * @name tag 156 * @function 157 * @memberOf TagContainerAPI 158 * @param {function} success 159 * @param {function} error 160 * @return TagAPI 161 */ 162 '!tag': function (id, success, error) { 163 return this._continue(GCN.TagAPI, id, success, error); 164 }, 165 166 /** 167 * Retrieves a collection of tags from this content object. 168 * 169 * @name tags 170 * @function 171 * @memberOf TagContainerAPI 172 * @param {object|string|number} settings (Optional) 173 * @param {function} success callback 174 * @param {function} error (Optional) 175 * @return TagContainerAPI 176 */ 177 '!tags': function () { 178 var args = Array.prototype.slice.call(arguments); 179 180 if (args.length === 0) { 181 return; 182 } 183 184 var i; 185 var j = args.length; 186 var filter = {}; 187 var filters; 188 var hasFilter = false; 189 var success; 190 var error; 191 192 // Determine `success', `error', `filter' 193 for (i = 0; i < j; ++i) { 194 switch (jQuery.type(args[i])) { 195 case 'function': 196 if (success) { 197 error = args[i]; 198 } else { 199 success = args[i]; 200 } 201 break; 202 case 'number': 203 case 'string': 204 filters = [args[i]]; 205 break; 206 case 'array': 207 filters = args[i]; 208 break; 209 default: 210 return; 211 } 212 } 213 214 if (jQuery.type(filters) === 'array') { 215 var k = filters.length; 216 while (k) { 217 filter[filters[--k]] = true; 218 } 219 hasFilter = true; 220 } 221 222 var that = this; 223 224 if (success) { 225 this._read(function () { 226 var tags = that._data.tags; 227 var tag; 228 var list = []; 229 230 for (tag in tags) { 231 if (tags.hasOwnProperty(tag)) { 232 if (!hasFilter || filter[tag]) { 233 list.push(that._continue(GCN.TagAPI, tags[tag], 234 null, error)); 235 } 236 } 237 } 238 239 that._invoke(success, [list]); 240 }, error); 241 } 242 }, 243 244 /** 245 * Internal method to create a tag of a given tagtype in this content 246 * object. 247 * 248 * @private 249 * @param {string|number} construct The name of the construct on which 250 * the tag to be created should be 251 * derived from. Or the id of that 252 * construct. 253 * @param {string=} magicValue Optional property that will override the 254 * default values of this tag type. 255 * @param {function(TagAPI)=} success Optional callback that will 256 * receive the newly created tag as 257 * its only argument. 258 * @param {function(GCNError):boolean=} error Optional custom error 259 * handler. 260 * @return {TagAPI} The newly created tag. 261 */ 262 '!_createTag': function () { 263 var args = Array.prototype.slice.call(arguments); 264 265 if (args.length === 0) { 266 GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' + 267 'least one argument. See documentation.'); 268 269 return; 270 } 271 272 var success; 273 var error; 274 var magicValue; 275 var construct = args[0]; 276 var i; 277 var j = args.length; 278 279 // Determine `success', `error', and `magicValue'. 280 for (i = 1; i < j; ++i) { 281 switch (jQuery.type(args[i])) { 282 case 'string': 283 magicValue = args[i]; 284 break; 285 case 'function': 286 if (success) { 287 error = args[i]; 288 } else { 289 success = args[i]; 290 } 291 break; 292 } 293 } 294 295 var that = this; 296 297 // We use a uniqueId to avoid a fetus being created. 298 // This is to avoid the following szenario: 299 // 300 // var tag1 = container.createTag(...); 301 // var tag2 = container.createTag(...); 302 // tag1 === tag2 // is true which is wrong 303 // 304 // However, for all other cases, where we get an existing object, we want this behaviour: 305 // var folder1 = page(1).folder(...); 306 // var folder2 = page(1).folder(...); 307 // folder1 === folder2 // is true which is correct 308 // 309 // So, createTag() is different from other chainback methods 310 // in that each invokation must create a new instance, while 311 // other chainback methods must return the same. 312 // 313 // The id will be reset as soon as the tag object is 314 // realized. This happens below as soon as we get a success 315 // response with the correct tag id. 316 var newId = GCN.uniqueId('TagApi-unique-'); 317 318 // First create a new TagAPI instance that will have this container 319 // as its ancestor. Also acquire a lock on the newly created tag 320 // object so that any further operations on it will be queued until 321 // we release the lock. 322 var tag = this._continue(GCN.TagAPI, newId)._procure(); 323 324 this.node().constructs(function (constructs) { 325 var constructId; 326 327 if ('number' === jQuery.type(construct)) { 328 constructId = construct; 329 } else if (constructs[construct]) { 330 constructId = constructs[construct].constructId; 331 } else { 332 var err = GCN.createError('CONSTRUCT_NOT_FOUND', 333 'Cannot find constuct `' + construct + '\'', 334 constructs); 335 336 GCN.handleError(err, error); 337 338 return; 339 } 340 341 var restUrl = GCN.settings.BACKEND_PATH + '/rest/' 342 + that._type + '/newtag/' + that._data.id + '?' 343 + jQuery.param({constructId: constructId}); 344 345 that._authAjax({ 346 type : 'POST', 347 url : restUrl, 348 json : {magicValue: magicValue}, 349 error : function (xhr, status, msg) { 350 GCN.handleHttpError(xhr, msg, error); 351 tag._vacate(); 352 }, 353 success: function (response) { 354 if (GCN.getResponseCode(response) === 'OK') { 355 var data = response.tag; 356 357 tag._name = data.name; 358 tag._data = data; 359 tag._fetched = true; 360 361 // The tag's id is still newId from 362 // above. We have to realize the tag so that 363 // it gets the correct id. The new id 364 // changes its hash, so it must also be 365 // removed and reinserted from the caches. 366 tag._removeFromTempCache(); 367 tag._setHash(data.id)._addToCache(); 368 369 // Add this tag into the tag's container `_shadow' 370 // object, and `_tagIdToNameMap hash'. 371 372 var shouldCreateObjectIfUndefined = true; 373 374 // Add this tag into the `_shadow' object. 375 that._update('tags.' + GCN.escapePropertyName(data.name), 376 data, error, shouldCreateObjectIfUndefined); 377 378 // TODO: We need to store the tag inside the 379 // `_data' object for now. A change should be made 380 // so that when containers are saved, the data in 381 // the _shadow object is properly transfered into 382 // the _data object. 383 384 that._data.tags[data.name] = data; 385 386 if (!that.hasOwnProperty('_createdTagIdToNameMap')) { 387 that._createdTagIdToNameMap = {}; 388 } 389 390 that._createdTagIdToNameMap[data.id] = data.name; 391 392 if (success) { 393 that._invoke(success, [tag]); 394 } 395 } else { 396 tag._die(GCN.getResponseCode(response)); 397 GCN.handleResponseError(response, error); 398 } 399 400 // Hold onto the mutex until this tag object has been 401 // fully realized and placed inside its container. 402 tag._vacate(); 403 } 404 }); 405 }, error); 406 407 return tag; 408 }, 409 410 /** 411 * Internal method to delete the specified tag from this content 412 * object. 413 * 414 * @private 415 * @param {string} id The id of the tag to be deleted. 416 * @param {function(TagContainerAPI)=} success Optional callback that 417 * receive this object as 418 * its only argument. 419 * @param {function(GCNError):boolean=} error Optional custom error 420 * handler. 421 */ 422 '!_removeTag': function (id, success, error) { 423 this.tag(id).remove(success, error); 424 }, 425 426 /** 427 * Internal method to delete a set of tags from this content object. 428 * 429 * @private 430 * @param {Array.<string>} ids The ids of the set of tags to be 431 * deleted. 432 * @param {function(TagContainerAPI)=} success Optional callback that 433 * receive this object as 434 * its only argument. 435 * @param {function(GCNError):boolean=} error Optional custom error 436 * handler. 437 */ 438 '!_removeTags': function (ids, success, error) { 439 var that = this; 440 441 this.tags(ids, function (tags) { 442 var j = tags.length; 443 444 while (j) { 445 tags[--j].remove(null, error); 446 } 447 448 if (success) { 449 that.save(success, error); 450 } 451 }, error); 452 }, 453 454 /** 455 * Given a data object received from a "/rest/page/render" call, map 456 * the blocks and editables into a list of each. 457 * 458 * Note that if a tag is both an editable and a block, it will be 459 * listed in both the blocks list and in the editables list. 460 * 461 * @param {object} data 462 * @return {object<string, Array.<object>>} A map containing a set of 463 * editables and a set of 464 * blocks. 465 */ 466 '!_getEditablesAndBlocks': function (data) { 467 if (!data || !data.tags) { 468 return { 469 blocks: [], 470 editables: [] 471 }; 472 } 473 474 var tag; 475 var tags = data.tags; 476 var numTags = tags.length; 477 var numEditables; 478 var blocks = []; 479 var editables = []; 480 var isBlock; 481 var i; 482 var j; 483 484 for (i = 0; i < numTags; i++) { 485 tag = tags[i]; 486 487 if (tag.editables) { 488 numEditables = tag.editables.length; 489 for (j = 0; j < numEditables; j++) { 490 tag.editables[j].tagname = tag.tagname; 491 } 492 editables = editables.concat(tag.editables); 493 } 494 495 /* 496 * Depending on tag.onlyeditables to determine whether or not a 497 * given tag is a block or editable is unreliable since it is 498 * possible to have a block which only contains editables: 499 * 500 * { 501 * "tagname":"content", 502 * "editables":[{ 503 * "element":"GENTICS_EDITABLE_1234", 504 * "readonly":false, 505 * "partname":"editablepart" 506 * }], 507 * "element":"GENTICS_BLOCK_1234", 508 * "onlyeditables":true 509 * "tagname":"tagwitheditable" 510 * } 511 * 512 * in the above example, tag.onlyeditable is true but tag is a 513 * block, since the tag's element and the editable's element 514 * are not the same. 515 */ 516 517 if (!tag.editables || tag.editables.length > 1) { 518 isBlock = true; 519 } else { 520 isBlock = (1 === tag.editables.length) 521 && (tag.editables[0].element !== tag.element); 522 } 523 524 if (isBlock) { 525 blocks.push(tag); 526 } 527 } 528 529 return { 530 blocks: blocks, 531 editables: editables 532 }; 533 } 534 535 }); 536 537 }(GCN)); 538