1 /*global window: true*/
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * @private
  8 	 * @const
  9 	 * @type {object<string, function(string, *)|string>} This hash is used to
 10 	 *                                                    determine what
 11 	 *                                                    property can be read
 12 	 *                                                    for a part of a given
 13 	 *                                                    type. *SELECT and
 14 	 *                                                    PAGE parttypes are
 15 	 *                                                    handled specially.
 16 	 */
 17 	var PART_TYPES = {};
 18 
 19 	PART_TYPES = {
 20 		STRING   : 'stringValue',
 21 		RICHTEXT : 'stringValue',
 22 		BOOLEAN  : 'booleanValue',
 23 		IMAGE    : 'imageId',
 24 		// (URL) file is the same as File (upload).
 25 		FILE     : 'fileId',
 26 		// (URL) folder is the same as Folder (upload).
 27 		FOLDER   : 'folderId',
 28 		OVERVIEW : 'overview',
 29 		PAGE     : function (part, value) {
 30 			if (jQuery.type(value) === 'number') {
 31 				part.pageId = value;
 32 				delete part.stringValue;
 33 				return value;
 34 			}
 35 
 36 			if (typeof value !== 'undefined') {
 37 				part.stringValue = value.toString();
 38 				delete part.pageId;
 39 				return value;
 40 			}
 41 
 42 			return part[jQuery.type(part.stringValue) === 'string'
 43 				? 'stringValue' : 'pageId'];
 44 		},
 45 
 46 		SELECT : function (part, value) {
 47 			if (value) {
 48 				if (typeof value.datasourceId !== 'undefinded') {
 49 					part.datasourceId = value.datasourceId;
 50 				}
 51 
 52 				if (value.options) {
 53 					part.options = value.options;
 54 				}
 55 
 56 				if (value.selectedOptions) {
 57 					part.selectedOptions = value.selectedOptions;
 58 				}
 59 			}
 60 
 61 			return {
 62 				datasourceId    : part.datasourceId,
 63 				options         : part.options,
 64 				selectedOptions : part.selectedOptions
 65 			};
 66 		},
 67 
 68 		MULTISELECT : function (part, value) {
 69 			return PART_TYPES.SELECT(part, value);
 70 		},
 71 
 72 		TEMPLATETAG : function (part, value) {
 73 			if (value) {
 74 				if (typeof value.templateId !== 'undefined') {
 75 					part.templateId = value.templateId;
 76 				}
 77 
 78 				if (typeof value.templateTagId !== 'undefined') {
 79 					part.templateTagId = value.templateTagId;
 80 				}
 81 			}
 82 
 83 			return {
 84 				templateId    : part.templateId,
 85 				templateTagId : part.templateTagId
 86 			};
 87 		},
 88 
 89 		PAGETAG : function (part, value) {
 90 			if (value) {
 91 				if (typeof value.pageId !== 'undefined') {
 92 					part.pageId = value.pageId;
 93 				}
 94 
 95 				if (typeof value.pageTagId !== 'undefined') {
 96 					part.pageTagId = value.pageTagId;
 97 				}
 98 			}
 99 
100 			return {
101 				templateId    : part.pageId,
102 				templateTagId : part.pageTagId
103 			};
104 		}
105 	};
106 
107 	/**
108 	 * Determine the key inside tag properties where our value is stored, and
109 	 * retreive that value.
110 	 *
111 	 * @param {object} part The tag part we want to read.
112 	 * @return {*} The value that the given tag part holds.
113 	 * @throws CANNOT_READ_TAG_PART
114 	 */
115 	function getPartValue(part) {
116 		var propName = PART_TYPES[part.type];
117 
118 		if (!propName) {
119 			GCN.error('CANNOT_READ_TAG_PART',
120 				'Cannot read or write to tag part', part);
121 
122 			return null;
123 		}
124 
125 		if (jQuery.type(propName) === 'function') {
126 			return propName(part);
127 		}
128 
129 		return part[propName];
130 	}
131 
132 
133 	function setPartValue(part, value) {
134 		var prop = PART_TYPES[part.type];
135 
136 		if (!prop) {
137 			GCN.error('CANNOT_READ_TAG_PART',
138 				'Cannot read or write to tag part', part);
139 
140 			return null;
141 		}
142 
143 		if (jQuery.type(prop) === 'function') {
144 			return prop(part, value);
145 		}
146 
147 		part[prop] = value;
148 
149 		return value;
150 	}
151 
152 	/**
153 	 * Will initialize the contents that have been rendered in a given
154 	 * container for front end editing.
155 	 *
156 	 * @TODO: This function should be moved out of gcn library.  We should
157 	 *        publish a message instead, and pass these arguments in the
158 	 *        message.
159 	 *
160 	 * @private
161 	 * @static
162 	 * @param {Array.<object>} editables Editables to be `aloha()'fied.
163 	 * @param {Array.<object>} blocks Blocks to receive tagfill buttons.
164 	 * @param {number|string} pageId id of the page the tag belongs to
165 	 * @param {jQuery<HTMLElement>} container The element that wraps the
166 	 *                              incoming tag contents.
167 	 */
168 	function initializeFrontendEditing(editables, blocks, pageId, container) {
169 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
170 
171 		if (!Aloha) {
172 			return;
173 		}
174 
175 		Aloha.ready(function () {
176 			if (Aloha.GCN) {
177 				// If we are in the backend, then we need to remove the id of
178 				// the container because it is duplicated in the incoming
179 				// content.
180 				if (container && Aloha.GCN.isBackendMode()) {
181 					container.removeAttr('id');
182 				}
183 
184 				Aloha.GCN.page = GCN.page(pageId);
185 
186 				Aloha.GCN.setupConstructsButton(pageId);
187 			}
188 
189 			var j = editables && editables.length;
190 
191 			while (j) {
192 				Aloha.jQuery('#' + editables[--j].element).aloha();
193 
194 				if (editables[j].readonly) {
195 					Aloha.editables[Aloha.editables.length - 1].disable();
196 				}
197 			}
198 
199 
200 			if (Aloha.GCN) {
201 				Aloha.GCN.alohaBlocks(blocks, pageId);
202 			}
203 		});
204 	}
205 
206 	/**
207 	 * Helper function to normalize the arguments that can be passed to the
208 	 * `edit()' and `render()' methods.
209 	 *
210 	 * @private
211 	 * @static
212 	 * @param {arguments} args A list of arguments.
213 	 * @return {object} Object containing an the properties `element',
214 	 *                         `success' and `error'.
215 	 */
216 	function getRenderOptions(args) {
217 		var argv  = Array.prototype.slice.call(args);
218 		var argc = args.length;
219 		var arg;
220 		var i;
221 
222 		var element;
223 		var success;
224 		var error;
225 
226 		for (i = 0; i < argc; ++i) {
227 			arg = argv[i];
228 
229 			switch (jQuery.type(arg)) {
230 			case 'string':
231 				element = jQuery(arg);
232 				break;
233 			case 'object':
234 				element = arg;
235 				break;
236 			case 'function':
237 				if (success) {
238 					error = arg;
239 				} else {
240 					success = arg;
241 				}
242 				break;
243 			// Descarding all other types of arguments...
244 			}
245 		}
246 
247 		return {
248 			element : element,
249 			success : success,
250 			error   : error
251 		};
252 	}
253 
254 	/**
255 	 * Exposes an API to operate on a Content.Node tag.
256 	 *
257 	 * @public
258 	 * @class TagAPI
259 	 */
260 	var TagAPI = GCN.defineChainback({
261 
262 		__chainbacktype__: 'TagAPI',
263 
264 		/**
265 		 * @type {GCN.ContentObject} A reference to the object in which this
266 		 *                           tag is contained.  This value is set
267 		 *                           during initialization.
268 		 */
269 		_parent: null,
270 
271 		/**
272 		 * @type {string} Name of this tag.
273 		 */
274 		_name: null,
275 
276 		/**
277 		 * Gets this tag's information from the object that contains it.
278 		 *
279 		 * @param {function(TagAPI)} success Callback to be invoked when this
280 		 *                                   operation completes normally.
281 		 * @param {function(GCNError):boolean} error Custom error handler.
282 		 */
283 		'!_read': function (success, error) {
284 			if (this._fetched) {
285 				if (success) {
286 					success(this);
287 				}
288 
289 				return;
290 			}
291 
292 			var that = this;
293 			var parent = this.parent();
294 
295 			// assert(parent)
296 
297 			// Take the data for this tag from it's container.
298 			parent._read(function () {
299 				that._data = parent._getTagData(that._name);
300 
301 				if (!that._data) {
302 					var err = GCN.createError('TAG_NOT_FOUND',
303 						'Could not find tag "' + that._name + '" in ' +
304 						parent._type + " " + parent._data.id, that);
305 
306 					GCN.handleError(err, error);
307 
308 					return;
309 				}
310 
311 				that._fetched = true;
312 
313 				if (success) {
314 					success(that);
315 				}
316 			}, error);
317 		},
318 
319 		/**
320 		 * Retrieve the object in which this tag is contained.  It does so by
321 		 * getting this chainback's "chainlink ancestor" object.
322 		 *
323 		 * @return {GCN.AbstractTagContainer}
324 		 */
325 		'!parent': function () {
326 			return this._ancestor();
327 		},
328 
329 		/**
330 		 * Initialize a tag object.  Unlike other chainback objects, tags will
331 		 * always have a parent.  If its parent have been loaded, we will
332 		 * immediately copy the this tag's data from the parent's `_data'
333 		 * object to the tag's `_data' object.
334 		 *
335 		 * @param {string|object} settings
336 		 * @param {function(TagAPI)} success Callback to be invoked when this
337 		 *                                   operation completes normally.
338 		 * @param {function(GCNError):boolean} error Custom error handler.
339 		 */
340 		_init: function (settings, success, error) {
341 			if (jQuery.type(settings) === 'object') {
342 				this._name    = settings.name;
343 				this._data    = settings;
344 				this._data.id = settings.id;
345 				this._fetched = true;
346 			} else {
347 				this._data = {};
348 				this._data.id = this._name = settings;
349 			}
350 
351 			if (success) {
352 				var that = this;
353 
354 				this._read(function (container) {
355 					that._read(success, error);
356 				}, error);
357 
358 			// Even if not success callback is given, read this tag's data from
359 			// is container, it that container has the data available.
360 			// If we are initializing a placeholder tag object (in the process
361 			// of creating brand new tag, for example), then its parent
362 			// container will not have any data for this tag yet.  We know that
363 			// we are working with a placeholder tag if no `_data.id' or `_name'
364 			// property is set.
365 			} else if (!this._fetched && this._name &&
366 			           this.parent()._fetched) {
367 				this._data = this.parent()._getTagData(this._name);
368 				this._fetched = !!this._data;
369 
370 			// We are propably initializing a placholder object, we will assign
371 			// it its own `_data' and `_fetched' properties so that it is not
372 			// accessing the prototype values.
373 			} else if (!this._fetched) {
374 				this._data = {};
375 				this._data.id = this._name = settings;
376 				this._fetched = false;
377 			}
378 		},
379 
380 		/**
381 		 * Get or set a property of this tags.
382 		 * Note that tags do not have a `_shadow' object, and we update the
383 		 * `_data' object directly.
384 		 *
385 		 * @param {string} name Name of tag part.
386 		 * @param {*=} set Optional value.  If provided, the tag part will be
387 		 *                 replaced with this value.
388 		 * @return {*} The value of the accessed tag part.
389 		 * @throws UNFETCHED_OBJECT_ACCESS
390 		 */
391 		'!prop': function (name, value) {
392 			var parent = this.parent();
393 
394 			if (!this._fetched) {
395 				GCN.error('UNFETCHED_OBJECT_ACCESS',
396 					'Calling method `prop()\' on an unfetched object: ' +
397 					parent._type + " " + parent._data.id, this);
398 
399 				return;
400 			}
401 
402 			if (jQuery.type(value) !== 'undefined') {
403 				this._data[name] = value;
404 				parent._update('tags.' + name, this._data);
405 			}
406 
407 			return this._data[name];
408 		},
409 
410 		/**
411 		 * Get or set a part of this tags.
412 		 *
413 		 * @param {string} name Name of tag opart.
414 		 * @param {*=} set Optional value.  If provided, the tag part will be
415 		 *                 replaced with this value.
416 		 * @return {*} The value of the accessed tag part.
417 		 * @throws UNFETCHED_OBJECT_ACCESS
418 		 * @throws PART_NOT_FOUND
419 		 */
420 		'!part': function (name, value) {
421 			var parent;
422 
423 			if (!this._fetched) {
424 				parent = this.parent();
425 
426 				GCN.error('UNFETCHED_OBJECT_ACCESS',
427 					'Calling method `prop()\' on an unfetched object: ' +
428 					parent._type + " " + parent._data.id, this);
429 
430 				return null;
431 			}
432 
433 			var part = this._data.properties[name];
434 
435 			if (!part) {
436 				parent = this.parent();
437 
438 				GCN.error('PART_NOT_FOUND', 'Tag "' + this._name +
439 					'" of ' + parent._type + ' ' + parent._data.id +
440 					' does not have a part "' + name + '"', this);
441 
442 				return null;
443 			}
444 
445 			if (jQuery.type(value) === 'undefined') {
446 				return getPartValue(part);
447 			}
448 
449 			setPartValue(part, value);
450 
451 			// Each time we perform a write operation on a tag, we will update
452 			// the tag in the tag container's `_shadow' object as well.
453 			this.parent()._update('tags.' + this._name, this._data);
454 
455 			return value;
456 		},
457 
458 		/**
459 		 * Remove this tag from its containing object (it's parent).
460 		 *
461 		 * @param {function} callback A function that receive this tag's parent
462 		 *                            object as its only arguments.
463 		 */
464 		remove: function (success, error) {
465 			var parent = this.parent();
466 
467 			if (!parent.hasOwnProperty('_deletedTags')) {
468 				parent._deletedTags = [];
469 			}
470 
471 			parent._deletedTags.push(this._name);
472 
473 			if (parent._data.tags &&
474 					parent._data.tags[this._name]) {
475 				delete parent._data.tags[this._name];
476 			}
477 
478 			if (parent._shadow.tags &&
479 					parent._shadow.tags[this._name]) {
480 				delete parent._shadow.tags[this._name];
481 			}
482 
483 			if (success) {
484 				parent._persist(success, error);
485 			}
486 		},
487 
488 		/**
489 		 * Will render this tag in the given render `mode'.  If an element is
490 		 * provided, the content will be placed in that element.  If the `mode'
491 		 * is "edit", any rendered editables will be initialized for Aloha
492 		 * Editor.  Any editable that are rendered into an element will also be
493 		 * added to the tag's parent object's `_editables' array so that they
494 		 * can have their changed contents copied back into their corresponding
495 		 * tags during saving.
496 		 *
497 		 * @param {string} mode The rendering mode.  Valid values are "view",
498 		 *                      and "edit".
499 		 * @param {jQuery.<HTMLElement>} element DOM element into which the
500 		 *                                       the rendered content should be
501 		 *                                       placed.
502 		 * @param {function(string, TagAPI, object)} Optional success handler.
503 		 * @param {function(GCNError):boolean} Optional custom error handler.
504 		 */
505 		'!_render': function (mode, element, success, error) {
506 			var that = this;
507 			var parent = this.parent();
508 
509 			this._read(function () {
510 				var template = '<node ' + that._name + '>';
511 
512 				that._procure();
513 
514 				parent._renderTemplate(template, mode, function (data) {
515 					var tags = parent._getEditablesAndBlocks(data);
516 
517 					parent._storeRenderedEditables(tags.editables);
518 					parent._storeRenderedBlocks(tags.blocks);
519 
520 					GCN._handleContentRendered(data.content, function (html) {
521 						if (element) {
522 							element.html(html);
523 							GCN.pub('content-inserted', [element, html]);
524 						}
525 
526 						if (success) {
527 							success(html, that, data);
528 						}
529 
530 						initializeFrontendEditing(tags.editables, tags.blocks,
531 							parent.id(), element);
532 
533 						that._vacate();
534 					});
535 				}, function () {
536 					that._vacate();
537 				});
538 			}, error);
539 		},
540 
541 		/**
542 		 * Render the tag based on its settings on the server.
543 		 * Can be called with the following arguments:
544 		 *
545 		 * Do nothing:
546 		 * render()
547 		 *
548 		 * Render tag contents into div whose id is "content-div":
549 		 * @param {string|jQuery.<HTMLElement>}
550 		 * render('#content-div') or render(jQuery('#content-div'))
551 		 *
552 		 * Pass the html rendering of the tag in the given callback:
553 		 * @param {function(string, GCN.TagAPI)}
554 		 * render(function (html, tag) {})
555 		 *
556 		 * Whenever a 2nd argument is provided, it will be taken as as custom
557 		 * error handler.
558 		 */
559 		render: function () {
560 			var that = this;
561 			var args = arguments;
562 
563 			// Wait until DOM is ready
564 			jQuery(function () {
565 				args = getRenderOptions(args);
566 
567 				if (args.element || args.success) {
568 					that._render('view', args.element, args.success,
569 						args.error);
570 				}
571 			});
572 		},
573 
574 		/**
575 		 * Like `render()', except that the content is rendered with additional
576 		 * elements that are required for front-end editing. ie: editables.
577 		 */
578 		edit: function () {
579 			var that = this;
580 			var args = arguments;
581 
582 			// Wait until DOM is ready
583 			jQuery(function () {
584 				args = getRenderOptions(args);
585 
586 				if (args.element || args.success) {
587 					that._render('edit', args.element, args.success,
588 						args.error);
589 				}
590 			});
591 		},
592 
593 		/**
594 		 * Persists the changes to this tag on its container object.
595 		 *
596 		 * @param {function(TagAPI)} success Callback to be invoked when this
597 		 *                                   operation completes normally.
598 		 * @param {function(GCNError):boolean} error Custom error handler.
599 		 */
600 		save: function (success, error) {
601 			var that = this;
602 			this.parent().save(function () {
603 				if (success) {
604 					success(that);
605 				}
606 			}, error);
607 		}
608 
609 	});
610 
611 	// Unlike content objects, tags do not have unique ids and so we uniquely I
612 	// dentify tags by their name, and their parent's id.
613 	TagAPI._needsChainedHash = true;
614 
615 	GCN.tag = GCN.exposeAPI(TagAPI);
616 	GCN.TagAPI = TagAPI;
617 
618 }(GCN));
619