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