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 var id; 132 133 if (this._fetched) { 134 var folder = this.folder(null, error); 135 136 if (folder._fetched) { 137 id = folder.nodeId; 138 } 139 } 140 141 return this._continue(GCN.NodeAPI, id, success, error); 142 }, 143 144 /** 145 * Get this content object's parent folder. 146 * 147 * @public 148 * @function 149 * @name folder 150 * @memberOf ContentObjectAPI 151 * @param {funtion(FolderAPI)=} success Optional callback to receive a 152 * {@link FolderAPI} object as the 153 * only argument. 154 * @param {function(GCNError):boolean=} error Optional custom error 155 * handler. 156 * @return {FolderAPI} This object's parent folder. 157 */ 158 '!folder': function (success, error) { 159 var id = this._fetched ? this.prop('folderId') : null; 160 return this._continue(GCN.FolderAPI, id, success, error); 161 }, 162 163 /** 164 * Gets a tag of the specified id, contained in this content object. 165 * 166 * @name tag 167 * @function 168 * @memberOf TagContainerAPI 169 * @param {function} success 170 * @param {function} error 171 * @return TagAPI 172 */ 173 '!tag': function (id, success, error) { 174 return this._continue(GCN.TagAPI, id, success, error); 175 }, 176 177 /** 178 * Retrieves a collection of tags from this content object. 179 * 180 * @name tags 181 * @function 182 * @memberOf TagContainerAPI 183 * @param {object|string|number} settings (Optional) 184 * @param {function} success callback 185 * @param {function} error (Optional) 186 * @return TagContainerAPI 187 */ 188 '!tags': function () { 189 var args = Array.prototype.slice.call(arguments); 190 191 if (args.length === 0) { 192 return; 193 } 194 195 var i; 196 var j = args.length; 197 var filter = {}; 198 var filters; 199 var hasFilter = false; 200 var success; 201 var error; 202 203 // Determine `success', `error', `filter' 204 for (i = 0; i < j; ++i) { 205 switch (jQuery.type(args[i])) { 206 case 'function': 207 if (success) { 208 error = args[i]; 209 } else { 210 success = args[i]; 211 } 212 break; 213 case 'number': 214 case 'string': 215 filters = [args[i]]; 216 break; 217 case 'array': 218 filters = args[i]; 219 break; 220 default: 221 return; 222 } 223 } 224 225 if (jQuery.type(filters) === 'array') { 226 var k = filters.length; 227 while (k) { 228 filter[filters[--k]] = true; 229 } 230 hasFilter = true; 231 } 232 233 var that = this; 234 235 if (success) { 236 this._read(function () { 237 var tags = that._data.tags; 238 var tag; 239 var list = []; 240 241 for (tag in tags) { 242 if (tags.hasOwnProperty(tag)) { 243 if (!hasFilter || filter[tag]) { 244 list.push(that._continue(GCN.TagAPI, tags[tag], 245 null, error)); 246 } 247 } 248 } 249 250 success(list); 251 }, error); 252 } 253 }, 254 255 /** 256 * Internal method to create a tag of a given tagtype in this content 257 * object. 258 * 259 * @private 260 * @param {string|number} construct The name of the construct on which 261 * the tag to be created should be 262 * derived from. Or the id of that 263 * construct. 264 * @param {string=} magicValue Optional property that will override the 265 * default values of this tag type. 266 * @param {function(TagAPI)=} success Optional callback that will 267 * receive the newly created tag as 268 * its only argument. 269 * @param {function(GCNError):boolean=} error Optional custom error 270 * handler. 271 * @return {TagAPI} The newly created tag. 272 */ 273 '!_createTag': function () { 274 var args = Array.prototype.slice.call(arguments); 275 276 if (args.length === 0) { 277 GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' + 278 'least one argument. See documentation.'); 279 280 return; 281 } 282 283 var success; 284 var error; 285 var magicValue; 286 var construct = args[0]; 287 var i; 288 var j = args.length; 289 290 // Determine `success', `error', and `magicValue'. 291 for (i = 1; i < j; ++i) { 292 switch (jQuery.type(args[i])) { 293 case 'string': 294 magicValue = args[i]; 295 break; 296 case 'function': 297 if (success) { 298 error = args[i]; 299 } else { 300 success = args[i]; 301 } 302 break; 303 } 304 } 305 306 var that = this; 307 308 // First create a new TagAPI instance that will have this container 309 // as its ancestor. Also aquire a lock on the newly created tag 310 // object so that any further operations on it will be queued until 311 // we release the lock. 312 var tag = this._continue(GCN.TagAPI)._procure(); 313 314 this.node().constructs(function (constructs) { 315 var constructId; 316 317 if ('number' === jQuery.type(construct)) { 318 constructId = construct; 319 } else if (constructs[construct]) { 320 constructId = constructs[construct].constructId; 321 } else { 322 var err = GCN.createError('CONSTRUCT_NOT_FOUND', 323 'Cannot find constuct `' + construct + '\'', 324 constructs); 325 326 GCN.handleError(err, error); 327 328 return; 329 } 330 331 var restUrl = GCN.settings.BACKEND_PATH + '/rest/' 332 + that._type + '/newtag/' + that._data.id + '?' 333 + jQuery.param({constructId: constructId}); 334 335 that._authAjax({ 336 type : 'POST', 337 url : restUrl, 338 json : {magicValue: magicValue}, 339 error : function (xhr, status, msg) { 340 GCN.handleHttpError(xhr, msg, error); 341 tag._vacate(); 342 }, 343 success: function (response) { 344 if (GCN.getResponseCode(response) === 'OK') { 345 var data = response.tag; 346 347 tag._data.id = data.id; 348 tag._name = data.name; 349 tag._data = data; 350 tag._fetched = true; 351 352 // Add this tag into the tag's container `_shadow' 353 // object, and `_tagIdToNameMap hash'. 354 355 var shouldCreateObjectIfUndefined = true; 356 357 // Add this tag into the `_shadow' object. 358 that._update('tags.' + data.name, data, error, 359 shouldCreateObjectIfUndefined); 360 361 // TODO: We need to store the tag inside the 362 // `_data' object for now. A change should be made 363 // so that when containers are saved, the data in 364 // the _shadow object is properly transfered into 365 // the _data object. 366 367 that._data.tags[data.name] = data; 368 369 if (!that.hasOwnProperty('_createdTagIdToNameMap')) { 370 that._createdTagIdToNameMap = {}; 371 } 372 373 that._createdTagIdToNameMap[data.id] = data.name; 374 375 if (success) { 376 success(tag); 377 } 378 } else { 379 tag._die(GCN.getResponseCode(response)); 380 GCN.handleResponseError(response, error); 381 } 382 383 // Hold onto the mutex until this tag object has been 384 // fully realized and placed inside its container. 385 tag._vacate(); 386 } 387 }); 388 }, error); 389 390 return tag; 391 }, 392 393 /** 394 * Internal method to delete the specified tag from this content 395 * object. 396 * 397 * @private 398 * @param {string} id The id of the tag to be deleted. 399 * @param {function(TagContainerAPI)=} success Optional callback that 400 * receive this object as 401 * its only argument. 402 * @param {function(GCNError):boolean=} error Optional custom error 403 * handler. 404 */ 405 '!_removeTag': function (id, success, error) { 406 this.tag(id).remove(success, error); 407 }, 408 409 /** 410 * Internal method to delete a set of tags from this content object. 411 * 412 * @private 413 * @param {Array.<string>} ids The ids of the set of tags to be 414 * deleted. 415 * @param {function(TagContainerAPI)=} success Optional callback that 416 * receive this object as 417 * its only argument. 418 * @param {function(GCNError):boolean=} error Optional custom error 419 * handler. 420 */ 421 '!_removeTags': function (ids, success, error) { 422 var that = this; 423 424 this.tags(ids, function (tags) { 425 var j = tags.length; 426 427 while (j) { 428 tags[--j].remove(null, error); 429 } 430 431 if (success) { 432 that.save(success, error); 433 } 434 }, error); 435 }, 436 437 /** 438 * Given a data object received from a "/rest/page/render" call, map 439 * the blocks and editables into a list of each. 440 * 441 * Note that if a tag is both an editable and a block, it will be 442 * listed in both the blocks list and in the editables list. 443 * 444 * @param {object} data 445 * @return {object<string, Array.<object>>} A map containing a set of 446 * editables and a set of 447 * blocks. 448 */ 449 '!_getEditablesAndBlocks': function (data) { 450 if (!data || !data.tags) { 451 return { 452 blocks: [], 453 editables: [] 454 }; 455 } 456 457 var tag; 458 var tags = data.tags; 459 var j = tags.length; 460 var i; 461 var blocks = []; 462 var editables = []; 463 464 while (j) { 465 tag = tags[--j]; 466 467 if (tag.editables) { 468 i = tag.editables.length; 469 while (i) { 470 tag.editables[--i].tagname = tag.tagname; 471 } 472 editables = editables.concat(tag.editables); 473 } 474 475 if (!tag.onlyeditables) { 476 blocks.push(tag); 477 } 478 } 479 480 return { 481 blocks : blocks, 482 editables : editables 483 }; 484 } 485 486 }); 487 488 }(GCN)); 489