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