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 			var id;
132 
133 			if (this._fetched) {
134 				var folder = this.folder(null, error);
135 
136 				if (folder._fetched) {
137 					id = folder.nodeId;
138 				}
139 			}
140 
141 			return this._continue(GCN.NodeAPI, id, success, error);
142 		},
143 
144 		/**
145 		 * Get this content object's parent folder.
146 		 *
147 		 * @public
148 		 * @function
149 		 * @name folder
150 		 * @memberOf ContentObjectAPI
151 		 * @param {funtion(FolderAPI)=} success Optional callback to receive a
152 		 *                                      {@link FolderAPI} object as the
153 		 *                                      only argument.
154 		 * @param {function(GCNError):boolean=} error Optional custom error
155 		 *                                            handler.
156 		 * @return {FolderAPI} This object's parent folder.
157 		 */
158 		'!folder': function (success, error) {
159 			var id = this._fetched ? this.prop('folderId') : null;
160 			return this._continue(GCN.FolderAPI, id, success, error);
161 		},
162 
163 		/**
164 		 * Gets a tag of the specified id, contained in this content object.
165 		 *
166 		 * @name tag
167 		 * @function
168 		 * @memberOf TagContainerAPI
169 		 * @param {function} success
170 		 * @param {function} error
171 		 * @return TagAPI
172 		 */
173 		'!tag': function (id, success, error) {
174 			return this._continue(GCN.TagAPI, id, success, error);
175 		},
176 
177 		/**
178 		 * Retrieves a collection of tags from this content object.
179 		 *
180 		 * @name tags
181 		 * @function
182 		 * @memberOf TagContainerAPI
183 		 * @param {object|string|number} settings (Optional)
184 		 * @param {function} success callback
185 		 * @param {function} error (Optional)
186 		 * @return TagContainerAPI
187 		 */
188 		'!tags': function () {
189 			var args = Array.prototype.slice.call(arguments);
190 
191 			if (args.length === 0) {
192 				return;
193 			}
194 
195 			var i;
196 			var j = args.length;
197 			var filter = {};
198 			var filters;
199 			var hasFilter = false;
200 			var success;
201 			var error;
202 
203 			// Determine `success', `error', `filter'
204 			for (i = 0; i < j; ++i) {
205 				switch (jQuery.type(args[i])) {
206 				case 'function':
207 					if (success) {
208 						error = args[i];
209 					} else {
210 						success = args[i];
211 					}
212 					break;
213 				case 'number':
214 				case 'string':
215 					filters = [args[i]];
216 					break;
217 				case 'array':
218 					filters = args[i];
219 					break;
220 				default:
221 					return;
222 				}
223 			}
224 
225 			if (jQuery.type(filters) === 'array') {
226 				var k = filters.length;
227 				while (k) {
228 					filter[filters[--k]] = true;
229 				}
230 				hasFilter = true;
231 			}
232 
233 			var that = this;
234 
235 			if (success) {
236 				this._read(function () {
237 					var tags = that._data.tags;
238 					var tag;
239 					var list = [];
240 
241 					for (tag in tags) {
242 						if (tags.hasOwnProperty(tag)) {
243 							if (!hasFilter || filter[tag]) {
244 								list.push(that._continue(GCN.TagAPI, tags[tag],
245 									null, error));
246 							}
247 						}
248 					}
249 
250 					success(list);
251 				}, error);
252 			}
253 		},
254 
255 		/**
256 		 * Internal method to create a tag of a given tagtype in this content
257 		 * object.
258 		 *
259 		 * @private
260 		 * @param {string|number} construct The name of the construct on which
261 		 *                                  the tag to be created should be
262 		 *                                  derived from.  Or the id of that
263 		 *                                  construct.
264 		 * @param {string=} magicValue Optional property that will override the
265 		 *                             default values of this tag type.
266 		 * @param {function(TagAPI)=} success Optional callback that will
267 		 *                                    receive the newly created tag as
268 		 *                                    its only argument.
269 		 * @param {function(GCNError):boolean=} error Optional custom error
270 		 *                                            handler.
271 		 * @return {TagAPI} The newly created tag.
272 		 */
273 		'!_createTag': function () {
274 			var args = Array.prototype.slice.call(arguments);
275 
276 			if (args.length === 0) {
277 				GCN.error('INVALID_ARGUMENTS', '`createTag()\' requires at ' +
278 					'least one argument.  See documentation.');
279 
280 				return;
281 			}
282 
283 			var success;
284 			var error;
285 			var magicValue;
286 			var construct = args[0];
287 			var i;
288 			var j = args.length;
289 
290 			// Determine `success', `error', and `magicValue'.
291 			for (i = 1; i < j; ++i) {
292 				switch (jQuery.type(args[i])) {
293 				case 'string':
294 					magicValue = args[i];
295 					break;
296 				case 'function':
297 					if (success) {
298 						error = args[i];
299 					} else {
300 						success = args[i];
301 					}
302 					break;
303 				}
304 			}
305 
306 			var that = this;
307 
308 			// First create a new TagAPI instance that will have this container
309 			// as its ancestor.  Also aquire a lock on the newly created tag
310 			// object so that any further operations on it will be queued until
311 			// we release the lock.
312 			var tag = this._continue(GCN.TagAPI)._procure();
313 
314 			this.node().constructs(function (constructs) {
315 				var constructId;
316 
317 				if ('number' === jQuery.type(construct)) {
318 					constructId = construct;
319 				} else if (constructs[construct]) {
320 					constructId = constructs[construct].constructId;
321 				} else {
322 					var err = GCN.createError('CONSTRUCT_NOT_FOUND',
323 						'Cannot find constuct `' + construct + '\'',
324 						constructs);
325 
326 					GCN.handleError(err, error);
327 
328 					return;
329 				}
330 
331 				var restUrl = GCN.settings.BACKEND_PATH + '/rest/'
332 				            + that._type + '/newtag/' + that._data.id + '?'
333 				            + jQuery.param({constructId: constructId});
334 
335 				that._authAjax({
336 					type  : 'POST',
337 					url   : restUrl,
338 					json  : {magicValue: magicValue},
339 					error : function (xhr, status, msg) {
340 						GCN.handleHttpError(xhr, msg, error);
341 						tag._vacate();
342 					},
343 					success: function (response) {
344 						if (GCN.getResponseCode(response) === 'OK') {
345 							var data = response.tag;
346 
347 							tag._data.id = data.id;
348 							tag._name    = data.name;
349 							tag._data    = data;
350 							tag._fetched = true;
351 
352 							// Add this tag into the tag's container `_shadow'
353 							// object, and `_tagIdToNameMap hash'.
354 
355 							var shouldCreateObjectIfUndefined = true;
356 
357 							// Add this tag into the `_shadow' object.
358 							that._update('tags.' + data.name, data, error,
359 								shouldCreateObjectIfUndefined);
360 
361 							// TODO: We need to store the tag inside the
362 							// `_data' object for now.  A change should be made
363 							// so that when containers are saved, the data in
364 							// the _shadow object is properly transfered into
365 							// the _data object.
366 
367 							that._data.tags[data.name] = data;
368 
369 							if (!that.hasOwnProperty('_createdTagIdToNameMap')) {
370 								that._createdTagIdToNameMap = {};
371 							}
372 
373 							that._createdTagIdToNameMap[data.id] = data.name;
374 
375 							if (success) {
376 								success(tag);
377 							}
378 						} else {
379 							tag._die(GCN.getResponseCode(response));
380 							GCN.handleResponseError(response, error);
381 						}
382 
383 						// Hold onto the mutex until this tag object has been
384 						// fully realized and placed inside its container.
385 						tag._vacate();
386 					}
387 				});
388 			}, error);
389 
390 			return tag;
391 		},
392 
393 		/**
394 		 * Internal method to delete the specified tag from this content
395 		 * object.
396 		 *
397 		 * @private
398 		 * @param {string} id The id of the tag to be deleted.
399 		 * @param {function(TagContainerAPI)=} success Optional callback that
400 		 *                                             receive this object as
401 		 *                                             its only argument.
402 		 * @param {function(GCNError):boolean=} error Optional custom error
403 		 *                                            handler.
404 		 */
405 		'!_removeTag': function (id, success, error) {
406 			this.tag(id).remove(success, error);
407 		},
408 
409 		/**
410 		 * Internal method to delete a set of tags from this content object.
411 		 *
412 		 * @private
413 		 * @param {Array.<string>} ids The ids of the set of tags to be
414 		 *                             deleted.
415 		 * @param {function(TagContainerAPI)=} success Optional callback that
416 		 *                                             receive this object as
417 		 *                                             its only argument.
418 		 * @param {function(GCNError):boolean=} error Optional custom error
419 		 *                                            handler.
420 		 */
421 		'!_removeTags': function (ids, success, error) {
422 			var that = this;
423 
424 			this.tags(ids, function (tags) {
425 				var j = tags.length;
426 
427 				while (j) {
428 					tags[--j].remove(null, error);
429 				}
430 
431 				if (success) {
432 					that.save(success, error);
433 				}
434 			}, error);
435 		},
436 
437 		/**
438 		 * Given a data object received from a "/rest/page/render" call, map
439 		 * the blocks and editables into a list of each.
440 		 *
441 		 * Note that if a tag is both an editable and a block, it will be
442 		 * listed in both the blocks list and in the editables list.
443 		 *
444 		 * @param {object} data
445 		 * @return {object<string, Array.<object>>} A map containing a set of
446 		 *                                          editables and a set of
447 		 *                                          blocks.
448 		 */
449 		'!_getEditablesAndBlocks': function (data) {
450 			if (!data || !data.tags) {
451 				return {
452 					blocks: [],
453 					editables: []
454 				};
455 			}
456 
457 			var tag;
458 			var tags = data.tags;
459 			var j = tags.length;
460 			var i;
461 			var blocks = [];
462 			var editables = [];
463 
464 			while (j) {
465 				tag = tags[--j];
466 
467 				if (tag.editables) {
468 					i = tag.editables.length;
469 					while (i) {
470 						tag.editables[--i].tagname = tag.tagname;
471 					}
472 					editables = editables.concat(tag.editables);
473 				}
474 
475 				if (!tag.onlyeditables) {
476 					blocks.push(tag);
477 				}
478 			}
479 
480 			return {
481 				blocks    : blocks,
482 				editables : editables
483 			};
484 		}
485 
486 	});
487 
488 }(GCN));
489