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 * @protected 14 * @type {object<number, string>} Hash, mapping tag ids to their 15 * corresponding names. 16 */ 17 _tagIdToNameMap: null, 18 19 /** 20 * @protected 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 * @protected 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 * @protected 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 * @protected 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 * @protected 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 * @protected 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 * Retreives a set of tags from this content object that match the 102 * names in the given array. 103 * 104 * @protected 105 * @param {Array.<string>} names A set of tag names. 106 * @return {Array.<objects>} Tag data objects. 107 */ 108 '!_getTagsData': function (names) { 109 if (!this._data.tags) { 110 return []; 111 } 112 113 var all = this._data.tags; 114 var j = names.length; 115 var tags = []; 116 117 if (j) { 118 if (all[names[--j]]) { 119 tags.push(all[names[j]]); 120 } 121 } 122 123 return tags; 124 }, 125 126 /** 127 * Get this content object's node. 128 * 129 * @public 130 * @function 131 * @name node 132 * @memberOf ContentObjectAPI 133 * @param {funtion(NodeAPI)=} success Optional callback to receive a 134 * {@link NodeAPI} object as the 135 * only argument. 136 * @param {function(GCNError):boolean=} error Optional custom error 137 * handler. 138 * @return {NodeAPI} This object's node. 139 */ 140 '!node': function (success, error) { 141 var id; 142 143 if (this._fetched) { 144 var folder = this.folder(null, error); 145 146 if (folder._fetched) { 147 id = folder.nodeId; 148 } 149 } 150 151 return this._continue(GCN.NodeAPI, id, success, error); 152 }, 153 154 /** 155 * Get this content object's parent folder. 156 * 157 * @public 158 * @function 159 * @name folder 160 * @memberOf ContentObjectAPI 161 * @param {funtion(FolderAPI)=} success Optional callback to receive a 162 * {@link FolderAPI} object as the 163 * only argument. 164 * @param {function(GCNError):boolean=} error Optional custom error 165 * handler. 166 * @return {FolderAPI} This object's parent folder. 167 */ 168 '!folder': function (success, error) { 169 var id = this._fetched ? this.prop('folderId') : null; 170 return this._continue(GCN.FolderAPI, id, success, error); 171 }, 172 173 /** 174 * Gets a tag of the specified id, contained in this content object. 175 * 176 * @name tag 177 * @function 178 * @memberOf TagContainerAPI 179 * @param {function} success 180 * @param {function} error 181 * @return TagAPI 182 */ 183 '!tag': function (id, success, error) { 184 return this._continue(GCN.TagAPI, id, success, error); 185 }, 186 187 /** 188 * Retrieves a collection of tags from this content object. 189 * 190 * @name tags 191 * @function 192 * @memberOf TagContainerAPI 193 * @param {object|string|number} settings (Optional) 194 * @param {function} success callback 195 * @param {function} error (Optional) 196 * @return TagContainerAPI 197 */ 198 '!tags': function () { 199 var args = Array.prototype.slice.call(arguments); 200 201 if (args.length === 0) { 202 return; 203 } 204 205 var i; 206 var j = args.length; 207 var filter = {}; 208 var filters; 209 var hasFilter = false; 210 var success; 211 var error; 212 213 // Determine `success', `error', `filter' 214 for (i = 0; i < j; ++i) { 215 switch (jQuery.type(args[i])) { 216 case 'function': 217 if (success) { 218 error = args[i]; 219 } else { 220 success = args[i]; 221 } 222 break; 223 case 'number': 224 case 'string': 225 filters = [args[i]]; 226 break; 227 case 'array': 228 filters = args[i]; 229 break; 230 default: 231 return; 232 } 233 } 234 235 if (jQuery.type(filters) === 'array') { 236 var k = filters.length; 237 while (k) { 238 filter[filters[--k]] = true; 239 } 240 hasFilter = true; 241 } 242 243 var that = this; 244 245 if (success) { 246 this._read(function () { 247 var tags = that._data.tags; 248 var tag; 249 var list = []; 250 251 for (tag in tags) { 252 if (tags.hasOwnProperty(tag)) { 253 if (!hasFilter || filter[tag]) { 254 list.push(that._continue(GCN.TagAPI, tags[tag], 255 null, error)); 256 } 257 } 258 } 259 260 success(list); 261 }, error); 262 } 263 }, 264 265 /** 266 * Internal method to create a tag of a given tagtype in this content 267 * object. 268 * 269 * @protected 270 * @param {string|number} construct The name of the construct on which 271 * the tag to be created should be 272 * derived from. Or the id of that 273 * construct. 274 * @param {string=} magicValue Optional property that will override the 275 * default values of this tag type. 276 * @param {function(TagAPI)=} success Optional callback that will 277 * receive the newly created tag as 278 * its only argument. 279 * @param {function(GCNError):boolean=} error Optional custom error 280 * handler. 281 * @return {TagAPI} The newly created tag. 282 */ 283 '!_createTag': function () { 284 var args = Array.prototype.slice.call(arguments); 285 286 if (args.length === 0) { 287 GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' + 288 'least one argument. See documentation.'); 289 290 return; 291 } 292 293 var success; 294 var error; 295 var magicValue; 296 var construct = args[0]; 297 var i; 298 var j = args.length; 299 300 // Determine `success', `error', and `magicValue'. 301 for (i = 1; i < j; ++i) { 302 switch (jQuery.type(args[i])) { 303 case 'string': 304 magicValue = args[i]; 305 break; 306 case 'function': 307 if (success) { 308 error = args[i]; 309 } else { 310 success = args[i]; 311 } 312 break; 313 } 314 } 315 316 var that = this; 317 318 // First create a new TagAPI instance that will have this container 319 // as its ancestor. Also aquire 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)._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._data.id = data.id; 358 tag._name = data.name; 359 tag._data = data; 360 tag._fetched = true; 361 362 // Add this tag into the tag's container `_shadow' 363 // object, and `_tagIdToNameMap hash'. 364 365 var shouldCreateObjectIfUndefined = true; 366 367 that._update('tags.' + data.name, data, error, 368 shouldCreateObjectIfUndefined); 369 370 // TODO: We need to store the tag inside the 371 // `_data' object for now. A change should be made 372 // so that when containers are saved, the data in 373 // the _shadow object is transfered into the _data 374 // object. 375 376 that._data.tags[data.name] = data; 377 378 if (!that.hasOwnProperty('_createdTagIdToNameMap')) { 379 that._createdTagIdToNameMap = {}; 380 } 381 382 that._createdTagIdToNameMap[data.id] = data.name; 383 384 if (success) { 385 success(tag); 386 } 387 } else { 388 tag._die(GCN.getResponseCode(response)); 389 GCN.handleResponseError(response, error); 390 } 391 392 // Hold onto the mutex until this tag object has been 393 // fully realized and placed inside its container. 394 tag._vacate(); 395 } 396 }); 397 }, error); 398 399 return tag; 400 }, 401 402 /** 403 * Internal method to delete the specified tag from this content 404 * object. 405 * 406 * @protected 407 * @param {string} id The id of the tag to be deleted. 408 * @param {function(TagContainerAPI)=} success Optional callback that 409 * receive this object as 410 * its only argument. 411 * @param {function(GCNError):boolean=} error Optional custom error 412 * handler. 413 */ 414 '!_removeTag': function (id, success, error) { 415 this.tag(id).remove(success, error); 416 }, 417 418 /** 419 * Internal method to delete a set of tags from this content object. 420 * 421 * @protected 422 * @param {Array.<string>} ids The ids of the set of tags to be 423 * deleted. 424 * @param {function(TagContainerAPI)=} success Optional callback that 425 * receive this object as 426 * its only argument. 427 * @param {function(GCNError):boolean=} error Optional custom error 428 * handler. 429 */ 430 '!_removeTags': function (ids, success, error) { 431 var that = this; 432 433 this.tags(ids, function (tags) { 434 var j = tags.length; 435 436 while (j) { 437 tags[--j].remove(null, error); 438 } 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 * @param {object} data 451 * @return {object<string, Array.<object>>} A map contain a set of 452 * editables and a set of 453 * blocks. 454 */ 455 '!_getEditablesAndBlocks': function (data) { 456 if (!data || !data.tags) { 457 return { 458 blocks: [], 459 editables: [] 460 }; 461 } 462 463 var tag; 464 var tags = data.tags; 465 var j = tags.length; 466 var i; 467 var blocks = []; 468 var editables = []; 469 470 while (j) { 471 tag = tags[--j]; 472 473 if (tag.editables) { 474 i = tag.editables.length; 475 while (i) { 476 tag.editables[--i].tagname = tag.tagname; 477 } 478 editables = editables.concat(tag.editables); 479 } 480 481 if (!tag.onlyeditables) { 482 blocks.push(tag); 483 } 484 } 485 486 return { 487 blocks : blocks, 488 editables : editables 489 }; 490 } 491 492 }); 493 494 }(GCN)); 495