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 frontend editing.
190 	 *
191 	 * @TODO: This function should be moved out of Gentics Content.Node
192 	 *        JavaScript API.  We should publish a message instead, and pass
193 	 *        these arguments in the 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 {function} callback
201 	 */
202 	function initializeFrontendEditing(editables, blocks, pageId, callback) {
203 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
204 		if (!Aloha) {
205 			return;
206 		}
207 		if (Aloha.GCN) {
208 			Aloha.GCN.page = GCN.page(pageId);
209 			Aloha.GCN.setupConstructsButton(pageId);
210 		}
211 		var editable;
212 		var unmodified = [];
213 		var j = editables && editables.length;
214 		while (j--) {
215 			Aloha.jQuery('#' + editables[j].element).aloha();
216 			editable = getAlohaEditableById(editables[j].element);
217 			if (editable) {
218 				unmodified.push(editable);
219 				if (editables[j].readonly) {
220 					editable.disable();
221 				}
222 			}
223 		}
224 		if (Aloha.GCN) {
225 			j = Aloha.editables.length;
226 			while (j--) {
227 				if (!Aloha.editables[j].isModified()) {
228 					unmodified.push(Aloha.editables[j]);
229 				}
230 			}
231 			Aloha.GCN.alohaBlocks(blocks, pageId, function () {
232 				var j = unmodified.length;
233 				while (j--) {
234 					unmodified[j].setUnmodified();
235 				}
236 				if (callback) {
237 					callback();
238 				}
239 			});
240 		} else if (callback) {
241 			callback();
242 		}
243 	}
244 
245 	/**
246 	 * Helper function to normalize the arguments that can be passed to the
247 	 * `edit()' and `render()' methods.
248 	 *
249 	 * @private
250 	 * @static
251 	 * @param {arguments} args A list of arguments.
252 	 * @return {object} Object containing an the properties `element',
253 	 *                         `success' and `error', and `data'.
254 	 */
255 	function getRenderOptions(args) {
256 		var argv = Array.prototype.slice.call(args);
257 		var argc = args.length;
258 		var arg;
259 		var i;
260 
261 		var element;
262 		var success;
263 		var error;
264 		var prerenderedData = false;
265 
266 		for (i = 0; i < argc; ++i) {
267 			arg = argv[i];
268 
269 			switch (jQuery.type(arg)) {
270 			case 'string':
271 				element = jQuery(arg);
272 				break;
273 			case 'object':
274 				if (element) {
275 					prerenderedData = arg;
276 				} else {
277 					element = arg;
278 				}
279 				break;
280 			case 'function':
281 				if (success) {
282 					error = arg;
283 				} else {
284 					success = arg;
285 				}
286 				break;
287 			// Descarding all other types of arguments...
288 			}
289 		}
290 
291 		return {
292 			element : element,
293 			success : success,
294 			error   : error,
295 			data    : prerenderedData
296 		};
297 	}
298 
299 	/**
300 	 * Exposes an API to operate on a Content.Node tag.
301 	 *
302 	 * @class
303 	 * @name TagAPI
304 	 */
305 	var TagAPI = GCN.defineChainback({
306 
307 		__chainbacktype__: 'TagAPI',
308 
309 		/**
310 		 * @type {GCN.ContentObject} A reference to the object in which this
311 		 *                           tag is contained.  This value is set
312 		 *                           during initialization.
313 		 */
314 		_parent: null,
315 
316 		/**
317 		 * @type {string} Name of this tag.
318 		 */
319 		_name: null,
320 
321 		/**
322 		 * Gets this tag's information from the object that contains it.
323 		 *
324 		 * @param {function(TagAPI)} success Callback to be invoked when this
325 		 *                                   operation completes normally.
326 		 * @param {function(GCNError):boolean} error Custom error handler.
327 		 */
328 		'!_read': function (success, error) {
329 			if (this._fetched) {
330 				if (success) {
331 					this._invoke(success, [this]);
332 				}
333 				return;
334 			}
335 
336 			var that = this;
337 			var parent = this.parent();
338 
339 			// assert(parent)
340 
341 			// Take the data for this tag from it's container.
342 			parent._read(function () {
343 				that._data = parent._getTagData(that._name);
344 
345 				if (!that._data) {
346 					var err = GCN.createError('TAG_NOT_FOUND',
347 						'Could not find tag "' + that._name + '" in ' +
348 						parent._type + " " + parent._data.id, that);
349 					GCN.handleError(err, error);
350 					return;
351 				}
352 
353 				that._fetched = true;
354 
355 				if (success) {
356 					that._invoke(success, [that]);
357 				}
358 			}, error);
359 		},
360 
361 		/**
362 		 * Retrieve the object in which this tag is contained.  It does so by
363 		 * getting this chainback's "chainlink ancestor" object.
364 		 *
365 		 * @function
366 		 * @name parent
367 		 * @memberOf TagAPI
368 		 * @return {GCN.AbstractTagContainer}
369 		 */
370 		'!parent': function () {
371 			return this._ancestor();
372 		},
373 
374 		/**
375 		 * Initialize a tag object. Unlike other chainback objects, tags will
376 		 * always have a parent. If its parent have been loaded, we will
377 		 * immediately copy the this tag's data from the parent's `_data' object
378 		 * to the tag's `_data' object.
379 		 * 
380 		 * @param {string|object}
381 		 *            settings
382 		 * @param {function(TagAPI)}
383 		 *            success Callback to be invoked when this operation
384 		 *            completes normally.
385 		 * @param {function(GCNError):boolean}
386 		 *            error Custom error handler.
387 		 */
388 		_init: function (settings, success, error) {
389 			if (jQuery.type(settings) === 'object') {
390 				this._name    = settings.name;
391 				this._data    = settings;
392 				this._data.id = settings.id;
393 				this._fetched = true;
394 			} else {
395 				// We don't want to reinitalize the data object when it 
396 				// has not been fetched yet.
397 				if (!this._fetched) {
398 					this._data = {};
399 					this._data.id = this._name = settings;
400 				}
401 			}
402 
403 			if (success) {
404 				var that = this;
405 
406 				this._read(function (container) {
407 					that._read(success, error);
408 				}, error);
409 
410 			// Even if not success callback is given, read this tag's data from
411 			// is container, it that container has the data available.
412 			// If we are initializing a placeholder tag object (in the process
413 			// of creating brand new tag, for example), then its parent
414 			// container will not have any data for this tag yet.  We know that
415 			// we are working with a placeholder tag if no `_data.id' or `_name'
416 			// property is set.
417 			} else if (!this._fetched && this._name &&
418 			           this.parent()._fetched) {
419 				this._data = this.parent()._getTagData(this._name);
420 				this._fetched = !!this._data;
421 
422 			// We are propably initializing a placholder object, we will assign
423 			// it its own `_data' and `_fetched' properties so that it is not
424 			// accessing the prototype values.
425 			} else if (!this._fetched) {
426 				this._data = {};
427 				this._data.id = this._name = settings;
428 				this._fetched = false;
429 			}
430 		},
431 
432 		/**
433 		 * Get or set a property of this tags. Note that tags do not have a
434 		 * `_shadow' object, and we update the `_data' object directly.
435 		 * 
436 		 * @function
437 		 * @name prop
438 		 * @memberOf TagAPI
439 		 * @param {string}
440 		 *            name Name of tag part.
441 		 * @param {*=}
442 		 *            set Optional value. If provided, the tag part will be
443 		 *            replaced with this value.
444 		 * @return {*} The value of the accessed tag part.
445 		 * @throws UNFETCHED_OBJECT_ACCESS
446 		 */
447 		'!prop': function (name, value) {
448 			var parent = this.parent();
449 
450 			if (!this._fetched) {
451 				GCN.error('UNFETCHED_OBJECT_ACCESS',
452 					'Calling method `prop()\' on an unfetched object: ' +
453 					parent._type + " " + parent._data.id, this);
454 
455 				return;
456 			}
457 
458 			if (jQuery.type(value) !== 'undefined') {
459 				this._data[name] = value;
460 				parent._update('tags.' + GCN.escapePropertyName(name),
461 					this._data);
462 			}
463 
464 			return this._data[name];
465 		},
466 
467 		/**
468 		 * Get or set a part of this tags.
469 		 * 
470 		 * @function
471 		 * @name part
472 		 * @memberOf TagAPI
473 		 * 
474 		 * @param {string}
475 		 *            name Name of tag opart.
476 		 * @param {*=}
477 		 *            set Optional value. If provided, the tag part will be
478 		 *            replaced with this value.
479 		 * @return {*} The value of the accessed tag part.
480 		 * @throws UNFETCHED_OBJECT_ACCESS
481 		 * @throws PART_NOT_FOUND
482 		 */
483 		'!part': function (name, value) {
484 			var parent;
485 
486 			if (!this._fetched) {
487 				parent = this.parent();
488 
489 				GCN.error('UNFETCHED_OBJECT_ACCESS',
490 					'Calling method `prop()\' on an unfetched object: ' +
491 					parent._type + " " + parent._data.id, this);
492 
493 				return null;
494 			}
495 
496 			var part = this._data.properties[name];
497 
498 			if (!part) {
499 				parent = this.parent();
500 
501 				GCN.error('PART_NOT_FOUND', 'Tag "' + this._name +
502 					'" of ' + parent._type + ' ' + parent._data.id +
503 					' does not have a part "' + name + '"', this);
504 
505 				return null;
506 			}
507 
508 			if (jQuery.type(value) === 'undefined') {
509 				return getPartValue(part);
510 			}
511 
512 			setPartValue(part, value);
513 
514 			// Each time we perform a write operation on a tag, we will update
515 			// the tag in the tag container's `_shadow' object as well.
516 			this.parent()._update('tags.' + GCN.escapePropertyName(this._name),
517 				this._data);
518 
519 			return value;
520 		},
521 
522 		/**
523 		 * Remove this tag from its containing object (it's parent).
524 		 *
525 		 * @function
526 		 * @memberOf TagAPI
527 		 * @name remove
528 		 * @param {function} callback A function that receive this tag's parent
529 		 *                            object as its only arguments.
530 		 */
531 		remove: function (success, error) {
532 			var parent = this.parent();
533 
534 			if (!parent.hasOwnProperty('_deletedTags')) {
535 				parent._deletedTags = [];
536 			}
537 
538 			parent._deletedTags.push(this._name);
539 
540 			if (parent._data.tags &&
541 					parent._data.tags[this._name]) {
542 				delete parent._data.tags[this._name];
543 			}
544 
545 			if (parent._shadow.tags &&
546 					parent._shadow.tags[this._name]) {
547 				delete parent._shadow.tags[this._name];
548 			}
549 
550 			parent._removeAssociatedTagData(this._name);
551 
552 			if (success) {
553 				parent._persist(success, error);
554 			}
555 		},
556 
557 		/**
558 		 * Given a DOM element, will generate a template which represents this
559 		 * tag as it would be if rendered in the element.
560 		 *
561 		 * @param {jQuery.<HTMLElement>} $element DOM element with which to
562 		 *                                        generate the template.
563 		 * @return {string} Template string.
564 		 */
565 		'!_makeTemplate': function ($element) {
566 			if (0 === $element.length) {
567 				return '<node ' + this._name + '>';
568 			}
569 			var placeholder =
570 					'-{(' + this.parent().id() + ':' + this._name + ')}-';
571 			var template = jQuery.trim(
572 					$element.clone().html(placeholder)[0].outerHTML
573 				);
574 			return template.replace(placeholder, '<node ' + this._name + '>');
575 		},
576 
577 		/**
578 		 * Will render this tag in the given render `mode'.  If an element is
579 		 * provided, the content will be placed in that element.  If the `mode'
580 		 * is "edit", any rendered editables will be initialized for Aloha
581 		 * Editor.  Any editable that are rendered into an element will also be
582 		 * added to the tag's parent object's `_editables' array so that they
583 		 * can have their changed contents copied back into their corresponding
584 		 * tags during saving.
585 		 *
586 		 * @param {string} mode The rendering mode.  Valid values are "view",
587 		 *                      and "edit".
588 		 * @param {jQuery.<HTMLElement>} element DOM element into which the
589 		 *                                       the rendered content should be
590 		 *                                       placed.
591 		 * @param {function(string, TagAPI, object)} Optional success handler.
592 		 * @param {function(GCNError):boolean} Optional custom error handler.
593 		 */
594 		'!_render': function (mode, $element, success, error) {
595 			var tag = this;
596 			tag._read(function () {
597 				tag._procure();
598 				var obj = tag.parent();
599 				var template = ($element && $element.length)
600 				             ? tag._makeTemplate($element)
601 				             : '<node ' + tag._name + '>';
602 				obj._renderTemplate(template, mode, function (data) {
603 					var tags = obj._processRenderedTags(data);
604 					GCN._handleContentRendered(data.content, tag,
605 						function (html) {
606 							if ($element && $element.length) {
607 								GCN.renderOnto($element, html);
608 								GCN.pub('content-inserted', [$element, html]);
609 							}
610 							var frontendEditing = function (callback) {
611 								if ('edit' === mode) {
612 									initializeFrontendEditing(tags.editables,
613 											tags.blocks, obj.id(), callback);
614 								} else {
615 									callback();
616 								}
617 							};
618 							if (success) {
619 								tag._invoke(success,
620 										[html, tag, data, frontendEditing]);
621 							} else {
622 								frontendEditing();
623 							}
624 							tag._vacate();
625 						});
626 				}, function () {
627 					tag._vacate();
628 				});
629 			}, error);
630 		},
631 
632 		/**
633 		 * <p>
634 		 * Render the tag based on its settings on the server. Can be called
635 		 * with the following arguments:<(p>
636 		 * 
637 		 * <pre>
638 		 * // Render tag contents into div whose id is "content-div"
639 		 * render('#content-div') or render(jQuery('#content-div'))
640 		 * </pre>
641 		 * 
642 		 * <pre>
643 		 * // Pass the html rendering of the tag in the given callback
644 		 * render(function(html, tag) {
645 		 *   // implementation!
646 		 * })
647 		 * </pre>
648 		 * 
649 		 * Whenever a 2nd argument is provided, it will be taken as as custom
650 		 * error handler. Invoking render() without any arguments will yield no
651 		 * results.
652 		 * 
653 		 * @function
654 		 * @name render
655 		 * @memberOf TagAPI
656 		 * @param {string|jQuery.HTMLElement}
657 		 *            selector jQuery selector or jQuery target element to be
658 		 *            used as render destination
659 		 * @param {function(string,
660 		 *            GCN.TagAPI)} success success function that will receive
661 		 *            the rendered html as well as the TagAPI object
662 		 */
663 		render: function () {
664 			var that = this;
665 			var args = arguments;
666 			// Wait until DOM is ready
667 			jQuery(function () {
668 				args = getRenderOptions(args);
669 				if (args.element || args.success) {
670 					that._render('view', args.element, args.success,
671 						args.error);
672 				}
673 			});
674 		},
675 
676 		/**
677 		 * <p>
678 		 * Renders this tag for editing.
679 		 * </p>
680 		 * 
681 		 * <p>
682 		 * Differs from the render() method in that it calls this tag to be
683 		 * rendered in "edit" mode via the REST API so that it is rendered with
684 		 * any additional content that is appropriate for when this tag is used
685 		 * in edit mode.
686 		 * </p>
687 		 * 
688 		 * <p>
689 		 * The GCN JS API library will also start keeping track of various
690 		 * aspects of this tag and its rendered content.
691 		 * </p>
692 		 * 
693 		 * @function
694 		 * @name edit
695 		 * @memberOf TagAPI
696 		 * @param {(string|jQuery.HTMLElement)=} element
697 		 *            The element into which this tag is to be rendered.
698 		 * @param {function(string,
699 		 *            TagAPI)=} success A function that will be called once the tag is
700 		 *            rendered.
701 		 * @param {function(GCNError):boolean=} error
702 		 *            A custom error handler.
703 		 */
704 		edit: function () {
705 			var tag = this;
706 			var args = getRenderOptions(arguments);
707 			if (args.data) {
708 				var parent = tag.parent();
709 				var tags = parent._processRenderedTags(args.data);
710 				initializeFrontendEditing(tags.editables, tags.blocks,
711 					parent.id(),
712 					function () {
713 						if (args.success) {
714 							tag._invoke(args.success,
715 									[args.content, tag, args.data]);
716 						}
717 					});
718 			} else {
719 				// Because we need to wait until the DOM is ready before we can
720 				// interact with elements in the DOM.
721 				jQuery(function () {
722 					if (args.element || args.success) {
723 						tag._render('edit', args.element, args.success,
724 								args.error);
725 					}
726 				});
727 			}
728 		},
729 
730 		/**
731 		 * Persists the changes to this tag on its container object.
732 		 *
733 		 * @function
734 		 * @name save
735 		 * @memberOf TagAPI
736 		 * @param {function(TagAPI)} success Callback to be invoked when this
737 		 *                                   operation completes normally.
738 		 * @param {function(GCNError):boolean} error Custom error handler.
739 		 */
740 		save: function (success, error) {
741 			var that = this;
742 			this.parent().save(function () {
743 				if (success) {
744 					that._invoke(success, [that]);
745 				}
746 			}, error);
747 		}
748 
749 	});
750 
751 	// Unlike content objects, tags do not have unique ids and so we uniquely I
752 	// dentify tags by their name, and their parent's id.
753 	TagAPI._needsChainedHash = true;
754 
755 	GCN.tag = GCN.exposeAPI(TagAPI);
756 	GCN.TagAPI = TagAPI;
757 
758 }(GCN));
759