hardcore.js

/*!
 * Hardcore.js
 * Copyright 2017-2021 Takuro Okada.
 * Released under the MIT License.
 */

"use strict";


////////////////////////////////////// View Controller //////////////////////////////////////

/**
 * View Controller
 * @class
 * @example
 *     // Define subclass of ViewController in ECMAScript 5
 *     var MyViewController = ViewController(function(self) {
 *         self.parent = "body";
 *         self.view = View({dataKey: "key"});
 *     });
 *     var instance = new MyViewController();
 *     instance.data = {key: "value"};
 * @example
 *     // Define subclass of ViewController in ECMAScript 6
 *     class MyViewController extends ViewController {
 *         constructor() {
 *             super();
 *             this.parent = "body";
 *             this.view = View({dataKey: "key"});
 *         }
 *     }
 *     var instance = new MyViewController();
 *     instance.data = {key: "value"};
 */
function ViewController() {
    if(this === undefined) {
        if(arguments.length == 1 && typeof arguments[0] == "function") {
            var initializer = arguments[0];
            var subClass = function() {
                ViewController.apply(this);
                initializer(this);
            };
            subClass.prototype = Object.create(ViewController.prototype);
            return subClass;
        }
        console.error("ViewController is must be create with the new keyword.");
        return;
    }

    /**
     * Whether or not to replace the child elements with the specified View.
     * @type {boolean}
     */
    this.replace = true;

    /**
     * Called when the data loading is complete.
     * @type {function(void): void}
     */
    this.dataLoadedHandler = null;
}

/**
 * Show the view.
 * To change the display method, override it with a subclass. This method must not be called directly.
 * @memberof ViewController
 * @type {function(void): void}
 * @protected
 */
ViewController.prototype.showView = function() {
    this.parent.appendChild(this.view);
};

/**
 * Reload the view.
 * @memberof ViewController
 * @type {function(void): void}
 */
ViewController.prototype.reloadData = function() {
    bindData(this.data, this.view);
}

/**
 * @name ViewController#parent
 * @type {HTMLElement|string}
 */
Object.defineProperty(ViewController.prototype, "parent", {
    get: function() {
        return this._parent;
    },
    set: function(newValue) {
        if(newValue instanceof HTMLElement) {
            this._parent = newValue;
        }else if(typeof newValue == "string") {
            var parentElement = document.querySelector(newValue);
            if(parentElement != null && parentElement instanceof HTMLElement) {
                this._parent = parentElement;
            }else {
                console.error("The parent can not be loaded. selector="+newValue);
                return;
            }
        }else {
            console.error("The parent must be HTMLElement or selector string.");
            return;
        }

        // Add the view to the parent if the view has been set.
        if(this.view != undefined) {
            if(this.replace) {
                this.parent.removeAll();
            }
            this.showView();

            // Bind the data to the view if the data has been set.
            if(this.data != null) {
                this._data = bindData(this.data, this.view);

                if(this.dataLoadedHandler != null) {
                    this.dataLoadedHandler();
                }
            }
        }
    }
});

/**
 * @name ViewController#view
 * @type {HTMLElement} 
 */
Object.defineProperty(ViewController.prototype, "view", {
    get: function() {
        return this._view;
    },
    set: function(newValue) {
        if(!(newValue instanceof HTMLElement)) {
            console.error("The view must be HTMLElement.");
            return;
        }
        this._view = newValue;

        // Add the view to the parent if the parent has been set.
        if(this.parent != undefined) {
            if(this.replace) {
                this.parent.removeAll();
            }
            this.showView();
        }

        // Bind the data to the view if the data has been set.
        if(this.data != null) {
            this._data = bindData(this.data, this.view);

            if(this.dataLoadedHandler != null) {
                this.dataLoadedHandler();
            }
        }
    }
});

/**
 * @name ViewController#data
 * @type {Object|Array}
 */
Object.defineProperty(ViewController.prototype, "data", {
    get: function() {
        return this._data;
    },
    set: function(newValue) {
        if(!(Array.isArray(newValue) || typeof newValue == "object")) {
            console.error("The data must be Object or Array.");
            return;
        }
        this._data = newValue;

        // Bind the data to the view if the view has been set.
        if(this.view != null) {
            this._data = bindData(this.data, this.view);
        }
        
        if(this.dataLoadedHandler != null) {
            this.dataLoadedHandler();
        }
    }
});

/**
 * Popover View Controller
 * @class
 * @extends ViewController
 */
function PopoverViewController() {
    if(this === undefined) {
        if(arguments.length == 1 && typeof arguments[0] == "function") {
            var initializer = arguments[0];
            var subClass = function() {
                PopoverViewController.apply(this);
                initializer(this);
            };
            subClass.prototype = Object.create(PopoverViewController.prototype);
            return subClass;
        }
        console.error("PopoverViewController is must be create with the new keyword.");
        return;
    }

    ViewController.call(this);

    var self = this;

    this.replace = false;

    /**
     * Display layout of Popover.
     * @type {string}
     */
    this.layout = "center";

    /**
     * Whether popover is modal or not.
     * @type {boolean}
     */
    this.modal = false;

    /**
     * Click outside the view to close it.
     * @type {boolean}
     */
    this.dismissOnOutside = true;
    
    /**
     * By changing the style of this view, you can change the way it pops over.
     * @type {HTMLDivElement}
     * @readonly
     */
    this.container = View("._hardcore-popover", {style: {
        "position": "absolute", 
        "border": "1px solid rgba(0,0,0,0.3)", 
        "box-shadow": "3px 3px 16px rgba(0,0,0,0.3)"
    }});

    /**
     * Processing when the view is destroyed.
     * @type {function(void): void}
     */
    this.dismissHandler;

    /**
     * Border color of the popover window
     * @type {string}
     */
    Object.defineProperty(this, "borderColor", {
        configurable: true,
        get: function() {
            return this.container.style.borderColor;
        },
        set: function(newValue) {
            this.container.style.borderColor = newValue;
        }
    });
}
PopoverViewController.prototype = Object.create(ViewController.prototype);

PopoverViewController.prototype.showView = function() {
    if(!this.modal) {
        var maskStyle = {
            "position": "absolute", 
            "width": "100%",
            "height": "100%",
            "top": "0",
            "left": "0"
        }
        var self = this;
        var mask = View({style: maskStyle, tapHandler: function(event) {
            if(self.dismissOnOutside) {
                self.dismiss();
            }
        }});
        this.mask = mask;
        this.parent.appendChild(mask);
    }

    this.container.appendChild(this.view);
    this.parent.appendChild(this.container);

    function lauout(parent, view, layout) {
        var parentWidth = parent.clientWidth;
        var parentHeight = parent.clientHeight;
        var width = view.clientWidth;
        var height = view.clientHeight;
        if(width == undefined) {
            width = view.style.getPropertyValue("width");
            if(typeof width == "string" && width.endsWith("px") && width.length > 2) {
                width = Number(width.substr(0, width.length-2));
            }else {
                width = undefined;
            }
        }
        if(height == undefined) {
            height = view.style.getPropertyValue("height");
            if(typeof height == "string" && height.endsWith("px") && height.length > 2) {
                height = Number(height.substr(0, height.length-2));
            }else {
                height = undefined;
            }
        }
        var x;
        var y;
        if(layout == "center") {
            x = parent.offsetLeft + parentWidth/2 - width/2;
            y = parent.offsetTop + parentHeight/2 - height/2;
        }else if(layout == "topLeft") {
            x = parent.offsetLeft;
            y = parent.offsetTop;
        }else if(layout == "topRight") {
            x = parent.offsetLeft + parentWidth - width;
            y = parent.offsetTop;
        }else if(layout == "bottomLeft") {
            x = parent.offsetLeft;
            y = parent.offsetTop + parentHeight - height;
        }else if(layout == "bottomRight") {
            x = parent.offsetLeft + parentWidth - width;
            y = parent.offsetTop + parentHeight - height;
        }
        if(x != undefined) {
            view.style.setProperty("left", x+"px");
        }
        if(y != undefined) {
            view.style.setProperty("top", y+"px");
        }
    }

    if(ResizeObserver !== undefined) {
        var layout = this.layout;
        if(layout != null) {
            var resizeObserver = new ResizeObserver(function(observations) {
                if(observations.length == 0) return;
                var element = observations[0].target;
                resizeObserver.disconnect();
                lauout(element.parentElement, element, layout);
            });
            resizeObserver.observe(this.container);
        }
    }
    if(this.layout != null) {
        lauout(this.parent, this.container, this.layout);
    }
}

/**
 * Dismiss the view.
 * To change the dismiss method, override it with a subclass. This method must not be called directly.
 * @memberof PopoverViewController
 * @type {function(void): void}
 * @protected
 */
 PopoverViewController.prototype.dismissView = function() {
    if(this.dismissHandler != undefined) {
        this.dismissHandler();
    }
    this.container.remove();
};

/**
 * Dismiss the view.
 * @memberof PopoverViewController
 * @type {function(void): void}
 */
PopoverViewController.prototype.dismiss = function() {
    if(this.mask != undefined) {
        this.mask.remove();
    }
    this.dismissView();
}

/**
 * Slideover View Controller
 * @class
 * @extends ViewController
 */
function SlideoverViewController() {
    if(this === undefined) {
        if(arguments.length == 1 && typeof arguments[0] == "function") {
            var initializer = arguments[0];
            var subClass = function() {
                SlideoverViewController.apply(this);
                initializer(this);
            };
            subClass.prototype = Object.create(SlideoverViewController.prototype);
            return subClass;
        }
        console.error("SlideoverViewController is must be create with the new keyword.");
        return;
    }

    ViewController.call(this);

    var self = this;
    
    self.replace = false;

    /**
     * Whether popover is modal or not.
     * @type {boolean}
     */
    self.modal = false;

    /**
     * Click outside the view to close it.
     * @type {boolean}
     */
     self.dismissOnOutside = true;

    /**
     * Processing when the view is destroyed.
     * @type {function(void): void}
     */
    self.dismissHandler;
    
    /**
     * Text that is displayed in header section.
     * @type {string}
     */
    self.title;

    /**
     * Style of the header section.
     * @type {object}
     */
    self.headerStyle;

    /**
     * By changing the style of this view, you can change the way it pops over.
     * @type {HTMLDivElement}
     * @readonly
     */
    self.container = View("._hardcore-slideover", {style: {
        position: "absolute", 
        border: "1px solid darkgray", 
        "box-shadow": "3px 3px 6px rgba(0,0,0,0.3)",
        height: "100%",
        top: 0
    }}, [
        View(".header", {style: {
            height: 40
        }}, [
            View(".title", {style: {"padding-left":8, "line-height":40}}),
            Canvas(".exitButton", {width:40, height:40, style: {
                position: "absolute",
                top: 0,
                right: 0
            }, drawer: function(context, size) {
                var iconSize = 8;
                context.moveTo(size.width/2-iconSize/2, size.height/2-iconSize/2);
                context.lineTo(size.width/2+iconSize/2, size.height/2+iconSize/2);
                context.moveTo(size.width/2+iconSize/2, size.height/2-iconSize/2);
                context.lineTo(size.width/2-iconSize/2, size.height/2+iconSize/2);
                context.strokeStyle = "white";
                context.lineWidth = 1;
                context.lineCap = "round";
                context.stroke();
            }, tapHandler: function(event) {
                self.dismiss();
            }}),
        ]),
        View(".contents", {style: {
            height: "100%",
            "overflow-y": "auto",
            "-ms-overflow-style": "none",
            "scrollbar-width": "none"
        }})
    ]);

    /**
     * Background color of the sliding window
     * @type {string}
     */
    Object.defineProperty(self, "backgroundColor", {
        get: function() {
            return self.container.querySelector(".contents").style.backgroundColor;
        },
        set: function(newValue) {
            self.container.querySelector(".contents").style.backgroundColor = newValue;
        }
    });
}
SlideoverViewController.prototype = Object.create(ViewController.prototype);

SlideoverViewController.prototype.showView = function() {
    var self = this;

    if(self.modal) {
        var maskStyle = {
            "position": "absolute", 
            "width": "100%",
            "height": "100%",
            "top": "0",
            "left": "0"
        }
        var mask = View({style: maskStyle, tapHandler: function(event) {
            if(self.dismissOnOutside) {
                self.dismiss();
            }
        }});
        this.mask = mask;
        this.parent.appendChild(mask);
    }

    if(this.title != null) {
        var titleView = this.container.querySelector(".title");
        titleView.innerText = this.title;
    }
    if(this.headerStyle != null) {
        var headerView = this.container.querySelector(".header");
        headerView.styles = this.headerStyle;
    }

    this.container.style.top = this.parent.offsetTop+"px";

    var contentsView = this.container.querySelector(".contents");
    contentsView.appendChild(this.view);

    this.container.style.height = this.parent.clientHeight+"px";
    if(this.title != null) {
        contentsView.style.height = (this.parent.clientHeight - 40)+"px";
    }
    this.parent.appendChild(this.container);

    if(ResizeObserver !== undefined) {
        var resizeObserver = new ResizeObserver(function(observations) {
            if(observations.length == 0) return;
            var element = observations[0].target;
            resizeObserver.disconnect();

            element.style.right = -element.clientWidth + "px";
            new StyleAnimation(element, "right", {finishValue: 0, method: FunctionalAnimation.methods.easeOut}).start();
        });
        resizeObserver.observe(this.container);
    }else {
        this.container.style.right = -this.container.clientWidth + "px";
    }
}

/**
 * Dismiss the view.
 * @memberof SlideoverViewController
 * @type {function(void): void}
 */
SlideoverViewController.prototype.dismiss = function() {
    if(this.dismissHandler != undefined) {
        this.dismissHandler();
    }
    if(this.mask != undefined) {
        this.mask.remove();
    }
    var self = this;
    new StyleAnimation(this.container, "right", {finishValue: -this.container.clientWidth, method: FunctionalAnimation.methods.easeOut}).start().finish(function() {
        self.container.remove();
    });
}

/**
 * @ignore
 */
function bindData(data, element) {
    if(data == null) return null;

    function getValue(data, dataKey, dataHandler) {
        var value;
        if(dataKey.includes(".")) {
            var target = data;
            var keyList = dataKey.split(".");
            for(var i=0; i<keyList.length; i++) {
                var key = keyList[i];
                if(target != null) {
                    target = target[key];
                }
            }
            value = target;
        }else {
            value = data[dataKey];
        }
        value = value !== undefined ? value : null;
        if(dataHandler != null) {
            value = dataHandler(value);
        }
        return value;
    }
    function setValue(data, dataKey, value, dataHandler) {
        value = value !== undefined ? value : null;
        if(dataHandler != null) {
            value = dataHandler(value);
        }
        if(dataKey.includes(".")) {
            var target = data;
            var keyList = dataKey.split(".");
            for(var i=0; i<keyList.length-1; i++) {
                var key = keyList[i];
                if(target != null) {
                    target = target[key];
                }
            }
            var lastKey = keyList[keyList.length-1];
            target[lastKey] = value;
        }else {
            data[dataKey] = value;
        }
    }

    var i;

    var inputElements = element.querySelectorAll("input,textarea,select");
    for(i=0; i<inputElements.length; i++) {
        var inputElement = inputElements[i];
        if(inputElement.dataKey != null) {
            inputElement.value = getValue(data, inputElement.dataKey, inputElement.dataHandler);
            // autocomplete support
            inputElement.addEventListener("input", function(event) {
                var inputElement = event.currentTarget;
                setValue(data, inputElement.dataKey, inputElement.value, inputElement.dataHandler);
            });
            // general input completion
            inputElement.addEventListener("blur", function(event) {
                var inputElement = event.currentTarget;
                setValue(data, inputElement.dataKey, inputElement.value, inputElement.dataHandler);
            });
        }
    }

    var dateElements = element.querySelectorAll("._hardcore-date");
    for(i=0; i<dateElements.length; i++) {
        var dateElement = dateElements[i];
        if(dateElement.dataKey != null) {
            dateElement.value = getValue(data, dateElement.dataKey, dateElement.dataHandler);
            dateElement.dataBindHandler = function(value, dataKey) {
                setValue(data, dataKey, value);
            };
        }
    }

    var selectElements = element.querySelectorAll("._hardcore-select");
    for(i=0; i<selectElements.length; i++) {
        var selectElement = selectElements[i];
        if(selectElement.items != null && selectElement.dataKey != null) {
            var items = selectElement.items;
            var dataKey = selectElement.dataKey;
            var valueKey = selectElement.valueKey;
            if(valueKey == null) {
                valueKey = dataKey;
            }
            var selectedValue = getValue(data, dataKey);
            var selectedIndex = -1;
            for(var j=0; j<items.length; j++) {
                var item = items[j];
                if(item[valueKey] == selectedValue) {
                    selectedIndex = j;
                    break;
                }
            }
            selectElement.selectedIndex = selectedIndex;
        }
        if(selectElement.dataKey != null) {
            selectElement.dataBindHandler = function(selectedIndex, _element) {
                if(_element.items != null && selectedIndex < _element.items.length) {
                    var selectedItem = _element.items[selectedIndex];
                    if(selectedItem != null && typeof selectedItem == "object") {
                        setValue(data, _element.dataKey, selectedItem[_element.valueKey]);
                    }
                }
            };
        }
    }

    var sliderElements = element.querySelectorAll("._hardcore-slider");
    for(i=0; i<sliderElements.length; i++) {
        var sliderElement = sliderElements[i];
        if(sliderElement.dataKey != null) {
            sliderElement.value = getValue(data, sliderElement.dataKey);
            sliderElement.dataBindHandler = function(value, dataKey) {
                setValue(data, dataKey, value);
            };
        }
    }

    var checkboxElements = element.querySelectorAll("._hardcore-checkbox");
    for(i=0; i<checkboxElements.length; i++) {
        var checkboxElement = checkboxElements[i];
        if(checkboxElement.dataKey != null) {
            checkboxElement.checked = getValue(data, checkboxElement.dataKey);
            checkboxElement.dataBindHandler = function(value, dataKey) {
                setValue(data, dataKey, value);
            };
        }
    }
    
    var tableElements = element.querySelectorAll("._hardcore-table");
    for(i=0; i<tableElements.length; i++) {
        var tableElement = tableElements[i];
        if(tableElement.dataKey != null) {
            // replace the table biding data with proxy object.
            if(tableElement.dataKey == ".") {
                tableElement.data = data;
                data = tableElement.data;
            }else {
                tableElement.data = getValue(data, tableElement.dataKey);
                setValue(data, tableElement.dataKey, tableElement.data);
            }
        }
    }
    
    var displayElements = element.querySelectorAll("._hardcore-label");
    for(i=0; i<displayElements.length; i++) {
        var displayElement = displayElements[i];
        if(displayElement.dataKey != null) {
            var value = getValue(data, displayElement.dataKey);
            if(displayElement.dataHandler != null) {
                displayElement.dataHandler(displayElement, value);
            }else {
                displayElement.innerText = value;
            }
        }
    }

    return data;
}


////////////////////////////////////// HTML Structures //////////////////////////////////////

/**
 * Create HTML element.
 * @param {string} tagName 
 * @param {string} [identifier] element ID (If CSS selector prefix is not included, register it as CSS class.)
 * @param {Object} [attributes] 
 * @param {Object} [attributes.style] 
 * @param {Object|Array} [attributes.data] 
 * @param {function(UIEvent): void} [attributes.tapHandler] 
 * @param {number} [attributes.width] 
 * @param {number} [attributes.height] 
 * @param {left|center|right} [attributes.contentsAlign] 
 * @param {Array} [children] child elements
 * @returns {HTMLElement}
 */
function HtmlTag() {
    var tagName;
    var elementId;
    var textContent;
    var children;
    var attributes;
    for(var i=0; i<arguments.length; i++) {
        var argument = arguments[i];
        if(typeof argument == "string") {
            if(tagName == undefined) {
                tagName = argument;
            }else if(elementId == undefined) {
                elementId = argument;
            }else if(textContent == undefined) {
                textContent = argument;
            }
        }else if(Array.isArray(argument)) {
            children = argument;
        }else if(typeof argument == "object") {
            attributes = argument;
        }
    }

    var element;
    if(tagName != undefined) {
        element = document.createElement(tagName);
    }
    if(element == undefined) {
        console.error("Invalid definition: ", arguments);
        return null;
    }

    // destyle
    element.style.setProperty("margin", "0");
    if(tagName == "input" || tagName == "textarea" || tagName == "select" || tagName == "button") {
        element.defineStyles({
            "padding": "0",
            "border": "none",
            "outline": "none",
            "-webkit-appearance": "none",
            "appearance": "none",
            "vertical-align": "middle",
            "background-color": "transparent",
            "font-size": "1em"
        });
    }
    if(tagName == "input" || tagName == "button") {
        element.style.setProperty("overflow", "visible");
    }
    if(tagName == "button" || tagName == "select") {
        element.style.setProperty("text-transform", "none");
    }
    if(tagName == "button") {
        element.style.setProperty("cursor", "pointer");
    }
    if(tagName == "table") {
        element.style.setProperty("border-collapse", "collapse");
        element.style.setProperty("border-spacing", "0");
    }
    if(tagName == "td") {
        element.style.setProperty("vertical-align", "top");
        element.style.setProperty("padding", "0");
    }

    if(elementId != undefined) {
        if(elementId.startsWith("#") && elementId.length > 1) {
            element.setAttribute("id", elementId.substring(1));
        }else if(elementId.startsWith(".") && elementId.length > 1) {
            element.setAttribute("class", elementId.substring(1));
        }else {
            element.setAttribute("class", elementId);
        }
    }

    if(textContent != undefined) {
        element.innerText = textContent;
    }

    var data;
    if(attributes != undefined) {
        var keys = Object.keys(attributes);
        for(i=0; i<keys.length; i++) {
            var key = keys[i];
            var value = attributes[key];
            if(key == "style" && typeof value == "object") {
                element.defineStyles(value);
            }else if(key == "width" || key == "height") {
                if(typeof value == "number") {
                    element.style.setProperty(key, value+"px");
                }else if(typeof value == "string") {
                    element.style.setProperty(key, value);
                }
            }else if(key == "margin" || key == "padding") {
                if(typeof value == "number") {
                    element.style.setProperty(key, value+"px");
                }else if(typeof value == "string") {
                    element.style.setProperty(key, value);
                }else if(Array.isArray(value)) {
                    var expression = "";
                    for(var j=0; j<value.length; j++) {
                        if(j > 0) {
                            expression += " ";
                        }
                        if(typeof value == "number") {
                            expression += value+"px";
                        }else if(typeof value == "string") {
                            expression += value;
                        }
                    }
                    element.style.setProperty(key, expression);
                }
            }else if(key == "contentsAlign" && typeof value == "string") {
                element.style.setProperty("text-align", value);
            }else if(key == "data") {
                data = value;
            }else if(key == "tapHandler" && typeof value == "function") {
                element.addEventListener("click", value);
            }else {
                element.setAttribute(key, value);
            }
        }
    }

    if(children != undefined && element.children.length == 0) {
        for(i=0; i<children.length; i++) {
            var child = children[i];
            if(typeof child == "string") {
                element.insertAdjacentHTML("beforeend", child);
            }else {
                element.append(child);
            }
        }
    }

    // data binding
    if(data != null) {
        Object.defineProperty(element, "data", {
            configurable: true,
            get: function() {
                return this.bindingData;
            },
            set: function(newValue) {
                this.bindingData = bindData(newValue, this);
            }
        });
        element.data = data;
    }

    return element;
}

/**
 * Create DIV element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [dataKey] The key for object set in the ViewController or parent View. 
 * @param {function(HTMLDivElement, any): void} [dataHandler] Use this callback if you want to set data directly in a element.
 * @param {HTMLDivElement} dataHandler.element
 * @param {*} dataHandler.value
 * @param {Array} [children] 
 * @returns {HTMLDivElement}
 */
function View() {
    var _arguments = Array.prototype.slice.call(arguments);

    var dataKey;
    var dataHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "dataHandler" && typeof argument[key] == "function") {
                    dataHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    _arguments.splice(0, 0, "div");
    var element = HtmlTag.apply(this, _arguments);

    if(dataKey != undefined) {
        element.dataKey = dataKey;
        element.classList.add("_hardcore-label");
    }
    if(dataHandler != undefined) {
        element.dataHandler = dataHandler;
    }

    return element;
}

/**
 * Create INPUT element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {boolean} [attributes.leaveWithEnter] Element applies the edited contents by ENTER key.
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @returns {HTMLInputElement}
 */
function Input() {
    var _arguments = Array.prototype.slice.call(arguments);

    var leaveWithEnter = true;
    var dataKey;
    var dataHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "leaveWithEnter" && typeof argument[key] == "boolean") {
                    leaveWithEnter = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "dataHandler" && typeof argument[key] == "function") {
                    dataHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    var element;
    if(_arguments.length > 0 && typeof _arguments[0] == "string") {
        var type = _arguments.splice(0, 1, "input")[0];
        element = HtmlTag.apply(this, _arguments);
        element.type = type;
    }else {
        _arguments.splice(0, 0, "input");
        element = HtmlTag.apply(this, _arguments);
    }

    if(leaveWithEnter) {
        element.addEventListener("keypress", function(event) {
            var enterKeyPressed = false;
            if(event.keyCode != undefined) {
                enterKeyPressed = event.keyCode == 13;
            }else if(event.code != undefined) {
                enterKeyPressed = event.keyCode == "Enter";
            }
            if(enterKeyPressed) {
                element.blur();
            }
        });
    }

    if(dataKey != null) {
        element.dataKey = dataKey;
    }

    if(dataHandler != null) {
        element.dataHandler = dataHandler;
    }

    return element;
}

