Modules for UI constructing
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @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="####" /> <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(####, 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 (#### === 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)