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