/**
 * Create INPUT type=text element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @param {string} [attributes.label] Labeling by InputContainer
 * @param {function(Event): void} [attributes.changeHandler]
 * @param {function(InputEvent): void} [attributes.inputHandler]
 * @returns {HTMLInputElement|HTMLDivElement}
 */
function TextField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    var changeHandler;
    var inputHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "changeHandler" && typeof argument[key] == "function") {
                    changeHandler = argument[key];
                    delete argument[key];
                }else if(key == "inputHandler" && typeof argument[key] == "function") {
                    inputHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    var element;
    if(label == null) {
        _arguments.splice(0, 0, "text");
        element = Input.apply(this, _arguments);
        if(changeHandler != undefined) {
            element.addEventListener("change", changeHandler);
        }
        if(inputHandler != undefined) {
            element.addEventListener("input", inputHandler);
        }
        return element;
    }else {
        _arguments.splice(0, 0, "text");
        element = Input.apply(this, _arguments);
        if(changeHandler != undefined) {
            element.addEventListener("change", changeHandler);
        }
        if(inputHandler != undefined) {
            element.addEventListener("input", inputHandler);
        }
        var inputComposite = InputComposite({label: label}, [element]);
        return inputComposite;
    }
}

/**
 * Create TEXTAREA element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @param {string} [attributes.label] Labeling by InputContainer
 * @param {string} [attributes.changeHandler]
 * @param {string} [attributes.inputHandler]
 * @returns {HTMLTextAreaElement}
 */
function TextArea() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    var dataKey;
    var changeHandler;
    var inputHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "changeHandler" && typeof argument[key] == "function") {
                    changeHandler = argument[key];
                    delete argument[key];
                }else if(key == "inputHandler" && typeof argument[key] == "function") {
                    inputHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    var element;
    if(label == null) {
        _arguments.splice(0, 0, "textarea");
        element = HtmlTag.apply(this, _arguments);
        if(dataKey != undefined) {
            element.dataKey = dataKey;
        }
        if(changeHandler != undefined) {
            element.addEventListener("change", changeHandler);
        }
        if(inputHandler != undefined) {
            element.addEventListener("input", inputHandler);
        }
        return element;
    }else {
        _arguments.splice(0, 0, "textarea");
        element = HtmlTag.apply(this, _arguments);
        if(dataKey != undefined) {
            element.dataKey = dataKey;
        }
        if(changeHandler != undefined) {
            element.addEventListener("change", changeHandler);
        }
        if(inputHandler != undefined) {
            element.addEventListener("input", inputHandler);
        }
        var inputComposite = InputComposite({label: label}, [element]);
        return inputComposite;
    }
}

/**
 * Create INPUT type=password element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @returns {HTMLInputElement}
 */
function PasswordField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    if(label == null) {
        _arguments.splice(0, 0, "password");
        return Input.apply(this, _arguments);
    }else {
        _arguments.splice(0, 0, "password");
        var element = Input.apply(this, _arguments);
        var inputComposite = InputComposite({label: label}, [element]);
        return inputComposite;
    }
}

/**
 * Create INPUT type=mail element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @returns {HTMLInputElement}
 */
function MailField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    if(label == null) {
        _arguments.splice(0, 0, "email");
        return Input.apply(this, _arguments);
    }else {
        _arguments.splice(0, 0, "email");
        var element = Input.apply(this, _arguments);
        var inputComposite = InputComposite({label: label}, [element]);
        return inputComposite;
    }
}

/**
 * Create input field for a date/time value element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @param {date|time|datetime} [attributes.type] 
 * @param {string} [attributes.format] 
 * @param {string} [attributes.color] 
 * @param {string} [attributes.weekendColor] 
 * @param {string} [attributes.indicatorColor] 
 * @param {boolean} [settings.removable=true] 
 * @param {boolean} [settings.editable=true] 
 * @param {function(Date): void} [settings.editingEndHandler]
 * @param {string} [attributes.placeholder] 
 * @param {number} [attributes.zIndex] 
 * @returns {HTMLInputElement}
 */
function DateField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    var type = "date";
    var format = "yyyy/M/d";
    var dataKey;
    var dataHandler;
    var editingEndHandler;
    var color = "black";
    var weekendColor;
    var indicatorColor = "darkgray";
    var removable = true;
    var editable = true;
    var placeholder;
    var style;
    var placeholderStyle;
    var zIndex = 0;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "type" && typeof argument[key] == "string") {
                    type = argument[key];
                    delete argument[key];
                }else if(key == "format" && typeof argument[key] == "string") {
                    format = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "dataHandler" && typeof argument[key] == "function") {
                    dataHandler = argument[key];
                    delete argument[key];
                }else if(key == "editingEndHandler" && typeof argument[key] == "function") {
                    editingEndHandler = argument[key];
                    delete argument[key];
                }else if(key == "color" && typeof argument[key] == "string") {
                    color = argument[key];
                    delete argument[key];
                }else if(key == "weekendColor" && typeof argument[key] == "string") {
                    weekendColor = argument[key];
                    delete argument[key];
                }else if(key == "indicatorColor" && typeof argument[key] == "string") {
                    indicatorColor = argument[key];
                    delete argument[key];
                }else if(key == "removable" && typeof argument[key] == "boolean") {
                    removable = argument[key];
                    delete argument[key];
                }else if(key == "editable" && typeof argument[key] == "boolean") {
                    editable = argument[key];
                    delete argument[key];
                }else if(key == "style" && typeof argument[key] == "object") {
                    style = argument[key];
                    delete argument[key];
                }else if(key == "placeholderStyle" && typeof argument[key] == "object") {
                    placeholderStyle = argument[key];
                    delete argument[key];
                }else if(key == "placeholder" && typeof argument[key] == "string") {
                    placeholder = argument[key];
                    delete argument[key];
                }else if(key == "zIndex" && typeof argument[key] == "number") {
                    zIndex = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    var element = View.apply(this, _arguments);
    
    element.styles = {
        height:32, 
        "line-height":32,
        cursor: "default",
        display: "inline-block"
    }
    if(style != null) {
        element.styles = style;
    }
    if(placeholderStyle != null) {
        element.styles = placeholderStyle;
    }
    if(placeholder != null) {
        element.innerText = placeholder;
    }

    if(dataKey != null) {
        element.dataKey = dataKey;
        element.classList.add("_hardcore-date");
    }
    if(dataHandler != null) {
        element.dataHandler = function(value) {
            if(value == null) {
                if(placeholderStyle != null) {
                    element.styles = placeholderStyle;
                }
            }else {
                if(style != null) {
                    element.styles = style;
                }
            }
            element.innerText = formatDate(value);
            return dataHandler(value);
        };
    }else {
        element.dataHandler = function(value) {
            if(value == null) {
                if(placeholderStyle != null) {
                    element.styles = placeholderStyle;
                }
            }else {
                if(style != null) {
                    element.styles = style;
                }
            }
            element.innerText = formatDate(value);
            return value;
        };
    }
    
    element.editable = editable;

    Object.defineProperty(element, "value", { 
        get: function() {
            return this._value !== undefined ? this._value : null;
        },
        set: function(newValue) {
            var value;
            if(typeof newValue == "number") {
                value = new Date(newValue);
            }else if(typeof newValue == "string") {
                value = DateUtil.parse(newValue, format);
            }else if(newValue instanceof Date) {
                value = newValue;
            }
            this._value = value;
            element.dataHandler(value);
        }
    });

    function formatDate(value) {
        var expression = "";
        if(value == null) {
            if(placeholder != null) {
                expression = placeholder;
            }
        }else if(typeof value == "number") {
            expression = DateUtil.format(new Date(value), format);
        }else if(value instanceof Date) {
            expression = DateUtil.format(value, format);
        }
        return expression;
    }

    var width = 240;
    var height = 144;

    var daySize = Size(Math.floor(width/7), Math.floor(height/6));
        
    function loadMonth(element, container, year, monthIndex, selectable, now, selectedDate) {
        var header = container.querySelector(".header > .title");
        header.innerText = year + "年" + (monthIndex+1) + "月";
        var days = container.querySelectorAll(".day");
        for(var i=0; i<days.length; i++) {
            days[i].remove();
        }
        var day = new Date(year, monthIndex, 1);
        var weekView;
        while(day.getMonth() == monthIndex) {
            var dayOfWeek = day.getDay();
            if(weekView == null) {
                weekView = View();
                for(var j=0; j<dayOfWeek; j++) {
                    var _dayView = View(".day", {style:{
                        display: "inline-block",
                        "vertical-align": "top",
                        width: daySize.width,
                        height: daySize.height
                    }});
                    weekView.appendChild(_dayView);
                }
                container.appendChild(weekView);
            }else if(dayOfWeek == 0) {
                weekView = View();
                container.appendChild(weekView);
            }
            var contents = [];
            if(now != null && day.getTime() == now.getTime()) {
                var nowView = Canvas({width: daySize.width, height: daySize.height});
                var context = nowView.getContext("2d");
                var center = Point(daySize.width/2, daySize.height/2);
                context.arc(center.x, center.y, Math.min(center.x, center.y), 0, 2*Math.PI, false);
                context.fillStyle = color;
                context.fill();
                context.fillStyle = "white";
                context.textBaseline = "middle";
                context.textAlign = "center";
                context.fillText(day.getDate(), center.x, center.y);
                contents.push(nowView);
            }else {
                contents.push(day.getDate());
            }
            if(selectedDate != null && day.getTime() == selectedDate.getTime()) {
                var indicatorView = createCalendarIndicator();
                contents.push(indicatorView);
            }
            var dayView = View(".day", {style:{
                position: "relative",
                "text-align": "center",
                display: "inline-block",
                "line-height": daySize.height,
                "vertical-align": "top",
                "user-select": "none",
                width: daySize.width,
                height: daySize.height,
                color: ((weekendColor != null && (dayOfWeek == 0 || dayOfWeek == 6)) ? weekendColor : color)
            }}, contents);
            if(selectable) {
                dayView.addEventListener("click", function(event) {
                    var dayElement = event.currentTarget;
                    var date = dayElement.value;
                    if(date != null) {
                        element.value = date;
                        element.innerText = formatDate(date);
                        if(dataKey != null) {
                            element.dataBindHandler(date, dataKey);
                        }

                        var nodeList = container.querySelectorAll(".indicator");
                        nodeList.forEach(function(node) {
                            node.remove();
                        });
                        var indicatorView = createCalendarIndicator();
                        dayElement.appendChild(indicatorView);
                    }
                });
                dayView.value = day;
            }
            weekView.appendChild(dayView);
            day = DateUtil.getDateByAdding(day, 1);
        }
    }

    function createCalendarIndicator() {
        var indicatorView = Canvas(".indicator", {width: daySize.width, height: daySize.height, drawer: function(context, size) {
            var center = Point(size.width/2, size.height/2);
            context.arc(center.x, center.y, Math.min(center.x, center.y), 0, 2*Math.PI, false);
            context.strokeStyle = indicatorColor;
            context.lineWidth = 1;
            context.stroke();
        }});
        indicatorView.styles = {
            position: "absolute",
            "left": "0px",
            "top": "0px",
            "pointer-events": "none"
        };
        return indicatorView;
    }

    function createCalendar(element) {
        var container = View({style:{
            width: width, 
            "overflow-x": "hidden", 
            "cursor": "default", 
            "overscroll-behavior-x": "none"
        }});

        var calendarContainer = View({style:{width: width, "white-space":"nowrap"}});
        container.appendChild(calendarContainer);

        var now = DateUtil.getDateBySlicingTime(new Date());
        var nowYear = now.getFullYear();
        var nowMonthIndex = now.getMonth();

        var selectedDate = element.value;
        if(selectedDate == null) {
            selectedDate = DateUtil.getDateBySlicingTime(new Date());
        }
        var year = selectedDate.getFullYear();
        var monthIndex = selectedDate.getMonth();

        var _year = year;
        var _monthIndex = monthIndex;
        if(_monthIndex > 0) {
            _monthIndex -= 1;
        }else {
            _year -= 1;
            _monthIndex = 11;
        }
        for(var i=0; i<3; i++) {
            var calendar = View(".month", {style: {
                width: width, 
                "display": "inline-block", 
                "vertical-align": "top"
            }}, [
                View(".header", {style:{"text-align": "center"}}, [
                    Canvas({width:32, height:32, style: {"vertical-align": "middle"}, drawer: function(context, size) {
                        var iconSize = Size(4,8);
                        context.beginPath();
                        context.moveTo(size.width/2 + iconSize.width/2, size.height/2 - iconSize.height/2);
                        context.lineTo(size.width/2 - iconSize.width/2, size.height/2);
                        context.lineTo(size.width/2 + iconSize.width/2, size.height/2 + iconSize.height/2);
                        context.strokeStyle = color;
                        context.lineCap = "round";
                        context.lineWidth = 1;
                        context.stroke();
                    }, tapHandler: function() {
                        backwardMonth();
                    }}),
                    View(".title", {width:"calc(100% - 64px)", height:32, style: {
                        display: "inline-block", 
                        "vertical-align": "middle",
                        "line-height": 32,
                        color: color
                    }}),
                    Canvas({width:32, height:32, style: {"vertical-align": "middle"}, drawer: function(context, size) {
                        var iconSize = Size(4,8);
                        context.beginPath();
                        context.moveTo(size.width/2 - iconSize.width/2, size.height/2 - iconSize.height/2);
                        context.lineTo(size.width/2 + iconSize.width/2, size.height/2);
                        context.lineTo(size.width/2 - iconSize.width/2, size.height/2 + iconSize.height/2);
                        context.strokeStyle = color;
                        context.lineCap = "round";
                        context.lineWidth = 1;
                        context.stroke();
                    }, tapHandler: function() {
                        forwardMonth();
                    }})
                ])
            ]);
            calendarContainer.appendChild(calendar);
            
            loadMonth(element, calendar, _year, _monthIndex, i==1, (nowYear == _year && nowMonthIndex == _monthIndex ? now : null), selectedDate);

            if(_monthIndex < 11) {
                _monthIndex += 1;
            }else {
                _year += 1;
                _monthIndex = 0;
            }
        }

        function reloadCalendars(element) {
            var _year = year;
            var _monthIndex = monthIndex;
            if(_monthIndex > 0) {
                _monthIndex -= 1;
            }else {
                _year -= 1;
                _monthIndex = 11;
            }
            
            var selectedDate = element.value;

            var nodeList = calendarContainer.querySelectorAll(".indicator");
            nodeList.forEach(function(node) {
                node.remove();
            });

            var calendars = calendarContainer.querySelectorAll(".month");
            for(var i=0; i<3; i++) {
                loadMonth(element, calendars[i], _year, _monthIndex, i==1, (nowYear == _year && nowMonthIndex == _monthIndex ? now : null), selectedDate);

                if(_monthIndex < 11) {
                    _monthIndex += 1;
                }else {
                    _year += 1;
                    _monthIndex = 0;
                }
            }
        }

        function backwardMonth() {
            container.scrolling = true;
            new FunctionalAnimation(function(progress) {
                container.scrollLeft = (1-progress)*width;
            }, FunctionalAnimation.methods.easeOut, 500).start().finish(function() {
                if(monthIndex > 0) {
                    monthIndex -= 1;
                }else {
                    year -= 1;
                    monthIndex = 11;
                }
                reloadCalendars(element);
                container.scrollLeft = width;

                container.scrolling = false;
            });
        }

        function forwardMonth() {
            container.scrolling = true;
            new FunctionalAnimation(function(progress) {
                container.scrollLeft = width+progress*width;
            }, FunctionalAnimation.methods.easeOut, 500).start().finish(function() {
                if(monthIndex < 11) {
                    monthIndex += 1;
                }else {
                    year += 1;
                    monthIndex = 0;
                }
                reloadCalendars(element);
                container.scrollLeft = width;

                container.scrolling = false;
            });
        }

        UIEventUtil.handleTouch(container, {
            touchBegan: function(event, context) {
                context.beginLocation = UIEventUtil.getLocation(event);
            }, 
            touchMove: function(event, context) {
                if(context == null || context.beginLocation == null || container.scrolling) {
                    return;
                }
                var currentLocation = UIEventUtil.getLocation(event);
                if(currentLocation.x - context.beginLocation.x > 40) {
                    backwardMonth();
                    return false;
                }else if(currentLocation.x - context.beginLocation.x < -40) {
                    forwardMonth();
                    return false;
                }
            }
        });
        
        return container;
    }

    function createTimePicker(element) {
        var container = View({style: {"overflow": "hidden"}});

        var indicator = Canvas({width: daySize.width*2, height: daySize.height, style:{
            position: "absolute",
            "pointer-events": "none"
        }});
        var context = indicator.getContext("2d");
        DrawUtil.drawRoundRect(context, Rect(5,1,daySize.width*2-6,daySize.height-2), indicatorColor, 4, true);
        container.appendChild(indicator);

        var hourView = View({style: {
            "display": "inline-block", 
            "overflow": "hidden",
            width: daySize.width,
            height: height,
            "vertical-align": "top"
        }});
        var minutesView = View({style: {
            "display": "inline-block", 
            "overflow": "hidden",
            width: daySize.width,
            height: height,
            "vertical-align": "top"
        }});
        for(var hour=0; hour<24; hour++) {
            hourView.appendChild(View({style: {
                "text-align": "right",
                "line-height": daySize.height,
                "user-select": "none",
                color: color,
                padding: [0,4]
            }}, [hour]));
        }
        for(hour=0; hour<5; hour++) {
            hourView.appendChild(View({style: {
                height: daySize.height
            }}));
        }
        for(var minute=0; minute<59; minute++) {
            minutesView.appendChild(View({style: {
                "text-align": "right",
                "line-height": daySize.height,
                "user-select": "none",
                color: color,
                padding: [0,4]
            }}, [minute]));
        }
        for(minute=0; minute<5; minute++) {
            minutesView.appendChild(View({style: {
                height: daySize.height
            }}));
        }
        container.appendChild(hourView);
        container.appendChild(minutesView);

        setupTouch(hourView, function(updatedValue) {
            setValue(element, updatedValue);
        });
        setupTouch(minutesView, function(updatedValue) {
            setValue(element, null, updatedValue);
        });

        var selectedDate = element.value;
        if(element.value == null) {
            selectedDate = new Date();
        }
        var hours = selectedDate.getHours();
        if(ResizeObserver !== undefined) {
            var resizeObserver = new ResizeObserver(function(observations) {
                if(observations.length == 0) return;
                var hourView = observations[0].target;
                resizeObserver.disconnect();
                hourView.scrollTop = daySize.height * hours;
            });
            resizeObserver.observe(hourView);
        }else {
            setTimeout(function() {
                hourView.scrollTop = daySize.height * hours;
            },0);
        }

        function setupTouch(container, updateHandler) {
            UIEventUtil.handleTouch(container, {
                touchBegan: function(event, context) {
                    context.beginLocation = UIEventUtil.getLocation(event);
                    context.beginOffsetTop = container.scrollTop;
                }, 
                touchMove: function(event, context) {
                    if(context == null || context.beginLocation == null || context.beginOffsetTop == null || container.scrolling) {
                        return;
                    }
                    var currentLocation = UIEventUtil.getLocation(event);
                    var distance = context.beginLocation.y - currentLocation.y;
                    container.scrollTop = context.beginOffsetTop + distance;
                },
                touchEnd: function(event, context) {
                    if(context == null || context.beginLocation == null || context.beginOffsetTop == null || container.scrolling) {
                        return;
                    }
                    var y = container.scrollTop;
                    var distance = y % daySize.height;
                    if(distance > 0) {
                        container.scrolling = true;
                        new FunctionalAnimation(function(progress) {
                            container.scrollTop = y-distance*progress;
                        }, FunctionalAnimation.methods.easeOut, 500).start().finish(function() {
                            updateHandler(container.scrollTop / daySize.height);
                            container.scrolling = false;
                        });
                    }else {
                        updateHandler(container.scrollTop / daySize.height);
                    }
                }
            });
        }

        function setValue(element, hours, minutes) {
            var date = element.value;
            if(date != null) {
                if(hours != null) {
                    date.setHours(hours);
                }
                if(minutes != null) {
                    date.setMinutes(minutes);
                }
                element.value = date;
                element.innerText = formatDate(date);
                if(dataKey != null) {
                    element.dataBindHandler(date, dataKey);
                }
            }
        }

        return container;
    }

    element.addEventListener("click", function(event) {
        var element = event.currentTarget;
        if(!element.editable) return;
        var content;
        if(type == "date") {
            content = createCalendar(element);
        }else if(type == "time") {
            content = createTimePicker(element);
        }else {
            content = View({style: {"white-space":"nowrap"}});
            var calendar = createCalendar(element);
            calendar.style.setProperty("display", "inline-block");
            calendar.style.setProperty("vertical-align", "top");
            content.appendChild(calendar);
            var timePicker = createTimePicker(element);
            timePicker.style.setProperty("display", "inline-block");
            timePicker.style.setProperty("vertical-align", "top");
            timePicker.style.setProperty("width", (daySize.width*2)+"px");
            timePicker.style.setProperty("height", height+"px");
            timePicker.style.setProperty("border-left", "1px solid darkgray");
            content.appendChild(timePicker);
        }
        var offset = element.offset();
        var scrollOffset = HtmlElementUtil.scrollOffset(element);
        Controls.Balloon(content, {
            location: Point(offset.left, offset.top+element.clientHeight-scrollOffset.y), 
            direction: "top", 
            shadow: true, 
            zIndex: zIndex,
            loadHandler: function(balloon, settings) {
                var location = Point(
                    offset.left+element.clientWidth/2-balloon.offsetWidth/2, 
                    (settings.direction == "top" ? offset.top+element.clientHeight-scrollOffset.y : offset.top-balloon.offsetHeight-scrollOffset.y)
                );
                var tipOffset = balloon.offsetWidth/2;
                if(location.x < 0) {
                    tipOffset = -location.x;
                    location.x = 0;
                }
                if(settings.direction == "top" && location.y+balloon.offsetHeight > window.innerHeight) {
                    settings.direction = "bottom";
                    location.y = offset.top-balloon.offsetHeight-scrollOffset.y;
                }
                settings.tipOffset = tipOffset;
                settings.location = location;
                if(type == "date") {
                    content.scrollLeft = width;
                }else if(type == "datetime") {
                    calendar.scrollLeft = width;
                }
                return settings;
            },
            dismissHandler: function() {
                if(editingEndHandler != null) {
                    editingEndHandler(element.value);
                }
            }
        });
    });

    element.addEventListener("keydown", function(event) {
        if(event.code == "Space") {
            this.dispatchEvent(new MouseEvent("click"));
            event.preventDefault();
        }
    });

    if(label == null) {
        return element;
    }else {
        var inputComposite = InputComposite({label: label}, [element]);

        if(removable) {
            element.style.width = "calc(100% - 32px)";
            element.style.verticalAlign = "middle";
            var removeButton = Canvas({width:32, height:32, drawer: function(context, size) {
                var iconSize = 8;
                context.beginPath();
                context.moveTo(size.width/2 - iconSize/2, size.height/2 - iconSize/2);
                context.lineTo(size.width/2 + iconSize/2, size.height/2 + iconSize/2);
                context.moveTo(size.width/2 + iconSize/2, size.height/2 - iconSize/2);
                context.lineTo(size.width/2 - iconSize/2, size.height/2 + iconSize/2);
                context.lineWidth = 1;
                context.lineCap = "round";
                context.strokeStyle = "darkgray";
                context.stroke();
            }});
            removeButton.styles = {
                position: "relative",
                "vertical-align": "middle"
            };
            removeButton.addEventListener("click", function() {
                element.value = null;
                element.innerText = "";
                if(dataKey != null) {
                    element.dataBindHandler(null, dataKey);
                }
            });
            inputComposite.appendChild(removeButton);
        }

        return inputComposite;
    }
}

/**
 * Create INPUT type=time element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.dataKey] The key for object set in the ViewController or parent View. 
 * @returns {HTMLInputElement}
 */
function TimeField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    if(label == null) {
        _arguments.splice(0, 0, "time");
        return Input.apply(this, _arguments);
    }else {
        _arguments.splice(0, 0, "time");
        var element = Input.apply(this, _arguments);
        var inputComposite = InputComposite({label: label}, [element]);
        return inputComposite;
    }
}

/**
 * Create INPUT type=file element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @returns {HTMLInputElement}
 */
function FileSelector() {
    var _arguments = Array.prototype.slice.call(arguments);

    var changeHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "changeHandler" && typeof argument[key] == "function") {
                    changeHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    _arguments.splice(0, 0, "file");
    var element = Input.apply(this, _arguments);

    if(changeHandler != null) {
        element.addEventListener("change", changeHandler);
    }

    return element;
}

