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 // First create a new TagAPI instance that will have this container 299 // as its ancestor. Also acquire a lock on the newly created tag 300 // object so that any further operations on it will be queued until 301 // we release the lock. 302 var tag = this._continue(GCN.TagAPI)._procure(); 303 304 this.node().constructs(function (constructs) { 305 var constructId; 306 307 if ('number' === jQuery.type(construct)) { 308 constructId = construct; 309 } else if (constructs[construct]) { 310 constructId = constructs[construct].constructId; 311 } else { 312 var err = GCN.createError('CONSTRUCT_NOT_FOUND', 313 'Cannot find constuct `' + construct + '\'', 314 constructs); 315 316 GCN.handleError(err, error); 317 318 return; 319 } 320 321 var restUrl = GCN.settings.BACKEND_PATH + '/rest/' 322 + that._type + '/newtag/' + that._data.id + '?' 323 + jQuery.param({constructId: constructId}); 324 325 that._authAjax({ 326 type : 'POST', 327 url : restUrl, 328 json : {magicValue: magicValue}, 329 error : function (xhr, status, msg) { 330 GCN.handleHttpError(xhr, msg, error); 331 tag._vacate(); 332 }, 333 success: function (response) { 334 if (GCN.getResponseCode(response) === 'OK') { 335 var data = response.tag; 336 337 tag._data.id = data.id; 338 tag._name = data.name; 339 tag._data = data; 340 tag._fetched = true; 341 342 // Add this tag into the tag's container `_shadow' 343 // object, and `_tagIdToNameMap hash'. 344 345 var shouldCreateObjectIfUndefined = true; 346 347 // Add this tag into the `_shadow' object. 348 that._update('tags.' + data.name, data, error, 349 shouldCreateObjectIfUndefined); 350 351 // TODO: We need to store the tag inside the 352 // `_data' object for now. A change should be made 353 // so that when containers are saved, the data in 354 // the _shadow object is properly transfered into 355 // the _data object. 356 357 that._data.tags[data.name] = data; 358 359 if (!that.hasOwnProperty('_createdTagIdToNameMap')) { 360 that._createdTagIdToNameMap = {}; 361 } 362 363 that._createdTagIdToNameMap[data.id] = data.name; 364 365 if (success) { 366 success(tag); 367 } 368 } else { 369 tag._die(GCN.getResponseCode(response)); 370 GCN.handleResponseError(response, error); 371 } 372 373 // Hold onto the mutex until this tag object has been 374 // fully realized and placed inside its container. 375 tag._vacate(); 376 } 377 }); 378 }, error); 379 380 return tag; 381 }, 382 383 /** 384 * Internal method to delete the specified tag from this content 385 * object. 386 * 387 * @private 388 * @param {string} id The id of the tag to be deleted. 389 * @param {function(TagContainerAPI)=} success Optional callback that 390 * receive this object as 391 * its only argument. 392 * @param {function(GCNError):boolean=} error Optional custom error 393 * handler. 394 */ 395 '!_removeTag': function (id, success, error) { 396 this.tag(id).remove(success, error); 397 }, 398 399 /** 400 * Internal method to delete a set of tags from this content object. 401 * 402 * @private 403 * @param {Array.<string>} ids The ids of the set of tags to be 404 * deleted. 405 * @param {function(TagContainerAPI)=} success Optional callback that 406 * receive this object as 407 * its only argument. 408 * @param {function(GCNError):boolean=} error Optional custom error 409 * handler. 410 */ 411 '!_removeTags': function (ids, success, error) { 412 var that = this; 413 414 this.tags(ids, function (tags) { 415 var j = tags.length; 416 417 while (j) { 418 tags[--j].remove(null, error); 419 } 420 421 if (success) { 422 that.save(success, error); 423 } 424 }, error); 425 }, 426 427 /** 428 * Given a data object received from a "/rest/page/render" call, map 429 * the blocks and editables into a list of each. 430 * 431 * Note that if a tag is both an editable and a block, it will be 432 * listed in both the blocks list and in the editables list. 433 * 434 * @param {object} data 435 * @return {object<string, Array.<object>>} A map containing a set of 436 * editables and a set of 437 * blocks. 438 */ 439 '!_getEditablesAndBlocks': function (data) { 440 if (!data || !data.tags) { 441 return { 442 blocks: [], 443 editables: [] 444 }; 445 } 446 447 var tag; 448 var tags = data.tags; 449 var numTags = tags.length; 450 var numEditables; 451 var blocks = []; 452 var editables = []; 453 var isBlock; 454 var i; 455 var j; 456 457 for (i = 0; i < numTags; i++) { 458 tag = tags[i]; 459 460 if (tag.editables) { 461 numEditables = tag.editables.length; 462 for (j = 0; j < numEditables; j++) { 463 tag.editables[j].tagname = tag.tagname; 464 } 465 editables = editables.concat(tag.editables); 466 } 467 468 /* 469 * Depending on tag.onlyeditables to determine whether or not a 470 * given tag is a block or editable is unreliable since it is 471 * possible to have a block which only contains editables: 472 * 473 * { 474 * "tagname":"content", 475 * "editables":[{ 476 * "element":"GENTICS_EDITABLE_1234", 477 * "readonly":false, 478 * "partname":"editablepart" 479 * }], 480 * "element":"GENTICS_BLOCK_1234", 481 * "onlyeditables":true 482 * "tagname":"tagwitheditable" 483 * } 484 * 485 * in the above example, tag.onlyeditable is true but tag is a 486 * block, since the tag's element and the editable's element 487 * are not the same. 488 */ 489 490 if (!tag.editables || tag.editables.length > 1) { 491 isBlock = true; 492 } else { 493 isBlock = (1 === tag.editables.length) 494 && (tag.editables[0].element !== tag.element); 495 } 496 497 if (isBlock) { 498 blocks.push(tag); 499 } 500 } 501 502 return { 503 blocks: blocks, 504 editables: editables 505 }; 506 } 507 508 }); 509 510 }(GCN)); 511