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