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