Danbooru: Improved Tooltips

By evazion Last update Dec 3, 2009 — Installed 356 times.

There are 4 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           Danbooru: Improved Tooltips
// @include        http://danbooru.donmai.us/*
// @include        http://safebooru.donmai.us/*
// ==/UserScript==

/*
  BUGS:
    Excessively long tags or favorites lists aren't handled well.
    Nonexistant posts (i.e. posts that have been double-deleted, or posts with rating:[qe] on Safebooru) aren't handled.
    Positioning broken inside tables (the offsetParent of a element that is a child of a table element is the table element).

    Chrome:
      Scrolling out of a tooltip doesn't hide the tooltip until the mouse is moved.
      Sometimes when clicking the more tags link then quickly moving to another thumbnail
      the original tooltip won't hide.
*/

function include_script(code, load) {
	var script  = document.createElement('script');
	script.type = 'text/javascript';

	if (typeof code === 'string') {
		script.src = code;
	} else {
		script.innerHTML = '(' + code + ')();';
	}

	script.addEventListener('load', load, false);

	document.getElementsByTagName('head')[0].appendChild(script);
}

function include(scripts) {
	if (scripts) {
		include_script(scripts[0], function () {
			include(scripts.slice(1));
		});
	}
}

function main() {
	$T = {};
	$T.buildTag = function (tag, attributes) {
		var element = new Element(tag, attributes);

		function coerce(obj) {
			if (Object.isElement(obj)) {
				element.appendChild(obj);
			} else if (obj === null || obj === undefined) {
				return;
			} else if (Object.isFunction(obj)) {
				coerce(obj(element));
			} else if (obj.each) {
				obj.each(function (elmt) {
					coerce(elmt);
				});
			} else {
				var text = document.createTextNode(obj.toString());
				element.appendChild(text);
			}
		}

		for (var i = 2; i < arguments.length; ++i) {
			coerce(arguments[i]);
		}

		return element;
	};

	var tags = $A([
		'a', 'button', 'br', 'canvas', 'dd', 'div', 'dl', 'dt', 'em', 'fieldset',
		'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'input',
		'label', 'legend', 'li', 'ol', 'optgroup', 'option', 'p', 'pre',
		'select', 'span', 'strong', 'style', 'table', 'tbody', 'td', 'textarea', 'tfoot',
		'th', 'thead', 'tr', 'tt', 'ul'
	]);

	tags.each(function (tag) {
		$T[tag] = $T.buildTag.curry(tag);
	});

/********************/

	CSS = {};
	CSS.build = function (rules) {
		var text = '';

		$H(rules).each(function (rule) {
			text += rule.key + " {\n";

			$H(rule.value).each(function (prop) {
				// Convert camelCase to camel-case.
				prop.key = prop.key.dasherize().gsub(/([A-Z])/, function (match) {
					return '-' + match[1].toLowerCase();
				});

				text += "\t" + prop.key + ': ' + prop.value + ";\n";
			});

			text += "}\n";
		});

		var style = $T.style({ type: 'text/css' }, text);

		$$('head').first().insert(style);
	};

/*************/

	Rect = Class.create({
		initialize: function (dimensions) {
			this.width  = dimensions.width  !== undefined ? dimensions.width  : Math.max(0, dimensions.right  - dimensions.left);
			this.height = dimensions.height !== undefined ? dimensions.height : Math.max(0, dimensions.bottom - dimensions.top);

			this.left   = dimensions.left   !== undefined ? dimensions.left   : dimensions.right  - dimensions.width;
			this.right  = dimensions.right  !== undefined ? dimensions.right  : dimensions.left   + dimensions.width;
			this.top    = dimensions.top    !== undefined ? dimensions.top    : dimensions.bottom - dimensions.height;
			this.bottom = dimensions.bottom !== undefined ? dimensions.bottom : dimensions.top    + dimensions.height;

			this.area   = this.width * this.height;
		},

		intersect: function(other) {
			var o = new Rect({
				top:    Math.max(this.top,  other.top),
				left:   Math.max(this.left, other.left),
				right:  Math.min(this.left + this.width,  other.left + other.width),
				bottom: Math.min(this.top  + this.height, other.top  + other.height)
			});

			return o;
		}
	});

/************/

	Policy = Class.create({
		initialize: function (policyType, newPolicies) {
			var policies = this.constructor.policies;
			var policy   = policies.get(policyType);

			if (!policy) {
				policy = $H();
				policies.set(policyType, policy);
			}

			$H(newPolicies).each(function (pair) {
				policy.set(pair.key, pair.value);
			});
		}
	});

	Policy.create = function (component, options) {
		options = Object.clone(options);

		var policies = this.policies;
		var policy   = policies.get(options.type);

		var constructor = policy.get(component.type);
		if (Object.isUndefined(constructor)) {
			constructor = policy.get('default');
		}

		return constructor(component, options);
	};

	Policy.Placement = Class.create(Policy, {});
	Policy.Placement.create = Policy.create;
	Policy.Placement.policies = $H();

	Policy.Display = Class.create(Policy, {});
	Policy.Display.create = Policy.create;
	Policy.Display.policies = $H();

/****************/

	Placement = Class.create({
		initialize: function(component, options) {
			this.options = options || { };

			this.component = component;
		},

		placeAt: function(left, top) {
			this.component.element.style.left = left + 'px';
			this.component.element.style.top  = top  + 'px';
		},

		placeElement: function () { }
	});

	Placement.getAbsoluteRect = function (element) {
		if (element.getClientRects && element.getClientRects().length) {
			var scrollOffsets = document.viewport.getScrollOffsets();
			var r = element.getClientRects()[0];

			return new Rect({
				left:   scrollOffsets.left + r.left,
				top:    scrollOffsets.top  + r.top,
				width:  r.right  - r.left,
				height: r.bottom - r.top
			});
		} else {
			var cumulativeOffset = element.cumulativeOffset();
			var dimensions       = element.getDimensions();

			return new Rect({
				left:   cumulativeOffset.left,
				top:	  cumulativeOffset.top,
				width:  dimensions.width,
				height: dimensions.height
			});			
		}
	};

/********/

	Placement.Static = Class.create(Placement, {
		type: 'static',

		initialize: function($super, component, options) {
			$super(component, options);
		},

		placeElement: function () { }
	});

	new Policy.Placement('static', {
		'default': function (component, options) {
			return new Placement.Static(component, options);
		}
	});

/**********/

	Placement.Absolute = Class.create(Placement, {
		type: 'absolute',

		initialize: function($super, component, options) {
			$super(component, options);

			this.options.offset   = this.options.offset   || { };
			this.options.offset.x = this.options.offset.x || 0;
			this.options.offset.y = this.options.offset.y || 0;

			this.component.element.style.position = 'absolute';
		},

		position: function (left, top, offset) {
			var parentOffset = this.component.element.getOffsetParent().cumulativeOffset();
			var elementRect  = Placement.getAbsoluteRect(this.component.element);

			return new Rect({
				left:   left + offset.x - parentOffset.left,
				top:    top  + offset.y - parentOffset.top,
				width:  elementRect.width,
				height: elementRect.height
			});
		},

		placeElement: function () {
			var dest = this.position(0, 0, this.options.offset);
			return this.placeAt(dest.left, dest.top);
		}
	});

	new Policy.Placement('absolute', {
		'default': function (component, options) {
			return new Placement.Absolute(component, options);
		}
	});

/*******/

	Placement.Relative = Class.create(Placement.Absolute, {
		type: 'relative',

		initialize: function($super, target, component, options) {
			$super(component, options);

			this.options.anchor         = this.options.anchor         || { };
			this.options.anchor.target  = this.options.anchor.target  || 'bottom-left';
			this.options.anchor.element = this.options.anchor.element || 'top-left';

			this.options.reposition = this.options.reposition === undefined ? true : this.options.reposition;

			this.target = target;
		},

		position: function ($super, anchor, offset) {
			var targetOffset  = this.computeOffset(this.target,            anchor.target);
			var elementOffset = this.computeOffset(this.component.element, anchor.element);

			var targetRect  = Placement.getAbsoluteRect(this.target);

			return $super(
				targetRect.left + elementOffset.x - targetOffset.x,
				targetRect.top  + elementOffset.y - targetOffset.y,
				offset
			);
		},

		rotatePosition: {
			'left-top':      'top-center',
			'top-left':      'top-center',
			'top-center':    'top-right',
			'top-right':     'right-center',
			'right-top':     'right-center',
			'right-center':  'right-bottom',
			'right-bottom':  'bottom-center',
			'bottom-right':  'bottom-center',
			'bottom-center': 'bottom-left',
			'bottom-left':   'left-center',
			'left-bottom':   'left-center',
			'left-center':   'left-top'
		},

		placeElement: function ($super) {
			var scrollOffsets = document.viewport.getScrollOffsets();
			var viewportRect = new Rect({
				top:    scrollOffsets.top,
				left:   scrollOffsets.left,
				width:  document.viewport.getWidth(),
				height: document.viewport.getHeight()
			});

			var targetRect  = Placement.getAbsoluteRect(this.target);
			var elementRect = Placement.getAbsoluteRect(this.component.element);

			var anchor = Object.clone(this.options.anchor);
			var offset = Object.clone(this.options.offset);

			var dest = this.position(anchor, offset);

			var viewportOverlap = dest.intersect(viewportRect);
			if (this.options.reposition && viewportOverlap.area < elementRect.area) {
				var candidates = $A([]);
				var weight = [3, 5, 1, 6, 0, 4, 2];

				for (var i = 0; i < 8; i++) {
					offset = {
						x: (offset.x - offset.y) / Math.sqrt(2),
						y: (offset.x + offset.y) / Math.sqrt(2)
					};

					anchor = {
						target:  this.rotatePosition[anchor.target],
						element: this.rotatePosition[anchor.element]
					};

					dest = this.position(anchor, offset);

					var viewportOverlap = dest.intersect(viewportRect);
					var targetOverlap   = dest.intersect(targetRect);

					candidates.push({
						rect:           Object.clone(dest),
						elementVisible: Math.min(1, viewportOverlap.area / elementRect.area),
						targetVisible:  1 - (targetOverlap.area / targetRect.area),
						weight:         weight[i]
					});
				}

				candidates = candidates.sort(function(left, right) {
					return  left.elementVisible < right.elementVisible ?  1
						: left.elementVisible > right.elementVisible ? -1
						: left.targetVisible  < right.targetVisible  ?  1
						: left.targetVisible  > right.targetVisible  ? -1
						: left.weight         < right.weight         ?  1
						: left.weight         > right.weight         ? -1
						: 0;
				});

				dest = candidates.first().rect;
			}

			this.placeAt(dest.left, dest.top);
		},

		computeOffset: function (element, location) {
			if (element.getClientRects && element.getClientRects().length) {
				var r = element.getClientRects()[0];
			} else {
				var r = { width: element.getWidth(), height: element.getHeight() };
			}

			var width  = r.width;
			var height = r.height;

			switch(location) {
				case 'left-top':
				case 'top-left':      return { x: 0, y: 0 };
				case 'top-center':    return { x: -(width / 2), y: 0 };
				case 'right-top':
				case 'top-right':     return { x: -width, y: 0 };
				case 'left-center':   return { x: 0, y: -(height / 2) };
				case 'right-center':  return { x: -width, y: -(height / 2) };
				case 'left-bottom':
				case 'bottom-left':   return { x: 0, y: -height };
				case 'bottom-center': return { x: -(width / 2), y: -height };
				case 'right-bottom':
				case 'bottom-right':  return { x: -width, y: -height };
				default:              throw "Invalid location: " + location;
			}
		}
	});


	new Policy.Placement('relative', {
		DropdownMenu: function (component, options) {
			return new Placement.Relative(component.root, component, options);
		},

		Tooltip: function (component, options) {
			return new Placement.Relative(options.target || component.trigger, component, options);
		},

		'default': function (component, options) {
			return new Placement.Relative(options.target, component, options);
		}
	});

/***********/

	Placement.Fixed = Class.create(Placement, {
		type: 'fixed',

		initialize: function ($super, component, options) {
			$super(component, options);

			this.component.element.style.position = 'fixed';

			this.options.horizontal = this.options.horizontal || 'center';
			this.options.vertical   = this.options.vertical   || 'center';
		},

		placeElement: function ($super) {
			var r = Placement.getAbsoluteRect(this.component.element);

			var left = this.computeOffset(this.options.horizontal, document.viewport.getWidth(),  r.width);
			var top  = this.computeOffset(this.options.vertical,   document.viewport.getHeight(), r.height);

			this.placeAt(left, top);
		},

		computeOffset: function (placement, viewportDimension, elementDimension) {
			switch (placement) {
				case 'top':
				case 'left':   return 0;
				case 'center': return viewportDimension / 2 - elementDimension / 2;
				case 'bottom':
				case 'right':  return viewportDimension - elementDimension;
			}
		}
	});

	new Policy.Placement('fixed', {
		'default': function (component, options) {
			return new Placement.Fixed(component, options);
		}
	});

/*****************************/

	Display = Class.create({
		initialize: function(component, options) {
			this.options = options || { };

			this.options.show = this.options.show || { };
			this.options.hide = this.options.hide || { };

			this.component = component;

			this.hasBeenShown = false;

			if (this.options.initialShow == false) {
				this.component.element.style.display = 'none';
			} else {
				this.show();
			}
		},

		visible: function () {
			// XXX what if parent is display: none?
			return this.component.element.visible();
		},

		toggle: function () {
			this.visible() ? this.hide() : this.show();
		},

		hide: function () {
			this.component.fire('hide');

			if (this.options.hide.effect) {
				new this.options.hide.effect(this.component.element, this.options.hide.options);
			} else {
				this.component.element.hide();
			}
		},

		show: function (reposition) {
			if (this.hasBeenShown == false) {
				this.component.fire('firstshown');
				this.hasBeenShown = true;
			}

			this.component.fire('show');

			if (reposition === undefined ? true : reposition) {
				this.reposition();
			}

			if (this.options.show.effect) {
				new this.options.show.effect(this.component.element, this.options.show.options);
			} else {
				this.component.element.show();
			}
		},

		reposition: function () {
			this.component.placement.placeElement();
		},

		enableEventListeners: function () {
		},

		disableEventListeners: function () {
		},
	});

/*******/

	Display.Static = Class.create(Display, {
		type: 'static'
	});

	new Policy.Display('static', {
		'default': function (component, options) {
			return new Display.Static(component, options);
		}
	});

/********/

	Display.Click = Class.create(Display, {
		type: 'click',

		initialize: function($super, trigger, component, options) {
			$super(component, $H({
				initialShow: false
			}).merge(options).toObject());

			this.trigger = trigger;

			var click = function (event) {
//				this.toggle();
				this.show();
			};

			var clickout = function (event) {
				if (event.findElement() != this.component.element && this.visible()) {
					this.hide();
				}
			};

			this.click    = click.bind(this);
			this.clickout = clickout.bind(this);

			this.enableEventListeners();
		},

		enableEventListeners: function () {
			this.trigger.observe('click', this.click);
			document.observe('click', this.clickout);
		},

		disableEventListeners: function () {
			this.trigger.stopObserving('click', this.click);
			document.stopObserving('click', this.clickout);
		}
	});

	new Policy.Display('click', {
		DropdownMenu: function (component, options) {
			return new Display.Click(component.root, component, options);
		},

		'default': function (component, options) {
			return new Display.Click(options.trigger, component, options);
		}
	});

/********/

	Display.Hover = Class.create(Display, {
		type: 'hover',

		initialize: function($super, trigger, component, options) {
			$super(component, $H({
				initialShow: false
			}).merge(options).toObject());

			this.options.show.delay = this.options.show.delay || 0;
			this.options.hide.delay = this.options.hide.delay || 0;

			this.trigger = trigger;

			this.delayShow = null;
			this.delayHide = null;

			var mouseout = function (event) {
				if (!this.inElements(event, [ this.trigger, this.component.element ])) {
					return;
				}

				if (this.delayShow) {
					window.clearTimeout(this.delayShow);
					this.delayShow = null;
					return;
				}

				if (this.options.hide.delay) {
					if (this.delayHide === null) {
						this.delayHide = this.hide.bind(this).delay(this.options.hide.delay);
					}
				} else {
					this.hide();
				}
			};

			var mouseover = function (event) {
				if (!this.inElements(event, [ this.trigger, this.component.element ])) {
					return;
				}

				if (this.delayHide) {
					window.clearTimeout(this.delayHide);
					this.delayHide = null;
					return;
				}

				if (this.options.show.delay) {
					if (this.delayShow === null) {
						this.delayShow = this.show.bind(this).delay(this.options.show.delay);
					}
				} else {
					this.show();
				}
			};

			var mousemove = function (event) {
				if (this.delayShow) {
					window.clearTimeout(this.delayShow);
					this.delayShow = this.show.bind(this).delay(this.options.show.delay);
				}
			};

			this.mouseout  = mouseout.bind(this);
			this.mouseover = mouseover.bind(this);
			this.mousemove = mousemove.bind(this);

			this.enableEventListeners();
		},

		hide: function ($super) {
			this.delayHide = null;
			$super();
		},

		show: function ($super, reposition) {
			this.delayShow = null;
			$super(reposition);
		},

		enableEventListeners: function () {
			this.trigger.observe('mouseout',  this.mouseout);
			this.trigger.observe('mouseover', this.mouseover);

			this.trigger.observe('mousemove', this.mousemove);

			this.component.element.observe('mouseout',  this.mouseout);
			this.component.element.observe('mouseover', this.mouseover);
		},

		disableEventListeners: function () {
			this.trigger.stopObserving('mouseout',  this.mouseout);
			this.trigger.stopObserving('mouseover', this.mouseover);

			this.trigger.stopObserving('mousemove', this.mousemove);

			this.component.element.stopObserving('mouseout',  this.mouseout);
			this.component.element.stopObserving('mouseover', this.mouseover);
		},

		inElements: function (event, elements) {
			var parent = event.relatedTarget;
	            while (parent && !elements.include(parent)) {
				try {
					parent = parent.parentNode;
				} catch (e) {
					return false;
				}
			}

			return !elements.include(parent) && event.relatedTarget !== null;
		}
	});

	new Policy.Display('hover', {
		Tooltip: function (component, options) {
			return new Display.Hover(options.trigger || component.trigger, component, options);
		},

		'default': function (component, options) {
			return new Display.Hover(options.trigger, component, options);
		}
	});

/*********************/

	Theme = Class.create({
		initialize: function (themeName, rules, parentTheme) {
			Theme.themes[themeName] = this;

			this.name   = themeName;
			this.rules  = $H(rules);
			this.parent = parentTheme;

			var css    = { };
			var object = this;

			this.rules.each(function (pair) {
				var componentName  = pair.key;
				var componentRules = pair.value;

				$H(componentRules).each(function (pair) {
					var elementName = pair.key;
					var cssRules    = pair.value;

					if (elementName === 'options') {
						return;
					}

					css['.' + object.getClass(componentName, elementName)] = cssRules;
				});
			});

			CSS.build(css);
		},

		extend: function (themeName, rules) {
			return new Theme(themeName, rules, this);
		},

		getClass: function(componentType, elementClass) {
			return "#{id}-#{themeName}-#{componentType}-#{elementClass}".interpolate({
				id:            Theme.id,
				themeName:     this.name,
				componentType: componentType,
				elementClass:  elementClass
			});
		},

		getOptions: function (component, options) {
			var options = $H(options);

			// XXX cross-browser?
			var klass = component.constructor;
			while (klass) {
				var componentRules = this.rules.get(klass.prototype.type);

				if (componentRules) {
					function recursiveMerge(left, right) {
						var result = new Hash();

						left.keys().concat(right.keys()).uniq().each(function (key) {
							var leftValue  = left.get(key);
							var rightValue = right.get(key);

							if (leftValue === undefined) {
								result.set(key, rightValue);
							} else if (rightValue === undefined) {
								result.set(key, leftValue);
							} else if (leftValue.constructor === Object && rightValue.constructor === Object) {
								result.set(key, recursiveMerge($H(leftValue), $H(rightValue)).toObject());
							} else {
								result.set(key, rightValue);
							}
						});

						return result;			
					}

					options = recursiveMerge($H($H(componentRules).get('options')), options);
				}

				klass = klass.superclass;
			}

			if (this.parent) {
				options = this.parent.getOptions(component, options);
			}

			return options;
		},

		apply: function(component, elementName) {
			if (this.parent) {
				this.parent.apply(component, elementName);
			}

			var element = component[elementName];

			// XXX
			var klass = component.constructor;
			while (klass) {
				element.addClassName(this.getClass(klass.prototype.type, elementName));
				klass = klass.superclass;
			}
		}
	});

	Theme.id = 'protrols';
	Theme.themes = {};

	new Theme('default', {
		Component: {
			options: {
				placement: {
					type: 'static'
				},
				display: {
					type: 'static'
				}
			},
			element: {
				backgroundColor: 'white'
			}
		},
		Layer: {
			element: {
				zIndex: 9000
			}
		},
		Overlay: {
			options: {
				placement: {
					type: 'fixed',
					horizontal: 'left',
					vertical:   'top'
				},
				display: {
					type: 'static',
					initialShow: false,
					show: { effect: Effect.Appear, options: { to: 0.5, duration: 0.50 } },
					hide: { effect: Effect.Fade,   options: { duration: 0.50 } }
				}
			},
			element: {
				backgroundColor: 'black',
				width:  '100%',
				height: '100%',
				zIndex: 8999,

				// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=201307
				overflow: 'auto'
			}
		},
		Window: {
			options: {
				close: true,

				placement: {
					type: 'fixed'
				}
			},
			header: {
				display: 'table',
				cursor:  'move',
				width:   '100%'
			},
			titlebar: {
				display: 'table-row'
			},
			title: {
				display: 'table-cell'
			},
			close: {
				display: 'table-cell'
			}
		},
		ModalWindow: {
			options: {
				placement: {
					type:       'fixed',
					horizontal: 'center',
					vertical:   'center'
				},
				display: {
					initialShow: false,
					show: { effect: Effect.Appear, options: { duration: 0.50 } },
					hide: { effect: Effect.Fade,   options: { duration: 0.50 } }
				}
			}
		},
		Tooltip: {
			options: {
				close: false,

				placement: {
					type: 'relative'
				},
				display: {
					type: 'hover'
				}
			},
		},
		MenuItem: {
			element: {
				padding: '0.25em 1em',
				cursor: 'default'
			}
		},
		DropdownMenu: {
			options: {
				placement: {
					type: 'relative',
					reposition: false,
					anchor: {
						target:  'bottom-left',
						element: 'top-left'
					}
				},
				display: {
					type: 'click'
//					type: 'static',
//					initialShow: false
				}
			},
			content: {
				border: '2px outset black'
			}
		}
	});

	Theme.themes['default'].extend('bare');

	Theme.themes['default'].extend('basic', {
		Window: {
			element: {
				border: '2px solid #666666',
				'-moz-border-radius': '4px',
				'-webkit-border-radius': '4px'
			},
			content: {
				padding: '1em'
			},
			header: {
				backgroundColor: 'lightgray'
			},
			title: {
				fontWeight:  'bold',
				paddingLeft: '1em'
			}
		}
	});


	Theme.themes.basic.extend('danbooru-tooltip', {
		Tooltip: {
			element: {
				backgroundColor: '#EEEEEE'
			},
			content: {
				display: 'table'
			}
		}
	});

	Theme.themes.bare.extend('danbooru-note-placeholder', {
		Layer: {
			element: {
				backgroundColor: '#FFFFEE',
				border: '1px solid black',
				opacity: 0.5
			}
		}
	});

	Theme.themes.bare.extend('danbooru-note', {
		Tooltip: {
			element: {
				backgroundColor: '#FFFFEE',
				border:    '1px solid black',
				minWidth:  '160px',
				maxHeight: '100px',
				overflow:  'auto',
				padding:   '5px'
			}
		}
	});

/****************/

	Component = Class.create({
		type: 'Component',

		initialize: function (element, options) {
			options = options || {};

			this.theme = options.theme || Theme.themes.basic;

			this.options = this.theme.getOptions(this, options).toObject();

			this.element = element;
			this.theme.apply(this, 'element');

			var component = this;
			$H(this.options.events).each(function (pair) {
				component.observe(pair.key, pair.value);
			});

			if (this.options.parent) {
				this.options.parent.insert({ top: this.element });
			} else {
				Component.components.appendChild(this.element);
			}

			this.placement = Policy.Placement.create(this, this.options.placement);
			this.display   = Policy.Display.create  (this, this.options.display);
		},

		observe: function (event, handler) {
			var component = this;
			var func = function (event) {
				event.stop();

				if (event.findElement() != component.element) {
					return;
				}

				return handler(event.memo);
			};

			this.element.observe('protrols:' + event, func);

			return func;
		},

		stopObserving: function (event, handler) {
			this.element.stopObserving('protrols:' + event, handler);
		},

		fire: function (eventName) {
			this.element.fire('protrols:' + eventName, this, false);
		}
	});

	Component.id = (Math.random() * Math.pow(10, 16)).round();

	Component.components = $T.div({ id: 'component-' + Component.id });
	document.body.insert(Component.components);

	Layer = Class.create(Component, {
		type: 'Layer',

		initialize: function ($super, content, options) {
			$super($T.div(), options);

			this.content = $T.div({ }, content);
			this.element.insert(this.content);

			this.theme.apply(this, 'content');
		}
	});

	Overlay = Class.create(Layer, {
		type: 'Overlay',

		initialize: function ($super, options) {
			$super(null, options);
		}
	});

	Window = Class.create(Layer, {
		type: 'Window',

		initialize: function ($super, content, options) {
			$super(content, options);

			if (this.options.title || this.options.close) {
				this.titlebar = $T.div();
				this.header   = $T.div({ }, this.titlebar);

				this.theme.apply(this, 'titlebar');
				this.theme.apply(this, 'header');

				this.element.insert({ top: this.header });


				var object = this;
				this.draggable = new Draggable(this.element, {
					handle: object.titlebar
				});

				Draggables.addObserver({
					element: this.element,

					onStart: function () {
						object.display.disableEventListeners();
					},

					onEnd: function () {
						object.display.enableEventListeners();
					}
				});
			}

			if (this.options.title) {
				this.title = $T.span({ }, this.options.title);

				this.theme.apply(this, 'title');

				this.titlebar.insert({ top: this.title });
			}

			if (this.options.close) {
				this.close = $T.a({ href: '#' }, 'X');

				var object = this;
				this.close.observe('click', function (event) {
					object.display.hide();
					event.stop();
				});

				this.theme.apply(this, 'close');

				this.titlebar.insert({ bottom: this.close });
			}
		}
	});

	ModalWindow = Class.create(Window, {
		type: 'ModalWindow',

		initialize: function ($super, content, options) {
			$super(content, options);

			this.overlay = new Overlay();

			var modal   = this;
			var overlay = this.overlay;
			overlay.element.observe('click', function (event) {
				modal.display.hide();
			});

			this.observe('hide', function (component) {
				overlay.display.hide();
			});

			this.observe('show', function (component) {
				overlay.display.show();
			});

			this.display.show();
		}
	});

	Tooltip = Class.create(Window, {
		type: 'Tooltip',

		initialize: function ($super, trigger, content, options) {
			this.trigger = $(trigger);

			$super(content, options);
		}
	});

	MenuItem = Class.create(Component, {
		type: 'MenuItem',

		initialize: function ($super, name, options) {
			$super($T.div({}, name), options);
		}
	});

	Menu = Class.create(Layer, {
		type: 'Menu',

		initialize: function ($super, options) {
			$super(null, options);
		},

		appendItem: function (item) {
			this.content.insert({ bottom: item.element });
		}
	});

	DropdownMenu = Class.create(Menu, {
		type: 'DropdownMenu',

		initialize: function ($super, root, options) {
			this.root = root;

			$super(options);
		}
	});

/**********************/


	CSS.build({
		'.danbooru-tooltip-preview': {
			display: 'table-cell',
			verticalAlign: 'middle',
			paddingRight: '1em'
		},

		'.danbooru-tooltip-info': {
			display: 'table-cell'
		},

		'.danbooru-tooltip-filesize': {
			'float': 'right',
			paddingRight: '1em'
		}
	});



	function to_human_size(size) {
		if (size < 1024) {
			return size + " bytes";
		} else if (size < 1024 * 1024) {
			return (size / 1024).toFixed(2) + " kb";
		} else {
			return (size / (1024 * 1024)).toFixed(2) + " Mb";
		}
	}



	function make_tooltip(link) {
		if (link.down() === null) {
			var trigger = link;
			var target  = trigger;
		} else if (link.down('img')) {
			var trigger = link.down('img');
			var target  = link.up('span');

			trigger.title = '';
		}

		function firstShown(tooltip) {
			var post_id = Number(link.href.match(/\/post\/show\/([0-9]+)/)[1]);
			var post    = Post.posts.get(post_id);
			
			if (post) {
				build_tooltip(tooltip, post);
				return;
			}

			new Ajax.Request('/post/index.json', {
				method: 'get',
				parameters: { tags: 'id:' + post_id },
				onSuccess: function (transport) {
					if (transport.responseJSON.length === 1) {
						post = transport.responseJSON[0];
						post.tags = post.tags.split(' ');
						build_tooltip(tooltip, post);
					} else if (transport.responseJSON.length === 0) {
						new Ajax.Request('/post/index.json', {
							method: 'get',
							parameters: { tags: 'status:deleted id:' + post_id },
							onSuccess: function (transport) {
								post = transport.responseJSON[0];
								post.tags = post.tags.split(' ');
								build_tooltip(tooltip, post);
							}
						});
					}
				} // onSuccess: function (transport)
			}); // new Ajax.Request
		} // function firstShown(tooltip)

		var options = { 
			display: {
				show: { delay: 0.50, effect: Effect.Appear, options: { duration: 0.20 } },
				hide: { delay: 0.25, effect: Effect.Fade,   options: { duration: 0.20 } }
			},
			placement: {
				anchor: {
					target:  'bottom-center',
					element: 'top-center'
				},
				offset: { y: 30 }
			},
			events: {
				firstshown: firstShown
			},
			close: true,
			title: $T.em({ }, 'Loading...'),
			theme: Theme.themes['danbooru-tooltip']
		};

		new Tooltip(
			trigger,
			$T.img({ style: 'width: 170px; height: 170px;', src: '/data/d68a1f25d17ca14afc14ef2335b61d2a.gif' }),
			options
		);
	}


	function build_tooltip(tooltip, post) {
		tooltip.content.innerHTML = '';
		tooltip.title.innerHTML   = '';

//		tooltip.element.style.minWidth = '36em';
//		tooltip.element.style.maxWidth = '40em';
		tooltip.element.style.width = '40em';

		tooltip.title.appendChild($T.span({ style: 'padding-right: 1em' }, '#' + post.id + (post.status !== 'active' ? ' (' + post.status.capitalize() + ')' : '')));
		tooltip.title.appendChild(
			$T.span({ className: 'danbooru-tooltip-filesize' },
				  post.width + ' x ' + post.height + ' (' + to_human_size(post.file_size) + ')'
			)
		);

		var vote_up = $T.a({ href: '#' }, 'up');
		vote_up.observe('click', function (event) {
			Post.vote(1, post.id);
			event.stop();
		});

		var vote_down = $T.a({ href: '#' }, 'down');
		vote_down.observe('click', function (event) {
			Post.vote(-1, post.id);
			event.stop();
		});

		var info = {};

		info.score = $T.span({ }, post.score, ' (vote ', vote_up, '/', vote_down, ')');

		info.rating = post.rating === 'e' ? 'Explicit'
				: post.rating === 'q' ? 'Questionable'
				: post.rating === 's' ? 'Safe'
				: null;

		var date = new Date();
		date.setTime(post.created_at.s * 1000);

		info.date = $T.a({ href: '/post/index?tags=date:' + encodeURIComponent(date.toDateString().replace(/\s+/g, '_')) }, date.toDateString());

		info.user = $T.a({ href: '/post/index?tags=user:' + encodeURIComponent(post.author) }, post.author);

		if (post.parent_id) {
			info.parent = $T.a({ href: '/post/show/' + post.parent_id }, post.parent_id);
			make_tooltip(info.parent);
		}

		if (post.source.match(/^http(s)?:\/\//)) {
			if (post.source.match(/^http:\/\/img[0-9]+\.pixiv\.net\/img\/.*\/([0-9]+)/)) {
				var illust_id = post.source.match(/^http:\/\/img[0-9]+\.pixiv\.net\/img\/.*\/([0-9]+)/)[1];
				info.source = $T.a({ href: 'http://www.pixiv.net/member_illust.php?mode=medium&illust_id=' + illust_id, title: post.source }, post.source.slice(7, 27) + '...');
			} else {
				info.source = $T.a({ href: post.source, title: post.source }, post.source.slice(7, 27) + '...');
			}
		} else if (post.source) {
			info.source = post.source;
		}

		function build_list(container, limit, search, items) {
			var elements = items.map(function (item) {
				var element = $T.a({
					style: 'margin-right: 0.5em;',
					href:  search + encodeURIComponent(item)
				}, item + ' ');

				return element;
			});

			elements.slice(0, limit).each(function (element) {
				container.appendChild(element);
			});

			var elements_remaining = elements.slice(limit).length;
			if (elements_remaining === 0) {
				return;
			}

			var more = $T.a({ href: '#' }, elements_remaining + ' more');
			var more_span = $T.span({ }, '(', more, ')' );
			more.observe('click', function (event) {
				event.stop();
				more_span.remove();
				elements.slice(limit).each(function (elements) {
					container.appendChild(elements);
				});
			});

			container.appendChild(more_span);
		}

		info.tags = $T.span();
		build_list(info.tags, 12, '/post/index?tags=', post.tags);

		if (post.has_children) {
			info.children = $T.span({ }, $T.em({ }, 'Loading...'));
			new Ajax.Request('/post/index.json', {
				method: 'get',
				parameters: { tags: 'parent:' + post.id },

				onSuccess: function (transport) {
					info.children.innerHTML = '';
					var children = transport.responseJSON.pluck('id').reject(function (id) { return id == post.id; });
					build_list(info.children, 3, '/post/show/', children);

					$A(info.children.children).each(function(child) {
						make_tooltip(child);
					});
				}
			});
		}

		info.favorited_by = $T.span({ }, $T.em({ }, 'Loading...'));
		new Ajax.Request('/favorite/list_users.json', {
			method: 'get',
			parameters: { id: post.id },

			onSuccess: function (transport) {
				if (transport.responseJSON.favorited_users.blank()) {
					info.favorited_by.replace($T.em({ }, 'Nobody'));
					return;
				}

				info.favorited_by.innerHTML = '';
				build_list(info.favorited_by, 6, '/post/index?tags=fav:', transport.responseJSON.favorited_users.split(/,/));
			}
		});

		var preview_image = $T.img({
			src:    post.preview_url,
			width:  post.preview_width,
			height: post.preview_height
		});

		var preview_container = $T.div({ style: 'position: relative' }, preview_image);

		var info_div  = $T.div({ className: 'danbooru-tooltip-info' });
		var container =
			$T.div({ style: 'display: table-row' },
				$T.div({ className: 'danbooru-tooltip-preview' }, preview_container),
				info_div
			);

		tooltip.content.appendChild(container);

		var note_placeholders = $A([]);
		if (post.has_notes) {
			new Ajax.Request('/note/index.json', {
				method: 'get',
				parameters: { post_id: post.id },

				onSuccess: function (transport) {
					var horiz_scale = post.preview_width  / post.width;
					var vert_scale  = post.preview_height / post.height;

					var tooltips = $A([]);
					$A(transport.responseJSON).each(function (note) {
						note.x *= horiz_scale;
						note.y *= vert_scale;

						note.width  *= horiz_scale;
						note.height *= vert_scale;

						if (note.is_active && note.width * note.height > 10) {
							var placeholder = new Layer(null, {
								theme:  Theme.themes['danbooru-note-placeholder'],
								parent: preview_container
							});

							var element = placeholder.element;

							element.style.position = 'absolute';
							element.style.left = note.x + 'px';
							element.style.top  = note.y + 'px';

							element.style.width  = note.width  + 'px';
							element.style.height = note.height + 'px';
							
							var tooltip = new Tooltip(element, note.body, {
								parent: preview_container,
								theme: Theme.themes['danbooru-note'],
								display: {
									show: { delay: 0.05 },
									hide: { delay: 0.25 }
								},
								placement: {
									reposition: false,
									target: preview_container,
									anchor: {
										target:  'top-right',
										element: 'top-left'
									},
									offset: { x: 12 }
								},
								events: {
									show: function(tooltip) {
										tooltips.each(function (tip) {
											if (tip != tooltip) {
												tip.display.hide();
											}
										});
									}
								}
							});

							tooltips.push(tooltip);
							note_placeholders.push(element);
						} else {
//							console.log(note);
						}
					});
				}
			});
		}

		preview_image.observe('click', function (event) {
			note_placeholders.invoke('toggle');
		});

		function build_row(attributes) {
			var row = $T.div();
			attributes.each(function (attribute) {
				if (info[attribute]) {
					row.appendChild(
						$T.span({ style: 'margin-right: 1em;' },
							$T.strong({ }, attribute.capitalize().replace('_', ' ') + ' '),
							info[attribute]
						)
					);
				}
			});

			info_div.appendChild(row);
		}

		build_row([ 'user', 'date' ]);
		build_row([ 'rating', 'score' ]);
		build_row([ 'source' ]);
		build_row([ 'parent', 'children' ]);
		build_row([ 'favorited_by' ]);
		build_row([ 'tags' ]);

		tooltip.display.reposition();
	} // function build_tooltip(tooltip, post)



	$$('a[href^=/post/show/]').each(function (link) {
		make_tooltip(link);
	});


/*
	$$('span.thumb').each(function (span) {
		var post_id = Number(span.id.match(/p(\d+)/)[1]);

		var menubar = $T.div({ style: 'width: 180px; border: 1px solid black' });

		var ratebutton = $T.span({}, 'Rate...');
		var ratemenu   = new DropdownMenu(ratebutton, {
			parent: menubar
		});

		var ratesafe, ratequestionable, rateexplicit;
		ratemenu.appendItem(ratesafe         = new MenuItem('Safe'));
		ratemenu.appendItem(ratequestionable = new MenuItem('Questionable'));
		ratemenu.appendItem(rateexplicit     = new MenuItem('Explicit'));

		ratesafe.element.observe('click', function (event) {
			Post.update(post_id, { 'post[rating]': 'safe' });
		});
		ratequestionable.element.observe('click', function (event) {
			Post.update(post_id, { 'post[rating]': 'questionable' });
		});
		rateexplicit.element.observe('click', function (event) {
			Post.update(post_id, { 'post[rating]': 'explicit' });
		});


		var editbutton = $T.span({}, 'Edit');

		editbutton.observe('click', function (event) {
			var post     = Post.posts.get(post_id);
			var old_tags = post.tags.join(' ');

			var textarea = $T.textarea({ rows: 7, cols: 30, style: 'vertical-align: middle; display: table-cell' }, old_tags)
			var prompt = $T.div({ style: 'display: table-row' },
				$T.img({
					src: post.preview_url, width: post.preview_width, height: post.preview_height,
					style: 'vertical-align: middle; display: table-cell; padding: 1em;'
				}),
				textarea
			);

			var edit_window = new ModalWindow(prompt, { title: 'Edit tags' });

			textarea.observe('keydown', function(event) {
				if (event.keyCode === Event.KEY_RETURN) {
					Post.update(post_id, {
						'post[old_tags]': old_tags,
						'post[tags]':     textarea.value
					});

					edit_window.display.hide();
				}
			});

			textarea.focus();
		});

		menubar.insert({ bottom: editbutton });
		menubar.insert({ bottom: ratebutton });

		var tooltip = new Tooltip(span, menubar, {
			theme: Theme.themes.bare,
			placement: {
				reposition: false,
				anchor: {
					target:  'bottom-left',
					element: 'bottom-left'
				},
			}
		});
	});
*/
}

include([
	'http://script.aculo.us/dragdrop.js',
	main
]);