/**
 * @typedef {Object} TableColumnDefinition
 * @property {string} [label] Label of table header
 * @property {number} [width] Column width
 * @property {Object} [style] Column styles
 * @property {Object} [class] Column CSS class
 * @property {string} [dataKey] The key for each record of the Array object set in the Table.
 * @property {function(HTMLTableCellElement, any, any): void} [dataHandler] Use this callback if you want to set data directly in a cell.
 * @param {HTMLTableCellElement} dataHandler.cell
 * @param {*} dataHandler.value
 * @param {*} dataHandler.record
 */

/**
 * Create TABLE element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array.<TableColumnDefinition>} [attributes.data] 
 * @param {Array.<TableColumnDefinition>} [attributes.dataKey] The key for object set in the ViewController or parent View. "." points to the root of the data set in the ViewController or parent View.
 * @param {Array.<TableColumnDefinition>} [attributes.columns] 
 * @param {function(any, number, HTMLTableRowElement): void} [attributes.tapHandler] Select a row handler. The first argument is the selected row data.
 * @param {number} [attributes.rowHeight]
 * @param {boolean} [attributes.animate=false]
 * @param {Object} [attributes.sort]
 * @param {Object} [attributes.sort.defaultHeaderCellStyle]
 * @param {Object} [attributes.sort.upperSortHeaderCellStyle]
 * @param {Object} [attributes.sort.lowerSortHeaderCellStyle]
 * @param {string} [attributes.sort.defaultSortDataKey]
 * @param {Object} [attributes.headerStyle]
 * @param {boolean} [attributes.rowBorder=true]
 * @param {string} [attributes.rowBorderStyle]
 * @param {boolean} [attributes.rowHighlight=true]
 * @param {string} [attributes.rowHighlightStyle]
 * @param {function(HTMLTableRowElement, any)} [attributes.rowHandler]
 * @param {Array} [children] 
 * @returns {HTMLTableElement}
 */
function Table() {
    var _arguments = Array.prototype.slice.call(arguments);

    var data;
    var dataKey;
    var columns;
    var rowHeight;
    var tapHandler;
    var animate = false;
    var sort;
    var headerStyle = {
        "color": "gray",
        "font-size": "small",
        "height": "20px",
        "line-height": "20px",
        "user-select": "none"
    };
    var rowBorderStyle = "1px solid darkgray";
    var rowHighlightStyle = "whitesmoke";
    var rowHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "data" && Array.isArray(argument[key])) {
                    data = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "columns" && Array.isArray(argument[key])) {
                    columns = argument[key];
                    delete argument[key];
                }else if(key == "rowHeight" && typeof argument[key] == "number") {
                    rowHeight = argument[key];
                    delete argument[key];
                }else if(key == "tapHandler" && typeof argument[key] == "function") {
                    tapHandler = argument[key];
                    delete argument[key];
                }else if(key == "animate" && typeof argument[key] == "boolean") {
                    animate = argument[key];
                    delete argument[key];
                }else if(key == "sort" && typeof argument[key] == "object") {
                    sort = argument[key];
                    delete argument[key];
                }else if(key == "headerStyle" && typeof argument[key] == "object") {
                    headerStyle = argument[key];
                    delete argument[key];
                }else if(key == "rowBorder" && typeof argument[key] == "boolean") {
                    if(!argument[key]) {
                        rowBorderStyle = undefined;
                    }
                    delete argument[key];
                }else if(key == "rowBorderStyle" && typeof argument[key] == "string") {
                    rowBorderStyle = argument[key];
                    delete argument[key];
                }else if(key == "rowHighlight" && typeof argument[key] == "boolean") {
                    if(!argument[key]) {
                        rowHighlightStyle = undefined;
                    }
                    delete argument[key];
                }else if(key == "rowHighlightStyle" && typeof argument[key] == "string") {
                    rowHighlightStyle = argument[key];
                    delete argument[key];
                }else if(key == "rowHandler" && typeof argument[key] == "function") {
                    rowHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    _arguments.splice(0, 0, "table");
    var element = HtmlTag.apply(this, _arguments);
    element.style.setProperty("display", "block");

    if(dataKey != null) {
        element.dataKey = dataKey;
        element.classList.add("_hardcore-table");
    }

    element.animate = animate;

    // header
    if(columns != undefined) {
        var header = TableHeader();
        var row = TableRow();
        row.style.setProperty("cursor", "default");
        if(headerStyle != undefined) {
            row.defineStyles(headerStyle);
        }
        if(rowBorderStyle != undefined) {
            row.style.setProperty("border-bottom", rowBorderStyle);
        }
        for(i=0; i<columns.length; i++) {
            var column = columns[i];
            var cell = TableCell();
            if(column.label != undefined) {
                cell.innerText = column.label;
            }
            if(column.width != undefined) {
                if(column.style != undefined) {
                    column.style["width"] = column.width+"px";
                }
            }
            if(column.style != undefined) {
                cell.defineStyles(column.style);
            }
            if(sort != null) {
                setupSorting(cell, sort);
            }
            row.appendChild(cell);
        }
        header.append(row);
        element.append(header);
    }

    function setupSorting(cell, sort) {
        if(sort.defaultHeaderCellStyle != null) {
            cell.styles = sort.defaultHeaderCellStyle;
        }
        cell.addEventListener("click", function(event) {
            var cell = event.currentTarget;
            if(cell.classList.contains("_sort_u")) {
                cell.classList.remove("_sort_u");
            }else if(cell.classList.contains("_sort_l")) {
                cell.classList.remove("_sort_l");
                cell.classList.add("_sort_u");
            }else {
                cell.classList.add("_sort_l");
            }
            var cells = row.querySelectorAll("td");
            var cellIndex = cells.indexOf(cell);
            for(var i=0; i<cells.length; i++) {
                var _cell = cells[i];
                if(i == cellIndex) continue;
                _cell.classList.remove("_sort_u");
                _cell.classList.remove("_sort_l");
            }
            var dataKey = columns[cellIndex]["dataKey"];
            var upperSort = false;
            if(cell.classList.contains("_sort_u")) {
                upperSort = true;
                if(sort.upperSortHeaderCellStyle != null) {
                    cell.styles = sort.upperSortHeaderCellStyle;
                }
            }else if(cell.classList.contains("_sort_l")) {
                upperSort = false;
                if(sort.lowerSortHeaderCellStyle != null) {
                    cell.styles = sort.lowerSortHeaderCellStyle;
                }
            }else {
                dataKey = sort.defaultSortDataKey;
                upperSort = false;
                if(sort.defaultHeaderCellStyle != null) {
                    cell.styles = sort.defaultHeaderCellStyle;
                }
            }
            if(dataKey == null) return;
            var data = element.data;
            if(data == null) return;
            data.sort(function(record1, record2) {
                var value1 = record1[dataKey];
                var value2 = record2[dataKey];
                if(value1 != null) {
                    if(value2 != null) {
                        if(upperSort) {
                            return value1 < value2 ? -1 : (value1 > value2 ? 1 : 0);
                        }else {
                            return value1 < value2 ? 1 : (value1 > value2 ? -1 : 0);
                        }
                    }else {
                        if(upperSort) {
                            return 1;
                        }else {
                            return -1;
                        }
                    }
                }else {
                    if(value2 != null) {
                        if(upperSort) {
                            return -1;
                        }else {
                            return 1;
                        }
                    }else {
                        return 0;
                    }
                }
            });
            element.data = data;
        });
    }

    // data binding
    Object.defineProperty(element, "data", { 
        get: function() {
            return this.bindingData;
        },
        set: function(newValue) {
            var element = this;
            function createRow(record, columns, tapHandler, animate) {
                var row = TableRow();
                row.style.setProperty("cursor", "pointer");
                if(rowHeight != undefined) {
                    row.style.setProperty("height", rowHeight+"px");
                    row.style.setProperty("line-height", rowHeight+"px");
                }
                if(rowBorderStyle != undefined) {
                    row.style.setProperty("border-bottom", rowBorderStyle);
                }
                if(rowHighlightStyle != undefined) {
                    row.addEventListener("mouseenter", function(event) {
                        var row = event.currentTarget;
                        row.originalBackgroundColor = row.style.getPropertyValue("background-color");
                        row.style.setProperty("background-color", rowHighlightStyle);
                    });
                    row.addEventListener("mouseleave", function(event) {
                        var row = event.currentTarget;
                        if(row.originalBackgroundColor != null) {
                            row.style.setProperty("background-color", row.originalBackgroundColor);
                            delete row.originalBackgroundColor;
                        }else {
                            row.style.setProperty("background-color", "inherit");
                        }
                    });
                }
                if(rowHandler != null) {
                    rowHandler(row, record);
                }
                function setValue(value, cell) {
                    if(value == null) {
                        // Safari bug fix
                        cell.innerHTML = "&nbsp;";
                    }else if(typeof value == "string") {
                        if(value.length == 0) {
                            // Safari bug fix
                            cell.innerHTML = "&nbsp;";
                        }else {
                            cell.innerText = value;
                        }
                    }else {
                        cell.innerText = value.toString();
                    }
                }
                if(columns != undefined) {
                    for(var i=0; i<columns.length; i++) {
                        var column = columns[i];
                        var cell = TableCell();
                        if(column.class != undefined) {
                            cell.setAttribute("class", column.class);
                        }
                        if(column.style != undefined) {
                            cell.defineStyles(column.style);
                        }
                        if(column.dataKey != null) {
                            var value = record[column.dataKey];
                            value = value !== undefined ? value : null;
                            if(column.dataHandler != null) {
                                column.dataHandler(cell, value, record);
                            }else {
                                setValue(value, cell);
                            }
                        }
                        row.appendChild(cell);
                    }
                }
                if(tapHandler != undefined) {
                    row.addEventListener("click", function(event) {
                        var row = event.currentTarget;
                        var tableBody = element.querySelector("tbody");
                        var index = tableBody.querySelectorAll("tr").indexOf(row);
                        tapHandler(data[index], index, row);
                    });
                }
                if(animate) {
                    row.style.setProperty("opacity", "0");
                    row.style.setProperty("margin-top", "16px");
                }
                return row;
            }

            var i;

            var observedArray;
            if(Array.isArray(newValue)) {
                observedArray = new ObservedArray(element, createRow, columns, tapHandler);
                for(i=0; i<newValue.length; i++) {
                    observedArray.push(newValue[i]);
                }
            }else if(newValue instanceof ObservedArray) {
                observedArray = newValue;
            }

            element.bindingData = observedArray;

            var tableHeader = element.querySelector("thead");
            var tableBody = element.querySelector("tbody");
            if(tableBody != null) {
                tableBody.remove();
            }
            if(element.bindingData != null) {
                var data = element.bindingData;
                tableBody = TableBody();
                if(ResizeObserver !== undefined) {
                    var resizeObserver = new ResizeObserver(function(observations) {
                        if(observations.length == 0) return;
                        var element = observations[0].target;
                        resizeObserver.disconnect();
                        tableBody.style.setProperty("height", (element.clientHeight-tableHeader.clientHeight)+"px");
                    });
                    resizeObserver.observe(element);
                }
                tableBody.style.setProperty("height", (this.clientHeight-tableHeader.clientHeight)+"px");
                for(i=0; i<data.length; i++) {
                    var record = data[i];
                    tableBody.appendChild(createRow(record, columns, tapHandler, element.animate));
                }
                element.appendChild(tableBody);

                var showRow;
                if(element.animate) {
                    showRow = function(rows, index) {
                        var row = rows[index];
                        new FunctionalAnimation(function(progress) {
                            row.style.setProperty("opacity", progress);
                            row.style.setProperty("margin-top", (16-16*progress)+"px");
                        }, FunctionalAnimation.methods.linear, 100).start(100*index);
                    }
                    var rows = tableBody.querySelectorAll("tr");
                    for(i=0; i<rows.length; i++) {
                        showRow(rows, i);
                    }
                }
            }
        }
    });
    if(data != null) {
        element.data = data;
    }

    return element;
}

function ObservedArray(element, createRow, columns, tapHandler) {
    this.element = element;
    this.createRow = createRow;
    this.columns = columns;
    this.tapHandler = tapHandler;
}
ObservedArray.prototype = Object.create(Array.prototype);
ObservedArray.prototype.push = function(record) {
    Array.prototype.push.call(this, record);
    var container = this.element.querySelector("tbody");
    if(container == null) return;
    container.appendChild(this.createRow(record, this.columns, this.tapHandler, false));
};
ObservedArray.prototype.splice = function() {
    Array.prototype.splice.apply(this, arguments);
    var container = this.element.querySelector("tbody");
    if(container == null) return;
    var startIndex;
    var endIndex;
    var deleteCount;
    var records = [];
    var i;
    for(i=0; i<arguments.length; i++) {
        if(i == 0) {
            startIndex = arguments[i];
        }else if(i == 1) {
            deleteCount = arguments[i];
        }else {
            records.push(arguments[i]);
        }
    }
    if(startIndex == undefined) {
        return;
    }
    var rows = this.element.querySelectorAll("tbody > tr");
    if(deleteCount == undefined) {
        deleteCount = rows.length;
    }
    if(deleteCount > 0) {
        endIndex = startIndex+deleteCount;
        if(endIndex <= rows.length) {
            for(i=endIndex-1; i>=startIndex; i--) {
                rows[i].remove();
            }
        }
    }
    rows = this.element.querySelectorAll("tbody > tr");
    if(records != undefined && records.length > 0 && deleteCount == records.length) {
        endIndex = startIndex+deleteCount;
        var recordIndex = 0;
        for(i=startIndex; i<endIndex; i++) {
            var row = this.createRow(records[recordIndex++], this.columns, this.tapHandler, false);
            if(i < rows.length) {
                rows[i].insertAdjacentElement("beforebegin", row);
            }else {
                container.appendChild(row);
            }
        }
    }
};

/**
 * Create THEAD element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLTableSectionElement}
 */
function TableHeader() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "thead");
    var element = HtmlTag.apply(this, _arguments);
    element.style.setProperty("display", "block");
    return element;
}

/**
 * Create TBODY element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLTableSectionElement}
 */
function TableBody() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "tbody");
    var element = HtmlTag.apply(this, _arguments);
    element.style.setProperty("display", "block");
    element.style.setProperty("overflow-y", "auto");
    return element;
}

/**
 * Create TR element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLTableRowElement}
 */
function TableRow() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "tr");
    var element = HtmlTag.apply(this, _arguments);
    element.style.setProperty("display", "table");
    element.style.setProperty("width", "100%");
    return element;
}

/**
 * Create TD element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLTableDataCellElement}
 */
function TableCell() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "td");
    return HtmlTag.apply(this, _arguments);
}

/**
 * Create UL element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLUListElement}
 */
function UnorderedList() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "ul");
    return HtmlTag.apply(this, _arguments);
}

/**
 * Create OL element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLOListElement}
 */
function OrderedList() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "ol");
    return HtmlTag.apply(this, _arguments);
}

/**
 * Create LI element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLLIElement}
 */
function ListItem() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "li");
    return HtmlTag.apply(this, _arguments);
}

/**
 * Create IMG element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLImageElement}
 */
function InlineImage() {
    var _arguments = Array.prototype.slice.call(arguments);
    _arguments.splice(0, 0, "img");
    return HtmlTag.apply(this, _arguments);
}

/**
 * Create CANVAS element.
 * If width and height have been set, Canvas size will be initialized to match the screen resolution.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {number} [attributes.width] 
 * @param {number} [attributes.height] 
 * @param {function(CanvasRenderingContext2D, Size)} [attributes.drawer] 
 * @param {Array} [children] 
 * @returns {HTMLCanvasElement}
 */
function Canvas() {
    var _arguments = Array.prototype.slice.call(arguments);

    var width;
    var height;
    var drawer;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "width" && typeof argument[key] == "number") {
                    width = argument[key];
                    delete argument[key];
                }else if(key == "height" && typeof argument[key] == "number") {
                    height = argument[key];
                    delete argument[key];
                }else if(key == "drawer" && typeof argument[key] == "function") {
                    drawer = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    _arguments.splice(0, 0, "canvas");
    var element = HtmlTag.apply(this, _arguments);

    if(width != undefined && height != undefined) {
        CanvasUtil.initCanvas(element, Size(width, height));
    }

    if(drawer != null) {
        if(ResizeObserver !== undefined) {
            var resizeObserver = new ResizeObserver(function(observations) {
                drawer(element.getContext("2d"), Size(element.clientWidth, element.clientHeight));
            });
            resizeObserver.observe(element);
        }
    }

    return element;
}

/**
 * Create BUTTON element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [lattributes.label] 
 * @param {boolean} [attributes.blocking=true] Whether or not to block subsequent taps after a tap.
 * @param {function(HTMLButtonElement): void} [attributes.tapHandler] When the restore method in the function argument is executed, It makes itself tapable again.
 * @param {Array} [children] 
 * @returns {HTMLButtonElement}
 */
function Button() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label;
    var tapHandler;
    var blocking = false;
    var style;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(Array.isArray(argument)) {
            label = null;
        }else if(typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "tapHandler" && typeof argument[key] == "function") {
                    tapHandler = argument[key];
                    delete argument[key];
                }else if(key == "blocking" && typeof argument[key] == "boolean") {
                    blocking = argument[key];
                    delete argument[key];
                }else if(key == "style" && typeof argument[key] == "object") {
                    style = argument[key];
                }
            }
            break;
        }
    }
    if(label != null) {
        _arguments.push([label]);
    }
    if(style == undefined) {
        _arguments.push({style:{"margin": "8px"}});
    }

    _arguments.splice(0, 0, "button");
    var element = HtmlTag.apply(this, _arguments);

    if(blocking) {
        element.restore = function() {
            this.classList.remove("_disabled");
        };
        element.classList.remove("_disabled");
        element.addEventListener("click", function(event) {
            if(element.classList.contains("_disabled")) {
                event.stopImmediatePropagation();
                return false;
            }
            element.classList.add("_disabled");
            if(tapHandler != undefined) {
                tapHandler(element);
            }
        });
    }else {
        element.addEventListener("click", function(event) {
            if(tapHandler != undefined) {
                tapHandler(element);
            }
        });
    }

    return element;
}

/**
 * Create label and INPUT composite. 
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.label] 
 * @param {Array} [children] define any INPUT elements.
 * @returns {HTMLDivElement}
 */
function InputComposite() {
    var _arguments = Array.prototype.slice.call(arguments);

    var identifierIndex = -1;
    var label;
    var style;
    var children;
    var borderColor = "darkgray";
    var labelColor = "darkgray";
    var labelFontSize = "10px";
    var unitColor = "darkgray";
    var unitFontSize = "10px";
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(identifierIndex == -1 && typeof argument == "string") {
            identifierIndex = i;
        }else if(Array.isArray(argument)) {
            children = argument;
        }else if(typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "style") {
                    style = argument[key];
                    delete argument[key];
                }else if(key == "borderColor") {
                    borderColor = argument[key];
                    delete argument[key];
                }else if(key == "labelColor") {
                    labelColor = argument[key];
                    delete argument[key];
                }else if(key == "labelFontSize") {
                    labelFontSize = argument[key];
                    if(typeof labelFontSize == "number") {
                        labelFontSize = labelFontSize + "px";
                    }
                    delete argument[key];
                }else if(key == "unitColor") {
                    unitColor = argument[key];
                    delete argument[key];
                }else if(key == "unitFontSize") {
                    unitFontSize = argument[key];
                    if(typeof unitFontSize == "number") {
                        unitFontSize = unitFontSize + "px";
                    }
                    delete argument[key];
                }
            }
        }
    }
    if(label != null) {
        if(children != undefined) {
            children.splice(0, 0, View("label", label));
        }else {
            _arguments.push([View("label", label)]);
        }
    }

    var element = View.apply(this, _arguments);

    function setProperty(element, key, value, selector) {
        if(selector != null) {
            var children = element.querySelectorAll(selector);
            for(var i=0; i<children.length; i++) {
                setProperty(children[i], key, value);
            }
            return;
        }
        var _value = element.style.getPropertyValue(key);
        if(_value == null || _value.length == 0) {
            element.style.setProperty(key, value);
        }
    }

    element.defineStyles({
        "min-height": "40px",
        "border": "1px solid "+borderColor,
        "border-radius": "4px",
        "text-align": "left",
        "padding": "4px",
        "margin": "8px 0",
        ".label": {
            "font-size": labelFontSize,
            color: labelColor,
            cursor: "default",
            "user-select": "none"
        },
        ".unit": {
            "font-size": unitFontSize,
            "margin-left": "4px",
            color: unitColor
        }
    });
    setProperty(element, "width", "calc(100% - 8px)", "input, select, textarea");

    if(style != undefined) {
        element.defineStyles(style);
    }

    element.addEventListener("click", function(event) {
        var targetElement = event.target;
        if(targetElement instanceof HTMLDivElement) {
            var innerElement = event.currentTarget.querySelector("input,textarea,selector");
            if(innerElement != null) {
                innerElement.focus();
            }
        }
    });

    return element;
}

/**
 * Create scrollable DIV element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {Array} [children] 
 * @returns {HTMLDivElement}
 */
function ScrollView() {
    var element = View.apply(this, arguments);
    element.defineStyles({
        "overflow-y": "auto",
        "-ms-overflow-style": "none",
        "-webkit-overflow-scrolling": "touch",
        "scrollbar-width": "none"
    });
    return element;
}

/**
 * Create INPUT element that is avalable input numeric value.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {string} [attributes.unit] 
 * @param {boolean} [attributes.currency=false] Whether or not to perform currency formatting.
 * @param {number} [attributes.multiplier=1] 
 * @param {number} [attributes.value] 
 * @param {number} [attributes.maxValue] 
 * @param {number} [attributes.minValue] 
 * @param {string} [attributes.dataKey] 
 * @returns {HTMLInputElement} element
 */
function NumericField() {
    var _arguments = Array.prototype.slice.call(arguments);

    var unit;
    var currency = false;
    var multiplier = 1;
    var maxValue;
    var minValue;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(typeof argument == "object" && !Array.isArray(argument)) {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "unit" && typeof argument[key] == "string") {
                    unit = argument[key];
                    delete argument[key];
                }else if(key == "currency" && typeof argument[key] == "boolean") {
                    currency = argument[key];
                    delete argument[key];
                }else if(key == "multiplier" && typeof argument[key] == "number") {
                    multiplier = argument[key];
                    delete argument[key];
                }else if(key == "maxValue" && typeof argument[key] == "number") {
                    maxValue = argument[key];
                    delete argument[key];
                }else if(key == "minValue" && typeof argument[key] == "number") {
                    minValue = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }
    var element = TextField.apply(this, _arguments);
    var inputElement = element;
    if(inputElement.tagName.toLowerCase() != "input") {
        inputElement = inputElement.querySelector("input");
    }
    inputElement.style.setProperty("text-align", "right");

    if(unit != undefined) {
        if(ResizeObserver !== undefined) {
            var resizeObserver = new ResizeObserver(function(observations) {
                if(observations.length == 0) return;
                var element = observations[0].target;
                resizeObserver.disconnect();
                showUnit(element, unit);
            });
            resizeObserver.observe(inputElement);
        }else {
            console.error("Your browser does not support ResizeObserver. Please try to run Polyfill for this function.");
        }
    }

    function showUnit(element, unit) {
        var unitElement;
        if(element.parentElement != null) {
            unitElement = element.parentElement.querySelector(".unit");
            if(unitElement != null) {
                unitElement.remove();
            }
        }
        unitElement = document.createElement("span");
        unitElement.classList.add("unit");
        unitElement.defineStyles({
            "font-size": "10px",
            "margin-left": "4px",
            "color": "darkgray"
        });
        unitElement.innerText = unit;
        element.after(unitElement);
    }

    if(currency) {
        inputElement.addEventListener("focus", function() {
            var value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").get.call(inputElement);
            if(value != null) {
                Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(inputElement, value.replace(/,/g, ""));
            }
        });
        inputElement.addEventListener("blur", function() {
            var value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").get.call(inputElement);
            if(value != null) {
                Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(inputElement, StringUtil.currencyString(value));
            }
        });
        var value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").get.call(inputElement);
        if(value != null) {
            Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(inputElement, StringUtil.currencyString(value));
        }
    }

    inputElement.multiplier = multiplier;

    function mutiplyInSafety(number, mutiplier) {
        if(number == null || isNaN(number) || mutiplier == null || isNaN(mutiplier)) return number;
        var string = number.toString();
        var pointIndex = string.indexOf(".");
        if(pointIndex == -1) return number * mutiplier;
        var decimalScale = string.length - pointIndex - 1;
        var integer = number * Math.pow(10, decimalScale);
        integer = Math.floor(integer);
        var result = integer * mutiplier;
        return result / Math.pow(10, decimalScale);
    }

    Object.defineProperty(inputElement, "value", { 
        get: function() {
            var value = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").get.call(inputElement);
            if(value == null) {
                return value;
            }
            if(currency) {
                value = value.replace(/,/g, "");
            }
            if(value.length == 0) {
                return null;
            }
            value = Number(value);
            if(Number.isNaN(value)) {
                return null;
            }

            value = value / inputElement.multiplier;

            if(maxValue != null && value > maxValue) {
                value = maxValue;
                inputElement.value = value;
            }else if(minValue != null && value < minValue) {
                value = minValue;
                inputElement.value = value;
            }
            
            return value;
        },
        set: function(newValue) {
            var value = null;
            if(newValue != null) {
                value = newValue;

                value = mutiplyInSafety(value, inputElement.multiplier);

                if(currency) {
                    value = StringUtil.currencyString(value);
                }
            }
            Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(inputElement, value);
        }
    });

    Object.defineProperty(inputElement, "unit", { 
        set: function(newValue) {
            showUnit(inputElement, newValue);
        }
    });

    return element;
}

