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' && value !== null) {
 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 !== 'undefined') {
 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 	 * Searches for the an Aloha editable object of the given id.
154 	 *
155 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
156 	 *        JavaScript exception if the element for the given ID is not found
157 	 *        then we can deprecate this function and use Aloha's instead.
158 	 *
159 	 * @static
160 	 * @param {string} id Id of Aloha.Editable object to find.
161 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
162 	 */
163 	function getAlohaEditableById(id) {
164 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
165 		if (!Aloha) {
166 			return null;
167 		}
168 
169 		// If the element is a textarea then route to the editable div.
170 		var element = jQuery('#' + id);
171 		if (element.length &&
172 				element[0].nodeName.toLowerCase() === 'textarea') {
173 			id += '-aloha';
174 		}
175 
176 		var editables = Aloha.editables;
177 		var j = editables.length;
178 		while (j) {
179 			if (editables[--j].getId() === id) {
180 				return editables[j];
181 			}
182 		}
183 
184 		return null;
185 	}
186 
187 	/**
188 	 * Will initialize the contents that have been rendered in a given
189 	 * container for front end editing.
190 	 *
191 	 * @TODO: This function should be moved out of Gentics Content.Node JavaScript API.  We should
192 	 *        publish a message instead, and pass these arguments in the
193 	 *        message.
194 	 *
195 	 * @private
196 	 * @static
197 	 * @param {Array.<object>} editables Editables to be `aloha()'fied.
198 	 * @param {Array.<object>} blocks Blocks to receive tagfill buttons.
199 	 * @param {number|string} pageId id of the page the tag belongs to
200 	 * @param {jQuery<HTMLElement>} container The element that wraps the
201 	 *                              incoming tag contents.
202 	 */
203 	function initializeFrontendEditing(editables, blocks, pageId, container) {
204 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
205 
206 		if (!Aloha) {
207 			return;
208 		}
209 
210 		Aloha.ready(function () {
211 			if (Aloha.GCN) {
212 				// If we are in the backend, then we need to remove the id of
213 				// the container because it is duplicated in the incoming
214 				// content.
215 				if (container && Aloha.GCN.isBackendMode()) {
216 					container.removeAttr('id');
217 				}
218 				Aloha.GCN.page = GCN.page(pageId);
219 				Aloha.GCN.setupConstructsButton(pageId);
220 			}
221 
222 			var j = editables && editables.length;
223 			var editable;
224 			var unmodified = [];
225 			while (j) {
226 				Aloha.jQuery('#' + editables[--j].element).aloha();
227 				editable = getAlohaEditableById(editables[j].element);
228 				if (editable) {
229 					unmodified.push(editable);
230 					if (editables[j].readonly) {
231 						editable.disable();
232 					}
233 				}
234 			}
235 
236 			if (Aloha.GCN) {
237 				j = Aloha.editables.length;
238 				while (j) {
239 					if (!Aloha.editables[--j].isModified()) {
240 						unmodified.push(Aloha.editables[j]);
241 					}
242 				}
243 				Aloha.GCN.alohaBlocks(blocks, pageId, function () {
244 					var j = unmodified.length;
245 					while (j) {
246 						unmodified[--j].setUnmodified();
247 					}
248 				});
249 			}
250 		});
251 	}
252 
253 	/**
254 	 * Helper function to normalize the arguments that can be passed to the
255 	 * `edit()' and `render()' methods.
256 	 *
257 	 * @private
258 	 * @static
259 	 * @param {arguments} args A list of arguments.
260 	 * @return {object} Object containing an the properties `element',
261 	 *                         `success' and `error', and `data'.
262 	 */
263 	function getRenderOptions(args) {
264 		var argv = Array.prototype.slice.call(args);
265 		var argc = args.length;
266 		var arg;
267 		var i;
268 
269 		var element;
270 		var success;
271 		var error;
272 		var prerenderedData = false;
273 
274 		for (i = 0; i < argc; ++i) {
275 			arg = argv[i];
276 
277 			switch (jQuery.type(arg)) {
278 			case 'string':
279 				element = jQuery(arg);
280 				break;
281 			case 'object':
282 				if (element) {
283 					prerenderedData = arg;
284 				} else {
285 					element = arg;
286 				}
287 				break;
288 			case 'function':
289 				if (success) {
290 					error = arg;
291 				} else {
292 					success = arg;
293 				}
294 				break;
295 			// Descarding all other types of arguments...
296 			}
297 		}
298 
299 		return {
300 			element : element,
301 			success : success,
302 			error   : error,
303 			data    : prerenderedData
304 		};
305 	}
306 
307 	/**
308 	 * Exposes an API to operate on a Content.Node tag.
309 	 *
310 	 * @public
311 	 * @class TagAPI
312 	 */
313 	var TagAPI = GCN.defineChainback({
314 
315 		__chainbacktype__: 'TagAPI',
316 
317 		/**
318 		 * @type {GCN.ContentObject} A reference to the object in which this
319 		 *                           tag is contained.  This value is set
320 		 *                           during initialization.
321 		 */
322 		_parent: null,
323 
324 		/**
325 		 * @type {string} Name of this tag.
326 		 */
327 		_name: null,
328 
329 		/**
330 		 * Gets this tag's information from the object that contains it.
331 		 *
332 		 * @param {function(TagAPI)} success Callback to be invoked when this
333 		 *                                   operation completes normally.
334 		 * @param {function(GCNError):boolean} error Custom error handler.
335 		 */
336 		'!_read': function (success, error) {
337 			if (this._fetched) {
338 				if (success) {
339 					this._invoke(success, [this]);
340 				}
341 
342 				return;
343 			}
344 
345 			var that = this;
346 			var parent = this.parent();
347 
348 			// assert(parent)
349 
350 			// Take the data for this tag from it's container.
351 			parent._read(function () {
352 				that._data = parent._getTagData(that._name);
353 
354 				if (!that._data) {
355 					var err = GCN.createError('TAG_NOT_FOUND',
356 						'Could not find tag "' + that._name + '" in ' +
357 						parent._type + " " + parent._data.id, that);
358 
359 					GCN.handleError(err, error);
360 
361 					return;
362 				}
363 
364 				that._fetched = true;
365 
366 				if (success) {
367 					that._invoke(success, [that]);
368 				}
369 			}, error);
370 		},
371 
372 		/**
373 		 * Retrieve the object in which this tag is contained.  It does so by
374 		 * getting this chainback's "chainlink ancestor" object.
375 		 *
376 		 * @return {GCN.AbstractTagContainer}
377 		 */
378 		'!parent': function () {
379 			return this._ancestor();
380 		},
381 
382 		/**
383 		 * Initialize a tag object.  Unlike other chainback objects, tags will
384 		 * always have a parent.  If its parent have been loaded, we will
385 		 * immediately copy the this tag's data from the parent's `_data'
386 		 * object to the tag's `_data' object.
387 		 *
388 		 * @param {string|object} settings
389 		 * @param {function(TagAPI)} success Callback to be invoked when this
390 		 *                                   operation completes normally.
391 		 * @param {function(GCNError):boolean} error Custom error handler.
392 		 */
393 		_init: function (settings, success, error) {
394 			if (jQuery.type(settings) === 'object') {
395 				this._name    = settings.name;
396 				this._data    = settings;
397 				this._data.id = settings.id;
398 				this._fetched = true;
399 			} else {
400 				// We don't want to reinitalize the data object when it 
401 				// has not been fetched yet.
402 				if (!this._fetched) {
403 					this._data = {};
404 					this._data.id = this._name = settings;
405 				}
406 			}
407 
408 			if (success) {
409 				var that = this;
410 
411 				this._read(function (container) {
412 					that._read(success, error);
413 				}, error);
414 
415 			// Even if not success callback is given, read this tag's data from
416 			// is container, it that container has the data available.
417 			// If we are initializing a placeholder tag object (in the process
418 			// of creating brand new tag, for example), then its parent
419 			// container will not have any data for this tag yet.  We know that
420 			// we are working with a placeholder tag if no `_data.id' or `_name'
421 			// property is set.
422 			} else if (!this._fetched && this._name &&
423 			           this.parent()._fetched) {
424 				this._data = this.parent()._getTagData(this._name);
425 				this._fetched = !!this._data;
426 
427 			// We are propably initializing a placholder object, we will assign
428 			// it its own `_data' and `_fetched' properties so that it is not
429 			// accessing the prototype values.
430 			} else if (!this._fetched) {
431 				this._data = {};
432 				this._data.id = this._name = settings;
433 				this._fetched = false;
434 			}
435 		},
436 
437 		/**
438 		 * Get or set a property of this tags.
439 		 * Note that tags do not have a `_shadow' object, and we update the
440 		 * `_data' object directly.
441 		 *
442 		 * @param {string} name Name of tag part.
443 		 * @param {*=} set Optional value.  If provided, the tag part will be
444 		 *                 replaced with this value.
445 		 * @return {*} The value of the accessed tag part.
446 		 * @throws UNFETCHED_OBJECT_ACCESS
447 		 */
448 		'!prop': function (name, value) {
449 			var parent = this.parent();
450 
451 			if (!this._fetched) {
452 				GCN.error('UNFETCHED_OBJECT_ACCESS',
453 					'Calling method `prop()\' on an unfetched object: ' +
454 					parent._type + " " + parent._data.id, this);
455 
456 				return;
457 			}
458 
459 			if (jQuery.type(value) !== 'undefined') {
460 				this._data[name] = value;
461 				parent._update('tags.' + GCN.escapePropertyName(name),
462 					this._data);
463 			}
464 
465 			return this._data[name];
466 		},
467 
468 		/**
469 		 * Get or set a part of this tags.
470 		 *
471 		 * @param {string} name Name of tag opart.
472 		 * @param {*=} set Optional value.  If provided, the tag part will be
473 		 *                 replaced with this value.
474 		 * @return {*} The value of the accessed tag part.
475 		 * @throws UNFETCHED_OBJECT_ACCESS
476 		 * @throws PART_NOT_FOUND
477 		 */
478 		'!part': function (name, value) {
479 			var parent;
480 
481 			if (!this._fetched) {
482 				parent = this.parent();
483 
484 				GCN.error('UNFETCHED_OBJECT_ACCESS',
485 					'Calling method `prop()\' on an unfetched object: ' +
486 					parent._type + " " + parent._data.id, this);
487 
488 				return null;
489 			}
490 
491 			var part = this._data.properties[name];
492 
493 			if (!part) {
494 				parent = this.parent();
495 
496 				GCN.error('PART_NOT_FOUND', 'Tag "' + this._name +
497 					'" of ' + parent._type + ' ' + parent._data.id +
498 					' does not have a part "' + name + '"', this);
499 
500 				return null;
501 			}
502 
503 			if (jQuery.type(value) === 'undefined') {
504 				return getPartValue(part);
505 			}
506 
507 			setPartValue(part, value);
508 
509 			// Each time we perform a write operation on a tag, we will update
510 			// the tag in the tag container's `_shadow' object as well.
511 			this.parent()._update('tags.' + GCN.escapePropertyName(this._name),
512 				this._data);
513 
514 			return value;
515 		},
516 
517 		/**
518 		 * Remove this tag from its containing object (it's parent).
519 		 *
520 		 * @param {function} callback A function that receive this tag's parent
521 		 *                            object as its only arguments.
522 		 */
523 		remove: function (success, error) {
524 			var parent = this.parent();
525 
526 			if (!parent.hasOwnProperty('_deletedTags')) {
527 				parent._deletedTags = [];
528 			}
529 
530 			parent._deletedTags.push(this._name);
531 
532 			if (parent._data.tags &&
533 					parent._data.tags[this._name]) {
534 				delete parent._data.tags[this._name];
535 			}
536 
537 			if (parent._shadow.tags &&
538 					parent._shadow.tags[this._name]) {
539 				delete parent._shadow.tags[this._name];
540 			}
541 
542 			parent._removeAssociatedTagData(this._name);
543 
544 			if (success) {
545 				parent._persist(success, error);
546 			}
547 		},
548 
549 		/**
550 		 * Will render this tag in the given render `mode'.  If an element is
551 		 * provided, the content will be placed in that element.  If the `mode'
552 		 * is "edit", any rendered editables will be initialized for Aloha
553 		 * Editor.  Any editable that are rendered into an element will also be
554 		 * added to the tag's parent object's `_editables' array so that they
555 		 * can have their changed contents copied back into their corresponding
556 		 * tags during saving.
557 		 *
558 		 * @param {string} mode The rendering mode.  Valid values are "view",
559 		 *                      and "edit".
560 		 * @param {jQuery.<HTMLElement>} element DOM element into which the
561 		 *                                       the rendered content should be
562 		 *                                       placed.
563 		 * @param {function(string, TagAPI, object)} Optional success handler.
564 		 * @param {function(GCNError):boolean} Optional custom error handler.
565 		 */
566 		'!_render': function (mode, element, success, error) {
567 			var that = this;
568 			var parent = this.parent();
569 			this._read(function () {
570 				that._procure();
571 				parent._renderTemplate('<node ' + that._name + '>', mode,
572 					function (data) {
573 						var tags = parent._processRenderedTags(data);
574 						GCN._handleContentRendered(data.content, that,
575 							function (html) {
576 								if (element) {
577 									element.html(html);
578 									GCN.pub('content-inserted', [element,
579 										html]);
580 								}
581 
582 								if (success) {
583 									that._invoke(success, [html, that, data]);
584 								}
585 
586 								initializeFrontendEditing(tags.editables,
587 									tags.blocks, parent.id(), element);
588 								that._vacate();
589 							});
590 					}, function () {
591 						that._vacate();
592 					});
593 			}, error);
594 		},
595 
596 		/**
597 		 * Render the tag based on its settings on the server.
598 		 * Can be called with the following arguments:
599 		 *
600 		 * Do nothing:
601 		 * render()
602 		 *
603 		 * Render tag contents into div whose id is "content-div":
604 		 * @param {string|jQuery.<HTMLElement>}
605 		 * render('#content-div') or render(jQuery('#content-div'))
606 		 *
607 		 * Pass the html rendering of the tag in the given callback:
608 		 * @param {function(string, GCN.TagAPI)}
609 		 * render(function (html, tag) {})
610 		 *
611 		 * Whenever a 2nd argument is provided, it will be taken as as custom
612 		 * error handler.
613 		 */
614 		render: function () {
615 			var that = this;
616 			var args = arguments;
617 			// Wait until DOM is ready
618 			jQuery(function () {
619 				args = getRenderOptions(args);
620 				if (args.element || args.success) {
621 					that._render('view', args.element, args.success,
622 						args.error);
623 				}
624 			});
625 		},
626 
627 		/**
628 		 * Like `render()', except that the content is rendered with additional
629 		 * elements that are required for front-end editing. ie: editables.
630 		 */
631 		edit: function () {
632 			var args = getRenderOptions(arguments);
633 			if (args.data) {
634 				var parent = this.parent();
635 				var tags = parent._processRenderedTags(args.data);
636 				if (args.success) {
637 					this._invoke(args.success, [args.content, this, args.data]);
638 				}
639 				initializeFrontendEditing(tags.editables, tags.blocks,
640 					parent.id());
641 			} else {
642 				// Wait until DOM is ready
643 				var that = this;
644 				jQuery(function () {
645 					if (args.element || args.success) {
646 						that._render('edit', args.element, args.success,
647 							args.error);
648 					}
649 				});
650 			}
651 		},
652 
653 		/**
654 		 * Persists the changes to this tag on its container object.
655 		 *
656 		 * @param {function(TagAPI)} success Callback to be invoked when this
657 		 *                                   operation completes normally.
658 		 * @param {function(GCNError):boolean} error Custom error handler.
659 		 */
660 		save: function (success, error) {
661 			var that = this;
662 			this.parent().save(function () {
663 				if (success) {
664 					that._invoke(success, [that]);
665 				}
666 			}, error);
667 		}
668 
669 	});
670 
671 	// Unlike content objects, tags do not have unique ids and so we uniquely I
672 	// dentify tags by their name, and their parent's id.
673 	TagAPI._needsChainedHash = true;
674 
675 	GCN.tag = GCN.exposeAPI(TagAPI);
676 	GCN.TagAPI = TagAPI;
677 
678 }(GCN));
679