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 			// We use a uniqueId to avoid a fetus being created.
299 			// This is to avoid the following szenario:
300 			//
301 			// var tag1 = container.createTag(...);
302 			// var tag2 = container.createTag(...);
303 			// tag1 === tag2 // is true which is wrong
304 			//
305 			// However, for all other cases, where we get an existing object, we want this behaviour:
306 			// var folder1 = page(1).folder(...);
307 			// var folder2 = page(1).folder(...);
308 			// folder1 === folder2 // is true which is correct
309 			//
310 			// So, createTag() is different from other chainback methods
311 			// in that each invokation must create a new instance, while
312 			// other chainback methods must return the same.
313 			//
314 			// The id will be reset as soon as the tag object is
315 			// realized. This happens below as soon as we get a success
316 			// response with the correct tag id.
317 			var newId = GCN.uniqueId('TagApi-unique-');
318 
319 			// First create a new TagAPI instance that will have this container
320 			// as its ancestor.  Also acquire a lock on the newly created tag
321 			// object so that any further operations on it will be queued until
322 			// we release the lock.
323 			var tag = this._continue(GCN.TagAPI, newId)._procure();
324 
325 			this.node().constructs(function (constructs) {
326 				var constructId;
327 
328 				if ('number' === jQuery.type(construct)) {
329 					constructId = construct;
330 				} else if (constructs[construct]) {
331 					constructId = constructs[construct].constructId;
332 				} else {
333 					var err = GCN.createError('CONSTRUCT_NOT_FOUND',
334 						'Cannot find constuct `' + construct + '\'',
335 						constructs);
336 
337 					GCN.handleError(err, error);
338 
339 					return;
340 				}
341 
342 				var restUrl = GCN.settings.BACKEND_PATH + '/rest/'
343 				            + that._type + '/newtag/' + that._data.id + '?'
344 				            + jQuery.param({constructId: constructId});
345 
346 				that._authAjax({
347 					type  : 'POST',
348 					url   : restUrl,
349 					json  : {magicValue: magicValue},
350 					error : function (xhr, status, msg) {
351 						GCN.handleHttpError(xhr, msg, error);
352 						tag._vacate();
353 					},
354 					success: function (response) {
355 						if (GCN.getResponseCode(response) === 'OK') {
356 							var data = response.tag;
357 
358 							tag._name    = data.name;
359 							tag._data    = data;
360 							tag._fetched = true;
361 
362 							// The tag's id is still newId from
363 							// above. We have to realize the tag so that
364 							// it gets the correct id. The new id
365 							// changes its hash, so it must also be
366 							// removed and reinserted from the caches.
367 							tag._removeFromTempCache();
368 							tag._setHash(data.id)._addToCache();
369 
370 							// Add this tag into the tag's container `_shadow'
371 							// object, and `_tagIdToNameMap hash'.
372 
373 							var shouldCreateObjectIfUndefined = true;
374 
375 							// Add this tag into the `_shadow' object.
376 							that._update('tags.' + data.name, data, error,
377 								shouldCreateObjectIfUndefined);
378 
379 							// TODO: We need to store the tag inside the
380 							// `_data' object for now.  A change should be made
381 							// so that when containers are saved, the data in
382 							// the _shadow object is properly transfered into
383 							// the _data object.
384 
385 							that._data.tags[data.name] = data;
386 
387 							if (!that.hasOwnProperty('_createdTagIdToNameMap')) {
388 								that._createdTagIdToNameMap = {};
389 							}
390 
391 							that._createdTagIdToNameMap[data.id] = data.name;
392 
393 							if (success) {
394 								success(tag);
395 							}
396 						} else {
397 							tag._die(GCN.getResponseCode(response));
398 							GCN.handleResponseError(response, error);
399 						}
400 
401 						// Hold onto the mutex until this tag object has been
402 						// fully realized and placed inside its container.
403 						tag._vacate();
404 					}
405 				});
406 			}, error);
407 
408 			return tag;
409 		},
410 
411 		/**
412 		 * Internal method to delete the specified tag from this content
413 		 * object.
414 		 *
415 		 * @private
416 		 * @param {string} id The id of the tag to be deleted.
417 		 * @param {function(TagContainerAPI)=} success Optional callback that
418 		 *                                             receive this object as
419 		 *                                             its only argument.
420 		 * @param {function(GCNError):boolean=} error Optional custom error
421 		 *                                            handler.
422 		 */
423 		'!_removeTag': function (id, success, error) {
424 			this.tag(id).remove(success, error);
425 		},
426 
427 		/**
428 		 * Internal method to delete a set of tags from this content object.
429 		 *
430 		 * @private
431 		 * @param {Array.<string>} ids The ids of the set of tags to be
432 		 *                             deleted.
433 		 * @param {function(TagContainerAPI)=} success Optional callback that
434 		 *                                             receive this object as
435 		 *                                             its only argument.
436 		 * @param {function(GCNError):boolean=} error Optional custom error
437 		 *                                            handler.
438 		 */
439 		'!_removeTags': function (ids, success, error) {
440 			var that = this;
441 
442 			this.tags(ids, function (tags) {
443 				var j = tags.length;
444 
445 				while (j) {
446 					tags[--j].remove(null, error);
447 				}
448 
449 				if (success) {
450 					that.save(success, error);
451 				}
452 			}, error);
453 		},
454 
455 		/**
456 		 * Given a data object received from a "/rest/page/render" call, map
457 		 * the blocks and editables into a list of each.
458 		 *
459 		 * Note that if a tag is both an editable and a block, it will be
460 		 * listed in both the blocks list and in the editables list.
461 		 *
462 		 * @param {object} data
463 		 * @return {object<string, Array.<object>>} A map containing a set of
464 		 *                                          editables and a set of
465 		 *                                          blocks.
466 		 */
467 		'!_getEditablesAndBlocks': function (data) {
468 			if (!data || !data.tags) {
469 				return {
470 					blocks: [],
471 					editables: []
472 				};
473 			}
474 
475 			var tag;
476 			var tags = data.tags;
477 			var numTags = tags.length;
478 			var numEditables;
479 			var blocks = [];
480 			var editables = [];
481 			var isBlock;
482 			var i;
483 			var j;
484 
485 			for (i = 0; i < numTags; i++) {
486 				tag = tags[i];
487 
488 				if (tag.editables) {
489 					numEditables = tag.editables.length;
490 					for (j = 0; j < numEditables; j++) {
491 						tag.editables[j].tagname = tag.tagname;
492 					}
493 					editables = editables.concat(tag.editables);
494 				}
495 
496 				/*
497 				 * Depending on tag.onlyeditables to determine whether or not a
498 				 * given tag is a block or editable is unreliable since it is
499 				 * possible to have a block which only contains editables:
500 				 *
501 				 * {
502 				 *  "tagname":"content",
503 				 *  "editables":[{
504 				 *	  "element":"GENTICS_EDITABLE_1234",
505 				 *	  "readonly":false,
506 				 *	  "partname":"editablepart"
507 				 *  }],
508 				 *  "element":"GENTICS_BLOCK_1234",
509 				 *  "onlyeditables":true
510 				 *  "tagname":"tagwitheditable"
511 				 * }
512 				 *
513 				 * in the above example, tag.onlyeditable is true but tag is a
514 				 * block, since the tag's element and the editable's element
515 				 * are not the same.
516 				 */
517 
518 				if (!tag.editables || tag.editables.length > 1) {
519 					isBlock = true;
520 				} else {
521 					isBlock = (1 === tag.editables.length)
522 						&& (tag.editables[0].element !== tag.element);
523 				}
524 
525 				if (isBlock) {
526 					blocks.push(tag);
527 				}
528 			}
529 
530 			return {
531 				blocks: blocks,
532 				editables: editables
533 			};
534 		}
535 
536 	});
537 
538 }(GCN));
539