/**
 * Create check-box element.
 * @param {string} [identifier] 
 * @param {Object} [settings] 
 * @param {string} [settings.label] 
 * @param {boolean} [settings.checked=false] 
 * @param {number|string} [settings.fontSize] 
 * @param {string} [settings.boxColor=black] 
 * @param {string} [settings.checkColor=black] 
 * @param {string} [settings.dataKey] The key for object set in the ViewController or parent View. 
 * @param {function(boolean): void} [changeHandler]
 * @returns {HTMLDivElement}
 */
function Checkbox() {
    var _arguments = Array.prototype.slice.call(arguments);

    var label = undefined;
    var checked = false;
    var boxColor = "black";
    var checkColor = "black";
    var fontSize = "1em";
    var dataKey;
    var changeHandler;
    var style;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(typeof argument == "object" && !Array.isArray(argument)) {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "label" && typeof argument[key] == "string") {
                    label = argument[key];
                    delete argument[key];
                }else if(key == "checked" && typeof argument[key] == "boolean") {
                    checked = argument[key];
                    delete argument[key];
                }else if(key == "fontSize" && (typeof argument[key] == "number" || typeof argument[key] == "string")) {
                    fontSize = argument[key];
                    delete argument[key];
                }else if(key == "boxColor" && typeof argument[key] == "string") {
                    boxColor = argument[key];
                    delete argument[key];
                }else if(key == "checkColor" && typeof argument[key] == "string") {
                    checkColor = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "changeHandler" && typeof argument[key] == "function") {
                    changeHandler = argument[key];
                    delete argument[key];
                }else if(key == "style" && typeof argument[key] == "object") {
                    style = argument[key];
                }
            }
            break;
        }
    }
    
    _arguments.splice(0, 0, "div");
    var element = HtmlTag.apply(this, _arguments);

    if(dataKey != undefined) {
        element.dataKey = dataKey;
    }

    element.classList.add("_hardcore-checkbox");

    if(style == null || style.display == null) {
        element.style.setProperty("display", "inline-block");
    }
    if(style == null || style.height == null) {
        element.style.setProperty("height", "32px");
    }
    if(style == null || style["line-height"] == null) {
        element.style.setProperty("line-height", "32px");
    }
    if(style == null || style["font-size"] == null) {
        element.style.setProperty("font-size", fontSize);
    }
    if(style == null || style.cursor == null) {
        element.style.setProperty("cursor", "pointer");
    }

    var canvas = document.createElement("canvas");
    canvas.style.setProperty("vertical-align", "bottom");
    var canvasSize = Size(24, 32);
    CanvasUtil.initCanvas(canvas, {width: 24, height: 32});
    element.appendChild(canvas);
    if(label != null) {
        var labelElement = document.createElement("span");
        labelElement.defineStyles({
            "user-select": "none"
        })
        labelElement.innerText = label;
        element.appendChild(labelElement);
    }
    var context = canvas.getContext('2d');

    var width = canvasSize.width;
    var height = canvasSize.height;
    var boxWidth = 16;
    var boxHeight = 16;

    var beginX = width/2 - boxWidth/2;
    var beginY = height/2 - boxHeight/2;
    var centerX = width/2;
    var centerY = height/2;
    var endX = width/2 + boxWidth/2;
    var endY = height/2 + boxHeight/2;

    function drawBox(context) {
        context.beginPath();
        context.rect(beginX, beginY, boxWidth, boxHeight);

        context.strokeStyle = boxColor;
        context.lineWidth = 1;
        context.stroke();
    }

    function drawCheck(context) {
        context.strokeStyle = checkColor;
        context.lineWidth = 4;
        context.lineCap = "square";

        context.beginPath();
        context.moveTo(beginX, centerY);
        context.lineTo(centerX, endY);
        context.lineTo(endX, beginY);

        context.strokeStyle = checkColor;
        context.lineWidth = 4;
        context.lineCap = "square";

        context.stroke();
    }

    function drawCheckWithAnimation(context) {
        var leftRadius = Math.sqrt(Math.pow(boxWidth/2, 2) + Math.pow(boxWidth/2, 2));
        var rightRadius = Math.sqrt(Math.pow(boxWidth/2, 2) + Math.pow(boxWidth, 2));

        var leftRadian = 45.0*(Math.PI/180.0);
        var rightRadian = 63.0*(Math.PI/180.0);

        context.strokeStyle = checkColor;
        context.lineWidth = 4;
        context.lineCap = "square";

        new FunctionalAnimation(function(progress) {
            var x, y;
            var ratio;
            context.beginPath();
            if(progress < 0.5) {
                ratio = (0.5-progress)/0.5;
                x = centerX - leftRadius * ratio * Math.cos(leftRadian);
                y = endY - leftRadius * ratio * Math.sin(leftRadian);
                if(x > centerX) x = centerX;
                if(y > endY) y = endY;
                context.moveTo(beginX, centerY);
            }else {
                ratio = (progress-0.5)/0.5;
                x = centerX + rightRadius * ratio * Math.cos(rightRadian);
                y = endY - rightRadius * ratio * Math.sin(rightRadian);
                if(x > endX) x = endX;
                if(y < beginY) y = beginY;
                context.moveTo(centerX, endY);
            }
            context.lineTo(x, y);
            context.stroke();
        }, FunctionalAnimation.methods.easeInOut, 300).start();
    }

    Object.defineProperty(element, "checked", { 
        get: function() {
            return this._checked;
        },
        set: function(newValue) {
            if(newValue == null) {
                newValue = false;
            }
            var initial = this._checked == undefined;
            this._checked = newValue;
            var canvas = this.querySelector("canvas");
            var context = canvas.getContext('2d');
            context.clearRect(0,0,canvas.width,canvas.height);
            drawBox(context);
            if(element.checked) {
                if(initial) {
                    drawCheck(context);
                }else {
                    drawCheckWithAnimation(context);
                }
            }
        }
    });
    element.checked = checked;

    drawBox(context);

    element.addEventListener("click", function(event) {
        var element = event.currentTarget;
        var canvas = element.querySelector("canvas");
        var context = canvas.getContext('2d');
        context.clearRect(0, 0, canvas.width, canvas.height);
        drawBox(context);
        element.checked = !element.checked;
        if(dataKey != null) {
            element.dataBindHandler(element.checked, dataKey);
        }
        if(changeHandler != undefined) {
            changeHandler(element.checked, element);
        }
        event.stopPropagation();
    });

    element.addEventListener("keydown", function(event) {
        if(event.code == "Space") {
            this.dispatchEvent(new MouseEvent("click"));
            event.preventDefault();
        }
    });

    return element;
}

/**
 * Create toggle button element.
 * @param {string} [identifier] 
 * @param {Object} [settings] 
 * @param {Array<string>} [settings.items] labels
 * @param {number} [settings.selectedIndex] 
 * @param {string} [settings.borderColor=black] 
 * @param {string} [settings.fillColor=black] 
 * @param {string} [settings.labelColor=black] 
 * @param {string} [settings.selectedLabelColor=black] 
 * @param {boolean} [settings.removable=true] 
 * @param {boolean} [settings.editable=true] 
 * @param {function(number): void} [selectHandler]
 * @returns {HTMLDivElement}
 */
function ToggleButton() {
    var _arguments = Array.prototype.slice.call(arguments);

    var items = [];
    var selectedIndex = -1;
    var borderColor = "black";
    var fillColor = "black";
    var labelColor = "black";
    var selectedLabelColor = "white";
    var removable = false;
    var editable = true;
    var selectHandler;
    var style;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(!Array.isArray(argument) && typeof argument == "object") {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "items" && typeof argument[key] == "object") {
                    items = argument[key];
                    delete argument[key];
                }else if(key == "selectedIndex" && typeof argument[key] == "number") {
                    selectedIndex = argument[key];
                    delete argument[key];
                }else if(key == "borderColor" && typeof argument[key] == "string") {
                    borderColor = argument[key];
                    delete argument[key];
                }else if(key == "fillColor" && typeof argument[key] == "string") {
                    fillColor = argument[key];
                    delete argument[key];
                }else if(key == "labelColor" && typeof argument[key] == "string") {
                    labelColor = argument[key];
                    delete argument[key];
                }else if(key == "selectedLabelColor" && typeof argument[key] == "string") {
                    selectedLabelColor = argument[key];
                    delete argument[key];
                }else if(key == "removable" && typeof argument[key] == "boolean") {
                    removable = argument[key];
                    delete argument[key];
                }else if(key == "editable" && typeof argument[key] == "boolean") {
                    editable = argument[key];
                    delete argument[key];
                }else if(key == "selectHandler" && typeof argument[key] == "function") {
                    selectHandler = argument[key];
                    delete argument[key];
                }else if(key == "style" && typeof argument[key] == "object") {
                    style = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    var element = View.apply(this, _arguments);

    element.editable = editable;

    if(style == undefined) {
        style = {};
    }
    if(style.display == undefined) {
        style.display = "inline-block";
    }
    if(style["white-space"] == undefined) {
        style["white-space"] = "nowrap";
    }
    if(style.cursor == undefined) {
        style.cursor = "default";
    }
    if(style["user-select"] == undefined) {
        style["user-select"] = "none";
    }
    if(style.height == undefined) {
        style.height = 32;
    }
    element.styles = style;

    if(items != null && items.length > 0) {
        if(selectedIndex != -1 && selectedIndex < items.length) {
            element.selectedIndex = selectedIndex;
        }else {
            element.selectedIndex = -1;
        }
        var itemWidth = (1/items.length * 100) + "%";
        for(i=0; i<items.length; i++) {
            var item = items[i];
            var itemElement = View(".item", {style: {
                display: "inline-block",
                width: itemWidth, 
                height: "100%", 
                color: labelColor,
                "font-size": style["font-size"] != null ? style["font-size"] : "small",
                "text-align": "center",
                "border-radius": (i==0 ? [4,0,0,4] : (i == items.length-1 ? [0,4,4,0] : 0)),
                "border": "1px solid "+borderColor,
                "line-height": style.height
            }}, [item]);
            if(i>0) {
                itemElement.style.borderLeft = "1px solid "+borderColor;
            }
            itemElement.addEventListener("click", function(event) {
                if(!element.editable) return false;
                var itemElement = event.currentTarget;
                var itemElements = element.querySelectorAll(".item");
                var index = itemElements.indexOf(itemElement);
                if(element.selectedIndex == index) {
                    if(removable) {
                        index = -1;
                        element.selectedIndex = -1;
                    }
                }else {
                    element.selectedIndex = index;
                }
                updateView();
                if(selectHandler != null) {
                    selectHandler(index);
                }
            });
            element.appendChild(itemElement);
        }
        updateView();
    }

    function updateView() {
        var itemElements = element.querySelectorAll(".item");
        for(var i=0; i<itemElements.length; i++) {
            var _itemElement = itemElements[i];
            if(i == element.selectedIndex) {
                _itemElement.style.backgroundColor = fillColor;
                _itemElement.style.color = selectedLabelColor;
            }else {
                _itemElement.style.backgroundColor = "transparent";
                _itemElement.style.color = labelColor;
            }
        }
    }

    Object.defineProperty(element, "selectedIndex", { 
        get: function() {
            return this._selectedIndex;
        },
        set: function(newValue) {
            this._selectedIndex = newValue;
            updateView();
        }
    });

    return element;
}

/**
 * Create compatible select element. It absorbs differences in expression between operating systems and browsers.
 * @param {string} [identifier] 
 * @param {Object} [settings] 
 * @param {Array} settings.items 
 * @param {number} [settings.selectedIndex] 
 * @param {number} [settings.itemWidth] 
 * @param {number} [settings.itemHeight] 
 * @param {function(item:any):string} [settings.labelHandler] 
 * @param {function(item:any, current:boolean):object} [settings.styleHandler] 
 * @param {function(item:any): HTMLDivElement} [settings.itemHandler]
 * @param {function(itemElement:HTMLDivElement, item:any): void} [settings.itemDrawer] 
 * @param {function(selectedIndex:number): void} [settings.selectHandler] 
 * @param {function(void): void} [settings.closeHandler] 
 * @param {boolean} [settings.selectedDrawing=true] 
 * @param {boolean} [settings.editable=true] 
 * @param {string} [settings.dataKey] The key for object set in the ViewController or parent View. 
 * @param {string} [settings.valueKey] When the element of the items is Object, the key for the value to be data-bounding.
 * @returns {HTMLDivElement}
 */
function Select() {
    var _arguments = Array.prototype.slice.call(arguments);

    var settingsIndex = -1;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(typeof argument == "object" && !Array.isArray(argument)) {
            settingsIndex = i;
            break;
        }
    }
    var settings;
    if(settingsIndex != -1) {
        settings = _arguments[settingsIndex];
        _arguments.splice(settingsIndex, 1);
    }

    _arguments.splice(0, 0, "div");
    var element = HtmlTag.apply(this, _arguments);

    element.classList.add("_hardcore-select");

    if(settings != undefined) {
        if(settings.style != undefined) {
            element.defineStyles(settings.style);
        }
        if(settings.valueKey != undefined) {
            element.valueKey = settings.valueKey;
        }
        if(settings.dataKey != undefined) {
            element.dataKey = settings.dataKey;
            if(element.valueKey == undefined) {
                element.valueKey = settings.dataKey;
            }
        }
        if(settings.itemHandler == undefined && settings.itemDrawer == undefined && settings.labelHandler == undefined) {
            settings.labelHandler = function(item) {
                return item["label"];
            }
        }
        if(settings.itemHandler == undefined && settings.itemDrawer == undefined && settings.styleHandler == undefined) {
            settings.styleHandler = function(item, current) {
                return {
                    padding: [0,8]
                };
            }
        }
    }

    var select = Controls.Select(element, settings);
    Object.defineProperty(element, "selectedIndex", { 
        get: function() {
            return select.selectedIndex;
        },
        set: function(newValue) {
            select.selectedIndex = newValue;
        }
    });
    Object.defineProperty(element, "items", { 
        get: function() {
            return select.items;
        },
        set: function(newValue) {
            select.items = newValue;
        }
    });
    Object.defineProperty(element, "editable", { 
        get: function() {
            return select.editable;
        },
        set: function(newValue) {
            select.editable = newValue;
        }
    });
    if(element.dataKey != undefined) {
        Object.defineProperty(element, "dataBindHandler", { 
            set: function(newValue) {
                if(settings.selectHandler != undefined) {
                    var originalHandler = settings.selectHandler;
                    select.selectHandler = function(selectedIndex, select) {
                        newValue(selectedIndex, element);
                        originalHandler(selectedIndex, select);
                    };
                }else {
                    select.selectHandler = function(selectedIndex, select) {
                        newValue(selectedIndex, element);
                    }
                }
            }
        });
    }
    element.show = function() {
        select.show();
    };
    element.close = function() {
        select.close();
    };

    return element;
}

/**
 * Create slider element.
 * @param {string} [identifier] 
 * @param {Object} [attributes] 
 * @param {number} [attributes.value] 
 * @param {number} [attributes.maxValue=100] 
 * @param {number} [attributes.minValue=0] 
 * @param {number} [attributes.stepValue=1] Unit value when the value is changed.
 * @param {string} [attributes.borderColor] 
 * @param {string} [attributes.indicatorColor] 
 * @param {string} [attributes.unit] Label suffix
 * @param {boolean} [attributes.editable=true] 
 * @param {function(number): void} [attributes.changeHandler] Called when the value is chnanged.
 * @param {string} [settings.dataKey] The key for object set in the ViewController or parent View. 
 * @returns {HTMLDivElement}
 */
function Slider() {
    var _arguments = Array.prototype.slice.call(arguments);

    var value;
    var maxValue = 100;
    var minValue = 0;
    var stepValue = 1;
    var borderColor = "darkgray";
    var indicatorColor = "black";
    var unit = "%";
    var editable = true;
    var dataKey;
    var changeHandler;
    for(var i=0; i<_arguments.length; i++) {
        var argument = _arguments[i];
        if(typeof argument == "object" && !Array.isArray(argument)) {
            var keys = Object.keys(argument);
            for(var j=0; j<keys.length; j++) {
                var key = keys[j];
                if(key == "value" && typeof argument[key] == "number") {
                    value = argument[key];
                    delete argument[key];
                }else if(key == "maxValue" && typeof argument[key] == "number") {
                    maxValue = argument[key];
                    delete argument[key];
                }else if(key == "minValue" && typeof argument[key] == "number") {
                    minValue = argument[key];
                    delete argument[key];
                }else if(key == "stepValue" && typeof argument[key] == "number") {
                    stepValue = argument[key];
                    delete argument[key];
                }else if(key == "borderColor" && typeof argument[key] == "string") {
                    borderColor = argument[key];
                    delete argument[key];
                }else if(key == "indicatorColor" && typeof argument[key] == "string") {
                    indicatorColor = argument[key];
                    delete argument[key];
                }else if(key == "unit" && typeof argument[key] == "string") {
                    unit = argument[key];
                    delete argument[key];
                }else if(key == "editable" && typeof argument[key] == "boolean") {
                    editable = argument[key];
                    delete argument[key];
                }else if(key == "dataKey" && typeof argument[key] == "string") {
                    dataKey = argument[key];
                    delete argument[key];
                }else if(key == "changeHandler" && typeof argument[key] == "function") {
                    changeHandler = argument[key];
                    delete argument[key];
                }
            }
            break;
        }
    }

    _arguments.splice(0, 0, "div");
    var element = HtmlTag.apply(this, _arguments);

    if(changeHandler != undefined) {
        element.changeHandler = changeHandler;
    }
    if(dataKey != undefined) {
        element.dataKey = dataKey;

        Object.defineProperty(element, "dataBindHandler", { 
            set: function(newValue) {
                var element = this;
                if(changeHandler != undefined) {
                    var originalHandler = element.changeHandler;
                    element.changeHandler = function(value) {
                        originalHandler(value);
                        newValue(value, element.dataKey);
                    };
                }else {
                    element.changeHandler = function(value) {
                        newValue(value, element.dataKey);
                    }
                }
            }
        });
    }

    element.classList.add("_hardcore-slider");

    element.style.setProperty("border", "1px solid "+borderColor);
    element.style.setProperty("height", "24px");
    
    var indicator = document.createElement("canvas");
    indicator.style.setProperty("position", "relative");
    indicator.style.setProperty("top", "0");
    indicator.style.setProperty("left", "0");
    element.appendChild(indicator);

    if(element.parentElement != null) {
        CanvasUtil.initCanvas(indicator, {width: element.clientWidth, height: element.clientHeight});
    }else {
        if(ResizeObserver !== undefined) {
            var resizeObserver = new ResizeObserver(function(observations) {
                if(observations.length == 0) return;
                var element = observations[0].target;
                resizeObserver.disconnect();
                CanvasUtil.initCanvas(indicator, {width: element.clientWidth, height: element.clientHeight});
                draw(element.value);
            });
            resizeObserver.observe(element);
        }else {
            console.error("Your browser does not support ResizeObserver. Please try to run Polyfill for this function.");
        }
    }

    function draw(progress) {
        var width = indicator.clientWidth;
        var height = indicator.clientHeight;
        var context = indicator.getContext("2d");

        context.clearRect(0, 0, width, height);

        var label;
        if(progress != undefined) {
            var position = width * (progress/(maxValue-minValue));
            context.fillStyle = indicatorColor;
            context.fillRect(0, 0, position, height);

            label = progress + unit;
            var size = context.measureText(label);
            if(position < size.width+8) {
                DrawUtil.drawText(context, label, {x: position+4, y: 0, width: size.width, height: height}, {size:"12px", color:indicatorColor});
            }else {
                DrawUtil.drawText(context, label, {x: 4, y: 0, width: size.width, height: height}, {size:"12px"}, "destination-out");
            }
        }else {
            if(editable) {
                if(navigator.language != null && navigator.language.includes("ja")) {
                    label = "ドラッグして設定してください";
                }else {
                    label = "Drag to set";
                }
            }else {
                if(navigator.language != null && navigator.language.includes("ja")) {
                    label = "未設定";
                }else {
                    label = "Not set";
                }
            }
            var color = new Color(indicatorColor).colorWithAlpha(0.5);
            color = color.toStringWithAlpha();
            DrawUtil.drawText(context, label, {x: 4, y: 0, width: width, height: height}, {size:"12px", color:color});
        }
    }

    function verifyValue(currentValue) {
        if(currentValue == null) {
            return null;
        }
        if(currentValue%stepValue != 0) {
            currentValue = Math.round(currentValue/stepValue) * stepValue;
        }
        if(currentValue > maxValue) {
            currentValue = maxValue;
        }else if(currentValue < minValue) {
            currentValue = minValue;
        }
        return currentValue;
    }

    Object.defineProperty(element, "value", { 
        get: function() {
            return this._value;
        },
        set: function(newValue) {
            this._value = newValue;
            draw(newValue);
        }
    });

    element.value = verifyValue(value);

    if(editable) {
        element.dragging = false;
        UIEventUtil.handleTouch(element, {
            touchBegan: function(event) {
                var element = event.currentTarget;
                element.dragging = true;
            }, 
            touchMove: function(event) {
                var element = event.currentTarget;
                if(!element.dragging) return false;
                var width = indicator.clientWidth;
                var location = UIEventUtil.getLocation(event);
                var currentValue = (location.x / width) * (maxValue-minValue);
                element.value = verifyValue(currentValue);
                draw(element.value);
            }, 
            touchEnd: function(event) {
                var element = event.currentTarget;
                element.dragging = false;
                if(element.changeHandler != undefined) {
                    element.changeHandler(element.value);
                }
            }
        });
    }

    return element;
}


////////////////////////////////////// Web API extensions //////////////////////////////////////

/**
 * @ignore
 */
var _hadcore_styleSheet;

/**
 * @interface Document
 */

/**
 * Multiple and nested global styles of an Document.
 * @name Document#globalStyles
 * @property {Object|Array.<string>|string} globalStyles
 */
Object.defineProperty(Document.prototype, "globalStyles", {
    set: function(newValue) {
        if(_hadcore_styleSheet == undefined) {
            var head = document.getElementsByTagName("head")[0];
            var style = document.createElement("style");
            head.appendChild(style);
            var styleSheets = document.styleSheets;
            if(styleSheets == null && styleSheets.length > 0) {
                console.error("Could not access styleSheet of document.");
                return;
            }
            _hadcore_styleSheet = styleSheets[document.styleSheets.length-1];
        }
        function indexOfRule(key) {
            if(_hadcore_styleSheet.cssRules == undefined) return -1;
            for(var i=0; i<_hadcore_styleSheet.cssRules.length; i++) {
                var rule = _hadcore_styleSheet.cssRules[i];
                if(rule.cssText.startsWith(key)) {
                    return i;
                }
            }
            return -1;
        }
        var i;
        if(typeof newValue == "string") {
            _hadcore_styleSheet.insertRule(newValue);
        }else if(Array.isArray(newValue)) {
            for(i=0; i<newValue.length; i++) {
                _hadcore_styleSheet.insertRule(newValue[i]);
            }
        }else if(typeof newValue == "object") {
            var keys = Object.keys(newValue);
            if(keys.length == 0) {
                return;
            }
            for(i=0; i<keys.length; i++) {
                var selector = keys[i];
                var styles = newValue[selector];
                if(typeof styles != "object") {
                    continue;
                }
                var currentStylesIndex = indexOfRule(selector);
                if(currentStylesIndex != -1) {
                    _hadcore_styleSheet.deleteRule(currentStylesIndex);
                }
                var expression = "";
                expression += selector;
                expression += "{";
                var styleKeys = Object.keys(styles);
                for(var j=0; j<styleKeys.length; j++) {
                    var styleKey = styleKeys[j];
                    var styleValue = styles[styleKey];
                    if(typeof styleValue == "number") {
                        styleValue = styleValue + "px";
                    }else if(Array.isArray(styleValue)) {
                        var valueExp = "";
                        for(var k=0; k<styleValue.length; k++) {
                            if(k>0) {
                                valueExp += " ";
                            }
                            if(typeof styleValue[k] == "number") {
                                valueExp += styleValue[k]+"px";
                            }else {
                                valueExp += styleValue[k];
                            }
                        }
                        styleValue = valueExp;
                    }
                    if(styleKey == "background-image" && styleValue != "none" && !styleValue.endsWith(")")) {
                        styleValue = "url('"+styleValue+"')"
                    }
                    expression += styleKey + ":" + styleValue + ";";
                }
                expression += "}";
                _hadcore_styleSheet.insertRule(expression);
            }
        }else {
            return;
        }
    }
});

