Greasy Fork is available in English.

UIModules

Modules for UI constructing

Ce script ne devrait pas être installé directement. C'est une librairie créée pour d'autres scripts. Elle doit être inclus avec la commande // @require https://update.greasyfork.org/scripts/384232/704113/UIModules.js

// ==UserScript==
// @name           UIModules
// @name:en        UIModules
// @description    Modules for UI constructing
// @description:en Modules for UI constructing
// @namespace      https://greasyfork.org/users/174399
// @version        0.1.0
// @include        *://*.mozilla.org/*
// @run-at         document-start
// @grant          none
// ==/UserScript==

(function(window) {
    'use strict';
    const listSymbol = Symbol('list');
    const modeSymbol = Symbol('mode');
    const valueSymbol = Symbol('value');
    const fn = {};
    fn.toJSON = function() {
        return {
            __function_body__: this.toString().replace(/\s+/g, ' '),
            __function_name__: this.name,
            __function_length__: this.length,
         };
    };
    function reviver(key, value) {
        if (key === '__function_body__') {
            return new Function('return ' + value)();
        }
        if (typeof value === 'object' && typeof value.__function_body__ === 'function') {
            return value.__function_body__;
        }
        return value;
    }
    /*
    const interfaceTemplate = {
        title: 'template interface',
        version: '0.1.0',
        mode: 'normal',
        tabs: {
            data: {
                'general': {},
            },
            order: ['general'],
        },
    };
    const tabDataTemplate = {
        name: 'general',
        label: 'General',
        options: {
            data: {
                'option-name': {},
            },
            order: ['option-name'],
        },
    };
    const optionDataTemplate = {
        name: 'option-name',
        label: 'Option name',
        value: 'option-value',
        description: 'description text (optional)',
        type: 'string',
        mode: 'normal',
        tab: 'general',
    };
    */
    /*
    <div class="interface-ui">
        <div class="tabs-ui tabs-ui-active">
            <div class="tab-ui tab-ui-active" data-name="{tabUI.name}">
                <div class="tab-ui-label">
                    <span>{tabUI.label}</span>
                </div>
                <div class="tab-ui-data">
                    <input type="radio" name="file-size" data-key="10G" value="10G" />
                    <input type="radio" name="file-size" data-key="1G"  value="1G" />
                    <input type="radio" name="file-size" data-key="1M"  value="1M" />
                    <input type="radio" name="file-size" data-key="1Kb"  value="1024" />
                    <input type="text" name="file-name" value="foo.txt" />
                </div>
            </div>
            <div class="tab-ui"/>
        </div>
    </div>
    */
    function dummy() {}
    function Mode(name = 'normal', label = '', onChange = dummy) {
        let nam;
        Object.defineProperty(this, 'name', { 
            get: function() { return nam; },
            set: function(n) {
                try {
                    Mode.validateName(n);
                    nam = n;
                    onChange(null, n);
                } catch (err) {
                    onChange(err);
                }
            },
            enumerable: true,
        });
        this.name = name;
        this.label = label || name;
    }
    Object.defineProperties(Mode, {
        'NORMAL': {
            value: 'normal',
            enumerable: true,
        },
        'ADVANCED': {
            value: 'advanced',
            enumerable: true,
        },
        'NAMES': {
            get: function(){
                return [Mode.NORMAL, Mode.ADVANCED];
            },
            enumerable: true,
        },
    });
    Mode.validateName = function(name) {
        switch (name) {
            case Mode.NORMAL:
            case Mode.ADVANCED:
                return true;
            default:
                throw new Error('invalid mode name (' + name + '), valid names = [' + Mode.NAMES.join(', ') + ']');
        }
    };
    Mode.descriptor = {
        get: function() { return this[modeSymbol]; },
        set: function(mode) {
            try {
                Mode.validateName(mode);
                this[modeSymbol] = mode;
            } catch (error) {
                if (typeof this.onChange === 'function') {
                    this.onChange(error);
                } else {
                    console.error('mode validation: ', error);
                }
            }
        },
        enumerable: true,
    };
    function Datum({
        mode = Mode.NORMAL,
        label,
        key,
        name,
        description,
        onChange = dummy,
        validate = dummy,
        setter = function(v) { return v; },
    } = {}) {
        console.log('new Datum() -> args: ', JSON.stringify(arguments[0], null, 2));
        this.onChange = onChange;
        this.validate = validate;
        this.setter = setter;
        this.label = label;
        this.description = description;
        this.mode = mode;
        Object.defineProperty(this, 'name', { value: name });
        Object.defineProperty(this, 'key', { value: (key || name) });
    }
    Datum.descriptor = {
        get: function(){ return this[valueSymbol]; },
        set: function(val) {
            try {
                this.validate(val);
                this[valueSymbol] = this.setter(val);
                this.onChange(null, this[valueSymbol], val);
            } catch (error) {
                this.onChange(error);
            }
        },
        enumerable: true,
    };
    Object.defineProperty(Datum.prototype, 'mode',  extend({}, Mode.descriptor));
    Datum.prototype.toJSON = function() {
        const { mode, name, label, description, key } = this;
        return { mode, name, label, description, key };
    };
    Datum.prototype.assign = function(item, checkName = false) {
        if (!item || item === this) {
            return this;
        }
        const {
            key,
            name,
            validate = this.validate,
            onChange = this.onChange,
            label = this.label,
            description = this.description,
        } = item;
        if (name !== this.name && checkName) {
            throw new Error('In assign (' + this.TYPE + ', name does not match, expected "' + this.name + '", but got "' + name + '"');
        }
        this.validate = validate;
        this.onChange = onChange;
        this.label = label;
        this.description = description;
        return this;
    };
    function Input({
        mode = Mode.NORMAL,
        label,
        name,
        key,
        value,
        description,
        type,
        onChange,
        validate,
        setter,
    } = {}) {
        this.index = Input.index++;
        console.log('new Input() -> args: ', JSON.stringify(arguments[0], null, 2));
        Datum.call(this, { mode, label, name, key, description, onChange, validate, setter });
        Object.defineProperty(this, 'value', extend({}, Datum.descriptor));
        Object.defineProperty(this, 'tag', { value: 'input' });
        let _type;
        Object.defineProperty(this, 'type', {
            get: function(){ return _type; },
            set: function(t) {
                if (Input.TYPES.indexOf(t) === -1) {
                    throw new TypeError('invalid input type, expected one of ' + JSON.stringify(Input.TYPES) + ', but got ' + t);
                }
                _type = t;
            },
            enumerable: true,
        });
        this.type = type;
        this.value = value;
    }
    Input.index = 0;
    Input.TYPES = ['number', 'text', 'checkbox', 'date', 'email', 'radio', 'tel', 'time'];
    Object.defineProperty(Input.prototype, 'mode',  extend({}, Mode.descriptor));
    Input.parse = function(item){
        if (item instanceof Input) {
            return item;
        }
        if (typeof item === 'object') {
            return new Input(item);
        }
        return new Input();
    };
    Input.prototype.toJSON = function() {
        const { value, type } = this;
        return extend({ value, type }, Datum.prototype.toJSON.call(this));
    };
    Input.prototype.toHTML = function() {
        const div = document.createElement('div');
        const elem = document.createElement('input');
        elem.setAttribute('type', this.type);
        elem.setAttribute('value', this.value);
        elem.setAttribute('name', this.name);
        elem.setAttribute('data-key', this.key);
        const id = this.name + '-' + (this.key === this.name ? this.globalIndex : this.key);
        elem.setAttribute('id', id);
        elem.setAttribute('title', this.description);
        div.appendChild(elem);
        return div.firstElementChild;
    };
    Input.prototype.assign = function(item, checkName = false) {
        if (!item || item === this) {
            return this;
        }
        Datum.prototype.assign.call(this, item, checkName);
        const { value = this.value } = item;
        this.value = value;
        return this;
    };
    
    function Option({
        label,
        name,
        key,
        value,
        description,
        onChange,
        validate,
        setter,
    } = {}) {
        this.globalIndex = Option.index++;
        console.log('new Option() -> args: ', JSON.stringify(arguments[0], null, 2));
        Datum.call(this, { label, name, key, description, onChange, validate, setter });
        Object.defineProperty(this, 'value', extend({}, Datum.descriptor));
        Object.defineProperty(this, 'tag', { value: 'option' });
        this.value = value;
    }
    Object.defineProperty(Option.prototype, 'mode',  extend({}, Mode.descriptor));
    Option.parse = function(item){
        if (item instanceof Option) {
            return item;
        }
        if (typeof item === 'object') {
            return new Option(item);
        }
        return new Option();
    };
    Option.prototype.toJSON = function() {
        const { value, type } = this;
        return extend({ value, type }, Datum.prototype.toJSON.call(this));
    };
    Option.index = 0;
    Option.prototype.toHTML = function() {
        const div = document.createElement('div');
        const elem = document.createElement('option');
        elem.setAttribute('value', this.value);
        elem.setAttribute('name', this.name);
        elem.setAttribute('data-key', this.key);
        const id = this.name + '-' + (this.key === this.name ? this.globalIndex : this.key);
        elem.setAttribute('id', id);
        elem.setAttribute('title', this.description);
        div.appendChild(elem);
        return div.firstElementChild;
    };
    Option.prototype.assign = function(item, checkName = false) {
        if (!item || item === this) {
            return this;
        }
        Datum.prototype.assign.call(this, item, checkName);
        const { value = this.value } = item;
        this.value = value;
        return this;
    };
    
    function List(Item = Input, itemKey = 'name', itemDefaults = {}) {
        Object.defineProperty(this, 'Item', { value: Item });
        this.data = {};
        this.order = [];
        this.itemDefaults = itemDefaults;
        List.validateItemKey(itemKey);
        Object.defineProperty(this, 'itemKey', { value: itemKey });
    }
    List.ITEM_KEYS = ['key', 'name'];
    List.validateItemKey = function(key) {
        if (List.ITEM_KEYS.indexOf(key) === -1) {
            throw new Error('Invalid key property name, expected ' + JSON.stringify(List.ITEM_KEYS) + ', but got ' + key + ' (typeof = ' + typeof key + ')');
        }
    };
    List.prototype.add = function(...data) {
        const { Item, itemKey, itemDefaults } = this;
        for (const datum of data) {
            const item = datum instanceof Item ? datum : new Item(extend({}, itemDefaults, datum));
            const { [itemKey]: name } = item;
            if (this.data[name] || this.order.indexOf(name) !== -1) {
                throw new Error('data already exists, type = "' + Item.name + '", name = "' + name + '"');
            }
            this.data[name] = item;
            this.order.push(name);
        }
    };
    List.prototype.update = function(datum) {
        if (!datum || typeof datum !== 'object') {
            return;
        }
        const { Item, itemKey } = this;
        const { [itemKey]: name } = datum;
        const item = this.data[name];
        if (item instanceof Item) {
            item.assign(datum);
        }
    };
    List.prototype.remove = function(name = '') {
        if (!name) {
            return;
        }
        const { Item } = this;
        const item = this.data[name];
        const index = this.order.indexOf(name);
        if (item) {
            delete this.data[name];
        }
        if (index !== -1) {
            this.order.splice(index, 1);
        }
    };
    List.prototype.setValue = function(name = '', value) {
        const { Item } = this;
        const item = this.data[name];
        if (item instanceof Item) {
            item.value = value;
        }
    };
    List.prototype.getValue = function(name = '') {
        const { Item } = this;
        const item = this.data[name];
        if (item instanceof Item) {
            return item.value;
        }
        return null;
    };
    List.prototype.assign = function(_list) {
        if (!_list || _list === this) {
            return this;
        }
        if (!(_list instanceof List) && typeof _list !== 'object') {
            return this;
        }
        const {
            data = this.data,
            order = this.order,
            itemKey = this.itemKey,
            itemDefaults = this.itemDefaults,
        } = _list;
        if (order !== this.order && itemKey !== 'key') {
            this.order = [...order];
        }
        if (itemKey !== this.itemKey) {
            this.itemKey = itemKey;
        }
        if (itemDefaults !== this.itemDefaults) {
            this.itemDefaults = itemDefaults;
        }
        if (data !== this.data) {
            this.data = extend({}, data);
        }
        return this;
    };
    List.parse = function(elem) {
        if (elem instanceof List) {
            return elem;
        }
        if (typeof elem === 'object') {
            let Item;
            switch (elem.itemType) {
                case 'Datum':
                    Item = Datum;
                    break;
                case 'Input':
                    Item = Input;
                    break;
                case 'Radio':
                    Item = Radio;
                    break;
                case 'Option':
                    Item = Option;
                    break;
                case 'Tab':
                    Item = Tab;
                    break;
                case 'TabItem':
                    Item = TabItem;
                    break;
                case 'List':
                    Item = List;
                    break;
                case 'Mode':
                    Item = Mode;
                    break;
                case 'Select':
                    Item = Select;
                    break;
                default:
                    throw new TypeError('Invalid itemType (' + itemType + ', ' + typeof itemType + ')');
            }
            const { itemKey, itemDefaults, data = {}, order = [] } = elem;
            const list = new List(Item, itemKey, itemDefaults);
            for (const key of order) {
                list.add(data[key]);
            }
            return list;
        }
        return new List();
    };
    List.prototype.toJSON = function() {
        const { data, order, itemKey, itemDefaults, Item } = this;
        return { data, order, itemKey, itemDefaults, itemType: Item.name };
    };
    
    function Radio({ mode, label, description, name, value, onChange, validate, setter } = {}) {
        Datum.call(this, { mode, label, description, name, onChange, validate, setter });
        Object.defineProperty(this, 'value', Datum.descriptor);
        List.call(this, Input, 'key', {
            type: 'radio',
            name: this.name,
            mode: this.mode,
            validate: this.validate,
            setter: this.setter,
        });
    }
    Object.defineProperty(Radio.prototype, 'mode',  Mode.descriptor);
    for (const fn of ['add', 'update', 'remove', 'setValue', 'getValue']) {
        Radio.prototype[fn] = function(...args) {
            return List.prototype[fn].apply(this, args);
        };
    }
    Radio.prototype.changeItem = function(itemValue) {
        const { Item, itemDefaults } = this;
        console.log('------ itemDefaults: ', JSON.stringify(itemDefaults, null, 2));
        console.log('changeItem: ', itemValue);
        let error;
        const elem = new Item(extend({}, itemDefaults, {
            value: itemValue,
            onChange: function(err){
                error = err;
            },
        }));
        console.log('changeItem -> elem: ', JSON.stringify(elem, null, 2));
        if (error) {
            console.log('In changeItem, ', error);
            return;
        }
        console.log('changeItem -> item: ', JSON.stringify(elem, null, 2));
        for (const key of this.order) {
            const item = this.data[key];
            console.log('[' + key + ']: ', JSON.stringify(item, null, 2));
            if (item.value === elem.value) {
                this.value = itemValue;
                console.log('-------------------- found');
                break;
            }
        }
    };
    Radio.prototype.assign = function(radio) {
        if (!radio || radio === this) {
            return this;
        }
        if (radio instanceof Radio || typeof radio === 'object') {
            const { value = this.value } = radio;
            this.value = value;
        }
        Datum.prototype.assign.call(this, radio);
        List.prototype.assign.call(this, radio);
        return this;
    };
    Radio.parse = function(elem) {
        if (elem instanceof Radio) {
            return elem;
        }
        if (typeof elem === 'object') {
            const radio = new Radio(elem);
            const { data = {}, order = [] } = elem;
            for (const key of order) {
                radio.add(data[key]);
            }
            return radio;
        }
        return new Radio();
    };
    Radio.prototype.toJSON = function() {
        const { value } = this;
        return extend(
            { value, type: 'radio' },
            Datum.prototype.toJSON.call(this),
            List.prototype.toJSON.call(this),
        );
    };
    function Select({ mode, label, description, name, value, onChange, validate, setter } = {}) {
        Datum.call(this, { mode, label, description, name, onChange, validate, setter });
        Object.defineProperty(this, 'value', Datum.descriptor);
        List.call(this, Option, 'key', {
            name: this.name,
            mode: this.mode,
            validate: this.validate,
            setter: this.setter,
        });
    }
    Object.defineProperty(Select.prototype, 'mode',  Mode.descriptor);
    for (const fn of ['add', 'update', 'remove', 'setValue', 'getValue']) {
        Radio.prototype[fn] = function(...args) {
            return List.prototype[fn].apply(this, args);
        };
    }
    Select.prototype.changeItem = function(itemValue) {
        const { Item, itemDefaults } = this;
        console.log('------ itemDefaults: ', JSON.stringify(itemDefaults, null, 2));
        console.log('changeItem: ', itemValue);
        let error;
        const elem = new Item(extend({}, itemDefaults, {
            value: itemValue,
            onChange: function(err){
                error = err;
            },
        }));
        console.log('changeItem -> elem: ', JSON.stringify(elem, null, 2));
        if (error) {
            console.log('changeItem -> elem: ', JSON.stringify(elem, null, 2));
            return;
        }
        console.log('changeItem -> item: ', JSON.stringify(elem, null, 2));
        for (const key of this.order) {
            const item = this.data[key];
            console.log('[' + key + ']: ', JSON.stringify(item, null, 2));
            if (item.value === elem.value) {
                this.value = itemValue;
                console.log('-------------------- found');
                break;
            }
        }
    };
    Select.prototype.assign = function(elem) {
        if (!elem || elem === this) {
            return this;
        }
        if (elem instanceof Select || typeof elem === 'object') {
            const { value = this.value } = elem;
            this.value = value;
        }
        Datum.prototype.assign.call(this, elem);
        List.prototype.assign.call(this, elem);
        return this;
    };
    Select.parse = function(elem) {
        if (elem instanceof Select) {
            return elem;
        }
        if (typeof elem === 'object') {
            const select = new Select(elem);
            const { data = {}, order = [] } = elem;
            for (const key of order) {
                select.add(data[key]);
            }
            return select;
        }
        return new Select();
    };
    Select.prototype.toJSON = function() {
        const { value } = this;
        return extend(
            { value, type: 'select' },
            Datum.prototype.toJSON.call(this),
            List.prototype.toJSON.call(this),
        );
    };
    const list = new List();
    const onChange = function(error, value) {
        if (error) {
            console.log('error occured while setting value, ', error);
            return;
        }
        console.log('next value: ', value);
    };
    onChange.toJSON = fn.toJSON;
    const validate = function(value) {
        let match;
        switch (typeof value) {
            case 'undefined':
            case 'number':
                break;
            case 'string':
                match = value.trim().match(/^(\d+(?:\.\d+)?)\s?(k|m|g|t)?b?/i);
                if (match && match[1]) break;
            default:
                throw new TypeError('invalid value');
        }
    };
    validate.toJSON = fn.toJSON;
    const setter = function(value) {
        let match;
        switch (typeof value) {
            case 'string':
                match = value.trim().match(/^(\d+(?:\.\d+)?)\s?(k|m|g|t)?b?/i);
                return +match[1] * Math.pow(1024, match[2] ? ['', 'k', 'm', 'g', 't'].indexOf(match[2].toLowerCase()) : undefined);
            case 'number':
                return value;
            default:
                throw new TypeError('invalid value');
        }
    };
    setter.toJSON = fn.toJSON;
    const input = new Input({
        name: 'file-size',
        label: 'File size (bytes)',
        description: 'Here is size of uploaded file in bytes',
        value: 1023,
        key: '1G',
        type: 'radio',
        onChange,
        validate,
        setter,
    });
    list.add(input);
    console.log('item: ', JSON.stringify(input, null, 2));
    console.log('list: ', JSON.stringify(list, null, 2));
    list.setValue('file-size', '1 GB');
    console.log('item.value: ', input.value);
    const radio = new Radio({ name: 'file-size', value: '1G', onChange, validate, setter });
    radio.add(input);
    radio.add(new Input({
        name: 'file-size',
        key: '10MB',
        label: 'File size (bytes)',
        value: '10 MB',
        type: 'radio',
        onChange,
        validate,
        setter,
    }));
    radio.changeItem('10MB');
    radio.value;
    console.log('radio: ', JSON.stringify(radio, null, 2));
    function TabItem({
        mode, type, name, label, description, value, onChange, validate, setter,
    } = {}) {
        let Item;
        switch (type) {
            case 'radio':
                Item = Radio;
                break;
            case 'select':
                Item = Select;
                break;
            default:
                Item = Input;
        }
        Object.defineProperty(this, 'Class', { value: Item });
        this.Class.call(this, arguments[0]);
    }
    Object.defineProperty(TabItem.prototype, 'mode',  Mode.descriptor);
    TabItem.prototype.assign = function() {
        return this.Class.prototype.assign.apply(this, arguments);
    };
    TabItem.parse = function(elem) {
        if (elem instanceof Option) {
            return elem;
        }
        if (typeof elem !== 'object') {
            return new Option();
        }
        const { type } = elem;
        if (type === 'radio') {
            const opt = new TabItem(elem);
            const { data = {}, order = [] } = elem;
            for (const key of order) {
                opt.add(data[key]);
            }
            return opt;
        }
        return new TabItem(elem);
    };
    TabItem.prototype.toJSON = function() {
        return this.Class.prototype.toJSON.apply(this, arguments);
    };
    for (const fn of ['add', 'update', 'remove', 'setValue', 'getValue', 'changeItem']) {
        TabItem.prototype[fn] = function() {
            if (this.Class === Radio) {
                return Radio.prototype[fn].apply(this, arguments);
            }
        };
    }
    const opt = new TabItem({ mode: Mode.ADVANCED, type: 'radio', name: 'file-size', value: '10.5 MB', onChange, validate, setter });
    opt.add({
        key: '1MB', value: '1MB',
    }, {
        key: '2MB', value: '2MB',
    }, {
        key: '1Gb', value: '1GB',
    }, {
        key: '2KB', value: '2K',
    });
    opt.changeItem('2kb');
    console.log('================== value: ', opt.value);
    const opt2 = new TabItem({ mode: Mode.ADVANCED, type: 'text', name: 'file-name', value: 'my_file.txt', onChange });
    console.log('================== opt2: ', JSON.stringify(opt2, null, 2));
    console.log('================== opt: ', JSON.stringify(opt, null, 2));
    opt2.value = 'my_file_xxx.out';
    console.log('================== opt2: ', JSON.stringify(opt2, null, 2));
    const json = JSON.stringify({ setter }, null, 2);
    console.log('{ setter }: ', json);
    const parsed = JSON.parse(json, reviver);
    console.log('parsed.setter: ', parsed.setter);
    try {
        if (1024 === parsed.setter('1Kb')) {
            console.log('PASSED');
        } else {
            console.log('FAILED');
        }
    } catch (err) {
        console.log('setter error: ', err);
    }
    const optJson = JSON.stringify(opt2, null, 2);
    const parsedOpt = JSON.parse(optJson, reviver);
    const opt3 = TabItem.parse(parsedOpt);
    console.log('opt3: ', opt3);
    
    function Tab({ name, key, label, description } = {}) {
        this.name = name;
        this.key = key || name;
        this.label = label;
        this.description = description;
        List.call(this, TabItem);
    }
    for (const fn of ['add', 'update', 'remove', 'setValue', 'getValue']) {
        Tab.prototype[fn] = function() {
            return List.prototype[fn].apply(this, arguments);
        };
    }
    Tab.prototype.assign = function(elem) {
        throw new Error('TODO');
    }; // TODO
    Tab.prototype.toJSON = function() {
        const { name, key, label, description } = this;
        return extend({
            name, key, label, description,
        }, List.prototype.toJSON.call(this));
    };
    Tab.parse = function(elem) {
        if (elem instanceof Tab) {
            return elem;
        }
        if (typeof elem !== 'object') {
            return new Tab();
        }
        const tb = new Tab(elem);
        const { data = {}, order = [] } = elem;
        for (const key of order) {
            tb.add(data[key]);
        }
        return tb;
    };
    const tab = new Tab({ name: 'general', label: 'General', description: 'Here are general options' });
    tab.add({ name: 'file-owner', type: 'text', value: 'Enakin Skywalker' });
    tab.add({ name: 'file-size', type: 'radio', value: '1Mb', validate, setter });
    tab.data['file-size'].add({
        key: '1Mb', value: '1M',
    }, {
        key: '10Mb', value: '10M',
    }, {
        key: '1Gb', value: '1GB',
    });
    const tabJson = JSON.stringify(tab, null, 2);
    console.log('tab: ', tabJson);
    const tab2 = Tab.parse(JSON.parse(tabJson, reviver));
    console.log('tab2: ', tab2);
    function Interface({
        mode: modeName = Mode.NORMAL, title = 'Interface', version = '0.1.0', onChangeMode = function(){},
    } = {}) {
        const mode = new Mode(modeName, null, onChangeMode);
        Object.defineProperty(this, 'mode', {
            get: function() { return mode.name; },
            set: function(n) { mode.name = n; },
            enumerable: true,
        });
        this.title = title;
        this.version = version;
        this.onChangeMode = onChangeMode;
        List.call(this, Tab);
    }
    for (const fn of ['add', 'update', 'remove']) {
        Interface.prototype[fn] = function() {
            return List.prototype[fn].apply(this, arguments);
        };
    }
    Interface.prototype.toJSON = function() {
        const { mode, title, version } = this;
        return extend({
            mode, title, version,
        }, List.prototype.toJSON.call(this));
    };
    Interface.parse = function(elem) {
        if (elem instanceof Interface) {
            return elem;
        }
        if (typeof elem !== 'object') {
            return new Interface();
        }
        const intf = new Interface(elem);
        const { data = {}, order = [] } = elem;
        for (const key of order) {
            intf.add(data[key]);
        }
        return intf;
    };
    const iface = new Interface({
        onChangeMode: function(error, mode) {
            if (error) {
                console.log('onChangeMode: ', error);
                return;
            }
            console.log('new mode: ', mode);
        },
    });
    iface.add(tab);
    tab2.name = 'advanced';
    iface.add(tab2);
    const ifaceJson = JSON.stringify(iface, null, 2);
    console.log('interface: ', ifaceJson);
    const iface2 = Interface.parse(JSON.parse(ifaceJson));
    console.log('interface2: ', iface2);

    function extend(target) {
        target = target || {};
        const args = Array.prototype.slice.call(arguments, 1);
        for (const arg of args) {
            for (const key of Object.keys(arg)) {
                target[key] = arg[key];
            }
        }
        return target;
    }
    const { ESModules = {} } = window;
    ESModules.UIModules = {
        Mode,
        Datum,
        Input,
        Option,
        List,
        Radio,
        Select,
        Tab,
        TabItem,
        Interface,
        functionToJSON: fn.toJSON,
        functionReviver: reviver,
        memoryValidator: validate,
        memorySetter: setter,
    };
    window.ESModules = ESModules;
})(window)