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