/**
 * @interface HTMLElement
 */

/**
 * Multiple and nested styles of an HTMLElement.
 * @name HTMLElement#styles
 * @property {Object} styles
 */
Object.defineProperty(HTMLElement.prototype, "styles", {
    set: function(newValue) {
        HtmlElementUtil.defineStyles(this, newValue);
    }
});

/**
 * @ignore 
 */
HTMLElement.prototype.defineStyles = function(styles) {
    HtmlElementUtil.defineStyles(this, styles);
    return this;
};

/**
 * @ignore 
 */
HTMLElement.prototype.replaceChildren = function(childNode) {
    HtmlElementUtil.replaceChildren(this, childNode);
    return this;
};

/**
 * @typedef {Object} Offset
 * @property {number} left
 * @property {number} top
 */

/**
 * Get the offset from the root element or parent element.
 * @param {HTMLElement} parentElement 
 * @returns {Offset}
 */
HTMLElement.prototype.offset = function(parentElement) {
    return HtmlElementUtil.offset(this, parentElement);
};

/**
 * Validates the input UI contained in the specified HTML element, and displays a browser-standard error if there is a validation error. The validation content follows the various attributes of the input tag.
 * @memberof HTMLElement
 * @type {function(void): boolean}
 * @returns {boolean}
 */
HTMLElement.prototype.validate = function() {
    return ValidationUtil.validate(this);
}

/**
 * @interface Node
 */

/**
 * Delete all child nodes
 * @memberof Node
 * @type {function(void): Node}
 * @returns {Node}
 */
Node.prototype.removeAll = function() {
    while(this.firstChild) {
        this.removeChild(this.firstChild);
    }
    return this;
};

/**
 * @interface NodeList
 */

/**
 * Get the index of a Node in a NodeList
 * @memberof NodeList
 * @type {function(Node): number}
 * @param {Node} childNode 
 * @returns {number}
 */
NodeList.prototype.indexOf = function(childNode) {
    return Array.prototype.slice.call(this).indexOf(childNode);
};

/**
 * @ignore 
 */
NodeList.prototype.forEach = function(iteration) {
    // Chrome bug fix
    var nodeList = Array.prototype.slice.call(this);
    if(nodeList != null && nodeList.length > 0) {
        for(var i=0; i<nodeList.length; i++) {
            iteration(nodeList[i], i, nodeList);
        }
    }
    return this;
};

/**
 * Draw on the Canvas.
 * @param {function(CanvasRenderingContext2D, Size): void} drawer 
 * @param {boolean} refresh Clear the previous drawing contents before drawing.
 * @returns HTMLCanvasElement
 */
HTMLCanvasElement.prototype.draw = function(drawer, refresh) {
    var size = Size();
    if(this.clientWidth == 0 && this.clientHeight == 0) {
        var width = this.style.getPropertyValue("width");
        var height = this.style.getPropertyValue("height");
        if(typeof width == "string" && width.endsWith("px")) {
            size.width = Number(width.substr(0, width.length-2));
        }
        if(typeof height == "string" && height.endsWith("px")) {
            size.height = Number(height.substr(0, height.length-2));
        }
    }else {
        size.width = this.clientWidth;
        size.height = this.clientHeight;
    }
    var context = this.getContext("2d");
    if(refresh != undefined && refresh) {
        context.clearRect(0, 0, size.width, size.height);
    }
    drawer(context, size);
    return this;
}


////////////////////////////////////// Animations //////////////////////////////////////

/**
 * Animation that executes a function every frame.
 * @class
 * @param {function(number): void} animationFunction
 * @param {FunctionalAnimation.methods} method
 * @param {number} duration
 * @returns {FunctionalAnimation}
 */
