1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * @class
  7 	 * @name TagContainerAPI
  8 	 */
  9 	GCN.TagContainerAPI = GCN.defineChainback({
 10 		/** @lends TagContainerAPI */
 11 
 12 		/**
 13 		 * @private
 14 		 * @type {object<number, string>} Hash, mapping tag ids to their
 15 		 *                                corresponding names.
 16 		 */
 17 		_tagIdToNameMap: null,
 18 
 19 		/**
 20 		 * @private
 21 		 * @type {object<number, string>} Hash, mapping tag ids to their
 22 		 *                                corresponding names for newly created
 23 		 *                                tags.
 24 		 */
 25 		_createdTagIdToNameMap: {},
 26 
 27 		/**
 28 		 * @private
 29 		 * @type {Array.<object>} A set of blocks that are are to be removed
 30 		 *                        from this content object when saving it.
 31 		 *                        This array is populated during the save
 32 		 *                        process.  It get filled just before
 33 		 *                        persisting the data to the server, and gets
 34 		 *                        emptied as soon as the save operation
 35 		 *                        succeeds.
 36 		 */
 37 		_deletedBlocks: [],
 38 
 39 		/**
 40 		 * @private
 41 		 * @type {Array.<object>} A set of tags that are are to be removed from
 42 		 *                        from this content object when it is saved.
 43 		 */
 44 		_deletedTags: [],
 45 
 46 		/**
 47 		 * Searching for a tag of a given id from the object structure that is
 48 		 * returned by the REST API would require O(N) time.  This function,
 49 		 * builds a hash that maps the tag id with its corresponding name, so
 50 		 * that it can be mapped in O(1) time instead.
 51 		 *
 52 		 * @private
 53 		 * @return {object<number,string>} A hash map where the key is the tag
 54 		 *                                 id, and the value is the tag name.
 55 		 */
 56 		'!_mapTagIdsToNames': function () {
 57 			var name;
 58 			var map = {};
 59 			var tags = this._data.tags;
 60 
 61 			for (name in tags) {
 62 				if (tags.hasOwnProperty(name)) {
 63 					map[tags[name].id] = name;
 64 				}
 65 			}
 66 
 67 			return map;
 68 		},
 69 
 70 		/**
 71 		 * Retrieves data for a tag from the internal data object.
 72 		 *
 73 		 * @private
 74 		 * @param {string} name The name of the tag.
 75 		 * @return {!object} The tag data, or null if a there if no tag
 76 		 *                   matching the given name.
 77 		 */
 78 		'!_getTagData': function (name) {
 79 			return (this._data.tags && this._data.tags[name]) ||
 80 			       (this._shadow.tags && this._shadow.tags[name]);
 81 		},
 82 
 83 		/**
 84 		 * Get the tag whose id is `id'.
 85 		 * Builds the `_tagIdToNameMap' hash map if it doesn't already exist.
 86 		 *
 87 		 * @todo: Should we deprecate this?
 88 		 * @private
 89 		 * @param {number} id Id of tag to retrieve.
 90 		 * @return {object} The tag's data.
 91 		 */
 92 		'!_getTagDataById': function (id) {
 93 			if (!this._tagIdToNameMap) {
 94 				this._tagIdToNameMap = this._mapTagIdsToNames();
 95 			}
 96 
 97 			return this._getTagData(this._tagIdToNameMap[id] ||
 98 				 this._createdTagIdToNameMap[id]);
 99 		},
100 
101 		/**
102 		 * Extracts the editables and blocks that have been rendered.
103 		 *
104 		 * @param {object} data The response object received from the
105 		 *                      renderTemplate() call.
106 		 * @return {object} An object containing two properties: an array of
107 		 *                  blocks, and an array of editables.
108 		 */
109 		'!_processRenderedTags': function (data) {
110 			var tags = this._getEditablesAndBlocks(data);
111 			this._storeRenderedEditables(tags.editables);
112 			this._storeRenderedBlocks(tags.blocks);
113 			return tags;
114 		},
115 
116 		/**
117 		 * Get this content object's node.
118 		 *
119 		 * @public
120 		 * @function
121 		 * @name node
122 		 * @memberOf ContentObjectAPI
123 		 * @param {funtion(NodeAPI)=} success Optional callback to receive a
124 		 *                                    {@link NodeAPI} object as the
125 		 *                                    only argument.
126 		 * @param {function(GCNError):boolean=} error Optional custom error
127 		 *                                            handler.
128 		 * @return {NodeAPI} This object's node.
129 		 */
130 		'!node': function (success, error) {
131 			return this.folder().node();
132 		},
133 
134 		/**
135 		 * Get this content object's parent folder.
136 		 *
137 		 * @public
138 		 * @function
139 		 * @name folder
140 		 * @memberOf ContentObjectAPI
141 		 * @param {funtion(FolderAPI)=} success Optional callback to receive a
142 		 *                                      {@link FolderAPI} object as the
143 		 *                                      only argument.
144 		 * @param {function(GCNError):boolean=} error Optional custom error
145 		 *                                            handler.
146 		 * @return {FolderAPI} This object's parent folder.
147 		 */
148 		'!folder': function (success, error) {
149 			var id = this._fetched ? this.prop('folderId') : null;
150 			return this._continue(GCN.FolderAPI, id, success, error);
151 		},
152 
153 		/**
154 		 * Gets a tag of the specified id, contained in this content object.
155 		 *
156 		 * @name tag
157 		 * @function
158 		 * @memberOf TagContainerAPI
159 		 * @param {function} success
160 		 * @param {function} error
161 		 * @return TagAPI
162 		 */
163 		'!tag': function (id, success, error) {
164 			return this._continue(GCN.TagAPI, id, success, error);
165 		},
166 
167 		/**
168 		 * Retrieves a collection of tags from this content object.
169 		 *
170 		 * @name tags
171 		 * @function
172 		 * @memberOf TagContainerAPI
173 		 * @param {object|string|number} settings (Optional)
174 		 * @param {function} success callback
175 		 * @param {function} error (Optional)
176 		 * @return TagContainerAPI
177 		 */
178 		'!tags': function () {
179 			var args = Array.prototype.slice.call(arguments);
180 
181 			if (args.length === 0) {
182 				return;
183 			}
184 
185 			var i;
186 			var j = args.length;
187 			var filter = {};
188 			var filters;
189 			var hasFilter = false;
190 			var success;
191 			var error;
192 
193 			// Determine `success', `error', `filter'
194 			for (i = 0; i < j; ++i) {
195 				switch (jQuery.type(args[i])) {
196 				case 'function':
197 					if (success) {
198 						error = args[i];
199 					} else {
200 						success = args[i];
201 					}
202 					break;
203 				case 'number':
204 				case 'string':
205 					filters = [args[i]];
206 					break;
207 				case 'array':
208 					filters = args[i];
209 					break;
210 				default:
211 					return;
212 				}
213 			}
214 
215 			if (jQuery.type(filters) === 'array') {
216 				var k = filters.length;
217 				while (k) {
218 					filter[filters[--k]] = true;
219 				}
220 				hasFilter = true;
221 			}
222 
223 			var that = this;
224 
225 			if (success) {
226 				this._read(function () {
227 					var tags = that._data.tags;
228 					var tag;
229 					var list = [];
230 
231 					for (tag in tags) {
232 						if (tags.hasOwnProperty(tag)) {
233 							if (!hasFilter || filter[tag]) {
234 								list.push(that._continue(GCN.TagAPI, tags[tag],
235 									null, error));
236 							}
237 						}
238 					}
239 
240 					success(list);
241 				}, error);
242 			}
243 		},
244 
245 		/**
246 		 * Internal method to create a tag of a given tagtype in this content
247 		 * object.
248 		 *
249 		 * @private
250 		 * @param {string|number} construct The name of the construct on which
251 		 *                                  the tag to be created should be
252 		 *                                  derived from.  Or the id of that
253 		 *                                  construct.
254 		 * @param {string=} magicValue Optional property that will override the
255 		 *                             default values of this tag type.
256 		 * @param {function(TagAPI)=} success Optional callback that will
257 		 *                                    receive the newly created tag as
258 		 *                                    its only argument.
259 		 * @param {function(GCNError):boolean=} error Optional custom error
260 		 *                                            handler.
261 		 * @return {TagAPI} The newly created tag.
262 		 */
263 		'!_createTag': function () {
264 			var args = Array.prototype.slice.call(arguments);
265 
266 			if (args.length === 0) {
267 				GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' +
268 					'least one argument.  See documentation.');
269 
270 				return;
271 			}
272 
273 			var success;
274 			var error;
275 			var magicValue;
276 			var construct = args[0];
277 			var i;
278 			var j = args.length;
279 
280 			// Determine `success', `error', and `magicValue'.
281 			for (i = 1; i < j; ++i) {
282 				switch (jQuery.type(args[i])) {
283 				case 'string':
284 					magicValue = args[i];
285 					break;
286 				case 'function':
287 					if (success) {
288 						error = args[i];
289 					} else {
290 						success = args[i];
291 					}
292 					break;
293 				}
294 			}
295 
296 			var that = this;
297 
298 			// First create a new TagAPI instance that will have this container
299 			// as its ancestor.  Also acquire a lock on the newly created tag
300 			// object so that any further operations on it will be queued until
301 			// we release the lock.
302 			var tag = this._continue(GCN.TagAPI)._procure();
303 
304 			this.node().constructs(function (constructs) {
305 				var constructId;
306 
307 				if ('number' === jQuery.type(construct)) {
308 					constructId = construct;
309 				} else if (constructs[construct]) {
310 					constructId = constructs[construct].constructId;
311 				} else {
312 					var err = GCN.createError('CONSTRUCT_NOT_FOUND',
313 						'Cannot find constuct `' + construct + '\'',
314 						constructs);
315 
316 					GCN.handleError(err, error);
317 
318 					return;
319 				}
320 
321 				var restUrl = GCN.settings.BACKEND_PATH + '/rest/'
322 				            + that._type + '/newtag/' + that._data.id + '?'
323 				            + jQuery.param({constructId: constructId});
324 
325 				that._authAjax({
326 					type  : 'POST',
327 					url   : restUrl,
328 					json  : {magicValue: magicValue},
329 					error : function (xhr, status, msg) {
330 						GCN.handleHttpError(xhr, msg, error);
331 						tag._vacate();
332 					},
333 					success: function (response) {
334 						if (GCN.getResponseCode(response) === 'OK') {
335 							var data = response.tag;
336 
337 							tag._data.id = data.id;
338 							tag._name    = data.name;
339 							tag._data    = data;
340 							tag._fetched = true;
341 
342 							// Add this tag into the tag's container `_shadow'
343 							// object, and `_tagIdToNameMap hash'.
344 
345 							var shouldCreateObjectIfUndefined = true;
346 
347 							// Add this tag into the `_shadow' object.
348 							that._update('tags.' + data.name, data, error,
349 								shouldCreateObjectIfUndefined);
350 
351 							// TODO: We need to store the tag inside the
352 							// `_data' object for now.  A change should be made
353 							// so that when containers are saved, the data in
354 							// the _shadow object is properly transfered into
355 							// the _data object.
356 
357 							that._data.tags[data.name] = data;
358 
359 							if (!that.hasOwnProperty('_createdTagIdToNameMap')) {
360 								that._createdTagIdToNameMap = {};
361 							}
362 
363 							that._createdTagIdToNameMap[data.id] = data.name;
364 
365 							if (success) {
366 								success(tag);
367 							}
368 						} else {
369 							tag._die(GCN.getResponseCode(response));
370 							GCN.handleResponseError(response, error);
371 						}
372 
373 						// Hold onto the mutex until this tag object has been
374 						// fully realized and placed inside its container.
375 						tag._vacate();
376 					}
377 				});
378 			}, error);
379 
380 			return tag;
381 		},
382 
383 		/**
384 		 * Internal method to delete the specified tag from this content
385 		 * object.
386 		 *
387 		 * @private
388 		 * @param {string} id The id of the tag to be deleted.
389 		 * @param {function(TagContainerAPI)=} success Optional callback that
390 		 *                                             receive this object as
391 		 *                                             its only argument.
392 		 * @param {function(GCNError):boolean=} error Optional custom error
393 		 *                                            handler.
394 		 */
395 		'!_removeTag': function (id, success, error) {
396 			this.tag(id).remove(success, error);
397 		},
398 
399 		/**
400 		 * Internal method to delete a set of tags from this content object.
401 		 *
402 		 * @private
403 		 * @param {Array.<string>} ids The ids of the set of tags to be
404 		 *                             deleted.
405 		 * @param {function(TagContainerAPI)=} success Optional callback that
406 		 *                                             receive this object as
407 		 *                                             its only argument.
408 		 * @param {function(GCNError):boolean=} error Optional custom error
409 		 *                                            handler.
410 		 */
411 		'!_removeTags': function (ids, success, error) {
412 			var that = this;
413 
414 			this.tags(ids, function (tags) {
415 				var j = tags.length;
416 
417 				while (j) {
418 					tags[--j].remove(null, error);
419 				}
420 
421 				if (success) {
422 					that.save(success, error);
423 				}
424 			}, error);
425 		},
426 
427 		/**
428 		 * Given a data object received from a "/rest/page/render" call, map
429 		 * the blocks and editables into a list of each.
430 		 *
431 		 * Note that if a tag is both an editable and a block, it will be
432 		 * listed in both the blocks list and in the editables list.
433 		 *
434 		 * @param {object} data
435 		 * @return {object<string, Array.<object>>} A map containing a set of
436 		 *                                          editables and a set of
437 		 *                                          blocks.
438 		 */
439 		'!_getEditablesAndBlocks': function (data) {
440 			if (!data || !data.tags) {
441 				return {
442 					blocks: [],
443 					editables: []
444 				};
445 			}
446 
447 			var tag;
448 			var tags = data.tags;
449 			var numTags = tags.length;
450 			var numEditables;
451 			var blocks = [];
452 			var editables = [];
453 			var isBlock;
454 			var i;
455 			var j;
456 
457 			for (i = 0; i < numTags; i++) {
458 				tag = tags[i];
459 
460 				if (tag.editables) {
461 					numEditables = tag.editables.length;
462 					for (j = 0; j < numEditables; j++) {
463 						tag.editables[j].tagname = tag.tagname;
464 					}
465 					editables = editables.concat(tag.editables);
466 				}
467 
468 				/*
469 				 * Depending on tag.onlyeditables to determine whether or not a
470 				 * given tag is a block or editable is unreliable since it is
471 				 * possible to have a block which only contains editables:
472 				 *
473 				 * {
474 				 *  "tagname":"content",
475 				 *  "editables":[{
476 				 *	  "element":"GENTICS_EDITABLE_1234",
477 				 *	  "readonly":false,
478 				 *	  "partname":"editablepart"
479 				 *  }],
480 				 *  "element":"GENTICS_BLOCK_1234",
481 				 *  "onlyeditables":true
482 				 *  "tagname":"tagwitheditable"
483 				 * }
484 				 *
485 				 * in the above example, tag.onlyeditable is true but tag is a
486 				 * block, since the tag's element and the editable's element
487 				 * are not the same.
488 				 */
489 
490 				if (!tag.editables || tag.editables.length > 1) {
491 					isBlock = true;
492 				} else {
493 					isBlock = (1 === tag.editables.length)
494 						&& (tag.editables[0].element !== tag.element);
495 				}
496 
497 				if (isBlock) {
498 					blocks.push(tag);
499 				}
500 			}
501 
502 			return {
503 				blocks: blocks,
504 				editables: editables
505 			};
506 		}
507 
508 	});
509 
510 }(GCN));
511