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