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 * Abstract class that is implemented by tag containers such as 189 * {@link PageAPI} or {@link TemplateAPI} 190 * 191 * @class 192 * @name TagContainerAPI 193 */ 194 GCN.TagContainerAPI = GCN.defineChainback({ 195 /** @lends TagContainerAPI */ 196 197 /** 198 * @private 199 * @type {object<number, string>} Hash, mapping tag ids to their 200 * corresponding names. 201 */ 202 _tagIdToNameMap: null, 203 204 /** 205 * @private 206 * @type {object<number, string>} Hash, mapping tag ids to their 207 * corresponding names for newly created 208 * tags. 209 */ 210 _createdTagIdToNameMap: {}, 211 212 /** 213 * @private 214 * @type {Array.<object>} A set of blocks that are are to be removed 215 * from this content object when saving it. 216 * This array is populated during the save 217 * process. It get filled just before 218 * persisting the data to the server, and gets 219 * emptied as soon as the save operation 220 * succeeds. 221 */ 222 _deletedBlocks: [], 223 224 /** 225 * @private 226 * @type {Array.<object>} A set of tags that are are to be removed from 227 * from this content object when it is saved. 228 */ 229 _deletedTags: [], 230 231 /** 232 * Searching for a tag of a given id from the object structure that is 233 * returned by the REST API would require O(N) time. This function, 234 * builds a hash that maps the tag id with its corresponding name, so 235 * that it can be mapped in O(1) time instead. 236 * 237 * @private 238 * @return {object<number,string>} A hash map where the key is the tag 239 * id, and the value is the tag name. 240 */ 241 '!_mapTagIdsToNames': function () { 242 var name; 243 var map = {}; 244 var tags = this._data.tags; 245 for (name in tags) { 246 if (tags.hasOwnProperty(name)) { 247 map[tags[name].id] = name; 248 } 249 } 250 return map; 251 }, 252 253 /** 254 * Retrieves data for a tag from the internal data object. 255 * 256 * @private 257 * @param {string} name The name of the tag. 258 * @return {!object} The tag data, or null if a there if no tag 259 * matching the given name. 260 */ 261 '!_getTagData': function (name) { 262 return (this._data.tags && this._data.tags[name]) || 263 (this._shadow.tags && this._shadow.tags[name]); 264 }, 265 266 /** 267 * Get the tag whose id is `id'. 268 * Builds the `_tagIdToNameMap' hash map if it doesn't already exist. 269 * 270 * @todo: Should we deprecate this? 271 * @private 272 * @param {number} id Id of tag to retrieve. 273 * @return {object} The tag's data. 274 */ 275 '!_getTagDataById': function (id) { 276 if (!this._tagIdToNameMap) { 277 this._tagIdToNameMap = this._mapTagIdsToNames(); 278 } 279 return this._getTagData(this._tagIdToNameMap[id] || 280 this._createdTagIdToNameMap[id]); 281 }, 282 283 /** 284 * Extracts the editables and blocks that have been rendered from the 285 * REST API render call's response data. 286 * 287 * @param {object} data The response object received from the 288 * renderTemplate() call. 289 * @return {object} An object containing two properties: an array of 290 * blocks, and an array of editables. 291 */ 292 '!_processRenderedTags': function (data) { 293 return this._getEditablesAndBlocks(data); 294 }, 295 296 // !!! 297 // WARNING adding to folder is neccessary as jsdoc will report a 298 // name confict otherwise 299 // !!! 300 /** 301 * Get this content object's node. 302 * 303 * @function 304 * @name node 305 * @memberOf TagContainerAPI 306 * @param {funtion(NodeAPI)=} success Optional callback to receive a 307 * {@link NodeAPI} object as the 308 * only argument. 309 * @param {function(GCNError):boolean=} error Optional custom error 310 * handler. 311 * @return {NodeAPI} This object's node. 312 */ 313 '!node': function (success, error) { 314 return this.folder().node(); 315 }, 316 317 // !!! 318 // WARNING adding to folder is neccessary as jsdoc will report a 319 // name confict otherwise 320 // !!! 321 /** 322 * Get this content object's parent folder. 323 * 324 * @function 325 * @name folder 326 * @memberOf TagContainerAPI 327 * @param {funtion(FolderAPI)=} 328 * success Optional callback to receive a {@link FolderAPI} 329 * object as the only argument. 330 * @param {function(GCNError):boolean=} 331 * error Optional custom error handler. 332 * @return {FolderAPI} This object's parent folder. 333 */ 334 '!folder': function (success, error) { 335 var id = this._fetched ? this.prop('folderId') : null; 336 return this._continue(GCN.FolderAPI, id, success, error); 337 }, 338 339 /** 340 * Gets a tag of the specified id, contained in this content object. 341 * 342 * @name tag 343 * @function 344 * @memberOf TagContainerAPI 345 * @param {function} success 346 * @param {function} error 347 * @return TagAPI 348 */ 349 '!tag': function (id, success, error) { 350 return this._continue(GCN.TagAPI, id, success, error); 351 }, 352 353 /** 354 * Retrieves a collection of tags from this content object. 355 * 356 * @name tags 357 * @function 358 * @memberOf TagContainerAPI 359 * @param {object|string|number} settings (Optional) 360 * @param {function} success callback 361 * @param {function} error (Optional) 362 * @return TagContainerAPI 363 */ 364 '!tags': function () { 365 var args = Array.prototype.slice.call(arguments); 366 367 if (args.length === 0) { 368 return; 369 } 370 371 var i; 372 var j = args.length; 373 var filter = {}; 374 var filters; 375 var hasFilter = false; 376 var success; 377 var error; 378 379 // Determine `success', `error', `filter' 380 for (i = 0; i < j; ++i) { 381 switch (jQuery.type(args[i])) { 382 case 'function': 383 if (success) { 384 error = args[i]; 385 } else { 386 success = args[i]; 387 } 388 break; 389 case 'number': 390 case 'string': 391 filters = [args[i]]; 392 break; 393 case 'array': 394 filters = args[i]; 395 break; 396 default: 397 return; 398 } 399 } 400 401 if (jQuery.type(filters) === 'array') { 402 var k = filters.length; 403 while (k) { 404 filter[filters[--k]] = true; 405 } 406 hasFilter = true; 407 } 408 409 var that = this; 410 411 if (success) { 412 this._read(function () { 413 var tags = that._data.tags; 414 var tag; 415 var list = []; 416 417 for (tag in tags) { 418 if (tags.hasOwnProperty(tag)) { 419 if (!hasFilter || filter[tag]) { 420 list.push(that._continue(GCN.TagAPI, tags[tag], 421 null, error)); 422 } 423 } 424 } 425 426 that._invoke(success, [list]); 427 }, error); 428 } 429 }, 430 431 /** 432 * Internal method to create a tag of a given tagtype in this content 433 * object. 434 * 435 * Not all tag containers allow for new tags to be created on them, 436 * therefore this method will only be surfaced by tag containers which 437 * do allow this. 438 * 439 * @private 440 * @param {string|number|object} construct either the keyword of the 441 * construct, or the ID of the construct 442 * or an object with the following 443 * properties 444 * <ul> 445 * <li><i>keyword</i> keyword of the construct</li> 446 * <li><i>constructId</i> ID of the construct</li> 447 * <li><i>magicValue</i> magic value to be filled into the tag</li> 448 * <li><i>sourcePageId</i> source page id</li> 449 * <li><i>sourceTagname</i> source tag name</li> 450 * </ul> 451 * @param {function(TagAPI)=} success Optional callback that will 452 * receive the newly created tag as 453 * its only argument. 454 * @param {function(GCNError):boolean=} error Optional custom error 455 * handler. 456 * @return {TagAPI} The newly created tag. 457 */ 458 '!_createTag': function () { 459 var args = parseCreateTagArguments(arguments); 460 461 if (args.error) { 462 GCN.handleError( 463 GCN.createError('INVALID_ARGUMENTS', args.error, arguments), 464 args.error 465 ); 466 return; 467 } 468 469 var obj = this; 470 471 // We use a uniqueId to avoid a fetus being created. 472 // This is to avoid the following scenario: 473 // 474 // var tag1 = container.createTag(...); 475 // var tag2 = container.createTag(...); 476 // tag1 === tag2 // is true which is wrong 477 // 478 // However, for all other cases, where we get an existing object, 479 // we want this behaviour: 480 // 481 // var folder1 = page(1).folder(...); 482 // var folder2 = page(1).folder(...); 483 // folder1 === folder2 // is true which is correct 484 // 485 // So, createTag() is different from other chainback methods in 486 // that each invokation must create a new instance, while other 487 // chainback methods must return the same. 488 // 489 // The id will be reset as soon as the tag object is realized. 490 // This happens below as soon as we get a success response with the 491 // correct tag id. 492 var newId = GCN.uniqueId('TagApi-unique-'); 493 494 // Create a new TagAPI instance linked to this tag container. Also 495 // acquire a lock on the newly created tag object so that any 496 // further operations on it will be queued until the tag object is 497 // fully realized. 498 var tag = obj._continue(GCN.TagAPI, newId)._procure(); 499 500 var options = args.options; 501 var copying = !!(options.sourcePageId && options.sourceTagname); 502 503 var onCreate = function () { 504 if (options.success) { 505 obj._invoke(options.success, [tag]); 506 } 507 tag._vacate(); 508 }; 509 510 if (copying) { 511 newTag(tag, { 512 copyPageId: options.sourcePageId, 513 copyTagname: options.sourceTagname 514 }, onCreate, options.error); 515 } else { 516 if (options.constructId) { 517 newTag(tag, { 518 magicValue: options.magicValue, 519 constructId: options.constructId 520 }, onCreate, options.error); 521 } else { 522 // ASSERT(options.keyword) 523 getConstruct(options.keyword, obj.node(), function (construct) { 524 newTag(tag, { 525 magicValue: options.magicValue, 526 constructId: construct.constructId 527 }, onCreate, options.error); 528 }, options.error); 529 } 530 } 531 532 return tag; 533 }, 534 535 /** 536 * Internal helper method to handle the create tag response. 537 * 538 * @private 539 * @param {TagAPI} tag 540 * @param {object} response response object from the REST call 541 * @param {function(TagContainerAPI)=} success optional success handler 542 * @param {function(GCNError):boolean=} error optional error handler 543 */ 544 '!_handleCreateTagResponse': function (tag, response, success, error) { 545 var obj = this; 546 547 if (GCN.getResponseCode(response) === 'OK') { 548 var data = response.tag; 549 tag._name = data.name; 550 tag._data = data; 551 tag._fetched = true; 552 553 // The tag's id is still the temporary unique id that was given 554 // to it in _createTag(). We have to realize the tag so that 555 // it gets the correct id. The new id changes its hash, so it 556 // must also be removed and reinserted from the caches. 557 tag._removeFromTempCache(); 558 tag._setHash(data.id)._addToCache(); 559 560 // Add this tag into the tag's container `_shadow' object, and 561 // `_tagIdToNameMap hash'. 562 var shouldCreateObjectIfUndefined = true; 563 obj._update('tags.' + GCN.escapePropertyName(data.name), 564 data, error, shouldCreateObjectIfUndefined); 565 566 // TODO: We need to store the tag inside the `_data' object for 567 // now. A change should be made so that when containers are 568 // saved, the data in the `_shadow' object is properly 569 // transfered into the _data object. 570 obj._data.tags[data.name] = data; 571 572 if (!obj.hasOwnProperty('_createdTagIdToNameMap')) { 573 obj._createdTagIdToNameMap = {}; 574 } 575 576 obj._createdTagIdToNameMap[data.id] = data.name; 577 578 if (success) { 579 success(); 580 } 581 } else { 582 tag._die(GCN.getResponseCode(response)); 583 GCN.handleResponseError(response, error); 584 } 585 }, 586 587 /** 588 * Internal method to delete the specified tag from this content 589 * object. 590 * 591 * @private 592 * @param {string} id The id of the tag to be deleted. 593 * @param {function(TagContainerAPI)=} success Optional callback that 594 * receive this object as 595 * its only argument. 596 * @param {function(GCNError):boolean=} error Optional custom error 597 * handler. 598 */ 599 '!_removeTag': function (id, success, error) { 600 this.tag(id).remove(success, error); 601 }, 602 603 /** 604 * Internal method to delete a set of tags from this content object. 605 * 606 * @private 607 * @param {Array.<string>} ids The ids of the set of tags to be 608 * deleted. 609 * @param {function(TagContainerAPI)=} success Optional callback that 610 * receive this object as 611 * its only argument. 612 * @param {function(GCNError):boolean=} error Optional custom error 613 * handler. 614 */ 615 '!_removeTags': function (ids, success, error) { 616 var that = this; 617 this.tags(ids, function (tags) { 618 var j = tags.length; 619 while (j--) { 620 tags[j].remove(null, error); 621 } 622 if (success) { 623 that.save(success, error); 624 } 625 }, error); 626 }, 627 628 /** 629 * Given a data object received from a REST API "/rest/page/render" 630 * call maps the blocks and editables into a list of each. 631 * 632 * The set of blocks and the set of editables that are returned are not 633 * mutually exclusive--if a tag is determined to be both an editable 634 * and a block, it will be included in both sets. 635 * 636 * @param {object} data 637 * @return {object<string, Array.<object>>} A map containing a set of 638 * editables and a set of 639 * blocks. 640 */ 641 '!_getEditablesAndBlocks': function (data) { 642 if (!data || !data.tags) { 643 return { 644 blocks: [], 645 editables: [] 646 }; 647 } 648 649 var tag; 650 var tags = data.tags; 651 var blocks = []; 652 var editables = []; 653 var i; 654 var j; 655 656 for (i = 0; i < tags.length; i++) { 657 tag = tags[i]; 658 if (tag.editables) { 659 for (j = 0; j < tag.editables.length; j++) { 660 tag.editables[j].tagname = tag.tagname; 661 } 662 editables = editables.concat(tag.editables); 663 } 664 if (isBlock(tag)) { 665 blocks.push(tag); 666 } 667 } 668 669 return { 670 blocks: blocks, 671 editables: editables 672 }; 673 } 674 675 }); 676 677 }(GCN)); 678