/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';
var __extends = (this && this.__extends) || (function () {
    var extendStatics = Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
import './builder.css';
import { TPromise } from '../common/winjs.base';
import * as types from '../common/types';
import { dispose } from '../common/lifecycle';
import * as strings from '../common/strings';
import * as assert from '../common/assert';
import * as DOM from './dom';
/**
 * 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, offdom) {
    assert.ok(types.isString(id), 'Expected String as parameter');
    var element = document.getElementById(id);
    if (element) {
        return new Builder(element, offdom);
    }
    return null;
}
export var Build = {
    withElementById: withElementById
};
// --- Implementation starts here
var MS_DATA_KEY = '_msDataKey';
var DATA_BINDING_ID = '__$binding';
var LISTENER_BINDING_ID = '__$listeners';
var VISIBILITY_BINDING_ID = '__$visibility';
var Dimension = /** @class */ (function () {
    function Dimension(width, height) {
        this.width = width;
        this.height = height;
    }
    return Dimension;
}());
export { Dimension };
function data(element) {
    if (!element[MS_DATA_KEY]) {
        element[MS_DATA_KEY] = {};
    }
    return element[MS_DATA_KEY];
}
function hasData(element) {
    return !!element[MS_DATA_KEY];
}
/**
 *  Wraps around the provided element to manipulate it and add more child elements.
 */
var Builder = /** @class */ (function () {
    function Builder(element, offdom) {
        this.offdom = offdom;
        this.container = element;
        this.currentElement = element;
        this.createdElements = [];
        this.toUnbind = {};
        this.captureToUnbind = {};
    }
    /**
     *  Returns a new builder that lets the current HTML Element of this builder be the container
     *  for future additions on the builder.
     */
    Builder.prototype.asContainer = function () {
        return withBuilder(this, this.offdom);
    };
    /**
     *  Clones the builder providing the same properties as this one.
     */
    Builder.prototype.clone = function () {
        var 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;
    };
    Builder.prototype.build = function (container, index) {
        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;
        }
        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.');
        var htmlContainer = container;
        // Append
        var i, len;
        var 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;
    };
    Builder.prototype.appendTo = function (container, index) {
        // Use builders own container if present
        if (!container) {
            container = this.container;
        }
        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.');
        var htmlContainer = container;
        // Remove node from parent, if needed
        if (this.currentElement.parentNode) {
            this.currentElement.parentNode.removeChild(this.currentElement);
        }
        var childNodes = htmlContainer.childNodes;
        if (types.isNumber(index) && index < childNodes.length) {
            htmlContainer.insertBefore(this.currentElement, childNodes[index]);
        }
        else {
            htmlContainer.appendChild(this.currentElement);
        }
        return this;
    };
    Builder.prototype.append = function (child, index) {
        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.
     */
    Builder.prototype.offDOM = function () {
        if (this.currentElement.parentNode) {
            this.currentElement.parentNode.removeChild(this.currentElement);
        }
        return this;
    };
    /**
     *  Returns the HTML Element the builder is currently active on.
     */
    Builder.prototype.getHTMLElement = function () {
        return this.currentElement;
    };
    /**
     *  Returns the HTML Element the builder is building in.
     */
    Builder.prototype.getContainer = function () {
        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.
     */
    Builder.prototype.div = function (attributes, fn) {
        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.
     */
    Builder.prototype.p = function (attributes, fn) {
        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.
     */
    Builder.prototype.ul = function (attributes, fn) {
        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.
     */
    Builder.prototype.li = function (attributes, fn) {
        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.
     */
    Builder.prototype.span = function (attributes, fn) {
        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.
     */
    Builder.prototype.img = function (attributes, fn) {
        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.
     */
    Builder.prototype.a = function (attributes, fn) {
        return this.doElement('a', 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.
     */
    Builder.prototype.element = function (name, attributes, fn) {
        return this.doElement(name, attributes, fn);
    };
    Builder.prototype.doElement = function (name, attributesOrFn, fn) {
        // Create Element
        var element = 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)) {
            var 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;
     */
    Builder.prototype.domFocus = function () {
        this.currentElement.focus();
        return this;
    };
    /**
     *  Calls blur() on the current HTML element;
     */
    Builder.prototype.domBlur = function () {
        this.currentElement.blur();
        return this;
    };
    Builder.prototype.on = function (arg1, fn, listenerToUnbindContainer, useCapture) {
        var _this = this;
        // Event Type Array
        if (types.isArray(arg1)) {
            arg1.forEach(function (type) {
                _this.on(type, fn, listenerToUnbindContainer, useCapture);
            });
        }
        else {
            var type = arg1;
            // Add Listener
            var unbind_1 = DOM.addDisposableListener(this.currentElement, type, function (e) {
                fn(e, _this, unbind_1); // 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_1);
            }
            else {
                if (!this.toUnbind[type]) {
                    this.toUnbind[type] = [];
                }
                this.toUnbind[type].push(unbind_1);
            }
            // Bind to Element
            var listenerBinding = this.getProperty(LISTENER_BINDING_ID, []);
            listenerBinding.push(unbind_1);
            this.setProperty(LISTENER_BINDING_ID, listenerBinding);
            // Add to Array if passed in
            if (listenerToUnbindContainer && types.isArray(listenerToUnbindContainer)) {
                listenerToUnbindContainer.push(unbind_1);
            }
        }
        return this;
    };
    Builder.prototype.off = function (arg1, useCapture) {
        var _this = this;
        // Event Type Array
        if (types.isArray(arg1)) {
            arg1.forEach(function (type) {
                _this.off(type);
            });
        }
        else {
            var type = arg1;
            if (useCapture) {
                if (this.captureToUnbind[type]) {
                    this.captureToUnbind[type] = dispose(this.captureToUnbind[type]);
                }
            }
            else {
                if (this.toUnbind[type]) {
                    this.toUnbind[type] = dispose(this.toUnbind[type]);
                }
            }
        }
        return this;
    };
    Builder.prototype.once = function (arg1, fn, listenerToUnbindContainer, useCapture) {
        var _this = this;
        // Event Type Array
        if (types.isArray(arg1)) {
            arg1.forEach(function (type) {
                _this.once(type, fn);
            });
        }
        else {
            var type = arg1;
            // Add Listener
            var unbind_2 = DOM.addDisposableListener(this.currentElement, type, function (e) {
                fn(e, _this, unbind_2); // Pass in Builder as Second Argument
                unbind_2.dispose();
            }, useCapture || false);
            // Add to Array if passed in
            if (listenerToUnbindContainer && types.isArray(listenerToUnbindContainer)) {
                listenerToUnbindContainer.push(unbind_2);
            }
        }
        return this;
    };
    Builder.prototype.attr = function (firstP, secondP) {
        // Apply Object Literal to Attributes of Element
        if (types.isObject(firstP)) {
            for (var prop in firstP) {
                if (firstP.hasOwnProperty(prop)) {
                    var 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;
    };
    Builder.prototype.doSetAttr = function (prop, value) {
        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.
     */
    Builder.prototype.removeAttribute = function (prop) {
        this.currentElement.removeAttribute(prop);
    };
    /**
     *  Sets the id attribute to the value provided for the current HTML element of the builder.
     */
    Builder.prototype.id = function (id) {
        this.currentElement.setAttribute('id', id);
        return this;
    };
    /**
     *  Sets the title attribute to the value provided for the current HTML element of the builder.
     */
    Builder.prototype.title = function (title) {
        this.currentElement.setAttribute('title', title);
        return this;
    };
    /**
     *  Sets the type attribute to the value provided for the current HTML element of the builder.
     */
    Builder.prototype.type = function (type) {
        this.currentElement.setAttribute('type', type);
        return this;
    };
    /**
     *  Sets the value attribute to the value provided for the current HTML element of the builder.
     */
    Builder.prototype.value = function (value) {
        this.currentElement.setAttribute('value', value);
        return this;
    };
    /**
     *  Sets the tabindex attribute to the value provided for the current HTML element of the builder.
     */
    Builder.prototype.tabindex = function (index) {
        this.currentElement.setAttribute('tabindex', index.toString());
        return this;
    };
    Builder.prototype.style = function (firstP, secondP) {
        // Apply Object Literal to Styles of Element
        if (types.isObject(firstP)) {
            for (var prop in firstP) {
                if (firstP.hasOwnProperty(prop)) {
                    var value = firstP[prop];
                    this.doSetStyle(prop, value);
                }
            }
            return this;
        }
        var hasFirstP = types.isString(firstP);
        // Get Style Value
        if (hasFirstP && types.isUndefined(secondP)) {
            return this.currentElement.style[this.cssKeyToJavaScriptProperty(firstP)];
        }
        else if (hasFirstP) {
            this.doSetStyle(firstP, secondP);
        }
        return this;
    };
    Builder.prototype.doSetStyle = function (key, value) {
        if (key.indexOf('-') >= 0) {
            var segments = key.split('-');
            key = segments[0];
            for (var i = 1; i < segments.length; i++) {
                var segment = segments[i];
                key = key + segment.charAt(0).toUpperCase() + segment.substr(1);
            }
        }
        this.currentElement.style[this.cssKeyToJavaScriptProperty(key)] = value;
    };
    Builder.prototype.cssKeyToJavaScriptProperty = function (key) {
        // Automagically convert dashes as they are not allowed when programmatically
        // setting a CSS style property
        if (key.indexOf('-') >= 0) {
            var segments = key.split('-');
            key = segments[0];
            for (var i = 1; i < segments.length; i++) {
                var segment = segments[i];
                key = key + segment.charAt(0).toUpperCase() + segment.substr(1);
            }
        }
        else if (key === 'float') {
            key = 'cssFloat';
        }
        return key;
    };
    /**
     *  Returns the computed CSS style for the current HTML element of the builder.
     */
    Builder.prototype.getComputedStyle = function () {
        return DOM.getComputedStyle(this.currentElement);
    };
    /**
     *  Adds the variable list of arguments as class names to the current HTML element of the builder.
     */
    Builder.prototype.addClass = function () {
        var _this = this;
        var classes = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            classes[_i] = arguments[_i];
        }
        classes.forEach(function (nameValue) {
            var names = nameValue.split(' ');
            names.forEach(function (name) {
                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.
     */
    Builder.prototype.setClass = function (className, shouldAddClass) {
        if (shouldAddClass === void 0) { shouldAddClass = null; }
        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.
     */
    Builder.prototype.hasClass = function (className) {
        return DOM.hasClass(this.currentElement, className);
    };
    /**
     *  Removes the variable list of arguments as class names from the current HTML element of the builder.
     */
    Builder.prototype.removeClass = function () {
        var _this = this;
        var classes = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            classes[_i] = arguments[_i];
        }
        classes.forEach(function (nameValue) {
            var names = nameValue.split(' ');
            names.forEach(function (name) {
                DOM.removeClass(_this.currentElement, name);
            });
        });
        return this;
    };
    /**
     *  Adds or removes the provided className for the current HTML element of the builder.
     */
    Builder.prototype.toggleClass = function (className) {
        if (this.hasClass(className)) {
            this.removeClass(className);
        }
        else {
            this.addClass(className);
        }
        return this;
    };
    /**
     *  Sets the CSS property color.
     */
    Builder.prototype.color = function (color) {
        this.currentElement.style.color = color;
        return this;
    };
    Builder.prototype.padding = function (top, right, bottom, left) {
        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;
    };
    Builder.prototype.margin = function (top, right, bottom, left) {
        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;
    };
    Builder.prototype.position = function (top, right, bottom, left, position) {
        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;
    };
    Builder.prototype.size = function (width, height) {
        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;
    };
    Builder.prototype.minSize = function (width, height) {
        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;
    };
    Builder.prototype.maxSize = function (width, height) {
        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 display.
     */
    Builder.prototype.display = function (display) {
        this.currentElement.style.display = display;
        return this;
    };
    /**
     *  Shows the current element of the builder.
     */
    Builder.prototype.show = function () {
        if (this.hasClass('builder-hidden')) {
            this.removeClass('builder-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.
     */
    Builder.prototype.showDelayed = function (delay) {
        var _this = this;
        // Cancel any pending showDelayed() invocation
        this.cancelVisibilityPromise();
        var promise = TPromise.timeout(delay);
        this.setProperty(VISIBILITY_BINDING_ID, promise);
        promise.done(function () {
            _this.removeProperty(VISIBILITY_BINDING_ID);
            _this.show();
        });
        return this;
    };
    /**
     *  Hides the current element of the builder.
     */
    Builder.prototype.hide = function () {
        if (!this.hasClass('builder-hidden')) {
            this.addClass('builder-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.
     */
    Builder.prototype.isHidden = function () {
        return this.hasClass('builder-hidden') || this.currentElement.style.display === 'none';
    };
    Builder.prototype.cancelVisibilityPromise = function () {
        var promise = this.getProperty(VISIBILITY_BINDING_ID);
        if (promise) {
            promise.cancel();
            this.removeProperty(VISIBILITY_BINDING_ID);
        }
    };
    Builder.prototype.border = function (width, style, color) {
        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;
    };
    Builder.prototype.borderTop = function (width, style, color) {
        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;
    };
    Builder.prototype.borderBottom = function (width, style, color) {
        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;
    };
    Builder.prototype.borderLeft = function (width, style, color) {
        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;
    };
    Builder.prototype.borderRight = function (width, style, color) {
        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;
    };
    Builder.prototype.toPixel = function (obj) {
        if (obj.toString().indexOf('px') === -1) {
            return obj.toString() + 'px';
        }
        return obj;
    };
    /**
     *  Sets the innerHTML attribute.
     */
    Builder.prototype.innerHtml = function (html, append) {
        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.
     */
    Builder.prototype.text = function (text, append) {
        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(document.createTextNode(text));
            }
        }
        else {
            this.currentElement.textContent = text;
        }
        return this;
    };
    /**
     *  Sets the innerHTML attribute in escaped form.
     */
    Builder.prototype.safeInnerHtml = function (html, append) {
        return this.innerHtml(strings.escape(html), append);
    };
    /**
     *  Allows to store arbritary data into the current element.
     */
    Builder.prototype.setProperty = function (key, value) {
        setPropertyOnElement(this.currentElement, key, value);
        return this;
    };
    /**
     *  Allows to get arbritary data from the current element.
     */
    Builder.prototype.getProperty = function (key, fallback) {
        return getPropertyFromElement(this.currentElement, key, fallback);
    };
    /**
     *  Removes a property from the current element that is stored under the given key.
     */
    Builder.prototype.removeProperty = function (key) {
        if (hasData(this.currentElement)) {
            delete data(this.currentElement)[key];
        }
        return this;
    };
    /**
     * Returns a new builder with the child at the given index.
     */
    Builder.prototype.child = function (index) {
        if (index === void 0) { index = 0; }
        var children = this.currentElement.children;
        return withElement(children.item(index));
    };
    /**
     * Recurse through all descendant nodes and remove their data binding.
     */
    Builder.prototype.unbindDescendants = function (current) {
        if (current && current.children) {
            for (var i = 0, length_1 = current.children.length; i < length_1; i++) {
                var element = current.children.item(i);
                // Unbind
                if (hasData(element)) {
                    // Listeners
                    var listeners = 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.
     */
    Builder.prototype.empty = function () {
        this.unbindDescendants(this.currentElement);
        this.clearChildren();
        if (this.offdom) {
            this.createdElements = [];
        }
        return this;
    };
    /**
     *  Removes all HTML elements from the current element of the builder.
     */
    Builder.prototype.clearChildren = function () {
        // 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.
     */
    Builder.prototype.destroy = function () {
        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
                var listeners = 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];
            }
        }
        var type;
        for (type in this.toUnbind) {
            if (this.toUnbind.hasOwnProperty(type) && types.isArray(this.toUnbind[type])) {
                this.toUnbind[type] = dispose(this.toUnbind[type]);
            }
        }
        for (type in this.captureToUnbind) {
            if (this.captureToUnbind.hasOwnProperty(type) && types.isArray(this.captureToUnbind[type])) {
                this.captureToUnbind[type] = dispose(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.
     */
    Builder.prototype.dispose = function () {
        this.destroy();
    };
    /**
     *  Gets the size (in pixels) of an element, including the margin.
     */
    Builder.prototype.getTotalSize = function () {
        var totalWidth = DOM.getTotalWidth(this.currentElement);
        var totalHeight = DOM.getTotalHeight(this.currentElement);
        return new Dimension(totalWidth, totalHeight);
    };
    /**
     *  Another variant of getting the inner dimensions of an element.
     */
    Builder.prototype.getClientArea = function () {
        // 0.) Try with DOM clientWidth / clientHeight
        if (this.currentElement !== document.body) {
            return new Dimension(this.currentElement.clientWidth, this.currentElement.clientHeight);
        }
        // 1.) Try innerWidth / innerHeight
        if (window.innerWidth && window.innerHeight) {
            return new Dimension(window.innerWidth, window.innerHeight);
        }
        // 2.) Try with document.body.clientWidth / document.body.clientHeigh
        if (document.body && document.body.clientWidth && document.body.clientWidth) {
            return new Dimension(document.body.clientWidth, document.body.clientHeight);
        }
        // 3.) Try with document.documentElement.clientWidth / document.documentElement.clientHeight
        if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientHeight) {
            return new Dimension(document.documentElement.clientWidth, document.documentElement.clientHeight);
        }
        throw new Error('Unable to figure out browser width and height');
    };
    return Builder;
}());
export { Builder };
/**
 *  The multi builder provides the same methods as the builder, but allows to call
 *  them on an array of builders.
 */
var MultiBuilder = /** @class */ (function (_super) {
    __extends(MultiBuilder, _super);
    function MultiBuilder(builders) {
        var _this = this;
        assert.ok(types.isArray(builders) || builders instanceof MultiBuilder, 'Expected Array or MultiBuilder as parameter');
        _this = _super.call(this) || this;
        _this.length = 0;
        _this.builders = [];
        // Add Builders to Array
        if (types.isArray(builders)) {
            for (var i = 0; i < builders.length; i++) {
                if (builders[i] instanceof HTMLElement) {
                    _this.push(withElement(builders[i]));
                }
                else {
                    _this.push(builders[i]);
                }
            }
        }
        else {
            for (var i = 0; i < builders.length; i++) {
                _this.push(builders.item(i));
            }
        }
        // Mixin Builder functions to operate on all builders
        var $outer = _this;
        var propertyFn = function (prop) {
            $outer[prop] = function () {
                var args = Array.prototype.slice.call(arguments);
                var returnValues;
                var mergeBuilders = false;
                for (var i = 0; i < $outer.length; i++) {
                    var res = $outer.item(i)[prop].apply($outer.item(i), args);
                    // Merge MultiBuilders into one
                    if (res instanceof MultiBuilder) {
                        if (!returnValues) {
                            returnValues = [];
                        }
                        mergeBuilders = true;
                        for (var j = 0; j < res.length; j++) {
                            returnValues.push(res.item(j));
                        }
                    }
                    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 (var prop in Builder.prototype) {
            if (prop !== 'clone' && prop !== 'and') {
                if (Builder.prototype.hasOwnProperty(prop) && types.isFunction(Builder.prototype[prop])) {
                    propertyFn(prop);
                }
            }
        }
        return _this;
    }
    MultiBuilder.prototype.item = function (i) {
        return this.builders[i];
    };
    MultiBuilder.prototype.push = function () {
        var items = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            items[_i] = arguments[_i];
        }
        for (var i = 0; i < items.length; i++) {
            this.builders.push(items[i]);
        }
        this.length = this.builders.length;
    };
    MultiBuilder.prototype.clone = function () {
        return new MultiBuilder(this);
    };
    return MultiBuilder;
}(Builder));
export { MultiBuilder };
function withBuilder(builder, offdom) {
    if (builder instanceof MultiBuilder) {
        return new MultiBuilder(builder);
    }
    return new Builder(builder.getHTMLElement(), offdom);
}
export function withElement(element, offdom) {
    return new Builder(element, offdom);
}
function offDOM() {
    return new Builder(null, true);
}
// Binding functions
/**
 *  Allows to store arbritary data into element.
 */
export function setPropertyOnElement(element, key, value) {
    data(element)[key] = value;
}
/**
 *  Allows to get arbritary data from element.
 */
export function getPropertyFromElement(element, key, fallback) {
    if (hasData(element)) {
        var value = data(element)[key];
        if (!types.isUndefined(value)) {
            return value;
        }
    }
    return fallback;
}
/**
 *  Adds the provided object as property to the given element. Call getBinding()
 *  to retrieve it again.
 */
export function bindElement(element, object) {
    setPropertyOnElement(element, DATA_BINDING_ID, object);
}
var SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/;
export var $ = function (arg) {
    // 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] === '<') {
            var element = void 0;
            var container = 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);
            }
            var builders = [];
            while (container.firstChild) {
                element = container.firstChild;
                container.removeChild(element);
                builders.push(withElement(element));
            }
            return new MultiBuilder(builders);
        }
        else if (arguments.length === 1) {
            var match = SELECTOR_REGEX.exec(arg);
            if (!match) {
                throw new Error('Bad use of $');
            }
            var tag = match[1] || 'div';
            var id = match[3] || undefined;
            var classes = (match[4] || '').replace(/\./g, ' ');
            var props = {};
            if (id) {
                props['id'] = id;
            }
            if (classes) {
                props['class'] = classes;
            }
            return offDOM().element(tag, props);
        }
        else {
            var result = offDOM();
            result.element.apply(result, arguments);
            return result;
        }
    }
    else {
        throw new Error('Bad use of $');
    }
};
$.Dimension = Dimension;
$.Builder = Builder;
$.Build = Build;
