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