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