function FunctionalAnimation() {
    if(this == undefined) {
        var _arguments = Array.prototype.slice.call(arguments);
        _arguments.splice(0, 0, null);
        return new (Function.prototype.bind.apply(FunctionalAnimation, _arguments));
    }

    var animationFunction;
    var method;
    var duration;
    for(var i=0; i<arguments.length; i++) {
        var argument = arguments[i];
        if(typeof argument == "function") {
            if(animationFunction == null) {
                animationFunction = argument;
            }
        }else if(typeof argument == "number") {
            if(method == null) {
                method = argument;
            }else if(duration == null) {
                duration = argument;
            }
        }
    }

    if(animationFunction == undefined) {
        console.error("No animation function is specified.");
        return;
    }else {
        this.animationFunction = animationFunction;
    }

    if(requestAnimationFrame === undefined) {
        console.error("The browser does not support the requestAnimationFrame function. Please try to run Polyfill for this function.");
        animationFunction(1);
        return;
    }

    if(method == undefined) {
        method = FunctionalAnimation.methods.linear;
    }else {
        this.method = method;
    }
    if(duration == undefined) {
        this.duration = 500;
    }else {
        this.duration = duration;
    }
}
FunctionalAnimation.methods = {
    linear: 0,
    easeInOut: 1,
    easeIn: 2,
    easeOut: 3
};
FunctionalAnimation.prototype = {

    // https://gist.github.com/gre/1650294
    easeInOut: function(t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
    easeIn: function(t) { return t*t },
    easeOut: function(t) { return t*(2-t) },

    /**
     * Start an animation.
     * @param {number} delay milliseconds
     * @returns {FunctionalAnimation}
     */
    start: function(delay) {
        var self = this;

        if(self.animationFunction == undefined) {
            return;
        }
        if(delay != undefined && typeof delay == "number") {
            setTimeout(function() {
                self.start();
            }, delay);
            return;
        }

        self.beginTime = performance.now();
        self.progress = 0;

        function frameFunction() {
            var progress = (performance.now() - self.beginTime) / self.duration;

            if(progress >= 1) {
                self.progress = 1;
                self.animationFunction(self.progress);
                if(self.finishHandler != undefined) {
                    self.finishHandler();
                }
            }else {
                self.progress = progress;
                if(self.method != null) {
                    if(self.method == FunctionalAnimation.methods.easeInOut) {
                        self.animationFunction(self.easeInOut(progress));
                    }else if(self.method == FunctionalAnimation.methods.easeIn) {
                        self.animationFunction(self.easeIn(progress));
                    }else if(self.method == FunctionalAnimation.methods.easeOut) {
                        self.animationFunction(self.easeOut(progress));
                    }else {
                        self.animationFunction(progress);
                    }
                }else {
                    self.animationFunction(progress);
                }
                requestAnimationFrame(frameFunction);
            }
        }
        requestAnimationFrame(frameFunction);

        return self;
    },

    /**
     * Set finish handler.
     * @param {function(void): void} finishHandler 
     * @returns {FunctionalAnimation}
     */
    finish: function(finishHandler) {
        this.finishHandler = finishHandler;
        return this;
    }
}

/**
 * An animation that changes the specified style for each frame.
 * @class
 * @extends FunctionalAnimation
 * @param {HTMLElement} target
 * @param {string} key
 * @param {Object} settings
 * @param {number} [settings.beginValue]
 * @param {number} settings.finishValue
 * @param {string} [settings.suffix]
 * @param {FunctionalAnimation.methods} [settings.method]
 * @param {number} [settings.duration]
 */
function StyleAnimation() {
    if(this == undefined) {
        var _arguments = Array.prototype.slice.call(arguments);
        _arguments.splice(0, 0, null);
        return new (Function.prototype.bind.apply(StyleAnimation, _arguments));
    }

    var target;
    var key;
    var method = FunctionalAnimation.methods.linear;
    var beginValue;
    var finishValue;
    var suffix;
    var duration;
    for(var i=0; i<arguments.length; i++) {
        var argument = arguments[i];
        if(argument instanceof HTMLElement) {
            if(target == undefined) {
                target = argument;
            }
        }else if(typeof argument == "string") {
            if(key == undefined) {
                key = argument;
            }
        }else if(typeof argument == "object") {
            if(beginValue == undefined && argument.beginValue != null) {
                if(typeof argument.beginValue == "string") {
                    if(argument.beginValue.endsWith("px")) {
                        beginValue = Number(argument.beginValue.substring(0, argument.beginValue.length-2));
                        suffix = "px";
                    }else {
                        beginValue = Number(argument.beginValue);
                    }
                }else if(typeof argument.beginValue == "number") {
                    beginValue = argument.beginValue;
                }
            }
            if(finishValue == undefined && argument.finishValue != null) {
                if(typeof argument.finishValue == "string") {
                    if(argument.finishValue.endsWith("px")) {
                        finishValue = Number(argument.finishValue.substring(0, argument.finishValue.length-2));
                        suffix = "px";
                    }else {
                        finishValue = Number(argument.finishValue);
                    }
                }else if(typeof argument.finishValue == "number") {
                    finishValue = argument.finishValue;
                }
            }
            if(suffix == undefined && argument.suffix != null) {
                suffix = argument.suffix;
            }
            if(method == undefined && argument.method != null) {
                method = argument.method;
            }
            if(duration == undefined && argument.duration != null) {
                duration = argument.duration;
            }
        }
    }

    if(target == undefined) {
        console.error("The element to be animated has not been specified.");
        return;
    }else {
        this.target = target;
    }
    if(key == undefined) {
        console.error("The animation target style is not specified.");
        return;
    }else {
        this.key = key;
    }
    if(beginValue == undefined) {
        var currentValue = target.style.getPropertyValue(key);
        if(typeof currentValue == "string") {
            if(currentValue.endsWith("px")) {
                currentValue = Number(currentValue.substring(0, currentValue.length-2));
            }else if(currentValue.endsWith("%")) {
                currentValue = Number(currentValue.substring(0, currentValue.length-1));
                if(key == "width") {
                    currentValue = target.offsetWidth * (currentValue/100);
                }else if(key == "height") {
                    currentValue = target.offsetHeight * (currentValue/100);
                }
            }else {
                currentValue = Number(currentValue);
            }
        }
        this.beginValue = currentValue;
    }else {
        this.beginValue = beginValue;
    }
    if(finishValue == undefined) {
        this.finishValue = 1;
    }else {
        this.finishValue = finishValue;
    }
    if(suffix == undefined) {
        if(key == "width" || key == "height" || key == "left" || key == "top" || key == "right" || key == "bottom") {
            suffix = "px";
        }
    }
    this.suffix = suffix;

    var self = this;

    var diffValue = self.finishValue - self.beginValue;

    // スーパークラス呼び出し
    FunctionalAnimation.call(this, function(progress) {
        var value = self.beginValue + diffValue * progress;
        if(suffix != undefined) {
            value += suffix;
        }
        self.target.style.setProperty(self.key, value);
    }, method, duration);
}
StyleAnimation.prototype = Object.create(FunctionalAnimation.prototype);


////////////////////////////////////// Utilities //////////////////////////////////////

var DateUnit = {
    year: "year",
    month: "month",
    date: "date",
    hour: "hour",
    minute: "minute",
    second: "second"
};

/**
 * Date utility
 * @namespace
 */
var DateUtil = {

    /**
     * Format Date object.
     * @param {Date} date
     * @param {string} [format=yyyy-MM-dd]
     * @returns {string}
     * @see http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns
     */
    format: function(date, format) {
        if(date == null || !(date instanceof Date)) {
            return null;
        }
        if(format == null) {
            format = "yyyy-MM-dd";
        }
        var result = format;
        if(result.includes("y")) {
            var year = date.getFullYear();
            result = result.replace(/yyyy/, year);
        }
        if(result.includes("M")) {
            var month = date.getMonth()+1;
            result = result.replace(/MM/, StringUtil.padding(month, "0", 2));
            result = result.replace(/M/, month);
        }
        if(result.includes("d")) {
            var _date = date.getDate();
            result = result.replace(/dd/, StringUtil.padding(_date, "0", 2));
            result = result.replace(/d/, _date);
        }
        if(result.includes("E")) {
            var day = date.getDay();
            var dayExp;
            switch(day) {
                case 0: dayExp = "Sunday"; break;
                case 1: dayExp = "Monday"; break;
                case 2: dayExp = "Tuesday"; break;
                case 3: dayExp = "Wednesday"; break;
                case 4: dayExp = "Thursday"; break;
                case 5: dayExp = "Friday"; break;
                case 6: dayExp = "Saturday"; break;
            }
            var shortDayExp;
            switch(day) {
                case 0: shortDayExp = "Sun"; break;
                case 1: shortDayExp = "Mon"; break;
                case 2: shortDayExp = "Tues"; break;
                case 3: shortDayExp = "Wed"; break;
                case 4: shortDayExp = "Thur"; break;
                case 5: shortDayExp = "Fri"; break;
                case 6: shortDayExp = "Sat"; break;
            }
            if(typeof dayExp == "string") {
                result = result.replace(/EEEE/, dayExp);
            }
            if(typeof shortDayExp == "string") {
                result = result.replace(/EEE/, shortDayExp);
            }
        }
        if(result.includes("e")) {
            var localDayExp = date.getDay();
            if(navigator.language != null && navigator.language.includes("ja")) {
                switch(localDayExp) {
                    case 0: localDayExp = "日曜日"; break;
                    case 1: localDayExp = "月曜日"; break;
                    case 2: localDayExp = "火曜日"; break;
                    case 3: localDayExp = "水曜日"; break;
                    case 4: localDayExp = "木曜日"; break;
                    case 5: localDayExp = "金曜日"; break;
                    case 6: localDayExp = "土曜日"; break;
                }
            }
            if(typeof localDayExp == "string") {
                result = result.replace(/eeee/, localDayExp);
                result = result.replace(/eee/, localDayExp.substr(0, 1));
            }
        }
        if(result.includes("H")) {
            var hour = date.getHours();
            result = result.replace(/HH/, StringUtil.padding(hour, "0", 2));
            result = result.replace(/H/, hour);
        }
        if(result.includes("h")) {
            var halfHour = date.getHours();
            if(halfHour <= 11) {
                halfHour++;
            }else {
                halfHour = halfHour%12+1;
            }
            result = result.replace(/hh/, StringUtil.padding(halfHour, "0", 2));
            result = result.replace(/h/, halfHour);
        }
        if(result.includes("m")) {
            var minutes = date.getMinutes();
            result = result.replace(/mm/, StringUtil.padding(minutes, "0", 2));
            result = result.replace(/m/, minutes);
        }
        if(result.includes("s")) {
            var seconds = date.getSeconds();
            result = result.replace(/ss/, StringUtil.padding(seconds, "0", 2));
            result = result.replace(/s/, seconds);
        }
        return result;
    },

    /**
     * Parse Date formatted string.
     * @param {string} dateString
     * @param {string} [format=yyyy-MM-dd]
     * @returns {Date}
     * @see http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns
     */
    parse: function(dateString, format) {
        if(dateString == null || typeof dateString != "string" || dateString.length == 0) {
            return null;
        }
        if(format == null) {
            format = "yyyy-MM-dd";
        }
        var result = null;

        function retrieveValue(souce, format, regex) {
            var result = regex.exec(format);
            if(result != null) {
                return Number(souce.substring(result.index, regex.lastIndex));
            }
            return null;
        }

        var value;
        if(format.includes("y")) {
            value = retrieveValue(dateString, format, /y+/g);
            if(result == null) {
                result = new Date();
            }
            result.setFullYear(value);
        }else {
            result.setFullYear(0);
        }
        if(format.includes("M")) {
            value = retrieveValue(dateString, format, /M+/g);
            if(result == null) {
                result = new Date();
            }
            result.setMonth(value-1);
        }else {
            result.setMonth(0);
        }
        if(format.includes("d")) {
            value = retrieveValue(dateString, format, /d+/g);
            if(result == null) {
                result = new Date();
            }
            result.setDate(value);
        }else {
            result.setDate(0);
        }
        if(format.includes("H")) {
            value = retrieveValue(dateString, format, /H+/g);
            if(result == null) {
                result = new Date();
            }
            result.setHours(value);
        }else {
            result.setHours(0);
        }
        if(format.includes("m")) {
            value = retrieveValue(dateString, format, /m+/g);
            if(result == null) {
                result = new Date();
            }
            result.setMinutes(value);
        }else {
            result.setMinutes(0);
        }
        if(format.includes("s")) {
            value = retrieveValue(dateString, format, /s+/g);
            if(result == null) {
                result = new Date();
            }
            result.setSeconds(value);
        }else {
            result.setSeconds(0);
        }
        result.setMilliseconds(0);

        return result;
    },

    /**
     * Get the date plus the specified date.
     * @param {Date} date
     * @param {number} addingValue 
     * @param {string} [dateUnit=DateUnit.date] 
     * @returns {Date}
     */
    getDateByAdding: function(date, addingValue, dateUnit) {
        if(date == null || !(date instanceof Date)) {
            return null;
        }
        if(addingValue == null || typeof addingValue != "number") {
            return null;
        }
        if(dateUnit == undefined) {
            dateUnit = DateUnit.date;
        }
        if(dateUnit == DateUnit.second) {
            return new Date(date.getTime() + addingValue*1000);
        }else if(dateUnit == DateUnit.minute) {
            return new Date(date.getTime() + addingValue*60*1000);
        }else if(dateUnit == DateUnit.hour) {
            return new Date(date.getTime() + addingValue*60*60*1000);
        }else if(dateUnit == DateUnit.date) {
            return new Date(date.getTime() + addingValue*24*60*60*1000);
        }else if(dateUnit == DateUnit.month) {
            return new Date(date.getFullYear(), date.getMonth()+addingValue, date.getDate(), 
                date.getHours(), date.getMinutes(), date.getSeconds());
        }else if(dateUnit == DateUnit.year) {
            return new Date(date.getFullYear()+addingValue, date.getMonth(), date.getDate(), 
                date.getHours(), date.getMinutes(), date.getSeconds());
        }
    },

    /**
     * Get the date with all the date times set to zero.
     * @param {Date} date 
     * @returns {Date}
     */
    getDateBySlicingTime: function(date) {
        if(date == null || !(date instanceof Date)) {
            return null;
        }
        return new Date(date.getFullYear(), date.getMonth(), date.getDate());
    }
};

/**
 * String utility
 * @namespace
 */
var StringUtil = {
    /**
     * For values that allow null, if null is set, replace with the specified string. 
     * If not specified, replace with a character from.
     * @param {*} target
     * @param {string} replaced
     * @returns {string}
     */
    getNullable: function(target, replaced) {
        if(replaced === undefined) replaced = "";
        return target != null ? String(target) : replaced;
    },

    /**
     * Fill a string with the specified characters until it reaches the specified length.
     * @param {string} source The string to be edited.
     * @param {string} char The string to be filled.
     * @param {number} [length] Max string length.
     * @returns {string}
     */
    padding: function(source, char, length) {
        if(source == null) return null;
        if(typeof source != "string") {
            source = String(source);
        }
        if(char == null || char.length == 0) return source;
        if(typeof char != "string") {
            char = String(char);
        }
        if(length == null || typeof length != "number" || length <= 0) return source;
        var result = "";
        while(result.length+source.length < length) {
            result += char;
        }
        result += source;
        return result;
    },

    /**
     * Get a three-digit delimited currency notation string.
     * @param {number} [source]
     * @param {number} [decimalPlaces]
     * @param {"round"|"floor"|"ceil"} [roundingMode]
     * @returns {string}
     */
    currencyString: function(source, decimalPlaces, roundingMode) {
        if(source == null) return "";
        if(typeof source != "number") {
            if(typeof source == "string" && source.length == 0) {
                return "";
            }
            
            // multi-byte string
            source = source.replace(/0/g, "0");
            source = source.replace(/1/g, "1");
            source = source.replace(/2/g, "2");
            source = source.replace(/3/g, "3");
            source = source.replace(/4/g, "4");
            source = source.replace(/5/g, "5");
            source = source.replace(/6/g, "6");
            source = source.replace(/7/g, "7");
            source = source.replace(/8/g, "8");
            source = source.replace(/9/g, "9");

            // comma separated number
            source = source.replace(/,/g, "");

            source = Number(source);
        }
        if(typeof source == "number") {
            if(Number.isNaN(source)) {
                return "";
            }
            var string = String(source);
            if(string.includes(".") && typeof decimalPlaces == "number") {
                var multiplier = Math.pow(10, decimalPlaces);
                if(roundingMode == null || roundingMode == "round") {
                    source = Math.round(source * multiplier) / multiplier;
                }else if(roundingMode == "floor") {
                    source = Math.floor(source * multiplier) / multiplier;
                }else if(roundingMode == "ceil") {
                    source = Math.ceil(source * multiplier) / multiplier;
                }
                source = String(source);
            }else {
                source = string;
            }
        }
        if(source.length == 0) return source;
        var result = [];
        var index = 0;
        var i;
        var negative = false;
        if(source.startsWith("-")) {
            negative = true;
            source = source.substring(1);
        }
        if(source.includes(".")) {
            for(i=source.length-1; i>=0; i--) {
                var char = source.charAt(i);
                result.push(char);
                if(char == ".") {
                    source = source.substring(0, i);
                    break;
                }
            }
        }
        index = 0;
        for(i=source.length-1; i>=0; i--) {
            if(index != 0 && index % 3 == 0) {
                result.push(",");
            }
            result.push(source.charAt(i));
            index++;
        }
        return (negative ? "-" : "") + result.reverse().join("");
    }
};

/**
 * @constructor
 * @classdesc Point
 * @param {Number} x 
 * @param {Number} y 
 */
function Point(x, y) {
    if(this == undefined) {
        return new Point(x, y);
    }

    this.x = 0;
    this.y = 0;

    if(x != undefined) {
        if(typeof x != "number") {
            x = Number(x);
        }
        this.x = x;
    }
    if(y != undefined) {
        if(typeof y != "number") {
            y = Number(y);
        }
        this.y = y;
    }
}

/**
 * @constructor
 * @classdesc Size
 * @param {Number} width
 * @param {Number} height
 */
function Size(width, height) {
    if(this == undefined) {
        return new Size(width, height);
    }

    this.width = 0;
    this.height = 0;

    if(width != undefined) {
        if(typeof width != "number") {
            width = Number(width);
        }
        this.width = width;
    }
    if(height != undefined) {
        if(typeof height != "number") {
            height = Number(height);
        }
        this.height = height;
    }
}

/**
 * @constructor
 * @classdesc Rectangle
 * @param {Number} x 
 * @param {Number} y 
 * @param {Number} width 
 * @param {Number} height 
 */
function Rect(x, y, width, height) {
    if(this == undefined) {
        return new Rect(x, y, width, height);
    }

    this.x = 0;
    this.y = 0;
    this.width = 0;
    this.height = 0;

    if(x != undefined) {
        if(typeof x != "number") {
            x = Number(x);
        }
        this.x = x;
    }
    if(y != undefined) {
        if(typeof y != "number") {
            y = Number(y);
        }
        this.y = y;
    }
    if(width != undefined) {
        if(typeof width != "number") {
            width = Number(width);
        }
        this.width = width;
    }
    if(height != undefined) {
        if(typeof height != "number") {
            height = Number(height);
        }
        this.height = height;
    }
}
Rect.prototype = {
    /**
     * Whether the specified position is included in this rectangle.
     * @type {function(Point): boolean}
     * @param {Point}  
     * @returns {boolean}
     */
    contains: function(target) {
        if(target == null) return false
        if(target.x != undefined && target.y != undefined && typeof target.x == "number" && typeof target.y == "number") {
            return this.x < target.x && this.x + this.width > target.x && this.y < target.y && this.y + this.height > target.y;
        }
        return false;
    },

    /**
     * Whether the specified rectangle intersects with this rectangle.
     * @type {function(Rect): boolean}
     * @param {Rect}  
     * @returns {boolean}
     */
    intersect: function(target) {
        if(target == null) return false
        if(target.x != undefined && target.y != undefined && typeof target.x == "number" && typeof target.y == "number" && 
            target.width != undefined && target.height != undefined && typeof target.width == "number" && typeof target.height == "number") {
            return (this.x < target.x && this.x + this.width > target.x &&
                this.y < target.y && this.y + this.height > target.y) ||
                (this.x < target.x + target.width && this.x + this.width > target.x + target.width &&
                    this.y < target.y && this.y + this.height > target.y) ||
                (this.x < target.x + target.width && this.x + this.width > target.x + target.width &&
                    this.y < target.y + target.height && this.y + this.height > target.y + target.height) ||
                (this.x < target.x && this.x + this.width > target.x &&
                    this.y < target.y + target.height && this.y + this.height > target.y + target.height);
        }
        return false;
    },

    midX: function() {
        return this.x + this.width/2;
    },
    midY: function() {
        return this.y + this.height/2;
    },
    maxX: function() {
        return this.x + this.width;
    },
    maxY: function() {
        return this.y + this.height;
    }
};

/**
 * @constructor
 * @classdesc Polygon
 * @param {...Point}
 */
function Polygon() {
    if(this == undefined) {
        var _arguments = Array.prototype.slice.call(arguments);
        _arguments.splice(0, 0, null);
        return new (Function.prototype.bind.apply(Polygon, _arguments));
    }

    this.locations = [];

    for(var i=0; i<arguments.length; i++) {
        var argument = arguments[i];
        if(argument.x != undefined && argument.y != undefined && 
            typeof argument.x == "number" && typeof argument.y == "number") {
            this.locations.push({x: argument.x, y: argument.y});
        }
    }
}
Polygon.prototype = {
    /**
     * Whether the specified position is included in this polygon.
     * @type {function(Point): boolean}
     * @param {Point}  
     * @returns {boolean}
     */
    contains: function(target) {
        if(target == null) return false;
        // https://github.com/substack/point-in-polygon
        var x = target.x, y = target.y;
        if(typeof x != "number" || typeof y != "number") return false;
        var inside = false;
        for(var i = 0, j = this.locations.length - 1; i < this.locations.length; j = i++) {
            var xi = this.locations[i].x, yi = this.locations[i].y;
            var xj = this.locations[j].x, yj = this.locations[j].y;
            if(((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) {
                inside = !inside;
            }
        }
        return inside;
    }
};

/**
 * @constructor
 * @classdesc Color
 * @param {...number|string} components RGBA values or hex expression 
 */
function Color() {
    this.red = 0;
    this.green = 0;
    this.blue = 0;
    this.alpha = 1;

    if(arguments.length == 1) {
        var parameter = arguments[0];
        if(typeof parameter == "string") {
            if(/#[0-9a-fA-F]+/.test(parameter)) {
                this.red = parseInt(parameter.substring(1,3), 16);
                this.green = parseInt(parameter.substring(3,5), 16);
                this.blue = parseInt(parameter.substring(5,7), 16);
            }else {
                if(parameter == "white") {
                    this.red = 255; this.green = 255; this.blue = 255;
                }else if(parameter == "lightgray") {
                    this.red = 211; this.green = 211; this.blue = 211;
                }else if(parameter == "gray") {
                    this.red = 128; this.green = 128; this.blue = 128;
                }else if(parameter == "darkgray") {
                    this.red = 169; this.green = 169; this.blue = 169;
                }
            }
        }
    }else if(arguments.length == 3) {
        if(typeof arguments[0] == "number" && typeof arguments[1] == "number" && typeof arguments[2] == "number") {
            this.red = arguments[0];
            this.green = arguments[1];
            this.blue = arguments[2];
        }
    }else if(arguments.length == 4) {
        if(typeof arguments[0] == "number" && typeof arguments[1] == "number" && typeof arguments[2] == "number" && typeof arguments[3] == "number") {
            this.red = arguments[0];
            this.green = arguments[1];
            this.blue = arguments[2];
            this.alpha = arguments[3];
        }
    }
}
Color.prototype = {
    /**
     * Get the color of this color with modified alpha.
     * @param {number} alpha 
     * @returns {Color}
     */
    colorWithAlpha: function(alpha) {
        this.alpha = alpha;
        return this;
    },

    /**
     * Get a hexadecimal string that can be applied to CSS
     * @returns {string}
     */
    toString: function() {
        return "#" + this.red.toString(16) + this.green.toString(16) + this.blue.toString(16);
    },

    /**
     * Get an RGBA string that can be applied to CSS
     * @returns {string}
     */
    toStringWithAlpha: function() {
        return "rgba(" + this.red + "," + this.green + "," + this.blue + "," + this.alpha + ")";
    }
};

/**
 * Canvas utility
 * @namespace
 */
var CanvasUtil = {
    /**
     * Initialize the canvas
     * @type {function(HTMLCanvasElement, Size): void}
     * @param {HTMLCanvasElement} 
     * @param {Size} size 
     */
    initCanvas: function(canvas, size) {
        if(!(canvas instanceof HTMLCanvasElement)) { 
            console.error("Element is not HTMLCanvasElement.", (canvas != null && canvas.toString != undefined ? canvas.toString() : "null"));
            return;
        }
        var scale = window.devicePixelRatio;
        canvas.width = size.width*scale;
        canvas.height = size.height*scale;
        canvas.style.width = size.width+"px";
        canvas.style.height = size.height+"px";
        var context = canvas.getContext("2d");
        context.scale(scale, scale);
    }
};

/**
 * Initialize CANVAS with specified size
 * @memberof HTMLCanvasElement
 * @param {Size} size
 */
HTMLCanvasElement.prototype.initWithSize = function(size) {
    CanvasUtil.initCanvas(this, size);
    return this;
}

/**
 * @constructor
 * @classdesc Rendering parameter for text.
 * @property {string} font
 * @property {string|number} size
 * @property {string} color
 * @property {left|right|center|start|end} align
 * @property {string|number} weight
 * @property {string|number} lineHeight
 * @property {string} fontExpression 
 */
function TextDrawingAttributes() {
    if(this == undefined) {
        var _arguments = Array.prototype.slice.call(arguments);
        _arguments.splice(0, 0, null);
        return new (Function.prototype.bind.apply(TextDrawingAttributes, _arguments));
    }
    
    for(var i=0; i<arguments.length; i++) {
        var argument = arguments[i];
        if(typeof argument == "string") {
            if(argument.endsWith("px")) {
                if(this.size == undefined) {
                    this.size = argument;
                }else if(this.weight == undefined) {
                    this.weight = argument;
                }else if(this.lineHeight == undefined) {
                    this.lineHeight = argument;
                }
            }else if(argument.startsWith("#")) {
                this.color = argument;
            }else if(argument == "center" || argument == "right" || argument == "left") {
                this.align = argument;
            }else if(argument == "bold" || argument == "bolder" || argument == "lighter" || argument == "normal") {
                this.weight = argument;
            }else {
                if(this.font == undefined) {
                    this.font = argument;
                }else if(this.color == undefined) {
                    this.color = argument;
                }
            }
        }else if(typeof argument == "number") {
            if(this.size == undefined) {
                this.size = argument;
            }else if(this.weight == undefined && argument != 0 && argument % 100 == 0) {
                this.weight = argument;
            }else if(this.lineHeight == undefined) {
                this.lineHeight = argument;
            }
        }
    }
}
TextDrawingAttributes.prototype = {
    get fontExpression() {
        var expression = "";
        if(this.weight != undefined) {
            expression += this.weight;
        }
        if(this.size != undefined) {
            if(expression.length > 0) { expression += " "; }
            if(typeof this.size == "number") {
                expression += this.size + "px";
            }else {
                expression += this.size;
            }
        }
        if(this.lineHeight != undefined) {
            if(expression.length > 0) { expression += " "; }
            if(typeof this.lineHeight == "number") {
                expression += this.lineHeight + "px";
            }else {
                expression += this.lineHeight;
            }
        }
        if(this.font != undefined) {
            if(expression.length > 0) { expression += " "; }
            expression += this.font;
        }
        return expression;
    }
}

/**
 * Drawing utility
 * @namespace
 */
var DrawUtil = {

    /**
     * @type {string}
     */
    defaultFont: "'YuGothic', 'Yu Gothic', 'Hiragino Sans', 'Noto Sans JP'",

    /**
     * @type {string}
     */
    defaultFontSize: "1em",

    /**
     * @type {string}
     */
    defaultFontColor: "black",

    /**
     * @type {number}
     */
    defaultLineHeight: 16,

    /**
     * Draw line.
     * @param {CanvasRenderingContext2D} context 
     * @param {Point} point1 
     * @param {Point} point2
     * @param {string} color
     * @param {boolean} dash
     */
    drawLine: function(context, point1, point2, color, dash) {
        context.save();
        context.beginPath();
        if(dash != undefined && dash) {
            context.setLineDash([2, 2]);
        }
        context.moveTo(point1.x, point1.y);
        context.lineTo(point2.x, point2.y);
        context.strokeStyle = color;
        context.stroke();
        context.restore();
    },

    /**
     * Draw roud rectangle.
     * @param {CanvasRenderingContext2D} context 
     * @param {Rect} rect 
     * @param {string} color 
     * @param {number} radius 
     * @param {boolean} stroke 
     */
    drawRoundRect: function(context, rect, color, radius, stroke) {
        if(radius == undefined) {
            radius = 4;
        }
        if(stroke == undefined) {
            stroke = false;
        }
        context.save();
        context.beginPath();
        context.moveTo(rect.x + radius, rect.y);
        context.lineTo(rect.x + rect.width - radius, rect.y);
        context.quadraticCurveTo(rect.x + rect.width, rect.y, rect.x + rect.width, rect.y + radius);
        context.lineTo(rect.x + rect.width, rect.y + rect.height - radius);
        context.quadraticCurveTo(rect.x + rect.width, rect.y + rect.height, rect.x + rect.width - radius, rect.y + rect.height);
        context.lineTo(rect.x + radius, rect.y + rect.height);
        context.quadraticCurveTo(rect.x, rect.y + rect.height, rect.x, rect.y + rect.height - radius);
        context.lineTo(rect.x, rect.y + radius);
        context.quadraticCurveTo(rect.x, rect.y, rect.x + radius, rect.y);
        context.closePath();
        if(stroke) {
            context.lineWidth = 1;
            context.strokeStyle = color;
            context.stroke();
        }else {
            context.fillStyle = color;
            context.fill();
        }
        context.restore();
    },

    /**
     * Draw text.
     * @param {CanvasRenderingContext2D} context 
     * @param {string} text 
     * @param {Rect} rect 
     * @param {TextDrawingAttributes} textDrawingAttributes
     */
    drawText: function(context, text, rect, textDrawingAttributes, blending) {
        context.save();
        if(blending != undefined) {
            context.globalCompositeOperation = blending;
        }
        var color = this.defaultFontColor;
        if(textDrawingAttributes != undefined) {
            if(textDrawingAttributes.color != undefined) {
                color = textDrawingAttributes.color;
            }
        }
        context.fillStyle = color;
        if(textDrawingAttributes.fontExpression != undefined) {
            context.font = textDrawingAttributes.fontExpression();
        }else {
            var font = this.defaultFont;
            var fontSize = this.defaultFontSize;
            if(textDrawingAttributes != undefined) {
                if(textDrawingAttributes.font != undefined) {
                    font = textDrawingAttributes.font;
                }
                if(textDrawingAttributes.size != undefined) {
                    fontSize = textDrawingAttributes.size;
                }
                if(textDrawingAttributes.color != undefined) {
                    color = textDrawingAttributes.color;
                }
            }
            if(typeof fontSize == "number") {
                fontSize = fontSize + "px"
            }
            context.font = fontSize+" "+font;
        }
        context.textBaseline = "middle";
        var x = rect.x;
        var y = rect.y + rect.height/2;
        if(textDrawingAttributes != undefined && textDrawingAttributes.align != undefined) {
            if(textDrawingAttributes.align == "center") {
                x = rect.x + rect.width/2;
            }else if(textDrawingAttributes.align == "right") {
                x = rect.x + rect.width;
            }
            context.textAlign = textDrawingAttributes.align;
        }
        context.fillText(text, x, y);
        context.restore();
    },

    /**
     * Acquire size of the text.
     * @param {CanvasRenderingContext2D} context 
     * @param {string} text 
     * @param {TextDrawingAttributes} textDrawingAttributes 
     * @returns {TextMetrics}
     */
    sizeOfText: function(context, text, textDrawingAttributes) {
        context.save();
        if(textDrawingAttributes.fontExpression != undefined) {
            context.font = textDrawingAttributes.fontExpression();
        }else {
            var font = this.defaultFont;
            var fontSize = this.defaultFontSize;
            if(textDrawingAttributes != undefined) {
                if(textDrawingAttributes.font != undefined) {
                    font = textDrawingAttributes.font;
                }
                if(textDrawingAttributes.size != undefined) {
                    fontSize = textDrawingAttributes.size;
                }
            }
            if(typeof fontSize == "number") {
                fontSize = fontSize + "px"
            }
            context.font = fontSize+" "+font;
        }
        var size = context.measureText(text);
        context.restore();
        return size;
    },

    /**
     * Draw with the ends marked as abbreviations, if a string exceeds the specified width.
     * @param {CanvasRenderingContext2D} context
     * @param {string} text 
     * @param {Rect} rect 
     * @param {TextDrawingAttributes} textDrawingAttributes
     */
    drawTextByTruncatingTail: function(context, text, rect, textDrawingAttributes) {
        if(text == null) return;
        
        var maxWidth = rect.width;

        context.save();
        var color = this.defaultFontColor;
        if(textDrawingAttributes != undefined) {
            if(textDrawingAttributes.color != undefined) {
                color = textDrawingAttributes.color;
            }
        }
        context.fillStyle = color;
        if(textDrawingAttributes.fontExpression != undefined) {
            context.font = textDrawingAttributes.fontExpression();
        }else {
            var font = this.defaultFont;
            var fontSize = this.defaultFontSize;
            if(textDrawingAttributes != undefined) {
                if(textDrawingAttributes.font != undefined) {
                    font = textDrawingAttributes.font;
                }
                if(textDrawingAttributes.size != undefined) {
                    fontSize = textDrawingAttributes.size;
                }
                if(textDrawingAttributes.color != undefined) {
                    color = textDrawingAttributes.color;
                }
            }
            if(typeof fontSize == "number") {
                fontSize = fontSize + "px"
            }
            context.font = fontSize+" "+font;
        }
        context.textBaseline = "middle";

        var width = context.measureText(text).width;
        if(width > maxWidth) {
            for(var i=0; i<text.length; i++) {
                var _text = text.substr(0, text.length-(i+1)) + "...";
                width = context.measureText(_text).width;
                if(width <= maxWidth) {
                    text = _text;
                    break;
                }
            }
        }
        var x = rect.x;
        var y = rect.y+rect.height/2;
        if(textDrawingAttributes != undefined && textDrawingAttributes.align != undefined) {
            if(textDrawingAttributes.align == "center") {
                x = rect.x + rect.width/2;
            }else if(textDrawingAttributes.align == "right") {
                x = rect.x + rect.width;
            }
            context.textAlign = textDrawingAttributes.align;
        }

        context.fillText(text, x, y);
        context.restore();
    },

    /**
     * Draws a string to fit within a specified rectangle.
     * @param {CanvasRenderingContext2D} context 
     * @param {string} text 
     * @param {Rect} rect 
     * @param {TextDrawingAttributes} textDrawingAttributes 
     */
    drawTextByCharacterWrap: function(context, text, rect, textDrawingAttributes) {
        if(text == null || text.length == 0) return;

        var color = this.defaultFontColor;
        var lineHeight = this.defaultLineHeight;
        if(textDrawingAttributes != undefined) {
            if(textDrawingAttributes.color != undefined) {
                color = textDrawingAttributes.color;
            }
            if(textDrawingAttributes.lineHeight != undefined) {
                lineHeight = textDrawingAttributes.lineHeight;
            }
        }

        context.save();
        context.fillStyle = color;
        if(textDrawingAttributes.fontExpression != undefined) {
            context.font = textDrawingAttributes.fontExpression();
        }else {
            var font = this.defaultFont;
            var fontSize = this.defaultFontSize;
            if(textDrawingAttributes != undefined) {
                if(textDrawingAttributes.font != undefined) {
                    font = textDrawingAttributes.font;
                }
                if(textDrawingAttributes.size != undefined) {
                    fontSize = textDrawingAttributes.size;
                }
                if(textDrawingAttributes.color != undefined) {
                    color = textDrawingAttributes.color;
                }
            }
            if(typeof fontSize == "number") {
                fontSize = fontSize + "px"
            }
            context.font = fontSize+" "+font;
        }
        context.textBaseline = "middle";

        var x = rect.x;
        if(textDrawingAttributes != undefined && textDrawingAttributes.align != undefined) {
            if(textDrawingAttributes.align == "center") {
                x = rect.x + rect.width/2;
            }else if(textDrawingAttributes.align == "right") {
                x = rect.x + rect.width;
            }
            context.textAlign = textDrawingAttributes.align;
        }

        var lines = this.splitTextToLinesByCharacterWrap(context, text, {width:rect.width, height:rect.height}, lineHeight);
        var y = rect.y + (rect.height - (lines.length * lineHeight))/2;
        var halfLineHeight = lineHeight/2;
        for(var i=0; i<lines.length; i++) {
            context.fillText(lines[i], x, y+(lineHeight*i)+halfLineHeight);
        }
        context.restore();
    },

    /**
     * Get the size of a multi-line text enclosed.
     * @param {CanvasRenderingContext2D} context 
     * @param {string} text 
     * @param {Size} maxSize 
     * @param {TextDrawingAttributes} textDrawingAttributes
     * @returns {Size}
     */
    getBoundingRectOfTextByCharacterWrap: function(context, text, maxSize, textDrawingAttributes) {
        if(text == null) return null;

        var lineHeight = this.defaultLineHeight;
        if(textDrawingAttributes != undefined) {
            if(textDrawingAttributes.lineHeight != undefined) {
                lineHeight = textDrawingAttributes.lineHeight;
            }
        }

        context.save();

        if(textDrawingAttributes != undefined && textDrawingAttributes.fontExpression != undefined) {
            context.font = textDrawingAttributes.fontExpression();
        }else {
            var font = this.defaultFont;
            var fontSize = this.defaultFontSize;
            if(textDrawingAttributes != undefined) {
                if(textDrawingAttributes.font != undefined) {
                    font = textDrawingAttributes.font;
                }
                if(textDrawingAttributes.size != undefined) {
                    fontSize = textDrawingAttributes.size;
                }
            }
            if(typeof fontSize == "number") {
                fontSize = fontSize + "px"
            }
            context.font = fontSize+" "+font;
        }

        var lines = this.splitTextToLinesByCharacterWrap(context, text, maxSize, lineHeight);
        var size;
        if(lines.length == 1) {
            size = {width:context.measureText(text).width, height:lineHeight};
        }else {
            var maxWidth = 0;
            for(var i=0; i<lines.length; i++) {
                var width = context.measureText(lines[i]).width;
                if(maxWidth < width) {
                    maxWidth = width;
                }
            }
            size = {width:maxWidth, height:(lineHeight*lines.length)};
        }

        context.restore();
        return size;
    },

    splitTextToLinesByCharacterWrap: function(context, text, size, lineHeight) {
        if(text == null) return null;
        var lines = [];

        var specialCharacters = text.match(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g);
        var specialCharacterLengthes = [];
        var specialCharacterIndexes = [];
        var _text = text;
        if(specialCharacters != null) {
            for(var i=0; i<specialCharacters.length; i++) {
                var specialCharacter = specialCharacters[i];
                var length = specialCharacter.length;
                var index = _text.indexOf(specialCharacter);
                specialCharacterLengthes.push(length);
                specialCharacterIndexes.push(index);
                if(index+length+1 < _text.length) {
                    _text = _text.substr(index+length+1);
                }
            }
        }

        function divideToLine() {
            for(var i=0; i<text.length; i++) {
                var charLength = 1;

                var offset = 0;
                for(var j=0; j<lines.length; j++) {
                    offset += lines[j].length;
                }
                var specialCharacterIndex = specialCharacterIndexes.indexOf(offset+i);
                if(specialCharacterIndex != -1) {
                    charLength = specialCharacterLengthes[specialCharacterIndex];
                }

                var _text = text.substr(0, (i+charLength));
                var width = context.measureText(_text).width;
                if(width > size.width) {
                    if(lineHeight*(lines.length+1) <= size.height) {
                        lines.push(text.substr(0, i));
                        if(i < text.length) {
                            text = text.substr(i, text.length-i);
                            divideToLine();
                        }
                    }
                    break;
                }
                if(i+charLength == text.length) {
                    lines.push(text.substr(0, text.length));
                }
                if(charLength>1) {
                    i += charLength-1;
                }
            }
        }
        divideToLine();
        return lines;
    },

    /**
     * Drawing a rounded rectangle callout.
     * @type {function(CanvasRenderingContext2D, Rect, object): void}
     * @param {CanvasRenderingContext2D} context
     * @param {Rect} rect
     * @param {Object} settings
     * @param {top|bottom|left|right} settings.direction 
     * @param {Size} settings.tipSize 
     * @param {number} settings.tipOffset Position of the balloon's tail (if not specified, middle).
     * @param {string} settings.fillColor
     * @param {boolean} settings.shadow
     */
    drawRoundRectBalloon: function(context, rect, settings) {
        var direction = "top";
        var tipSize = 8;
        var tipOffset = undefined;
        var fillColor = "white";
        var shadow = false
        if(settings != undefined) {
            if(settings.direction != undefined) {
                direction = settings.direction;
            }
            if(settings.tipSize != undefined) {
                tipSize = settings.tipSize;
            }
            if(settings.tipOffset != undefined) {
                tipOffset = settings.tipOffset;
            }
            if(settings.fillColor != undefined) {
                fillColor = settings.fillColor;
            }
            if(settings.shadow != undefined) {
                shadow = settings.shadow;
            }
        }
        var cornerSize = 4;
        var point = {x: rect.x + cornerSize, y: rect.y};
        context.save();
        context.beginPath();
        if(direction == "top") {
            point.y = rect.y + tipSize;
        }else if(direction == "left") {
            point.x += tipSize;
        }
        context.moveTo(point.x, point.y);
        if(direction == "top") {
            if(tipOffset === undefined || tipOffset === null) {
                point.x = rect.x + rect.width/2 - tipSize/2;
            }else {
                point.x = rect.x + tipOffset - tipSize/2;
            }
            context.lineTo(point.x, point.y);
            point.x += tipSize/2; point.y -= tipSize;
            context.lineTo(point.x, point.y);
            point.x += tipSize/2; point.y += tipSize;
            context.lineTo(point.x, point.y);
        }
        point.x = rect.x + rect.width - cornerSize;
        if(direction == "right") {
            point.x-= tipSize;
        }
        context.lineTo(point.x, point.y);
        point.x += cornerSize;
        context.quadraticCurveTo(point.x, point.y, point.x, point.y+cornerSize);
        if(direction == "right") {
            if(tipOffset === undefined || tipOffset === null) {
                point.y = rect.y + rect.height/2 - tipSize/2;
            }else {
                point.y = rect.y + tipOffset - tipSize/2;
            }
            context.lineTo(point.x, point.y);
            point.x += tipSize; point.y += tipSize/2;
            context.lineTo(point.x, point.y);
            point.x -= tipSize; point.y += tipSize/2;
            context.lineTo(point.x, point.y);
        }
        point.y = rect.y + rect.height - cornerSize;
        if(direction == "bottom") {
            point.y -= tipSize;
        }
        context.lineTo(point.x, point.y);
        point.y += cornerSize;
        context.quadraticCurveTo(point.x, point.y, point.x-cornerSize, point.y);
        if(direction == "bottom") {
            if(tipOffset === undefined || tipOffset === null) {
                point.x = rect.x + rect.width/2 + tipSize/2;
            }else {
                point.x = rect.x + tipOffset + tipSize/2;
            }
            context.lineTo(point.x, point.y);
            point.x -= tipSize/2; point.y += tipSize;
            context.lineTo(point.x, point.y);
            point.x -= tipSize/2; point.y -= tipSize;
            context.lineTo(point.x, point.y);
        }
        point.x = rect.x + cornerSize;
        if(direction == "left") {
            point.x += tipSize;
        }
        context.lineTo(point.x, point.y);
        point.x -= cornerSize;
        context.quadraticCurveTo(point.x, point.y, point.x, point.y-cornerSize);
        if(direction == "left") {
            if(tipOffset === undefined || tipOffset === null) {
                point.y = rect.y + rect.height/2 + tipSize/2;
            }else {
                point.y = rect.y + tipOffset + tipSize/2;
            }
            context.lineTo(point.x, point.y);
            point.x -= tipSize; point.y -= tipSize/2;
            context.lineTo(point.x, point.y);
            point.x += tipSize; point.y -= tipSize/2;
            context.lineTo(point.x, point.y);
        }
        point.y = rect.y + cornerSize;
        if(direction == "top") {
            point.y += tipSize;
        }
        context.lineTo(point.x, point.y);
        point.y -= cornerSize;
        context.quadraticCurveTo(point.x, point.y, point.x+cornerSize, point.y);
        context.closePath();
        context.fillStyle = fillColor;
        if(shadow) {
            context.shadowBlur = 16;
            context.shadowOffsetX = 3;
            context.shadowOffsetY = 3;
            context.shadowColor = "rgba(0,0,0,0.5)";
        }
        context.fill();
        context.restore();
    }
};

/**
 * Image utility
 * @namespace
 */
var ImageUtil = {

    /**
     * Load image.
     * @param {string} filePathe 
     * @returns {Promise} can be used to get an Image objects by Promise#then. 
     */
    loadImage: function(filePath) {
        return new Promise(function(resolve, reject) {
            function loadHandler(event) {
                var image = event.currentTarget;
                resolve(image);
                image.removeEventListener("load", loadHandler);
            }
            function errorHandler(event) {
                reject();
            }
            var image = new Image();
            image.addEventListener("load", loadHandler);
            image.addEventListener("error", errorHandler);
            image.src = filePath;
            if(image.complete) {
                image.removeEventListener("load", loadHandler);
                image.removeEventListener("error", errorHandler);
                resolve(image);
            }
        });
    },

    /**
     * Load several images.
     * @param {Array.<string>} filePathes 
     * @returns {Promise} can be used to get an array of Image objects by Promise#then. 
     */
    loadImages: function(filePathes) {
        if(filePathes == null || !Array.isArray(filePathes)) {
            return;
        }
        var processes = [];
        for(var i=0; i<filePathes.length; i++) {
            processes.push(this.loadImage(filePathes[i]));
        }
        return Promise.all(processes);
    },

    /**
     * Load CANVAS element from the image data. 
     * @param {File} file
     * @param {Size} size 
     * @param {function(HTMLCanvasElement): void} completeHandler 
     */
    loadImageData: function(file, size, completeHandler) {
        if(FileReader == undefined) {
            console.error("This browser is not supported FileReader.");
            return;
        }
        var fileReader = new FileReader();
        fileReader.onload = function () {
            var image = new Image();
            image.src = fileReader.result;
            image.onload = function() {
                var canvas = document.createElement("canvas");
                canvas.style.display = "none";
                ImageUtil.drawImage(image, canvas, size);
                completeHandler(canvas);
            };
        };
        fileReader.readAsDataURL(file);
    },

    /**
     * Draws an image on the canvas based on the specified information.
     * @param {Image} image 
     * @param {HTMLCanvasElement} canvas 
     * @param {Size} size 
     * @param {2|3|4|5|6|7|8} orientation Image rotation
     *   2: Flip horizontal, 
     *   3: Rotate 180 degrees counterclockwise, 
     *   4: Flip vertical, 
     *   5: Flip vertical + rotate 90 degrees clockwise,
     *   6: 90 degree clockwise rotation, 
     *   7: Horizontal Flip + 90° Clockwise Rotation, 
     *   8: 90° Clockwise Rotation
     */
    drawImage: function(image, canvas, size, orientation) {
        var context = canvas.getContext("2d");
        context.save();

        var sourceRect = {x:0, y:0, width:image.width, height:image.height};

        canvas.width = sourceRect.width;
        canvas.height = sourceRect.height;

        if(size !== undefined) {
            if(sourceRect.height > sourceRect.width) {
                var height = sourceRect.width * (size.height/size.width);
                sourceRect.y = sourceRect.height/2 - height/2;
                sourceRect.height = height;
            }else {
                var width = sourceRect.height * (size.width/size.height);
                sourceRect.x = sourceRect.width/2 - width/2;
                sourceRect.width = width;
            }

            canvas.width = size.width;
            canvas.height = size.height;
        }

        if(orientation !== undefined) {
            if(orientation > 4) {
                var canvasWidth = canvas.width;
                canvas.width = canvas.height;
                canvas.height = canvasWidth;
            }

            switch(orientation) {
                case 2:
                    context.translate(canvas.width, 0);
                    context.scale(-1, 1);
                    break;
                case 3:
                    context.translate(canvas.width, canvas.height);
                    context.rotate(Math.PI);
                    break;
                case 4:
                    context.translate(0, canvas.height);
                    context.scale(1, -1);
                    break;
                case 5:
                    context.rotate(0.5 * Math.PI);
                    context.scale(1, -1);
                    break;
                case 6:
                    context.rotate(0.5 * Math.PI);
                    context.translate(0, -canvas.height);
                    break;
                case 7:
                    context.rotate(0.5 * Math.PI);
                    context.translate(canvas.width, -canvas.height);
                    context.scale(-1, 1);
                    break;
                case 8:
                    context.rotate(-0.5 * Math.PI);
                    context.translate(canvas.width, 0);
                    break;
            }
        }

        context.drawImage(image, sourceRect.x, sourceRect.y, sourceRect.width, sourceRect.height, 0, 0, canvas.width, canvas.height);

        context.restore();
    },

    /**
     * Convert CANVAS display data to binary. Return value can be set to FormData object.
     * @param {HTMLCanvasElement} canvas 
     * @param {string} format MIME types (image/jpeg、image/png...)
     * @return {Blob} binary data
     */
    convertCanvasToBinary: function(canvas, format) {
        var dataUrl = canvas.toDataURL(format);
        var data = dataUrl.split(',');
        var header = data[0];
        var body = data[1];

        var byteString;
        if(header.indexOf('base64') >= 0) {
            byteString = atob(body);
        }else {
            byteString = unescape(body);
        }
        var buffer = new Uint8Array(byteString.length);
        for(var i = 0; i < byteString.length; i++) {
            buffer[i] = byteString.charCodeAt(i);
        }
        
        var mimeType = header.split(':')[1].split(';')[0];

        return new Blob([buffer], {type:mimeType});
    },

    /**
     * Convert Image data to Base64 representation.
     * @param {Image} image
     * @returns {string}
     */
    convertImageToBase64String: function(image) {
        var canvas = document.createElement("canvas");
        canvas.style.display = "none";
        ImageUtil.drawImage(image, canvas);
        return canvas.toDataURL();
    }
};

/**
 * UI event utility
 * @namespace
 */
var UIEventUtil = {

    /**
     * Obtaining the position of mouse or touch occurrence from UI events
     * @param {UIEvent} event
     * @return {Point} Pointer position in case of mouse event, array of this object in case of touch event
     */
    getLocation: function(event) {
        var clientRect, x, y;
        if(event.type == "click" || event.type == "dblclick" || event.type.indexOf("mouse") == 0) {
            clientRect = event.currentTarget.getBoundingClientRect();
            x = event.clientX - clientRect.left;
            y = event.clientY - clientRect.top;
            return {x:x, y:y};
        }
        else if(event.type.indexOf("touch") == 0) {
            clientRect = event.currentTarget.getBoundingClientRect();
            if(event.touches.length > 0) {
                x = event.touches[0].clientX - clientRect.left;
                y = event.touches[0].clientY - clientRect.top;
                return {x:x, y:y};
            }else {
                if(event.changedTouches.length > 0) {
                    x = event.changedTouches[0].clientX - clientRect.left;
                    y = event.changedTouches[0].clientY - clientRect.top;
                    return {x:x, y:y};
                }else {
                    return {x:-1, y:-1};
                }
            }
        }
    },

    /**
     * Set a tap handler for a specified HTML element (PC/tablet compatible)
     * @param {HTMLElement} element
     * @param {Object} settings
     * @param {function(UIEvent, ?Object)} [settings.touchBegan] The second argument is Object that can be set arbitrarily.
     * @param {function(UIEvent, ?Object)} [settings.touchMove] The second argument is Object that can be set arbitrarily.
     * @param {function(UIEvent, ?Object, boolean)} [settings.touchEnd] The second argument is Object that can be set arbitrarily. The third argument is whether or not the touch event has completed.
     * @param {boolean} [settings.touchBeganCancellable] Whether or not the touch began event can be cancell by UIEvent#preventDefault.
     * @param {boolean} [settings.touchMoveCancellable] Whether or not the touch began event can be cancell by UIEvent#preventDefault.
     * @param {boolean} [settings.touchEndCancellable] Whether or not the touch began event can be cancell by UIEvent#preventDefault.
     */
    handleTouch: function(element, settings) {
        var context;
        if("ontouchstart" in document.documentElement) {
            element.addEventListener("touchstart", function(event) {
                context = {};
                if(settings.touchBegan != undefined) {
                    settings.touchBegan(event, context);
                }
            }, {passive: (settings.touchBeganCancellable != undefined ? settings.touchBeganCancellable : true)});
            if(settings.touchMove != undefined) {
                element.addEventListener("touchmove", function(event) {
                    settings.touchMove(event, context);
                }, {passive: (settings.touchMoveCancellable != undefined ? settings.touchMoveCancellable : true)});
            }
            element.addEventListener("touchend", function(event) {
                if(settings.touchEnd != undefined) {
                    settings.touchEnd(event, context, true);
                }
                context = undefined;
            }, {passive: (settings.touchEndCancellable != undefined ? settings.touchEndCancellable : true)});
            element.addEventListener("touchcancel", function(event) {
                if(settings.touchEnd != undefined) {
                    settings.touchEnd(event, context, false);
                }
                context = undefined;
            }), {passive: (settings.touchEndCancellable != undefined ? settings.touchEndCancellable : true)};
        }else {
            element.addEventListener("mousedown", function(event) {
                context = {}; 
                if(settings.touchBegan != undefined) {
                    settings.touchBegan(event, context);
                }
            }); 
            if(settings.touchMove != undefined) {
                element.addEventListener("mousemove", function(event) {
                    settings.touchMove(event, context);
                });
            }
            element.addEventListener("mouseup", function(event) {
                if(settings.touchEnd != undefined) {
                    settings.touchEnd(event, context, true);
                }
                context = undefined;
            });
            element.addEventListener("mouseleave", function (event) {
                if(settings.touchEnd != undefined) {
                    settings.touchEnd(event, context, false);
                }
                context = undefined;
            });
        }
    },

    /**
     * Configure the ENTER key to move between input UIs.
     * @param {HTMLElement} element 
     */
    setEnterKeyChain: function(element) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return;
        }
        var selector = "input, select, button";
        var inputList = element.querySelectorAll(selector);
        for(var i=0; i<inputList.length; i++) {
            var inputElement = inputList[i];
            inputElement.addEventListener("keypress", function(event) {
                var enterKeyPressed = false;
                if(event.keyCode != undefined) {
                    enterKeyPressed = event.keyCode == 13;
                }else if(event.code != undefined) {
                    enterKeyPressed = event.keyCode == "Enter";
                }
                if(enterKeyPressed) {
                    var index = -1;
                    for(var i=0; i<inputList.length; i++) {
                        if(inputList[i] === event.currentTarget) {
                            index = i;
                            break;
                        }
                    }
                    if(index+1 == inputList.length) {
                        index = 0;
                    }else {
                        index = index+1;
                    }
                    var nextInput = inputList[index];
                    if(nextInput.tagName.toLowerCase() == "button") {
                        nextInput.dispatchEvent(new MouseEvent("click"));
                    }else {
                        nextInput.focus();
                    }
                    event.preventDefault();
                }
            });
        }
    }
};

/**
 * Input validation utility
 * @namespace
 */
var ValidationUtil = {

    /**
     * Validates the input UI contained in the specified HTML element, and displays a browser-standard error if there is a validation error. The validation content follows the various attributes of the input tag.
     * @param {HTMLInputElement|HTMLTextAreaElement} element
     * @returns {boolean}
     */
    validate: function(element) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return;
        }
        var result = true;
        function resetReport(event) {
            var inputElement = event.currentTarget;
            inputElement.setCustomValidity("");
            inputElement.removeEventListener("input", resetReport);
        }
        var inputElements = element.querySelectorAll("input, select, textarea");
        for(var i=0; i<inputElements.length; i++) {
            var inputElement = inputElements[i];
            if(result && !inputElement.checkValidity()) {
                if(inputElement.reportValidity != undefined) {
                    var message = inputElement.getAttribute("title");
                    if(message != null) {
                        inputElement.setCustomValidity(message);
                    }
                    inputElement.reportValidity();
                    inputElement.addEventListener("input", resetReport);
                }
                result = false;
            }
        }
        return result;
    }
};

