/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import 'vs/css!./builder'; import {Promise} from 'vs/base/common/winjs.base'; import types = require('vs/base/common/types'); import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; import strings = require('vs/base/common/strings'); import assert = require('vs/base/common/assert'); import DOM = require('vs/base/browser/dom'); import BrowserService = require('vs/base/browser/browserService'); /** * Welcome to the monaco builder. The recommended way to use it is: * * import Builder = require('vs/base/browser/builder'); * let $ = Builder.$; * $(....).fn(...); * * See below for examples how to invoke the $(): * * $() - creates an offdom builder * $(builder) - wraps the given builder * $(builder[]) - wraps the given builders into a multibuilder * $('div') - creates a div * $('.big') - creates a div with class `big` * $('#head') - creates a div with id `head` * $('ul#head') - creates an unordered list with id `head` * $('') - constructs a builder from the given HTML * $('a', { href: 'back'}) - constructs a builder, similarly to the Builder#element() call */ export interface QuickBuilder { (): Builder; (builders: Builder[]): Builder; (element: HTMLElement): Builder; (window: Window): Builder; (htmlOrQuerySyntax: string): Builder; // Or, MultiBuilder (name: string, args?: any, fn?: (builder: Builder) => any): Builder; (one: string, two: string, three: string): Builder; (builder: Builder): Builder; } /** * Create a new builder from the element that is uniquely identified by the given identifier. If the * second parameter "offdom" is set to true, the created elements will only be added to the provided * element when the build() method is called. */ export function withElementById(id: string, offdom?: boolean): Builder { assert.ok(types.isString(id), 'Expected String as parameter'); let element = BrowserService.getService().document.getElementById(id); if (element) { return new Builder(element, offdom); } return null; } export let Build = { withElementById: withElementById }; // --- Implementation starts here let MS_DATA_KEY = '_msDataKey'; let DATA_BINDING_ID = '__$binding'; let LISTENER_BINDING_ID = '__$listeners'; let VISIBILITY_BINDING_ID = '__$visibility'; export class Position { public x: number; public y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } export class Box { public top: number; public right: number; public bottom: number; public left: number; constructor(top: number, right: number, bottom: number, left: number) { this.top = top; this.right = right; this.bottom = bottom; this.left = left; } } export class Dimension { public width: number; public height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } public substract(box: Box): Dimension { return new Dimension(this.width - box.left - box.right, this.height - box.top - box.bottom); } } export interface IRange { start: number; end: number; } function data(element: any): any { if (!element[MS_DATA_KEY]) { element[MS_DATA_KEY] = {}; } return element[MS_DATA_KEY]; } function hasData(element: any): boolean { return !!element[MS_DATA_KEY]; } /** * Wraps around the provided element to manipulate it and add more child elements. */ export class Builder implements IDisposable { private currentElement: HTMLElement; private offdom: boolean; private container: HTMLElement; private createdElements: HTMLElement[]; private toUnbind: { [type: string]: IDisposable[]; }; private captureToUnbind: { [type: string]: IDisposable[]; }; private browserService: BrowserService.IBrowserService; constructor(element?: HTMLElement, offdom?: boolean) { this.offdom = offdom; this.container = element; this.currentElement = element; this.createdElements = []; this.toUnbind = {}; this.captureToUnbind = {}; this.browserService = BrowserService.getService(); } /** * Returns a new builder that lets the current HTML Element of this builder be the container * for future additions on the builder. */ public asContainer(): Builder { return withBuilder(this, this.offdom); } /** * Clones the builder providing the same properties as this one. */ public clone(): Builder { let builder = new Builder(this.container, this.offdom); builder.currentElement = this.currentElement; builder.createdElements = this.createdElements; builder.captureToUnbind = this.captureToUnbind; builder.toUnbind = this.toUnbind; return builder; } /** * Creates a new Builder that performs all operations on the current element of the builder and * the builder or element being passed in. */ public and(element: HTMLElement): MultiBuilder; public and(builder: Builder): MultiBuilder; public and(obj: any): MultiBuilder { // Convert HTMLElement to Builder as necessary if (!(obj instanceof Builder) && !(obj instanceof MultiBuilder)) { obj = new Builder((obj), this.offdom); } // Wrap Builders into MultiBuilder let builders:Builder[] = [this]; if (obj instanceof MultiBuilder) { for (let i = 0; i < (obj).length; i++) { builders.push((obj).item(i)); } } else { builders.push(obj); } return new MultiBuilder(builders); } /** * Inserts all created elements of this builder as children to the given container. If the * container is not provided, the element that was passed into the Builder at construction * time is being used. The caller can provide the index of insertion, or omit it to append * at the end. * This method is a no-op unless the builder was created with the offdom option to be true. */ public build(container?: Builder, index?: number): Builder; public build(container?: HTMLElement, index?: number): Builder; public build(container?: any, index?: number): Builder { assert.ok(this.offdom, 'This builder was not created off-dom, so build() can not be called.'); // Use builders own container if present if (!container) { container = this.container; } // Handle case of passed in Builder else if (container instanceof Builder) { container = (container).getHTMLElement(); } assert.ok(container, 'Builder can only be build() with a container provided.'); assert.ok(DOM.isHTMLElement(container), 'The container must either be a HTMLElement or a Builder.'); let htmlContainer = container; // Append let i: number, len: number; let childNodes = htmlContainer.childNodes; if (types.isNumber(index) && index < childNodes.length) { for (i = 0, len = this.createdElements.length; i < len; i++) { htmlContainer.insertBefore(this.createdElements[i], childNodes[index++]); } } else { for (i = 0, len = this.createdElements.length; i < len; i++) { htmlContainer.appendChild(this.createdElements[i]); } } return this; } /** * Similar to #build, but does not require that the builder is off DOM, and instead * attached the current element. If the current element has a parent, it will be * detached from that parent. */ public appendTo(container?: Builder, index?: number): Builder; public appendTo(container?: HTMLElement, index?: number): Builder; public appendTo(container?: any, index?: number): Builder { // Use builders own container if present if (!container) { container = this.container; } // Handle case of passed in Builder else if (container instanceof Builder) { container = (container).getHTMLElement(); } assert.ok(container, 'Builder can only be build() with a container provided.'); assert.ok(DOM.isHTMLElement(container), 'The container must either be a HTMLElement or a Builder.'); let htmlContainer = container; // Remove node from parent, if needed if (this.currentElement.parentNode) { this.currentElement.parentNode.removeChild(this.currentElement); } let childNodes = htmlContainer.childNodes; if (types.isNumber(index) && index < childNodes.length) { htmlContainer.insertBefore(this.currentElement, childNodes[index]); } else { htmlContainer.appendChild(this.currentElement); } return this; } /** * Performs the exact reverse operation of #append. * Doing `a.append(b)` is the same as doing `b.appendTo(a)`, with the difference * of the return value being the builder which called the operation (`a` in the * first case; `b` in the second case). */ public append(child: HTMLElement, index?: number): Builder; public append(child: Builder, index?: number): Builder; public append(child: any, index?: number): Builder { assert.ok(child, 'Need a child to append'); if (DOM.isHTMLElement(child)) { child = withElement(child); } assert.ok(child instanceof Builder || child instanceof MultiBuilder, 'Need a child to append'); (child).appendTo(this, index); return this; } /** * Removes the current element of this builder from its parent node. */ public offDOM(): Builder { if (this.currentElement.parentNode) { this.currentElement.parentNode.removeChild(this.currentElement); } return this; } /** * Returns the HTML Element the builder is currently active on. */ public getHTMLElement(): HTMLElement { return this.currentElement; } /** * Returns the HTML Element the builder is building in. */ public getContainer(): HTMLElement { return this.container; } // HTML Elements /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public div(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('div', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public p(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('p', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public ul(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('ul', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public ol(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('ol', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public li(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('li', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public span(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('span', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public img(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('img', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public a(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('a', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public header(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('header', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public section(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('section', attributes, fn); } /** * Creates a new element of this kind as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public footer(attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement('footer', attributes, fn); } /** * Creates a new element of given tag name as child of the current element or parent. * Accepts an object literal as first parameter that can be used to describe the * attributes of the element. * Accepts a function as second parameter that can be used to create child elements * of the element. The function will be called with a new builder created with the * provided element. */ public element(name: string, attributes?: any, fn?: (builder: Builder) => void): Builder { return this.doElement(name, attributes, fn); } private doElement(name: string, attributesOrFn?: any, fn?: (builder: Builder) => void): Builder { // Create Element let element = this.browserService.document.createElement(name); this.currentElement = element; // Off-DOM: Remember in array of created elements if (this.offdom) { this.createdElements.push(element); } // Object (apply properties as attributes to HTML element) if (types.isObject(attributesOrFn)) { this.attr(attributesOrFn); } // Support second argument being function if (types.isFunction(attributesOrFn)) { fn = attributesOrFn; } // Apply Functions (Elements created in Functions will be added as child to current element) if (types.isFunction(fn)) { let builder = new Builder(element); fn.call(builder, builder); // Set both 'this' and the first parameter to the new builder } // Add to parent if (!this.offdom) { this.container.appendChild(element); } return this; } /** * Calls focus() on the current HTML element; */ public domFocus(): Builder { this.currentElement.focus(); return this; } /** * Returns true if the current element of this builder is the active element. */ public hasFocus(): boolean { let activeElement: Element = this.browserService.document.activeElement; return (activeElement === this.currentElement); } /** * Calls select() on the current HTML element; */ public domSelect(range: IRange = null): Builder { let input = this.currentElement; input.select(); if (range) { input.setSelectionRange(range.start, range.end); } return this; } /** * Calls blur() on the current HTML element; */ public domBlur(): Builder { this.currentElement.blur(); return this; } /** * Calls click() on the current HTML element; */ public domClick(): Builder { this.currentElement.click(); return this; } /** * Registers listener on event types on the current element. */ public on(type: string, fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public on(typeArray: string[], fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public on(arg1: any, fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder { // Event Type Array if (types.isArray(arg1)) { arg1.forEach((type: string) => { this.on(type, fn, listenerToUnbindContainer, useCapture); }); } // Single Event Type else { let type = arg1; // Add Listener let unbind: IDisposable = DOM.addDisposableListener(this.currentElement, type, (e: Event) => { fn(e, this, unbind); // Pass in Builder as Second Argument }, useCapture || false); // Remember for off() use if (useCapture) { if (!this.captureToUnbind[type]) { this.captureToUnbind[type] = []; } this.captureToUnbind[type].push(unbind); } else { if (!this.toUnbind[type]) { this.toUnbind[type] = []; } this.toUnbind[type].push(unbind); } // Bind to Element let listenerBinding: IDisposable[] = this.getProperty(LISTENER_BINDING_ID, []); listenerBinding.push(unbind); this.setProperty(LISTENER_BINDING_ID, listenerBinding); // Add to Array if passed in if (listenerToUnbindContainer && types.isArray(listenerToUnbindContainer)) { listenerToUnbindContainer.push(unbind); } } return this; } /** * Removes all listeners from all elements created by the builder for the given event type. */ public off(type: string, useCapture?: boolean): Builder; public off(typeArray: string[], useCapture?: boolean): Builder; public off(arg1: any, useCapture?: boolean): Builder { // Event Type Array if (types.isArray(arg1)) { arg1.forEach((type: string) => { this.off(type); }); } // Single Event Type else { let type = arg1; if (useCapture) { if (this.captureToUnbind[type]) { this.captureToUnbind[type] = disposeAll(this.captureToUnbind[type]); } } else { if (this.toUnbind[type]) { this.toUnbind[type] = disposeAll(this.toUnbind[type]); } } } return this; } /** * Registers listener on event types on the current element and removes * them after first invocation. */ public once(type: string, fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public once(typesArray: string[], fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public once(arg1: any, fn: (e: Event, builder: Builder, unbind: IDisposable) => void, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder { // Event Type Array if (types.isArray(arg1)) { arg1.forEach((type: string) => { this.once(type, fn); }); } // Single Event Type else { let type = arg1; // Add Listener let unbind: IDisposable = DOM.addDisposableListener(this.currentElement, type, (e: Event) => { fn(e, this, unbind); // Pass in Builder as Second Argument unbind.dispose(); }, useCapture || false); // Add to Array if passed in if (listenerToUnbindContainer && types.isArray(listenerToUnbindContainer)) { listenerToUnbindContainer.push(unbind); } } return this; } /** * Registers listener on event types on the current element and causes * the event to prevent default execution (e.preventDefault()). If the * parameter "cancelBubble" is set to true, it will also prevent bubbling * of the event. */ public preventDefault(type: string, cancelBubble: boolean, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public preventDefault(typesArray: string[], cancelBubble: boolean, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder; public preventDefault(arg1: any, cancelBubble: boolean, listenerToUnbindContainer?: IDisposable[], useCapture?: boolean): Builder { let fn = function(e: Event) { e.preventDefault(); if (cancelBubble) { if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } } }; return this.on(arg1, fn, listenerToUnbindContainer); } /** * This method has different characteristics based on the parameter provided: * a) a single string passed in as argument will return the attribute value using the * string as key from the current element of the builder. * b) two strings passed in will set the value of an attribute identified by the first * parameter to match the second parameter * c) an object literal passed in will apply the properties of the literal as attributes * to the current element of the builder. */ public attr(name: string): string; public attr(name: string, value: string): Builder; public attr(name: string, value: boolean): Builder; public attr(name: string, value: number): Builder; public attr(attributes: any): Builder; public attr(firstP: any, secondP?: any): any { // Apply Object Literal to Attributes of Element if (types.isObject(firstP)) { for (let prop in firstP) { if (firstP.hasOwnProperty(prop)) { let value = firstP[prop]; this.doSetAttr(prop, value); } } return this; } // Get Attribute Value if (types.isString(firstP) && !types.isString(secondP)) { return this.currentElement.getAttribute(firstP); } // Set Attribute Value if (types.isString(firstP)) { if (!types.isString(secondP)) { secondP = String(secondP); } this.doSetAttr(firstP, secondP); } return this; } private doSetAttr(prop: string, value: any): void { if (prop === 'class') { prop = 'addClass'; // Workaround for the issue that a function name can not be 'class' in ES } if ((this)[prop]) { if (types.isArray(value)) { (this)[prop].apply(this, value); } else { (this)[prop].call(this, value); } } else { this.currentElement.setAttribute(prop, value); } } /** * Removes an attribute by the given name. */ public removeAttribute(prop: string): void { this.currentElement.removeAttribute(prop); } /** * Sets the id attribute to the value provided for the current HTML element of the builder. */ public id(id: string): Builder { this.currentElement.setAttribute('id', id); return this; } /** * Sets the src attribute to the value provided for the current HTML element of the builder. */ public src(src: string): Builder { this.currentElement.setAttribute('src', src); return this; } /** * Sets the href attribute to the value provided for the current HTML element of the builder. */ public href(href: string): Builder { this.currentElement.setAttribute('href', href); return this; } /** * Sets the title attribute to the value provided for the current HTML element of the builder. */ public title(title: string): Builder { this.currentElement.setAttribute('title', title); return this; } /** * Sets the name attribute to the value provided for the current HTML element of the builder. */ public name(name: string): Builder { this.currentElement.setAttribute('name', name); return this; } /** * Sets the type attribute to the value provided for the current HTML element of the builder. */ public type(type: string): Builder { this.currentElement.setAttribute('type', type); return this; } /** * Sets the value attribute to the value provided for the current HTML element of the builder. */ public value(value: string): Builder { this.currentElement.setAttribute('value', value); return this; } /** * Sets the alt attribute to the value provided for the current HTML element of the builder. */ public alt(alt: string): Builder { this.currentElement.setAttribute('alt', alt); return this; } /** * Sets the name draggable to the value provided for the current HTML element of the builder. */ public draggable(isDraggable: boolean): Builder { this.currentElement.setAttribute('draggable', isDraggable ? 'true' : 'false'); return this; } /** * Sets the tabindex attribute to the value provided for the current HTML element of the builder. */ public tabindex(index: number): Builder { this.currentElement.setAttribute('tabindex', index.toString()); return this; } /** * This method has different characteristics based on the parameter provided: * a) a single string passed in as argument will return the style value using the * string as key from the current element of the builder. * b) two strings passed in will set the style value identified by the first * parameter to match the second parameter. * c) an object literal passed in will apply the properties of the literal as styles * to the current element of the builder. */ public style(name: string): string; public style(name: string, value: string): Builder; public style(attributes: any): Builder; public style(firstP: any, secondP?: any): any { // Apply Object Literal to Styles of Element if (types.isObject(firstP)) { for (let prop in firstP) { if (firstP.hasOwnProperty(prop)) { let value = firstP[prop]; this.doSetStyle(prop, value); } } } // Get Style Value else if (types.isString(firstP) && !types.isString(secondP)) { return this.currentElement.style[this.cssKeyToJavaScriptProperty(firstP)]; } // Set Style Value else if (types.isString(firstP) && types.isString(secondP)) { this.doSetStyle(firstP, secondP); } return this; } private doSetStyle(key: string, value: string): void { if (key.indexOf('-') >= 0) { let segments = key.split('-'); key = segments[0]; for (let i = 1; i < segments.length; i++) { let segment = segments[i]; key = key + segment.charAt(0).toUpperCase() + segment.substr(1); } } this.currentElement.style[this.cssKeyToJavaScriptProperty(key)] = value; } private cssKeyToJavaScriptProperty(key: string): string { // Automagically convert dashes as they are not allowed when programmatically // setting a CSS style property if (key.indexOf('-') >= 0) { let segments = key.split('-'); key = segments[0]; for (let i = 1; i < segments.length; i++) { let segment = segments[i]; key = key + segment.charAt(0).toUpperCase() + segment.substr(1); } } // Float is special too else if (key === 'float') { key = 'cssFloat'; } return key; } /** * Returns the computed CSS style for the current HTML element of the builder. */ public getComputedStyle(): CSSStyleDeclaration { return DOM.getComputedStyle(this.currentElement); } /** * Adds the variable list of arguments as class names to the current HTML element of the builder. */ public addClass(...classes: string[]): Builder { classes.forEach((nameValue: string) => { let names = nameValue.split(' '); names.forEach((name: string) => { DOM.addClass(this.currentElement, name); }); }); return this; } /** * Sets the class name of the current HTML element of the builder to the provided className. * If shouldAddClass is provided - for true class is added, for false class is removed. */ public setClass(className: string, shouldAddClass: boolean = null): Builder { if (shouldAddClass === null) { this.currentElement.className = className; } else if (shouldAddClass) { this.addClass(className); } else { this.removeClass(className); } return this; } /** * Returns whether the current HTML element of the builder has the provided class assigned. */ public hasClass(className: string): boolean { return DOM.hasClass(this.currentElement, className); } /** * Removes the variable list of arguments as class names from the current HTML element of the builder. */ public removeClass(...classes: string[]): Builder { classes.forEach((nameValue: string) => { let names = nameValue.split(' '); names.forEach((name: string) => { DOM.removeClass(this.currentElement, name); }); }); return this; } /** * Sets the first class to the current HTML element of the builder if the second class is currently set * and vice versa otherwise. */ public swapClass(classA: string, classB: string): Builder { if (this.hasClass(classA)) { this.removeClass(classA); this.addClass(classB); } else { this.removeClass(classB); this.addClass(classA); } return this; } /** * Adds or removes the provided className for the current HTML element of the builder. */ public toggleClass(className: string): Builder { if (this.hasClass(className)) { this.removeClass(className); } else { this.addClass(className); } return this; } /** * Sets the CSS property color. */ public color(color: string): Builder { this.currentElement.style.color = color; return this; } /** * Sets the CSS property background. */ public background(color: string): Builder { this.currentElement.style.backgroundColor = color; return this; } /** * Sets the CSS property padding. */ public padding(padding: string): Builder; public padding(top: number, right?: number, bottom?: number, left?: number): Builder; public padding(top: string, right?: string, bottom?: string, left?: string): Builder; public padding(top: any, right?: any, bottom?: any, left?: any): Builder { if (types.isString(top) && top.indexOf(' ') >= 0) { return this.padding.apply(this, top.split(' ')); } if (!types.isUndefinedOrNull(top)) { this.currentElement.style.paddingTop = this.toPixel(top); } if (!types.isUndefinedOrNull(right)) { this.currentElement.style.paddingRight = this.toPixel(right); } if (!types.isUndefinedOrNull(bottom)) { this.currentElement.style.paddingBottom = this.toPixel(bottom); } if (!types.isUndefinedOrNull(left)) { this.currentElement.style.paddingLeft = this.toPixel(left); } return this; } /** * Sets the CSS property margin. */ public margin(margin: string): Builder; public margin(top: number, right?: number, bottom?: number, left?: number): Builder; public margin(top: string, right?: string, bottom?: string, left?: string): Builder; public margin(top: any, right?: any, bottom?: any, left?: any): Builder { if (types.isString(top) && top.indexOf(' ') >= 0) { return this.margin.apply(this, top.split(' ')); } if (!types.isUndefinedOrNull(top)) { this.currentElement.style.marginTop = this.toPixel(top); } if (!types.isUndefinedOrNull(right)) { this.currentElement.style.marginRight = this.toPixel(right); } if (!types.isUndefinedOrNull(bottom)) { this.currentElement.style.marginBottom = this.toPixel(bottom); } if (!types.isUndefinedOrNull(left)) { this.currentElement.style.marginLeft = this.toPixel(left); } return this; } /** * Sets the CSS property position. */ public position(position: string): Builder; public position(top: number, right?: number, bottom?: number, left?: number, position?: string): Builder; public position(top: string, right?: string, bottom?: string, left?: string, position?: string): Builder; public position(top: any, right?: any, bottom?: any, left?: any, position?: string): Builder { if (types.isString(top) && top.indexOf(' ') >= 0) { return this.position.apply(this, top.split(' ')); } if (!types.isUndefinedOrNull(top)) { this.currentElement.style.top = this.toPixel(top); } if (!types.isUndefinedOrNull(right)) { this.currentElement.style.right = this.toPixel(right); } if (!types.isUndefinedOrNull(bottom)) { this.currentElement.style.bottom = this.toPixel(bottom); } if (!types.isUndefinedOrNull(left)) { this.currentElement.style.left = this.toPixel(left); } if (!position) { position = 'absolute'; } this.currentElement.style.position = position; return this; } /** * Sets the CSS property size. */ public size(size: string): Builder; public size(width: number, height?: number): Builder; public size(width: string, height?: string): Builder; public size(width: any, height?: any): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.size.apply(this, width.split(' ')); } if (!types.isUndefinedOrNull(width)) { this.currentElement.style.width = this.toPixel(width); } if (!types.isUndefinedOrNull(height)) { this.currentElement.style.height = this.toPixel(height); } return this; } /** * Sets the CSS property min-size. */ public minSize(size: string): Builder; public minSize(width: number, height?: number): Builder; public minSize(width: string, height?: string): Builder; public minSize(width: any, height?: any): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.minSize.apply(this, width.split(' ')); } if (!types.isUndefinedOrNull(width)) { this.currentElement.style.minWidth = this.toPixel(width); } if (!types.isUndefinedOrNull(height)) { this.currentElement.style.minHeight = this.toPixel(height); } return this; } /** * Sets the CSS property max-size. */ public maxSize(size: string): Builder; public maxSize(width: number, height?: number): Builder; public maxSize(width: string, height?: string): Builder; public maxSize(width: any, height?: any): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.maxSize.apply(this, width.split(' ')); } if (!types.isUndefinedOrNull(width)) { this.currentElement.style.maxWidth = this.toPixel(width); } if (!types.isUndefinedOrNull(height)) { this.currentElement.style.maxHeight = this.toPixel(height); } return this; } /** * Sets the CSS property float. */ public float(float: string): Builder { this.currentElement.style.cssFloat = float; return this; } /** * Sets the CSS property clear. */ public clear(clear: string): Builder { this.currentElement.style.clear = clear; return this; } /** * Sets the CSS property for fonts back to default. */ public normal(): Builder { this.currentElement.style.fontStyle = 'normal'; this.currentElement.style.fontWeight = 'normal'; this.currentElement.style.textDecoration = 'none'; return this; } /** * Sets the CSS property font-style to italic. */ public italic(): Builder { this.currentElement.style.fontStyle = 'italic'; return this; } /** * Sets the CSS property font-weight to bold. */ public bold(): Builder { this.currentElement.style.fontWeight = 'bold'; return this; } /** * Sets the CSS property text-decoration to underline. */ public underline(): Builder { this.currentElement.style.textDecoration = 'underline'; return this; } /** * Sets the CSS property overflow. */ public overflow(overflow: string): Builder { this.currentElement.style.overflow = overflow; return this; } /** * Sets the CSS property display. */ public display(display: string): Builder { this.currentElement.style.display = display; return this; } public disable(): Builder { this.currentElement.setAttribute('disabled', 'disabled'); return this; } public enable(): Builder { this.currentElement.removeAttribute('disabled'); return this; } /** * Shows the current element of the builder. */ public show(): Builder { if (this.hasClass('hidden')) { this.removeClass('hidden'); } this.attr('aria-hidden', 'false'); // Cancel any pending showDelayed() invocation this.cancelVisibilityPromise(); return this; } /** * Shows the current builder element after the provided delay. If the builder * was set to hidden using the hide() method before this method executed, the * function will return without showing the current element. This is useful to * only show the element when a specific delay is reached (e.g. for a long running * operation. */ public showDelayed(delay: number): Builder { // Cancel any pending showDelayed() invocation this.cancelVisibilityPromise(); let promise = Promise.timeout(delay); this.setProperty(VISIBILITY_BINDING_ID, promise); promise.done(() => { this.removeProperty(VISIBILITY_BINDING_ID); this.show(); }); return this; } /** * Hides the current element of the builder. */ public hide(): Builder { if (!this.hasClass('hidden')) { this.addClass('hidden'); } this.attr('aria-hidden', 'true'); // Cancel any pending showDelayed() invocation this.cancelVisibilityPromise(); return this; } /** * Returns true if the current element of the builder is hidden. */ public isHidden(): boolean { return this.hasClass('hidden') || this.currentElement.style.display === 'none'; } /** * Toggles visibility of the current element of the builder. */ public toggleVisibility(): Builder { // Cancel any pending showDelayed() invocation this.cancelVisibilityPromise(); this.swapClass('builder-visible', 'hidden'); if (this.isHidden()) { this.attr('aria-hidden', 'true'); } else { this.attr('aria-hidden', 'false'); } return this; } private cancelVisibilityPromise(): void { let promise: Promise = this.getProperty(VISIBILITY_BINDING_ID); if (promise) { promise.cancel(); this.removeProperty(VISIBILITY_BINDING_ID); } } /** * Sets the CSS property border. */ public border(border: string): Builder; public border(width: number, style?: string, color?: string): Builder; public border(width: any, style?: string, color?: string): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.border.apply(this, width.split(' ')); } this.currentElement.style.borderWidth = this.toPixel(width); if (color) { this.currentElement.style.borderColor = color; } if (style) { this.currentElement.style.borderStyle = style; } return this; } /** * Sets the CSS property border-top. */ public borderTop(border: string): Builder; public borderTop(width: number, style: string, color: string): Builder; public borderTop(width: any, style?: string, color?: string): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.borderTop.apply(this, width.split(' ')); } this.currentElement.style.borderTopWidth = this.toPixel(width); if (color) { this.currentElement.style.borderTopColor = color; } if (style) { this.currentElement.style.borderTopStyle = style; } return this; } /** * Sets the CSS property border-bottom. */ public borderBottom(border: string): Builder; public borderBottom(width: number, style: string, color: string): Builder; public borderBottom(width: any, style?: string, color?: string): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.borderBottom.apply(this, width.split(' ')); } this.currentElement.style.borderBottomWidth = this.toPixel(width); if (color) { this.currentElement.style.borderBottomColor = color; } if (style) { this.currentElement.style.borderBottomStyle = style; } return this; } /** * Sets the CSS property border-left. */ public borderLeft(border: string): Builder; public borderLeft(width: number, style: string, color: string): Builder; public borderLeft(width: any, style?: string, color?: string): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.borderLeft.apply(this, width.split(' ')); } this.currentElement.style.borderLeftWidth = this.toPixel(width); if (color) { this.currentElement.style.borderLeftColor = color; } if (style) { this.currentElement.style.borderLeftStyle = style; } return this; } /** * Sets the CSS property border-right. */ public borderRight(border: string): Builder; public borderRight(width: number, style: string, color: string): Builder; public borderRight(width: any, style?: string, color?: string): Builder { if (types.isString(width) && width.indexOf(' ') >= 0) { return this.borderRight.apply(this, width.split(' ')); } this.currentElement.style.borderRightWidth = this.toPixel(width); if (color) { this.currentElement.style.borderRightColor = color; } if (style) { this.currentElement.style.borderRightStyle = style; } return this; } /** * Sets the CSS property text-align. */ public textAlign(textAlign: string): Builder { this.currentElement.style.textAlign = textAlign; return this; } /** * Sets the CSS property vertical-align. */ public verticalAlign(valign: string): Builder { this.currentElement.style.verticalAlign = valign; return this; } private toPixel(obj: any): string { if (obj.toString().indexOf('px') === -1) { return obj.toString() + 'px'; } return obj; } /** * Sets the innerHTML attribute. */ public innerHtml(html: string, append?: boolean): Builder { if (append) { this.currentElement.innerHTML += html; } else { this.currentElement.innerHTML = html; } return this; } /** * Sets the textContent property of the element. * All HTML special characters will be escaped. */ public text(text: string, append?: boolean): Builder { if (append) { // children is child Elements versus childNodes includes textNodes if (this.currentElement.children.length === 0) { this.currentElement.textContent += text; } else { // if there are elements inside this node, append the string as a new text node // to avoid wiping out the innerHTML and replacing it with only text content this.currentElement.appendChild(this.browserService.document.createTextNode(text)); } } else { this.currentElement.textContent = text; } return this; } /** * Sets the innerHTML attribute in escaped form. */ public safeInnerHtml(html: string, append?: boolean): Builder { return this.innerHtml(strings.escape(html), append); } /** * Adds the provided object as property to the current element. Call getBinding() * to retrieve it again. */ public bind(object: any): Builder { bindElement(this.currentElement, object); return this; } /** * Removes the binding of the current element. */ public unbind(): Builder { unbindElement(this.currentElement); return this; } /** * Returns the object that was passed into the bind() call. */ public getBinding(): any { return getBindingFromElement(this.currentElement); } /** * Allows to store arbritary data into the current element. */ public setProperty(key: string, value: any): Builder { setPropertyOnElement(this.currentElement, key, value); return this; } /** * Allows to get arbritary data from the current element. */ public getProperty(key: string, fallback?: any): any { return getPropertyFromElement(this.currentElement, key, fallback); } /** * Removes a property from the current element that is stored under the given key. */ public removeProperty(key: string): Builder { if (hasData(this.currentElement)) { delete data(this.currentElement)[key]; } return this; } /** * Returns a new builder with the parent element of the current element of the builder. */ public parent(offdom?: boolean): Builder { assert.ok(!this.offdom, 'Builder was created with offdom = true and thus has no parent set'); return withElement(this.currentElement.parentNode, offdom); } /** * Returns a new builder with all child elements of the current element of the builder. */ public children(offdom?: boolean): MultiBuilder { let children = this.currentElement.children; let builders: Builder[] = []; for (let i = 0; i < children.length; i++) { builders.push(withElement(children.item(i), offdom)); } return new MultiBuilder(builders); } /** * Removes the current HTMLElement from the given builder from this builder if this builders * current HTMLElement is the direct parent. */ public removeChild(builder: Builder): Builder { if (this.currentElement === builder.parent().getHTMLElement()) { this.currentElement.removeChild(builder.getHTMLElement()); } return this; } /** * Returns a new builder with all elements matching the provided selector scoped to the * current element of the builder. Use Build.withElementsBySelector() to run the selector * over the entire DOM. * The returned builder is an instance of array that can have 0 elements if the selector does not match any * elements. */ public select(selector: string, offdom?: boolean): MultiBuilder { assert.ok(types.isString(selector), 'Expected String as parameter'); let elements = this.currentElement.querySelectorAll(selector); let builders: Builder[] = []; for (let i = 0; i < elements.length; i++) { builders.push(withElement(elements.item(i), offdom)); } return new MultiBuilder(builders); } /** * Returns true if the current element of the builder matches the given selector and false otherwise. */ public matches(selector: string): boolean { let element = this.currentElement; let matches = (element).webkitMatchesSelector || (element).mozMatchesSelector || (element).msMatchesSelector || (element).oMatchesSelector; return matches && matches.call(element, selector); } /** * Returns true if the current element of the builder has no children. */ public isEmpty(): boolean { return !this.currentElement.childNodes || this.currentElement.childNodes.length === 0; } /** * Recurse through all descendant nodes and remove their data binding. */ private unbindDescendants(current: HTMLElement): void { if (current && current.children) { for (let i = 0, length = current.children.length; i < length; i++) { let element = current.children.item(i); // Unbind if (hasData(element)) { // Listeners let listeners: IDisposable[] = data(element)[LISTENER_BINDING_ID]; if (types.isArray(listeners)) { while (listeners.length) { listeners.pop().dispose(); } } // Delete Data Slot delete element[MS_DATA_KEY]; } // Recurse this.unbindDescendants(element); } } } /** * Removes all HTML elements from the current element of the builder. Will also clean up any * event listners registered and also clear any data binding and properties stored * to any child element. */ public empty(): Builder { this.unbindDescendants(this.currentElement); this.clearChildren(); if (this.offdom) { this.createdElements = []; } return this; } /** * Removes all HTML elements from the current element of the builder. */ public clearChildren(): Builder { // Remove Elements if (this.currentElement) { DOM.clearNode(this.currentElement); } return this; } /** * Removes the current HTML element and all its children from its parent and unbinds * all listeners and properties set to the data slots. */ public destroy(): void { if (this.currentElement) { // Remove from parent if (this.currentElement.parentNode) { this.currentElement.parentNode.removeChild(this.currentElement); } // Empty to clear listeners and bindings from children this.empty(); // Unbind if (hasData(this.currentElement)) { // Listeners let listeners: IDisposable[] = data(this.currentElement)[LISTENER_BINDING_ID]; if (types.isArray(listeners)) { while (listeners.length) { listeners.pop().dispose(); } } // Delete Data Slot delete this.currentElement[MS_DATA_KEY]; } } let type: string; for (type in this.toUnbind) { if (this.toUnbind.hasOwnProperty(type) && types.isArray(this.toUnbind[type])) { this.toUnbind[type] = disposeAll(this.toUnbind[type]); } } for (type in this.captureToUnbind) { if (this.captureToUnbind.hasOwnProperty(type) && types.isArray(this.captureToUnbind[type])) { this.captureToUnbind[type] = disposeAll(this.captureToUnbind[type]); } } // Nullify fields this.currentElement = null; this.container = null; this.offdom = null; this.createdElements = null; this.captureToUnbind = null; this.toUnbind = null; } /** * Removes the current HTML element and all its children from its parent and unbinds * all listeners and properties set to the data slots. */ public dispose(): void { this.destroy(); } /** * Gets the coordinates of the element relative to the specified parent. */ public getPositionRelativeTo(element: HTMLElement): Box; public getPositionRelativeTo(element: Builder): Box; public getPositionRelativeTo(element: any): Box { if (element instanceof Builder) { element = (element).getHTMLElement(); } let left = DOM.getRelativeLeft(this.currentElement, element); let top = DOM.getRelativeTop(this.currentElement, element); return new Box(top, -1, -1, left); } /** * Gets the absolute coordinates of the element. */ public getPosition(): Box { let position = DOM.getTopLeftOffset(this.currentElement); return new Box(position.top, -1, -1, position.left); } /** * Gets the size (in pixels) of an element, including the margin. */ public getTotalSize(): Dimension { let totalWidth = DOM.getTotalWidth(this.currentElement); let totalHeight = DOM.getTotalHeight(this.currentElement); return new Dimension(totalWidth, totalHeight); } /** * Gets the size (in pixels) of the inside of the element, excluding the border and padding. */ public getContentSize(): Dimension { let contentWidth = DOM.getContentWidth(this.currentElement); let contentHeight = DOM.getContentHeight(this.currentElement); return new Dimension(contentWidth, contentHeight); } /** * Another variant of getting the inner dimensions of an element. */ public getClientArea(): Dimension { // 0.) Try with DOM getDomNodePosition if (this.currentElement !== this.browserService.document.body) { let dimensions = DOM.getDomNodePosition(this.currentElement); return new Dimension(dimensions.width, dimensions.height); } // 1.) Try innerWidth / innerHeight if (this.browserService.window.innerWidth && this.browserService.window.innerHeight) { return new Dimension(this.browserService.window.innerWidth, this.browserService.window.innerHeight); } // 2.) Try with document.body.clientWidth / document.body.clientHeigh if (this.browserService.document.body && this.browserService.document.body.clientWidth && this.browserService.document.body.clientWidth) { return new Dimension(this.browserService.document.body.clientWidth, this.browserService.document.body.clientHeight); } // 3.) Try with document.documentElement.clientWidth / document.documentElement.clientHeight if (this.browserService.document.documentElement && this.browserService.document.documentElement.clientWidth && this.browserService.document.documentElement.clientHeight) { return new Dimension(this.browserService.document.documentElement.clientWidth, this.browserService.document.documentElement.clientHeight); } throw new Error('Unable to figure out browser width and height'); } } /** * The multi builder provides the same methods as the builder, but allows to call * them on an array of builders. */ export class MultiBuilder extends Builder { public length: number; private builders: Builder[]; constructor(multiBuilder: MultiBuilder); constructor(builder: Builder); constructor(builders: Builder[]); constructor(elements: HTMLElement[]); constructor(builders: any) { assert.ok(types.isArray(builders) || builders instanceof MultiBuilder, 'Expected Array or MultiBuilder as parameter'); super(); this.length = 0; this.builders = []; // Add Builders to Array if (types.isArray(builders)) { for (let i = 0; i < builders.length; i++) { if (builders[i] instanceof HTMLElement) { this.push(withElement(builders[i])); } else { this.push(builders[i]); } } } else { for (let i = 0; i < (builders).length; i++) { this.push((builders).item(i)); } } // Mixin Builder functions to operate on all builders let $outer = this; let propertyFn = (prop: string) => { ($outer)[prop] = function(): any { let args = Array.prototype.slice.call(arguments); let returnValues: any[]; let mergeBuilders = false; for (let i = 0; i < $outer.length; i++) { let res = ($outer.item(i))[prop].apply($outer.item(i), args); // Merge MultiBuilders into one if (res instanceof MultiBuilder) { if (!returnValues) { returnValues = []; } mergeBuilders = true; for (let j = 0; j < (res).length; j++) { returnValues.push((res).item(j)); } } // Any other Return Type (e.g. boolean, integer) else if (!types.isUndefined(res) && !(res instanceof Builder)) { if (!returnValues) { returnValues = []; } returnValues.push(res); } } if (returnValues && mergeBuilders) { return new MultiBuilder(returnValues); } return returnValues || $outer; }; }; for (let prop in Builder.prototype) { if (prop !== 'clone' && prop !== 'and') { // Skip methods that are explicitly defined in MultiBuilder if (Builder.prototype.hasOwnProperty(prop) && types.isFunction((Builder).prototype[prop])) { propertyFn(prop); } } } } public item(i: number): Builder { return this.builders[i]; } public push(...items: Builder[]): void { for (let i = 0; i < items.length; i++) { this.builders.push(items[i]); } this.length = this.builders.length; } public pop(): Builder { let element = this.builders.pop(); this.length = this.builders.length; return element; } public concat(items: Builder[]): Builder[] { let elements = this.builders.concat(items); this.length = this.builders.length; return elements; } public shift(): Builder { let element = this.builders.shift(); this.length = this.builders.length; return element; } public unshift(item: Builder): number { let res = this.builders.unshift(item); this.length = this.builders.length; return res; } public slice(start: number, end?: number): Builder[] { let elements = this.builders.slice(start, end); this.length = this.builders.length; return elements; } public splice(start: number, deleteCount?: number): Builder[] { let elements = this.builders.splice(start, deleteCount); this.length = this.builders.length; return elements; } public clone(): MultiBuilder { return new MultiBuilder(this); } public and(element: HTMLElement): MultiBuilder; public and(builder: Builder): MultiBuilder; public and(obj: any): MultiBuilder { // Convert HTMLElement to Builder as necessary if (!(obj instanceof Builder) && !(obj instanceof MultiBuilder)) { obj = new Builder((obj)); } let builders: Builder[] = []; if (obj instanceof MultiBuilder) { for (let i = 0; i < (obj).length; i++) { builders.push((obj).item(i)); } } else { builders.push(obj); } this.push.apply(this, builders); return this; } } function withBuilder(builder: Builder, offdom?: boolean): Builder { if (builder instanceof MultiBuilder) { return new MultiBuilder((builder)); } return new Builder(builder.getHTMLElement(), offdom); } function withElement(element: HTMLElement, offdom?: boolean): Builder { return new Builder(element, offdom); } function offDOM(): Builder { return new Builder(null, true); } // Binding functions /** * Allows to store arbritary data into element. */ export function setPropertyOnElement(element: HTMLElement, key: string, value: any): void { data(element)[key] = value; } /** * Allows to get arbritary data from element. */ export function getPropertyFromElement(element: HTMLElement, key: string, fallback?: any): any { if (hasData(element)) { let value = data(element)[key]; if (!types.isUndefined(value)) { return value; } } return fallback; } /** * Removes a property from an element. */ export function removePropertyFromElement(element: HTMLElement, key: string): void { if (hasData(element)) { delete data(element)[key]; } } /** * Adds the provided object as property to the given element. Call getBinding() * to retrieve it again. */ export function bindElement(element: HTMLElement, object: any): void { setPropertyOnElement(element, DATA_BINDING_ID, object); } /** * Removes the binding of the given element. */ export function unbindElement(element: HTMLElement): void { removePropertyFromElement(element, DATA_BINDING_ID); } /** * Returns the object that was passed into the bind() call for the element. */ export function getBindingFromElement(element: HTMLElement): any { return getPropertyFromElement(element, DATA_BINDING_ID); } export let Binding = { setPropertyOnElement: setPropertyOnElement, getPropertyFromElement: getPropertyFromElement, removePropertyFromElement: removePropertyFromElement, bindElement: bindElement, unbindElement: unbindElement, getBindingFromElement: getBindingFromElement }; let SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; export let $: QuickBuilder = function(arg?: any): Builder { // Off-DOM use if (types.isUndefined(arg)) { return offDOM(); } // Falsified values cause error otherwise if (!arg) { throw new Error('Bad use of $'); } // Wrap the given element if (DOM.isHTMLElement(arg) || arg === window) { return withElement(arg); } // Wrap the given builders if (types.isArray(arg)) { return new MultiBuilder(arg); } // Wrap the given builder if (arg instanceof Builder) { return withBuilder((arg)); } if (types.isString(arg)) { // Use the argument as HTML code if (arg[0] === '<') { let element: Node; let container = BrowserService.getService().document.createElement('div'); container.innerHTML = strings.format.apply(strings, arguments); if (container.children.length === 0) { throw new Error('Bad use of $'); } if (container.children.length === 1) { element = container.firstChild; container.removeChild(element); return withElement(element); } let builders: Builder[] = []; while (container.firstChild) { element = container.firstChild; container.removeChild(element); builders.push(withElement(element)); } return new MultiBuilder(builders); } // Use the argument as a selector constructor else if (arguments.length === 1) { let match = SELECTOR_REGEX.exec(arg); if (!match) { throw new Error('Bad use of $'); } let tag = match[1] || 'div'; let id = match[3] || undefined; let classes = (match[4] || '').replace(/\./g, ' '); let props: any = {}; if (id) { props['id'] = id; } if (classes) { props['class'] = classes; } return offDOM().element(tag, props); } // Use the arguments as the arguments to Builder#element(...) else { let result = offDOM(); result.element.apply(result, arguments); return result; } } else { throw new Error('Bad use of $'); } }; ($).Box = Box; ($).Dimension = Dimension; ($).Position = Position; ($).Builder = Builder; ($).MultiBuilder = MultiBuilder; ($).Build = Build; ($).Binding = Binding;