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