/**
 * HTML element utility
 * @namespace
 */
var HtmlElementUtil = {

    offset: function(element, parentElement) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return null;
        }

        if(parentElement == undefined) {
            parentElement = document.querySelector("body");
        }else {
            if(!(parentElement instanceof HTMLElement)) {
                console.error("Element is not HTMLElement.", (parentElement != null && parentElement.toString != undefined ? parentElement.toString() : "null"));
                return null;
            }
        }

        function acquireOffset(offset, parent) {
            if(parent == null || parent === parentElement) {
                return offset;
            }
            offset.left += parent.offsetLeft;
            offset.top += parent.offsetTop;
            return acquireOffset(offset, parent.offsetParent);
        }
        return acquireOffset({left: element.offsetLeft, top: element.offsetTop}, element.offsetParent);
    },

    size: function(element) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return null;
        }

        var size = {width: 0, height: 0};
        var paddingLeft = element.style.getPropertyValue("padding-left");
        if(paddingLeft != null) {
            if(typeof paddingLeft == "string" && paddingLeft.endsWith("px")) {
                paddingLeft = Number(paddingLeft.substring(0, paddingLeft.length-2));
            }
        }else {
            paddingLeft = 0;
        }
        var paddingRight = element.style.getPropertyValue("padding-right");
        if(paddingRight != null) {
            if(typeof paddingRight == "string" && paddingRight.endsWith("px")) {
                paddingRight = Number(paddingRight.substring(0, paddingRight.length-2));
            }
        }else {
            paddingRight = 0;
        }
        var paddingTop = element.style.getPropertyValue("padding-top");
        if(paddingTop != null) {
            if(typeof paddingTop == "string" && paddingTop.endsWith("px")) {
                paddingTop = Number(paddingTop.substring(0, paddingTop.length-2));
            }
        }else {
            paddingTop = 0;
        }
        var paddingBottom = element.style.getPropertyValue("padding-bottom");
        if(paddingBottom != null) {
            if(typeof paddingBottom == "string" && paddingBottom.endsWith("px")) {
                paddingBottom = Number(paddingBottom.substring(0, paddingBottom.length-2));
            }
        }else {
            paddingBottom = 0;
        }
        size.width = element.clientWidth - (paddingLeft + paddingRight);
        size.height = element.clientHeight - (paddingTop + paddingBottom);
        return size;
    },

    scrollOffset: function(element) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return;
        }
        function retrieve(element, context) {
            var result = Point(context.x+element.scrollLeft, context.y+element.scrollTop);
            if(element.parentElement == null || element.tagName == "BODY") {
                return result;
            }else {
                return retrieve(element.parentElement, result);
            }
        }
        return retrieve(element, Point());
    },

    defineStyles: function(element, styles) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return;
        }
        if(typeof styles != "object") {
            console.error("The definition is not JSON.", (styles != null && styles.toString != undefined ? styles.toString() : "null"));
            return;
        }
        function setProperties(element, styles) {
            var keys = Object.keys(styles);
            for(var i=0; i<keys.length; i++) {
                var key = keys[i];
                var value = styles[key];
                var priority = undefined;
                if(Array.isArray(value)) {
                    var expression = "";
                    for(var j=0; j<value.length; j++) {
                        if(j>0) {
                            expression += " ";
                        }
                        if(typeof value[j] == "number") {
                            expression += value[j]+"px";
                        }else {
                            expression += value[j];
                        }
                    }
                    element.style.setProperty(key, expression);
                }else if(typeof value != "object") {
                    if(typeof value == "number") {
                        value = value+"px";
                    }else if(typeof value == "string") {
                        if(value.endsWith("!important")) {
                            priority = "important";
                            value = value.substr(0, value.length-10);
                        }
                    }
                    if(key == "background-image" && typeof value == "string" && value != "none" && !value.endsWith(")")) {
                        value = "url('"+value+"')"
                    }
                    element.style.setProperty(key, value, priority);
                    if(key == "user-select") {
                        element.style.setProperty("-webkit-"+key, value, priority);
                        element.style.setProperty("-moz-"+key, value, priority);
                        element.style.setProperty("-ms-"+key, value, priority);
                    }
                }else {
                    var innerElements = element.querySelectorAll(key);
                    if(innerElements != null) {
                        for(var k=0; k<innerElements.length; k++) {
                            var innerElement = innerElements[k];
                            setProperties(innerElement, value);
                        }
                    }
                }
            }
        }
        setProperties(element, styles);
    },

    replaceChildren: function(element, childNode) {
        if(!(element instanceof HTMLElement)) {
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return;
        }
        for(var i=0; i<element.children.length; i++) {
            element.children[i].remove();
        }
        element.appendChild(childNode);
    }
};

/**
 * UIs
 * @namespace
 */
