(function () {

'use strict';

function capitalize(string) {
    return string.charAt(0).toUpperCase() + string.slice(1)
}

const element = document.createElement('div');
const entityCache = {};

function unescapeHtml(string){
  return string.replace(/&[^\s;]+;{0,1}/gi, function(entity){
    if(!entityCache[entity]){
      element.innerHTML = string;
      entityCache[entity] = element.childNodes.length == 0 ? "" : element.childNodes[0].nodeValue;
    }
    return entityCache[entity]
  })
}

class StringReader {

    constructor(string){
        this.string = (string || '').toString();
    }

    get length(){
        return this.string.length
    }

    toString(){
        return this.string
    }

    match(...args){
        const out = this.string.match(...args);
        if(out){
            this.string = this.string.substr(out[0].length);
        }
        return out
    }

}

const SELF_CLOSING_TAGS = [
    'area',
    'base',
    'br',
    'embed',
    'hr',
    'iframe',
    'img',
    'input',
    'link',
    'meta',
    'param',
    'source',
    'track'
];

const TEXT_ONLY_TAGS = [
    'script',
    'style'
];

class CloseTag {

    constructor(type){
        this.type = type;
    }

}

class VirtualNode {

    static fromString(html){
        const out = new this();
        out.appendHtml(html);
        out.normalize();
        return out
    }

    constructor(parent = null, type = '#fragment', attributes = {}){
        this.parent = parent;
        this.type = type;
        this.attributes = attributes;
        this.children = [];
    }

    appendNode(type, attributes = {}){
        const out = new this.constructor(this, type, attributes);
        this.children.push(out);
        return out
    }

    appendHtml(html){
        if(!(html instanceof StringReader)){
            html = new StringReader(html);

            while(html.length > 0){
                try {
                    this.appendHtml(html);
                } catch(e){
                    if(e instanceof CloseTag); else {
                        throw e
                    }
                }
            }

            return this
        }

        while(html.length > 0){
            let matches;

            if(matches = html.match(/^[^<]+/)){
                this.appendNode('#text', {value: matches[0]});
            } else if(matches = html.match(/^<!DOCTYPE[^>]*>/i)){
                if(!this.parent){
                    this.appendNode('#doctype');
                }
            } else if(matches = html.match(/^<!--([\s\S]*?)-->/i)){
                this.appendNode('#comment', {value: matches[1]});
            } else if(matches = html.match(/^<([^>\s]+)/)){
                const type = matches[1].toLowerCase();
                const attributes = {};

                while(html.length > 0){
                    if(matches = html.match(/^\s*([\w-]+)\s*=\s*\"([^\">]*)\"/)){
                        attributes[matches[1]] = unescapeHtml(matches[2]);
                    } else if(matches = html.match(/^\s*([\w-]+)\s*=\s*\'([^\'>]*)\'/)){
                        attributes[matches[1]] = unescapeHtml(matches[2]);
                    } else if(matches = html.match(/^\s*([\w-]+)\s*=\s*([^\s>]+)/)){
                        attributes[matches[1]] = unescapeHtml(matches[2]);
                    } else if(matches = html.match(/^\s*([\w-]+)/)){
                        attributes[matches[1]] = null;
                    } else {
                        html.match(/^[^>]*>/);
                        break
                    }
                }

                if(matches = type.match(/^\/(.*)/)){
                    throw new CloseTag(matches[1])
                }

                const child = this.appendNode(type, attributes);

                if(SELF_CLOSING_TAGS.includes(type)); else if(TEXT_ONLY_TAGS.includes(type) && (matches = html.match(new RegExp(`^([\\s\\S]*?)<\\/${type}[^>]*>`)))){
                    child.appendNode('#text', {value: matches[1]});
                } else if(TEXT_ONLY_TAGS.includes(type) && (matches = html.match(/^([\s\S]+)/))){
                    child.appendNode('#text', {value: matches[1]});
                } else {
                    try {
                        child.appendHtml(html);
                    } catch(e){
                        if(e instanceof CloseTag && e.type == type); else {
                            throw e
                        }
                    }
                }
            } else if(matches = html.match(/^[\s\S]/)) {
                this.appendNode('#text', {value: matches[0]});
            } else {
                break;
            }
        }

    }

    normalize(){
        if(!this.parent && this.children.some(child => child.type == 'html')){
            this.children = [
                new this.constructor(this, '#doctype'),
                ...this.children.filter(child => child.type == 'html')
            ];
        }

        if(this.type == '#text'){
            this.attributes.value = this.attributes.value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
        }

        if(this.parent && this.parent.type == 'textarea' && this.type == '#text'){
            this.attributes.value = this.attributes.value.replace(/^\n/, '');
        }

        this.children.forEach(child => child.normalize());
    }

}

class EventWrapper {

    static instanceFor(event){
        if(!event.$p){
            event.$p = new this(event);
        }
        return event.$p
    }

    constructor(event){
        this.event = event;
    }

    get target(){
        return NodeWrapper.instanceFor(this.event.target)
    }

    stopPropagation(){
        this.event.stopPropagation();
    }

    preventDefault(){
        this.event.preventDefault();
    }

}

const nodeWrappers = [];

class NodeWrapper {

    static get selector(){ return `.${this.name}` }

    static register(){
        const klass = this;

        nodeWrappers.unshift(klass);

        Object.defineProperty(NodeWrapper.prototype, klass.name, {
            get: function(){
                let current = this.parent;
                while(current){
                    if(current instanceof klass){
                        return current
                    }
                    current = current.parent;
                }
            } 
        });

        Object.defineProperty(NodeWrapper.prototype, `is${capitalize(klass.name)}`, {
            get: function(){
                return this instanceof klass
            } 
        });
    }

    static instanceFor(node){
        if(!node.$p){
            node.$p = new NodeWrapper(node);
            nodeWrappers.some((klass) => {
                if(node.$p.is(klass.selector)){
                    node.$p = new klass(node);
                    return true
                }
            });
        }
        return node.$p
    }

    constructor(node){
        this.node = node;
        this.$registeredEventListeners = [];
    }

    get type(){
        return this.node instanceof DocumentType ? '#doctype' : this.node.nodeName.toLowerCase()
    }

    get attributes(){
        const out = {};
        if(this.node.attributes){
            for(let i = 0; i < this.node.attributes.length; i++){
                out[this.node.attributes[i].name] = this.node.attributes[i].value;
            }
        }
        return out
    }

    get text(){
        return this.node.textContent
    }

    get realParent(){
        return this.node.parentNode ? this.constructor.instanceFor(this.node.parentNode) : null
    }

    get parent(){
        if(this.$parent){
            return this.$parent
        }
        return this.realParent
    }

    get parents(){
        const out = [];
        let current = this;
        while(current.parent){
            current = current.parent;
            out.push(parent);
        }
        return out
    }

    get children(){
        return [...this.node.childNodes].map(
            node => this.constructor.instanceFor(node)
        )
    }

    get siblings(){
        if(this.parent){
            return this.parent.children
        } else {
            return [this]
        }
    }

    get previousSibling(){
        if(this.node.previousSibling){
            return this.constructor.instanceFor(this.node.previousSibling)
        } else {
            return null
        }
    }

    get nextSibling(){
        if(this.node.nextSibling){
            return this.constructor.instanceFor(this.node.nextSibling)
        } else {
            return null
        }
    }

    get nextSiblings(){
        const out = [];
        let current = this;
        while(current.nextSibling){
            current = current.nextSibling;
            out.push(current);
        }
        return out
    }

    get previousSiblings(){
        const out = [];
        let current = this;
        while(current.previousSibling){
            current = current.previousSibling;
            out.push(current);
        }
        return out
    }

    get descendants(){
        return this.find(() => true)
    }

    find(selector, out = []){
        this.children.forEach((child) => {
            if(child.is(selector)){
                out.push(child);
            }
            child.find(selector, out);
        });
        return out;
    }

    is(selector){
        if(typeof selector == 'function'){
            return selector.call(this, this)
        }
        return (this.node.matches || this.node.matchesSelector || this.node.msMatchesSelector || this.node.mozMatchesSelector || this.node.webkitMatchesSelector || this.node.oMatchesSelector || (() => false)).call(this.node, selector)
    }

    on(name, ...args){
        const fn = args.pop();
        const selector = args.pop();

        const wrapperFn = (event, ...args) => {
            const eventWrapper = EventWrapper.instanceFor(event);
            if(selector){
                if(eventWrapper.target.is(selector)){
                    return fn.call(eventWrapper.target, eventWrapper, ...args)
                }
            } else {
                return fn.call(this, eventWrapper, ...args)
            }
        };

        this.node.addEventListener(name, wrapperFn);

        this.$registeredEventListeners.push([name, wrapperFn]);

        return this
    }

    trigger(name, data){
        if (window.CustomEvent && typeof window.CustomEvent === 'function') {
            var event = new CustomEvent(name, { bubbles: true, cancelable: true, detail: data } );
        } else {
            var event = document.createEvent('CustomEvent');
            event.initCustomEvent(name, true, true, data);
        }

        this.node.dispatchEvent(event);

        return this
    }

    remove(){
        if(this.type != '#doctype'){
            this.realParent.node.removeChild(this.node);
        }
        return this
    }

    addClass(name){
        this.node.classList.add(name);
        return this
    }

    removeClass(name){
        this.node.classList.remove(name);
        return this
    }

    patch(html){
        cleanChildren.call(this);
        patchChildren.call(this, VirtualNode.fromString(html).children);
        initChildren.call(this);
        return this.children
    }

    append(html){
        return prepend.call(this, html)
    }

    prepend(html){
        return prepend.call(this, html, this.children[0])
    }

    insertBefore(html){
        return prepend.call(this.realParent, html, this)
    }

    insertAfter(html){
        return prepend.call(this.realParent, html, this.nextSibling)
    }

}

function cleanChildren(){
    this.children.forEach(child => clean.call(child));
}

function clean(){
    [...this.node.childNodes].forEach(node => node.$p && clean.call(node.$p));

    while(this.$registeredEventListeners.length){
        this.node.removeEventListener(...this.$registeredEventListeners.pop());
    }
    this.node.$p = undefined;
}

function initChildren(){
    this.children.forEach(child => initChildren.call(child));
}

function prepend(html, referenceChild){
    const out = [];
    VirtualNode.fromString(html).children.forEach((virtualChild) => {
        out.push(insert.call(this, virtualChild, referenceChild));
    });
    return out
}

function patch(attributes, virtualChildren){
    patchAttributes.call(this, attributes);
    patchChildren.call(this, virtualChildren);
}

function patchAttributes(attributes){
    if(this.type == '#text' || this.type == '#comment'){
        if(this.node.textContent != attributes.value){
            this.node.textContent = attributes.value;
        }
    } else if(this.type != '#doctype'){
        const currentAttributes = this.attributes;
        Object.keys(currentAttributes).forEach((key) => {
            if(attributes[key] === undefined){
                this.node.removeAttribute(key);
            }
        });
        Object.keys(attributes).forEach((key) => {
            if(currentAttributes[key] != attributes[key]){
                this.node.setAttribute(key, attributes[key]);
            }
        });
    }
}

function patchChildren(virtualChildren){
    const children = [...this.node.childNodes].map(
        node => new NodeWrapper(node)
    );

    for(let i = 0; i < virtualChildren.length; i++){
        let child = children[0];
        const virtualChild = virtualChildren[i];

        if(child && child.type == virtualChild.type){
            patch.call(children.shift(), virtualChild.attributes, virtualChild.children);
        } else if(virtualChild.type == '#doctype'); else if(virtualChild.type.match(/^#(text|comment)/)){
            insert.call(this, virtualChild, child);
        } else {
            while(children.length > 0 && children[0].type.match(/^#/)){
                children.shift().remove();
            }
            child = children[0];
            if(child && child.type == virtualChild.type){
                patch.call(children.shift(), virtualChild.attributes, virtualChild.children);
            } else {
                insert.call(this, virtualChild, child);
            }
        }
    }

    while(children.length > 0){
        children.shift().remove();
    }
}

function insert(virtualNode, referenceChild, returnNodeWrapper = true){
    const { type, attributes, children } = virtualNode;

    let node;

    if(type == '#text'){
        node = document.createTextNode(attributes.value);
    } else if(type == '#comment'){
        node = document.createComment(attributes.value);
    } else {
        node = document.createElement(type);
        Object.keys(attributes).forEach((key) => {
            node.setAttribute(key, attributes[key]); 
        });
    }

    children.forEach(child => {
        insert.call(new NodeWrapper(node), child, null, false);
    });

    this.node.insertBefore(
        node,
        referenceChild && referenceChild.node
    );

    if(returnNodeWrapper){
        return NodeWrapper.instanceFor(node)
    }
}

class Url {

    static fromString(url, referenceUrl){
        const out = new Url();
        url = new StringReader(url);
        if(!(referenceUrl instanceof Url)){
            referenceUrl = Url.fromString(referenceUrl || window.location, new Url());
        }

        let matches;

        if(matches = url.match(/^([a-z]+):\/\/([a-z\.-]+)/i)){
            out.protocol = matches[1].toLowerCase();
            out.host = matches[2].toLowerCase();
            if(matches = url.match(/^:(\d+)/)){
                out.port = parseInt(matches[1]);
            }
        } else {
            out.protocol = referenceUrl.protocol;
            out.host = referenceUrl.host;
            out.port = referenceUrl.port;
        }

        if(matches = url.match(/^\/[^\?\#]*/)){
            out.path = normalizePath(matches[0]);
        } else if(matches = url.match(/^[^\?\#]+/)){
            out.path = normalizePath(`${referenceUrl.path.replace(/[^\/]*$/, '')}${matches[0]}`);
        } else {
            out.path = referenceUrl.path;
        }

        if(matches = url.match(/^\?([^\#]*)/)){
            matches[1].split(/&/).forEach((pair) => {
                const [key, value] = pair.split(/=/);
                out.params[decodeURIComponent(key)] = decodeURIComponent(value);
            });
        }

        return out
    }

    constructor(protocol = 'http', host = 'localhost', port = 80, path = '/', params = {}){
        this.protocol = protocol;
        this.host = host;
        this.port = port;
        this.path = path;
        this.params = params;
    }

    toString(){
        const out = [`${this.protocol}://${this.host}`];

        const defaultPort = this.protocol == 'https' ? 443 : 80;
        if(this.port != defaultPort){
            out.push(`:${this.port}`);
        }

        out.push(this.path);

        const pairs = [];
        Object.keys(this.params).forEach((key) => {
            pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(this.params[key])}`);
        });
        if(pairs.length > 0){
            out.push(`?${pairs.join('&')}`);
        }

        return out.join('')
    }

    get defaultPort(){
        if(this.protocol == 'https'){
            return 443
        } else {
            return 80
        }
    }

}

function normalizePath(path){
    const out = [];
    path.split(/\//).forEach((segment) => {
        if(segment == '..'){
            out.pop();
        } else if(segment != '.'){
            out.push(segment);
        }
    });
    return out.join('/');
}

class Anchor extends NodeWrapper {

    static get name(){ return 'anchor' }

    static get selector(){ return 'a, .anchor' }

    constructor(...args){
        super(...args);

        this.on('click', (event) => {
            if(this.url.host == this.frame.url.host && this.url.port == this.frame.url.port){
                event.preventDefault();

                const confirm = this.attributes['data-confirm'];
                const method = this.attributes['data-method'] || 'GET';
                const target = this.attributes['target'] || this.attributes['data-target'] || '_top';

                if(!confirm || window.confirm(confirm)){
                    if(target == '_modal'){
                        this.document.find('html').pop().addClass('is-clipped');
                        this.document.find('body').pop().append(`<div class="modal is-active" data-url="${this.url}"></div>`).forEach((modal) => {
                            modal.$parent = this;
                            modal.load({});
                        });
                    } else {
                        this.frame.load({ $method: method, $url: this.url });
                    }
                }
            }
        });
    }

    get url(){
        if(this.$url === undefined){
            this.$url = Url.fromString(
                this.attributes['href'] || this.attributes['data-url'],
                this.frame.url
            );
        }
        return this.$url
    }

    set url(url){
        this.$url = Url.fromString(
            url,
            this.url
        );
    }

}

Anchor.register();

class Frame extends NodeWrapper {

    static get name(){ return 'frame' }

    constructor(...args){
        super(...args);
    }

    get url(){
        if(this.$url === undefined){
            this.$url = Url.fromString(
                this.attributes['data-url'] || window.location,
                this.frame ? this.frame.url : window.location
            );
        }
        return this.$url
    }

    set url(url){
        this.$url = Url.fromString(
            url,
            this.url
        );
    }

    load({$method = 'GET', $url = this.url.toString(), $headers = {}, ...params }){
        if(this.request){
            this.request.abort();
        }

        $method = $method.toUpperCase();

        this.url = $url;
        const isRequestBody = $method == 'POST' || $method == 'PUT' || $method == 'PATCH';
        if(!isRequestBody){
            this.url.params = {...this.url.params, ...params};
        }

        this.request = new XMLHttpRequest();

        this.request.open($method, this.url.toString(), true);

        this.request.onload = () => {
            if (this.request.status >= 200 && this.request.status < 400) {
                this.patch(this.request.response);
            }
        };

        const defaultHeaders = {};
        const document = this.document || this;
        const csrfToken = document.find('meta[name="csrf-token"').map(nodeWrapper => nodeWrapper.attributes.content).pop();
        if(csrfToken){
            defaultHeaders['X-CSRF-Token'] = csrfToken;
        }
        const headers = {...defaultHeaders, ...this.headers};
        Object.keys(headers).forEach((name) => this.request.setRequestHeader(name, headers[name]));

        const formData = new FormData();
        if(isRequestBody){
            Object.keys(params).forEach((name) => formData.append(name, params[name]));
        }

        this.request.send(formData);
    }

}

Frame.register();

class Document extends Frame {

    static get name(){ return 'document' }

    static get selector(){ return function(){ return this.type == '#document' } }

    constructor(...args){
        super(...args);
        window.onpopstate = (event) => {
            this.load({ $pushState: false, $url: event.state || window.location });
        };
        window.$p = this;
    }

    load({ $pushState = true, ...params }){
        const previousUrl = this.url.toString();
        super.load(params);
        if($pushState && params.$method == 'GET' && previousUrl != this.url.toString()){
            history.pushState(this.url.toString(), null, this.url.toString());
        }
    }

}

Document.register();

class Form extends NodeWrapper {

    static get name(){ return 'form' }

    static get selector(){ return 'form, .form' }

    constructor(...args){
        super(...args);

        this.on('submit', (event) => {
            console.log('Form submit', event);
            event.preventDefault();
            this.frame.load({$method: this.method, $url: this.url, ...this.params });
        });
    }

    get method(){
        return this.attributes['method'] || this.attributes['data-method'] || 'POST'
    }

    get url(){
        if(this.$url === undefined){
            this.$url = Url.fromString(
                this.attributes['action'] || this.attributes['data-url'],
                this.frame.url
            );
        }
        return this.$url
    }

    get inputs(){
        return this.descendants.filter((descendant) => descendant.isInput)
    }

    get params(){
        const out = {};
        this.inputs.forEach(input => {
            const value = input.value;
            if(value !== undefined){
                out[input.name] = value;
            }
        });
        return out
    }

}

Form.register();

class Input extends NodeWrapper {

    static get name(){ return 'input' }

    static get selector(){ return 'input, textarea, .input' }

    get name(){
        return this.attributes.name
    }

    get value(){
        if(this.is('input[type="checkbox"], input[type="radio"]')){
            return this.is(':checked') ? this.node.value : undefined
        }
        return this.node.value
    }

}

Input.register();

class Modal extends Frame {

    static get name(){ return 'modal' }

    constructor(...args){
        super(...args);
        this.on('click', '.modal-background, .modal-close', (event) => {
            event.stopPropagation();
            this.close();
        });
    }

    close(){
        this.remove();
        if(!this.document.find('body').pop().children.filter((child) => child.is('.modal')).length){
            this.document.find('html').pop().removeClass('is-clipped');
        }
    }

    patch(html){
        return super.patch(`
        <div class="modal-background"></div>
        <div class="modal-content">
            <div class="box">${html}</div>
        </div>
        <button class="modal-close is-large" aria-label="close"></button>
    `)
    }

}

Modal.register();

class Script extends NodeWrapper {

    static get name(){ return 'script' }

    static get selector(){ return 'script[type="pinstripe"]' }

    constructor(...args){
        super(...args);
        eval(this.text);
    }

}

Script.register();

let ready = false;

function initializeTree(node){
    NodeWrapper.instanceFor(node).descendants;
}

const observer = new MutationObserver(mutations => {
    if(ready){
        mutations.forEach(
            mutation => mutation.addedNodes.forEach(
                node => initializeTree(node)
            )
        );
    }
});

observer.observe(document.documentElement, {
    attributes: false,
    childList: true,
    subtree: true
});

setTimeout(() => {
    ready = true;
    initializeTree(document);
}, 0);

}()); //# sourceMappingURL=index-bundle.js.map