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