var Controls = {

    /**
     * Create compatible select element. It absorbs differences in expression between operating systems and browsers.
     * @type {function(HTMLElement, object): object}
     * 
     * @param {HTMLElement} element
     * 
     * @param {Object} settings 
     * @param {Array} settings.items 
     * @param {number} settings.selectedIndex 
     * @param {number} settings.itemWidth 
     * @param {number} settings.itemHeight 
     * @param {function(any):string} [settings.labelHandler] The argument is a element of settings.items.
     * @param {function(any, boolean):object} [settings.styleHandler] The argument is a element of settings.items.
     * @param {function(any): HTMLDivElement} [settings.itemHandler] The argument is a element of settings.items.
     * @param {function(HTMLDivElement, any): void} [settings.itemDrawer] The second argument is a element of settings.items.
     * @param {function(number): void} [settings.selectHandler] The argument is selected index of settings.items.
     * @param {function():void} [settings.closeHandler] 
     * @param {boolean} [settings.selectedDrawing=true] 
     * @param {boolean} [settings.editable=true] 
     * @param {number} [settings.zIndex] 
     * 
     * @returns {Object} interface
     * @returns {number} interface.selectedIndex 
     * @returns {function(): void} interface.show 
     * @returns {function(): void} interface.close 
     */
    Select: function(element, settings) {
        if(!(element instanceof HTMLElement)) { 
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return; 
        }

        var reference = {
            items: undefined,
            selectedIndex: -1,
            selectHandler: undefined,
            editable: true
        };

        var parent;
        var itemWidth = 120;
        var itemHeight = 32;
        var backgroundColor = "white";
        var borderColor = "rgba(0,0,0,0.3)";
        var zIndex;

        var itemDrawer;
        var labelHandler;
        var styleHandler;
        var itemHandler;
        var hoverStyleHandler;

        var closeHandler;

        var selectedDrawing = true;
        var animate = true;
        var hilighting = false;

        if(settings != undefined) {
            if(settings.items != undefined) {
                reference.items = settings.items;
            }
            if(settings.parent != undefined && settings.parent instanceof HTMLElement) {
                parent = settings.parent;
            }
            if(settings.itemWidth != undefined) {
                itemWidth = settings.itemWidth;
            }
            if(settings.itemHeight != undefined) {
                itemHeight = settings.itemHeight;
            }
            if(settings.backgroundColor != undefined) {
                backgroundColor = settings.backgroundColor;
            }
            if(settings.borderColor != undefined) {
                borderColor = settings.borderColor;
            }
            if(settings.itemDrawer != undefined) {
                itemDrawer = settings.itemDrawer;
            }
            if(settings.labelHandler != undefined) {
                labelHandler = settings.labelHandler;
            }
            if(settings.styleHandler != undefined) {
                styleHandler = settings.styleHandler;
            }
            if(settings.itemHandler != undefined) {
                itemHandler = settings.itemHandler;
            }
            if(settings.hoverStyleHandler != undefined) {
                hoverStyleHandler = settings.hoverStyleHandler;
            }
            if(settings.selectHandler != undefined) {
                reference.selectHandler = settings.selectHandler;
            }
            if(settings.closeHandler != undefined) {
                closeHandler = settings.closeHandler;
            }
            if(settings.selectedDrawing != undefined) {
                selectedDrawing = settings.selectedDrawing;
            }
            if(settings.animate != undefined) {
                animate = settings.animate;
            }
            if(settings.hilighting != undefined) {
                hilighting = settings.hilighting;
            }
            if(settings.editable != undefined) {
                reference.editable = settings.editable;
            }
            if(settings.zIndex != undefined) {
                zIndex = settings.zIndex;
            }
        }
        if(parent == undefined) {
            parent = document.querySelector("body");
        }
        if(hilighting && hoverStyleHandler == undefined) {
            hoverStyleHandler = function() {
                return {
                    "background-color": "whitesmoke"
                };
            }
        }

        element.style.setProperty("cursor", "pointer");

        var parentOffset = HtmlElementUtil.offset(parent);

        var selection = document.createElement("div");
        selection.classList.add("selection");
        selection.style.setProperty("visibility", "hidden");
        selection.style.setProperty("position", "absolute");
        selection.style.setProperty("overflow", "auto");
        selection.style.setProperty("-ms-overflow-style", "none");
        selection.style.setProperty("-webkit-overflow-scrolling", "touch");
        selection.style.setProperty("scrollbar-width", "none");
        selection.style.setProperty("background-color", backgroundColor);
        selection.style.setProperty("box-shadow", "3px 3px 16px rgba(0,0,0,0.3)");
        selection.style.setProperty("border", "1px solid " + borderColor);
        selection.style.setProperty("cursor", "pointer");
        selection.style.setProperty("user-select", "none");
        if(itemDrawer != null) {
            selection.style.setProperty("width", itemWidth+"px");
            selection.style.setProperty("line-height", "0px");
        }
        if(zIndex != null) {
            selection.style.setProperty("z-index", zIndex);
        }

        var mask = document.createElement("div");
        mask.style.setProperty("position", "absolute");
        mask.style.setProperty("left", parentOffset.left+"px");
        mask.style.setProperty("top", parentOffset.top+"px");
        mask.style.setProperty("width", "100%");
        mask.style.setProperty("height", "100%");
        mask.style.setProperty("background-color", "transparent");
        if(zIndex != null) {
            mask.style.setProperty("z-index", zIndex);
        }

        function createItem(item, current) {
            var itemElement;
            if(labelHandler != undefined) {
                var label = labelHandler(item);
                itemElement = document.createElement("div");
                itemElement.classList.add("item");
                itemElement.innerText = label;
                itemElement.style.setProperty("position", "relative");
                itemElement.style.setProperty("height", itemHeight+"px");
                itemElement.style.setProperty("line-height", itemHeight+"px");
                itemElement.style.setProperty("white-space", "nowrap");
                itemElement.style.setProperty("font-size", "1em");
                itemElement.style.setProperty("cursor", "pointer");

                if(styleHandler != undefined) {
                    var styles = styleHandler(item, current);
                    if(styles != null && typeof styles == "object") {
                        itemElement.styles = styles;
                    }
                }

                if(hoverStyleHandler != undefined && !current) {
                    var hoverStyles = hoverStyleHandler();
                    var originalStyles = {};
                    var hoverStyleKeys = Object.keys(hoverStyles);
                    for(var i=0; i<hoverStyleKeys.length; i++) {
                        var key = hoverStyleKeys[i];
                        originalStyles[key] = itemElement.style.getPropertyValue(key);
                    }
                    itemElement.addEventListener("mouseover", function() {
                        var hoverStyleKeys = Object.keys(hoverStyles);
                        for(var i=0; i<hoverStyleKeys.length; i++) {
                            var key = hoverStyleKeys[i];
                            itemElement.style.setProperty(key, hoverStyles[key]);
                        }
                    });
                    itemElement.addEventListener("mouseout", function() {
                        var originalStyleKeys = Object.keys(originalStyles);
                        for(var i=0; i<originalStyleKeys.length; i++) {
                            var key = originalStyleKeys[i];
                            itemElement.style.setProperty(key, originalStyles[key]);
                        }
                    });
                }
            }else if(itemHandler != undefined) {
                itemElement = itemHandler(item);
                if(!itemElement.classList.contains("item")) {
                    itemElement.classList.add("item");
                }
            }else if(itemDrawer != undefined) {
                itemElement = document.createElement("canvas");
                CanvasUtil.initCanvas(itemElement, {width: itemWidth, height: itemHeight});
                itemDrawer(itemElement, item);
            }
            return itemElement;
        }

        function drawSelectedItem() {
            if(!selectedDrawing) return;
            if(reference.items == null || reference.items.length == 0) {
                return;
            }
            if(reference.selectedIndex < 0 || reference.selectedIndex >= reference.items.length) {
                var _contentElement = element.querySelector("div.item");
                if(_contentElement != null) {
                    _contentElement.remove();
                }
                return;
            }
            var item = reference.items[reference.selectedIndex];
            if(item == null) return;
            if(labelHandler != undefined) {
                var label = labelHandler(item);
                var contentElement = element.querySelector("div.item");
                if(contentElement == null) {
                    contentElement = createItem(item, true);
                    element.appendChild(contentElement);
                }
                contentElement.innerText = label != null ? label : "";
                if(styleHandler != undefined) {
                    var styles = styleHandler(item, true);
                    if(styles != null && typeof styles == "object") {
                        contentElement.styles = styles;
                    }
                }
            }else if(itemHandler != undefined) {
                var itemlement = element.querySelector("div.item");
                if(itemlement != null) {
                    itemlement.remove();
                }
                itemlement = createItem(item, true);
                element.appendChild(itemlement);
            }else if(itemDrawer != undefined) {
                itemDrawer(element.querySelector("canvas"), item);
            }
        }

        function loadItems() {
            selection.childNodes.forEach(function(itemElement, index) {
                itemElement.remove();
            });
            for(var i=0; i<reference.items.length; i++) {
                var item = reference.items[i];
                var itemElement = createItem(item);
                itemElement.setAttribute("tabIndex", i);
                selection.appendChild(itemElement);
            }
            if(reference.editable) {
                var elementSelector;
                if(labelHandler != undefined || itemHandler != undefined) {
                    elementSelector = "div.item";
                }else if(itemDrawer != undefined) {
                    elementSelector = "canvas";
                }
                selection.querySelectorAll(elementSelector).forEach(function(itemElement, index) {
                    itemElement.addEventListener("click", function(event) {
                        reference.selectedIndex = index;
                        drawSelectedItem();
                        if(reference.selectHandler != undefined) {
                            reference.selectHandler(reference.selectedIndex, reference);
                        }
                        reference.close();
                    });
                });
            }
        }

        function showSelection() {
            if(reference.items == null || reference.items.length == 0) {
                return;
            }
            loadItems();

            parent.appendChild(mask);
            parent.appendChild(selection);

            var offset = HtmlElementUtil.offset(element, parent);
            var scrollOffset = HtmlElementUtil.scrollOffset(element);
            var x = offset.left;
            if(x + selection.clientWidth > parent.clientWidth) {
                x = parentOffset.left + parent.clientWidth - selection.clientWidth;
            }
            var y = offset.top + element.clientHeight - scrollOffset.y;
            var maxHeight = parentOffset.top + parent.clientHeight - y - 16;
            if(maxHeight < 120) {
                y = offset.top - selection.clientHeight - scrollOffset.y;
                maxHeight = offset.top - parentOffset.top - 16;
            }
            selection.style.setProperty("left", x+"px");
            selection.style.setProperty("top", y+"px");
            selection.style.setProperty("max-height", maxHeight+"px");

            if(animate) {
                var height = selection.offsetHeight;
                var visibleItemSize = Math.ceil(selection.clientHeight / itemHeight);

                selection.childNodes.forEach(function(selectionItem, index) {
                    selectionItem.style.setProperty("opacity", 0);
                    if(index < visibleItemSize) {
                        selectionItem.style.setProperty("top", "32px");
                    }else {
                        selectionItem.style.setProperty("top", "0px");
                    }
                });
                selection.style.setProperty("height", 0);
                selection.style.setProperty("visibility", "visible");
                selection.style.setProperty("overflow", "hidden");

                var fadeinDelay = 200;
                var slideinDelay = 50;

                new StyleAnimation(selection, "height", {finishValue: height, duration: fadeinDelay}).start().finish(function() {
                    var timing = 0;
                    var lastItemIndex = selection.childNodes.length-1;
                    selection.childNodes.forEach(function(selectionItem, index) {
                        var lastItem = index == lastItemIndex;
                        if(index < visibleItemSize) {
                            setTimeout(function() {
                                new FunctionalAnimation(function(progress) {
                                    selectionItem.style.setProperty("opacity", progress);
                                    selectionItem.style.setProperty("top", (16*(1-progress))+"px");
                                }, FunctionalAnimation.methods.linear, slideinDelay).start().finish(function() {
                                    if(lastItem) {
                                        selection.style.setProperty("overflow", "auto");
                                    }
                                });
                            }, timing);
                            timing += slideinDelay;
                        }else {
                            selectionItem.style.setProperty("opacity", 1);
                            if(lastItem) {
                                selection.style.setProperty("overflow", "auto");
                            }
                        }
                    });
                });
            }else {
                selection.style.setProperty("visibility", "visible");
            }
        }
    
        Object.defineProperty(reference, "selectedIndex", { 
            get: function() {
                return this._selectedIndex;
            },
            set: function(newValue) {
                this._selectedIndex = newValue;
                drawSelectedItem();
            }
        });

        if(settings != undefined && 
            settings.items != undefined && 
            settings.selectedIndex != undefined && 
            settings.selectedIndex >= 0 && 
            settings.selectedIndex < settings.items.length) {
            reference.selectedIndex = settings.selectedIndex;
        }
        
        if(selectedDrawing) {
            drawSelectedItem();
        }

        element.addEventListener("click", function() {
            if(!reference.editable) return;
            showSelection();
        });

        element.addEventListener("keydown", function(event) {
            if(event.code == "Space" || event.code == "ArrowDown" || event.code == "ArrowUp") {
                if(selection.style.visibility == "hidden") {
                    event.currentTarget.dispatchEvent(new MouseEvent("click"));
                    event.preventDefault();
                }else {
                    var items = selection.querySelectorAll(".item");
                    if(items.length > 0) {
                        items[0].focus();
                    }
                }
            }
        });

        selection.addEventListener("keydown", function(event) {
            var selection = event.currentTarget;
            var items = selection.querySelectorAll(".item");
            var i;
            if(event.code == "Enter" || event.code == "Space") {
                if(items.length > 0) {
                    if(document.activeElement != null) {
                        for(i=0; i<items.length; i++) {
                            if(items[i] == document.activeElement) {
                                items[i].dispatchEvent(new MouseEvent("click"));
                                event.preventDefault();
                                element.focus();
                                break;
                            }
                        }
                    }
                }
            }else if(event.code == "ArrowDown" || event.code == "ArrowUp") {
                if(items.length > 0) {
                    if(document.activeElement == null) {
                        items[0].focus();
                    }else {
                        for(i=0; i<items.length; i++) {
                            if(items[i] == document.activeElement) {
                                if(event.code == "ArrowDown") {
                                    if(i < items.length-1) {
                                        items[i+1].focus();
                                    }else {
                                        items[0].focus();
                                    }
                                }else {
                                    if(i > 0) {
                                        items[i-1].focus();
                                    }else {
                                        items[items.length-1].focus();
                                    }
                                }
                                break;
                            }
                        }
                    }
                }
            }
        });

        reference.show = function() {
            showSelection();
        };
        reference.close = function() {
            mask.remove();
            selection.style.setProperty("overflow", "hidden");
            new StyleAnimation(selection, "height", {finishValue: 0, duration: 200}).start().finish(function() {
                selection.remove();
                selection.style.setProperty("visibility", "hidden");
                selection.style.removeProperty("height");
                selection.style.setProperty("overflow", "auto");
            });
        };

        mask.addEventListener("click", function() {
            reference.close();
            if(closeHandler != undefined) {
                closeHandler(reference);
            }
        });

        return reference;
    },

    /**
     * Show popup dialog.
     * @param {HTMLElement} element
     * @returns {Object} interface
     * @returns {function(void): void} interface.close
     */
    Popup: function(element, settings) {
        if(!(element instanceof HTMLElement)) { 
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return; 
        }

        var duration = 200;
        if(settings != undefined) {
            if(settings.duration != undefined) {
                duration = settings.duration;
            }
        }
        
        var parent = document.querySelector("body");

        element.style.setProperty("position", "absolute");
        element.style.setProperty("box-shadow", "3px 3px 12px rgba(0,0,0,0.3)");
        element.style.setProperty("border", "1px solid rgba(0,0,0,0.3)");
        parent.appendChild(element);

        element.style.setProperty("left", (parent.clientWidth/2 - element.offsetWidth/2)+"px");
        element.style.setProperty("top", parent.clientHeight+"px");
        var offset = HtmlElementUtil.offset(element, parent);
        new StyleAnimation(element, "top", {beginValue: offset.top, finishValue: parent.clientHeight/2 - element.offsetHeight/2, duration: duration}).start();
        return {
            close: function() {
                var offset = HtmlElementUtil.offset(element, parent);
                new StyleAnimation(element, "top", {beginValue: offset.top, finishValue: parent.clientHeight, duration: duration}).start().finish(function() {
                    element.remove();
                });
            }
        }
    },

    /**
     * Show message dialog
     * @param {string} message
     * @param {"info"|"warning"|"confirm"} type
     * @param {string} applyLabel
     * @param {function(): void} applyHandler
     * @param {string} cancelLabel
     * @param {function(): void} cancelHandler
     */
    Message: function(text, type, applyLabel, applyHandler, cancelLabel, cancelHandler) {
        // irregular arguments
        if(typeof applyLabel == "function") {
            // applyHandler, cancelHandler
            if(typeof applyHandler == "function") {
                cancelHandler = applyHandler;
                cancelLabel = "Cancel";
            }
            applyHandler = applyLabel;
            applyLabel = "OK";
        }

        var element = document.createElement("div");
        element.classList.add("message");
        var contentsElement = document.createElement("div");
        contentsElement.classList.add("contents");
        var textElement = document.createElement("div");
        textElement.style.setProperty("display", "inline-block");
        if(text != null && typeof text == "string") {
            if(!text.includes("\n")) {
                textElement.style.setProperty("line-height", "32px");
            }
            textElement.innerText = text;
        }
        contentsElement.appendChild(textElement);
        element.appendChild(contentsElement);
        var controlsElement = document.createElement("div");
        controlsElement.classList.add("controls");
        element.appendChild(controlsElement);

        element.style.setProperty("z-index", 999999);
        element.style.setProperty("background-color", "white");
        element.style.setProperty("color", "black");
        element.style.setProperty("padding", "16px");
        element.style.setProperty("user-select", "none");

        element.querySelector(".contents").style.setProperty("min-height", "32px");

        if(applyHandler == undefined && cancelHandler == undefined) {
            element.querySelector(".controls").style.setProperty("display", "none");
        }else {
            element.querySelector(".controls").style.setProperty("margin-top", "16px");
            element.querySelector(".controls").style.setProperty("text-align", "center");
        }

        if(type != undefined) {
            var icon = document.createElement("canvas");
            var iconSize = Size(32, 32);
            CanvasUtil.initCanvas(icon, iconSize);
            var context = icon.getContext("2d");
            var labelSize = 16;
            var point = Point(0,0);
            if(type == "warning") {
                var cornerSize = 4;
                point.x = iconSize.width/2;
                context.beginPath();
                context.arc(point.x, point.y+cornerSize, cornerSize, 1.2*Math.PI, 1.8*Math.PI, false);
                point.x = iconSize.width;
                point.y = iconSize.height;
                context.arc(point.x-cornerSize, point.y-cornerSize, cornerSize, -0.2*Math.PI, 0.5*Math.PI, false);
                point.x = 0;
                context.arc(point.x+cornerSize, point.y-cornerSize, cornerSize, 0.5*Math.PI, 1.2*Math.PI, false);
                context.closePath();
                context.fillStyle = "black";
                context.fill();

                context.beginPath();
                point.x = iconSize.width/2 - 2;
                point.y = iconSize.height/2 - labelSize/2+2;
                context.moveTo(point.x, point.y);
                point.x += 4;
                context.lineTo(point.x, point.y);
                point.x = iconSize.width/2 + 1.5;
                point.y += 12;
                context.lineTo(point.x, point.y);
                point.x -= 3;
                context.lineTo(point.x, point.y);
                context.closePath();
                point.x = iconSize.width/2;
                point.y += 2+2;
                context.arc(point.x, point.y, 2, 0, 2*Math.PI, false);
                context.globalCompositeOperation = "destination-out";
                context.fill();
            }else if(type == "confirm") {
                context.beginPath();
                context.arc(iconSize.width/2, iconSize.height/2, iconSize.width/2, 0, 2*Math.PI, false);
                context.fillStyle = "black";
                context.fill();

                context.beginPath();
                point.x = iconSize.width/2;
                point.y = iconSize.height/2-4;
                context.arc(point.x, point.y, 5, 1*Math.PI, 2.5*Math.PI, false);
                context.arc(point.x, point.y, 2, 2.5*Math.PI, 1*Math.PI, true);
                context.closePath();
                context.globalCompositeOperation = "destination-out";
                context.fill();
                point.x = iconSize.width/2-1.5;
                point.y = iconSize.height/2-2;
                context.fillRect(point.x, point.y, 3, 4);

                point.x = iconSize.width/2;
                point.y += 4+2+2;
                context.arc(point.x, point.y, 2, 0, 2*Math.PI, false);
                context.fill();
            }else {
                context.beginPath();
                context.arc(iconSize.width/2, iconSize.height/2, iconSize.width/2, 0, 2*Math.PI, false);
                context.fillStyle = "black";
                context.fill();

                context.beginPath();
                point.x = iconSize.width/2;
                point.y = iconSize.height/2-labelSize/2+2;
                context.arc(point.x, point.y, 2, 0, 2*Math.PI, false);
                context.globalCompositeOperation = "destination-out";
                context.fill();

                point.x = iconSize.width/2-1.5;
                point.y += 2+2;
                context.fillRect(point.x, point.y, 3, 10);
            }
            icon.style.setProperty("vertical-align", "top");
            icon.style.setProperty("margin-right", "8px");
            element.querySelector(".contents").prepend(icon);
        }
        var popup;
        if(applyHandler != undefined) {
            var applyButton = document.createElement("div");
            applyButton.innerText = applyLabel != null ? applyLabel : "OK";
            applyButton.style.setProperty("display", "inline-block");
            applyButton.style.setProperty("margin", "0px 8px 8px 8px");
            applyButton.style.setProperty("font-weight", "600");
            applyButton.style.setProperty("cursor", "pointer");
            applyButton.addEventListener("click", function() {
                applyHandler();
                if(popup != undefined) {
                    popup.close();
                }
            });
            element.querySelector(".controls").appendChild(applyButton);
        }
        if(cancelHandler != undefined) {
            var cancelButton = document.createElement("div");
            cancelButton.innerText = cancelLabel != null ? cancelLabel : "Cancel";
            cancelButton.style.setProperty("display", "inline-block");
            cancelButton.style.setProperty("margin", "0px 8px 8px 8px");
            cancelButton.style.setProperty("font-weight", "600");
            cancelButton.style.setProperty("cursor", "pointer");
            cancelButton.addEventListener("click", function() {
                cancelHandler();
                if(popup != undefined) {
                    popup.close();
                }
            });
            element.querySelector(".controls").appendChild(cancelButton);
        }
        popup = this.Popup(element);
        if(applyHandler == undefined && cancelHandler == undefined) {
            setTimeout(function() {
                popup.close();
            }, 1000);
        }
    },

    /**
     * Show HTML element inclued by the balloon.
     * @param {HTMLElement} element
     * @param {Object} settings 
     * @param {Point} [settings.location]
     * @param {HTMLElement} [settings.parent]
     * @param {function(HTMLElement): object} [settings.loadHandler]
     * @param {function(void): void} [settings.dismissHandler]
     * @param {number} [settings.padding]
     * @param {top|bottom|left|right} [settings.direction]
     * @param {number} [settings.tipSize]
     * @param {number} [settings.tipOffset]
     * @param {string} [settings.fillColor]
     * @param {boolean} [settings.shadow]
     * @param {boolean} [settings.modal]
     * @param {boolean} [settings.visible]
     * @returns {Object} interface
     * @returns {Point} interface.location
     * @returns {Size} interface.size
     * @returns {function(): void} interface.show
     * @returns {function(): void} interface.close
     */
    Balloon: function(element, settings) {
        if(!(element instanceof HTMLElement)) { 
            console.error("Element is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return; 
        }

        var context = {};

        var location = Point(0, 0);
        var parent;
        var loadHandler;
        var dismissHandler;
        var padding = 8;
        var margin = 8;
        var direction = "top";
        var tipSize = 8;
        var tipOffset = undefined;
        var fillColor = "white";
        var shadow = false;
        var modal = true;
        var visible = true;
        var zIndex;
        if(settings != undefined) {
            if(settings.location != undefined) {
                location = settings.location;
            }
            if(settings.parent != undefined) {
                if(!(settings.parent instanceof HTMLElement)) { 
                    console.error("親要素がHTMLElementではありません。", settings.parent.toString());
                    return; 
                }
                parent = settings.parent;
            }
            if(settings.loadHandler != undefined) {
                loadHandler = settings.loadHandler;
            }
            if(settings.dismissHandler != undefined) {
                dismissHandler = settings.dismissHandler;
            }
            if(settings.padding != undefined) {
                padding = settings.padding;
            }
            if(settings.direction != undefined) {
                direction = settings.direction;
            }
            if(settings.tipSize != undefined) {
                tipSize = settings.tipSize;
            }
            if(settings.tipOffset != undefined) {
                tipOffset = settings.tipOffset;
            }
            if(settings.fillColor != undefined) {
                fillColor = settings.fillColor;
            }
            if(settings.shadow != undefined) {
                shadow = settings.shadow;
            }
            if(settings.modal != undefined) {
                modal = settings.modal;
            }
            if(settings.visible != undefined) {
                visible = settings.visible;
            }
            if(settings.zIndex != undefined) {
                zIndex = settings.zIndex;
            }
        }
        if(parent == undefined) {
            parent = document.querySelector("body");
        }

        var container = document.createElement("div");
        container.classList.add("balloon");
        container.style.setProperty("position", "absolute");
        container.style.setProperty("left", location.x+"px");
        container.style.setProperty("top", location.y+"px");
        if(zIndex != null) {
            container.style.setProperty("z-index", zIndex);
        }

        var mask = document.createElement("div");
        mask.style.setProperty("position", "absolute");
        mask.style.setProperty("left", "0");
        mask.style.setProperty("top", "0");
        mask.style.setProperty("width", "100%");
        mask.style.setProperty("height", "100%");
        mask.style.setProperty("background-color", "transparent");
        if(zIndex != null) {
            mask.style.setProperty("z-index", zIndex);
        }

        var canvasElement = document.createElement("canvas");
        container.appendChild(canvasElement);

        element.style.setProperty("position", "absolute");
        element.style.setProperty("left", (margin+padding+(direction == "left" ? tipSize : 0))+"px");
        element.style.setProperty("top", (margin+padding+(direction == "top" ? tipSize : 0))+"px");
        container.appendChild(element);

        if(modal) {
            parent.appendChild(mask);
        }
        parent.appendChild(container);

        function drawBaloon() {
            var canvasSize = Size(element.offsetWidth+padding*2+margin*2, element.offsetHeight+padding*2+margin*2);
            if(direction == "top" || direction == "bottom") {
                canvasSize.height += tipSize;
            }else {
                canvasSize.width += tipSize;
            }

            CanvasUtil.initCanvas(canvasElement, canvasSize);

            var offset = HtmlElementUtil.offset(container, parent);
            if(offset.left + container.offsetWidth > parent.clientWidth) {
                container.style.setProperty("left", (parent.clientWidth-container.offsetWidth)+"px");
                
                if(tipOffset == undefined) {
                    if(direction == "top" || direction == "bottom") {
                        tipOffset = container.offsetWidth - margin*2 - (parent.clientWidth - location.x - margin);
                    }
                }
            }

            if(loadHandler != undefined) {
                var _settings = loadHandler(container, settings);
                if(_settings != undefined) {
                    if(_settings.location != undefined) {
                        container.style.setProperty("left", _settings.location.x+"px");
                        container.style.setProperty("top", _settings.location.y+"px");
                    }
                    if(_settings.tipOffset != undefined) {
                        tipOffset = _settings.tipOffset;
                    }
                    if(_settings.direction != undefined) {
                        direction = _settings.direction;
                    }
                }
            }
            
            var context = canvasElement.getContext("2d");
            context.clearRect(0, 0, canvasSize.width, canvasSize.height);
            DrawUtil.drawRoundRectBalloon(context, Rect(margin, margin, canvasElement.clientWidth-margin*2, canvasElement.clientHeight-margin*2), {
                direction: direction,
                tipSize: tipSize,
                tipOffset: tipOffset,
                fillColor: fillColor,
                margin: margin,
                shadow: shadow
            });
        }

        var observer;
        if(MutationObserver !== undefined) {
            observer = new MutationObserver(function(mutations) {
                drawBaloon();
                if(!visible) {
                    container.style.setProperty("visibility", "hidden");
                }
            });
            observer.observe(element, {childList: true, subtree: true, attributes: true});
        }else {
            console.error("Your browser does not support MutationObserver. Please try to run Polyfill for this feature.");
        }

        Object.defineProperty(context, "location", { 
            set: function(location) {
                container.style.setProperty("left", location.x+"px");
                container.style.setProperty("top", location.y+"px");
            }
        });
        Object.defineProperty(context, "size", { 
            get: function() {
                return Size(container.offsetWidth, container.offsetHeight);
            }
        });

        context.show = function() {
            visible = true;
            container.style.setProperty("visibility", "visible");
        };
        context.close = function() {
            if(observer != null) {
                observer.disconnect();
            }
            if(modal) {
                mask.remove();
            }
            container.remove();
        };

        if(element.clientWidth != 0 && element.clientHeight != 0) {
            drawBaloon();

            if(!visible) {
                container.style.setProperty("visibility", "hidden");
            }
        }

        // Prevent close from being called in the event of Balloon call in Mobile Safari
        setTimeout(function() {
            mask.addEventListener("click", function() {
                if(dismissHandler != undefined) {
                    dismissHandler();
                }
                context.close();
            });
        }, 0);

        return context;
    },

    /**
     * Show context menu.
     * @param {HTMLElement|Point} source
     * @param {Object} settings 
     * @param {Array.<string>} settings.items
     * @param {string} settings.labelColor
     * @param {string} settings.backgroundColor
     * @param {function(number): void} settings.selectHandler
     */
    ContextMenu: function(source, settings) {
        if(!(typeof source == "object" && source.x != undefined && source.y != undefined) && !(source instanceof HTMLElement)) { 
            console.error("First argument is not a Point or HTMLElement.", (source != null && source.toString != undefined ? source.toString() : "null"));
            return;
        }
        
        var items;
        var labelColor = "white";
        var backgroundColor = "black";
        var selectHandler;
        if(settings != undefined) {
            if(settings.items != undefined) {
                items = settings.items;
            }
            if(settings.labelColor != undefined) {
                labelColor = settings.labelColor;
            }
            if(settings.backgroundColor != undefined) {
                backgroundColor = settings.backgroundColor;
            }
            if(settings.selectHandler != undefined) {
                selectHandler = settings.selectHandler;
            }
        }
        if(items == undefined) {
            items = [];
        }

        var menu = document.createElement("div");
        menu.classList.add("contextMenu");
        menu.style.setProperty("line-height", "0px");
        for(var i=0; i<items.length; i++) {
            var item = items[i];
            var menuItem = document.createElement("div");
            menuItem.innerText = item;
            menu.appendChild(menuItem);
        }
        menu.querySelectorAll("div").forEach(function(menuItem) {
            menuItem.style.setProperty("display", "inline-block");
            menuItem.style.setProperty("color", labelColor);
            menuItem.style.setProperty("font-size", "12px");
            menuItem.style.setProperty("line-height", "12px");
            menuItem.style.setProperty("cursor", "pointer");
            menuItem.style.setProperty("user-select", "none");
            menuItem.style.setProperty("white-space", "nowrap");
        });
        var location;
        var direction = "bottom";
        function detectScroll(parentElement) {
            if(parentElement == null) return;
            location.x -= parentElement.scrollLeft;
            location.y -= parentElement.scrollTop;
            if(parentElement.parentElement != null) {
                detectScroll(parentElement.parentElement);
            }
        }
        if(source instanceof HTMLElement) {
            var offset = HtmlElementUtil.offset(source);
            location = Point(offset.left + source.clientWidth/2, offset.top);
            if(location.y < 0) {
                location.y = offset.top + source.offsetHeight;
                direction = "top";
            }
            detectScroll(source.parentElement);
        }else if(typeof source == "object" && source.x != undefined && source.y != undefined) {
            location = Point(source.x, source.y);
        }else {
            location = Point();
        }
        var balloon = Controls.Balloon(menu, {
            location: location, 
            fillColor: backgroundColor, 
            direction: direction,
            loadHandler: function(balloon) {
                if(source instanceof HTMLElement) {
                    balloon.style.setProperty("left", (location.x - balloon.clientWidth/2)+"px");
                    balloon.style.setProperty("top", (location.y - balloon.clientHeight+16)+"px");
                }else if(typeof source == "object" && source.x != undefined && source.y != undefined) {
                    balloon.style.setProperty("left", (location.x - balloon.clientWidth/2)+"px");
                    balloon.style.setProperty("top", (location.y - balloon.clientHeight)+"px");
                }
            }
        });
        
        menu.querySelector("div").addEventListener("click", function(event) {
            var index = -1;
            var menuItemList = menu.querySelectorAll("div");
            for(var i=0; i<menuItemList.length; i++) {
                if(menuItemList[i] == event.currentTarget) {
                    index = i;
                }
            }
            selectHandler(index);
            balloon.close();
        });
    },

    /**
     * Make the specified HTML element draggable
     * @param {HTMLElement} element
     * @param {function(Point): void} callback
     */
    Draggable: function(element, callback) {
        if(!(element instanceof HTMLElement)) { 
            console.error("First argument is not HTMLElement.", (element != null && element.toString != undefined ? element.toString() : "null"));
            return; 
        }

        var context = {
            pressing: false,
            beginLocation: {x: 0, y: 0},
            beginScrollOffset: {x: -1, y: -1}
        };
        element.addEventListener("mousedown", function(event) {
            var target = event.currentTarget;
            context.pressing = true;
            context.beginLocation.x = event.clientX - target.offsetLeft;
            context.beginLocation.y = event.clientY - target.offsetTop;
            context.beginScrollOffset.x = target.scrollLeft;
            context.beginScrollOffset.y = target.scrollTop;
        });
        element.addEventListener("mousemove", function(event) {
            if(!context.pressing) { return; }
            var target = event.currentTarget;
            var currentX = event.clientX - target.offsetLeft;
            var currentY = event.clientY - target.offsetTop;
            var currentScrollOffsetX = context.beginScrollOffset.x - (currentX - context.beginLocation.x);
            var currentScrollOffsetY = context.beginScrollOffset.y - (currentY - context.beginLocation.y);
            target.scrollLeft = currentScrollOffsetX;
            target.scrollTop = currentScrollOffsetY;
            if(callback != undefined) {
                callback(Point(target.scrollLeft, target.scrollTop));
            }
        });
        element.addEventListener("mouseup", function(event) {
            if(context.pressing) {
                event.stopPropagation();
            }
            context.pressing = false;
            context.beginLocation = {x: 0, y: 0};
            context.beginScrollOffset = {x: -1, y: -1};
        });
        element.addEventListener("mouseleave", function(event) {
            context.pressing = false;
            context.beginLocation = {x: 0, y: 0};
            context.beginScrollOffset = {x: -1, y: -1};
        });
    }
};