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