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