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