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 		 * @protected
 14 		 * @type {object<number, string>} Hash, mapping tag ids to their
 15 		 *                                corresponding names.
 16 		 */
 17 		_tagIdToNameMap: null,
 18 
 19 		/**
 20 		 * @protected
 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 		 * @protected
 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 		 * @protected
 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 		 * @protected
 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 		 * @protected
 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 		 * @protected
 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 		 * Retreives a set of tags from this content object that match the
102 		 * names in the given array.
103 		 *
104 		 * @protected
105 		 * @param {Array.<string>} names A set of tag names.
106 		 * @return {Array.<objects>} Tag data objects.
107 		 */
108 		'!_getTagsData': function (names) {
109 			if (!this._data.tags) {
110 				return [];
111 			}
112 
113 			var all = this._data.tags;
114 			var j = names.length;
115 			var tags = [];
116 
117 			if (j) {
118 				if (all[names[--j]]) {
119 					tags.push(all[names[j]]);
120 				}
121 			}
122 
123 			return tags;
124 		},
125 
126 		/**
127 		 * Get this content object's node.
128 		 *
129 		 * @public
130 		 * @function
131 		 * @name node
132 		 * @memberOf ContentObjectAPI
133 		 * @param {funtion(NodeAPI)=} success Optional callback to receive a
134 		 *                                    {@link NodeAPI} object as the
135 		 *                                    only argument.
136 		 * @param {function(GCNError):boolean=} error Optional custom error
137 		 *                                            handler.
138 		 * @return {NodeAPI} This object's node.
139 		 */
140 		'!node': function (success, error) {
141 			var id;
142 
143 			if (this._fetched) {
144 				var folder = this.folder(null, error);
145 
146 				if (folder._fetched) {
147 					id = folder.nodeId;
148 				}
149 			}
150 
151 			return this._continue(GCN.NodeAPI, id, success, error);
152 		},
153 
154 		/**
155 		 * Get this content object's parent folder.
156 		 *
157 		 * @public
158 		 * @function
159 		 * @name folder
160 		 * @memberOf ContentObjectAPI
161 		 * @param {funtion(FolderAPI)=} success Optional callback to receive a
162 		 *                                      {@link FolderAPI} object as the
163 		 *                                      only argument.
164 		 * @param {function(GCNError):boolean=} error Optional custom error
165 		 *                                            handler.
166 		 * @return {FolderAPI} This object's parent folder.
167 		 */
168 		'!folder': function (success, error) {
169 			var id = this._fetched ? this.prop('folderId') : null;
170 			return this._continue(GCN.FolderAPI, id, success, error);
171 		},
172 
173 		/**
174 		 * Gets a tag of the specified id, contained in this content object.
175 		 *
176 		 * @name tag
177 		 * @function
178 		 * @memberOf TagContainerAPI
179 		 * @param {function} success
180 		 * @param {function} error
181 		 * @return TagAPI
182 		 */
183 		'!tag': function (id, success, error) {
184 			return this._continue(GCN.TagAPI, id, success, error);
185 		},
186 
187 		/**
188 		 * Retrieves a collection of tags from this content object.
189 		 *
190 		 * @name tags
191 		 * @function
192 		 * @memberOf TagContainerAPI
193 		 * @param {object|string|number} settings (Optional)
194 		 * @param {function} success callback
195 		 * @param {function} error (Optional)
196 		 * @return TagContainerAPI
197 		 */
198 		'!tags': function () {
199 			var args = Array.prototype.slice.call(arguments);
200 
201 			if (args.length === 0) {
202 				return;
203 			}
204 
205 			var i;
206 			var j = args.length;
207 			var filter = {};
208 			var filters;
209 			var hasFilter = false;
210 			var success;
211 			var error;
212 
213 			// Determine `success', `error', `filter'
214 			for (i = 0; i < j; ++i) {
215 				switch (jQuery.type(args[i])) {
216 				case 'function':
217 					if (success) {
218 						error = args[i];
219 					} else {
220 						success = args[i];
221 					}
222 					break;
223 				case 'number':
224 				case 'string':
225 					filters = [args[i]];
226 					break;
227 				case 'array':
228 					filters = args[i];
229 					break;
230 				default:
231 					return;
232 				}
233 			}
234 
235 			if (jQuery.type(filters) === 'array') {
236 				var k = filters.length;
237 				while (k) {
238 					filter[filters[--k]] = true;
239 				}
240 				hasFilter = true;
241 			}
242 
243 			var that = this;
244 
245 			if (success) {
246 				this._read(function () {
247 					var tags = that._data.tags;
248 					var tag;
249 					var list = [];
250 
251 					for (tag in tags) {
252 						if (tags.hasOwnProperty(tag)) {
253 							if (!hasFilter || filter[tag]) {
254 								list.push(that._continue(GCN.TagAPI, tags[tag],
255 									null, error));
256 							}
257 						}
258 					}
259 
260 					success(list);
261 				}, error);
262 			}
263 		},
264 
265 		/**
266 		 * Internal method to create a tag of a given tagtype in this content
267 		 * object.
268 		 *
269 		 * @protected
270 		 * @param {string|number} construct The name of the construct on which
271 		 *                                  the tag to be created should be
272 		 *                                  derived from.  Or the id of that
273 		 *                                  construct.
274 		 * @param {string=} magicValue Optional property that will override the
275 		 *                             default values of this tag type.
276 		 * @param {function(TagAPI)=} success Optional callback that will
277 		 *                                    receive the newly created tag as
278 		 *                                    its only argument.
279 		 * @param {function(GCNError):boolean=} error Optional custom error
280 		 *                                            handler.
281 		 * @return {TagAPI} The newly created tag.
282 		 */
283 		'!_createTag': function () {
284 			var args = Array.prototype.slice.call(arguments);
285 
286 			if (args.length === 0) {
287 				GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' +
288 					'least one argument.  See documentation.');
289 
290 				return;
291 			}
292 
293 			var success;
294 			var error;
295 			var magicValue;
296 			var construct = args[0];
297 			var i;
298 			var j = args.length;
299 
300 			// Determine `success', `error', and `magicValue'.
301 			for (i = 1; i < j; ++i) {
302 				switch (jQuery.type(args[i])) {
303 				case 'string':
304 					magicValue = args[i];
305 					break;
306 				case 'function':
307 					if (success) {
308 						error = args[i];
309 					} else {
310 						success = args[i];
311 					}
312 					break;
313 				}
314 			}
315 
316 			var that = this;
317 
318 			// First create a new TagAPI instance that will have this container
319 			// as its ancestor.  Also aquire 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)._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._data.id = data.id;
358 							tag._name    = data.name;
359 							tag._data    = data;
360 							tag._fetched = true;
361 
362 							// Add this tag into the tag's container `_shadow'
363 							// object, and `_tagIdToNameMap hash'.
364 
365 							var shouldCreateObjectIfUndefined = true;
366 
367 							that._update('tags.' + data.name, data, error,
368 								shouldCreateObjectIfUndefined);
369 
370 							// TODO: We need to store the tag inside the
371 							// `_data' object for now.  A change should be made
372 							// so that when containers are saved, the data in
373 							// the _shadow object is transfered into the _data
374 							// object.
375 
376 							that._data.tags[data.name] = data;
377 
378 							if (!that.hasOwnProperty('_createdTagIdToNameMap')) {
379 								that._createdTagIdToNameMap = {};
380 							}
381 
382 							that._createdTagIdToNameMap[data.id] = data.name;
383 
384 							if (success) {
385 								success(tag);
386 							}
387 						} else {
388 							tag._die(GCN.getResponseCode(response));
389 							GCN.handleResponseError(response, error);
390 						}
391 
392 						// Hold onto the mutex until this tag object has been
393 						// fully realized and placed inside its container.
394 						tag._vacate();
395 					}
396 				});
397 			}, error);
398 
399 			return tag;
400 		},
401 
402 		/**
403 		 * Internal method to delete the specified tag from this content
404 		 * object.
405 		 *
406 		 * @protected
407 		 * @param {string} id The id of the tag to be deleted.
408 		 * @param {function(TagContainerAPI)=} success Optional callback that
409 		 *                                             receive this object as
410 		 *                                             its only argument.
411 		 * @param {function(GCNError):boolean=} error Optional custom error
412 		 *                                            handler.
413 		 */
414 		'!_removeTag': function (id, success, error) {
415 			this.tag(id).remove(success, error);
416 		},
417 
418 		/**
419 		 * Internal method to delete a set of tags from this content object.
420 		 *
421 		 * @protected
422 		 * @param {Array.<string>} ids The ids of the set of tags to be
423 		 *                             deleted.
424 		 * @param {function(TagContainerAPI)=} success Optional callback that
425 		 *                                             receive this object as
426 		 *                                             its only argument.
427 		 * @param {function(GCNError):boolean=} error Optional custom error
428 		 *                                            handler.
429 		 */
430 		'!_removeTags': function (ids, success, error) {
431 			var that = this;
432 
433 			this.tags(ids, function (tags) {
434 				var j = tags.length;
435 
436 				while (j) {
437 					tags[--j].remove(null, error);
438 				}
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 		 * @param {object} data
451 		 * @return {object<string, Array.<object>>} A map contain a set of
452 		 *                                          editables and a set of
453 		 *                                          blocks.
454 		 */
455 		'!_getEditablesAndBlocks': function (data) {
456 			if (!data || !data.tags) {
457 				return {
458 					blocks: [],
459 					editables: []
460 				};
461 			}
462 
463 			var tag;
464 			var tags = data.tags;
465 			var j = tags.length;
466 			var i;
467 			var blocks = [];
468 			var editables = [];
469 
470 			while (j) {
471 				tag = tags[--j];
472 
473 				if (tag.editables) {
474 					i = tag.editables.length;
475 					while (i) {
476 						tag.editables[--i].tagname = tag.tagname;
477 					}
478 					editables = editables.concat(tag.editables);
479 				}
480 
481 				if (!tag.onlyeditables) {
482 					blocks.push(tag);
483 				}
484 			}
485 
486 			return {
487 				blocks    : blocks,
488 				editables : editables
489 			};
490 		}
491 
492 	});
493 
494 }(GCN));
495