An gui for users to select items from given standardized json
สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/458132/1138364/ItemSelector.js
/* eslint-disable no-multi-spaces */ /* eslint-disable dot-notation */ // ==UserScript== // @name ItemSelector // @namespace ItemSelector // @version 0.3.4 // @description An gui for users to select items from given standardized json // @author PY-DNG // @license GPL-v3 // ==/UserScript== /* global structuredClone */ let ItemSelector = (function() { // function DoLog() {} // Arguments: level=LogLevel.Info, logContent, trace=false const [LogLevel, DoLog] = (function() { const LogLevel = { None: 0, Error: 1, Success: 2, Warning: 3, Info: 4, }; return [LogLevel, DoLog]; function DoLog() { // Get window const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window; const LogLevelMap = {}; LogLevelMap[LogLevel.None] = { prefix: '', color: 'color:#ffffff' } LogLevelMap[LogLevel.Error] = { prefix: '[Error]', color: 'color:#ff0000' } LogLevelMap[LogLevel.Success] = { prefix: '[Success]', color: 'color:#00aa00' } LogLevelMap[LogLevel.Warning] = { prefix: '[Warning]', color: 'color:#ffa500' } LogLevelMap[LogLevel.Info] = { prefix: '[Info]', color: 'color:#888888' } LogLevelMap[LogLevel.Elements] = { prefix: '[Elements]', color: 'color:#000000' } // Current log level DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error // Log counter DoLog.logCount === undefined && (DoLog.logCount = 0); // Get args let [level, logContent, trace] = parseArgs([...arguments], [ [2], [1,2], [1,2,3] ], [LogLevel.Info, 'DoLog initialized.', false]); // Log when log level permits if (level <= DoLog.logLevel) { let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : ''); let subst = LogLevelMap[level].color; switch (typeof(logContent)) { case 'string': msg += '%s'; break; case 'number': msg += '%d'; break; default: msg += '%o'; break; } if (++DoLog.logCount > 512) { console.clear(); DoLog.logCount = 0; } console[trace ? 'trace' : 'log'](msg, subst, logContent); } } }) (); return ItemSelector; function ItemSelector(useWrapper=true) { const IS = this; const DATA = { showing: false, json: null, data: null, options: null }; const elements = IS.elements = {}; defineGetter(IS, 'showing', () => DATA.showing); defineGetter(IS, 'json', () => MakeReadonlyObj(DATA.json)); defineGetter(IS, 'data', () => MakeReadonlyObj(DATA.data)); defineGetter(IS, 'options', () => MakeReadonlyObj(DATA.options)); IS.show = show; IS.close = close; IS.setTheme = setTheme; IS.getSelectedItems = getSelectedItems; init(); function init() { const wrapperDoc = elements.wrapperDoc = useWrapper ? (function() { const wrapper = elements.wrapper = $CrE(randstr(4, false, false) + '-' + randstr(4, false, false)); const shadow = wrapper.attachShadow({mode: 'closed'}); wrapper.style.display = 'block'; wrapper.style.zIndex = 99999999; document.body.appendChild(wrapper); return shadow; }) () : document; const wrapper = elements.wrapper = useWrapper ? wrapperDoc : wrapperDoc.body; const container = elements.container = $CrE('div'); const header = elements.header = $CrE('div'); const body = elements.body = $CrE('div'); const footer = elements.footer = $CrE('div'); container.classList.add('itemselector-container'); header.classList.add('itemselector-header'); body.classList.add('itemselector-body'); footer.classList.add('itemselector-footer'); container.appendChild(header); container.appendChild(body); container.appendChild(footer); wrapper.appendChild(container); const title = elements.title = $CrE('span'); title.classList.add('itemselector-title'); header.appendChild(title); const bglist = elements.bglist = $CrE('div'); bglist.classList.add('itemselector-bglist'); body.appendChild(bglist); const list = elements.list = $CrE('pre'); list.classList.add('itemselector-list'); body.appendChild(list); const btnOK = $CrE('button'); const btnCancel = $CrE('button'); const btnClose = $CrE('button'); btnOK.innerText = 'OK'; btnCancel.innerText = 'Cancel'; btnClose.innerText = 'x'; btnOK.className = 'itemselector-button itemselector-button-ok'; btnCancel.className = 'itemselector-button itemselector-button-cancel'; btnClose.className = 'itemselector-button itemselector-button-close'; $AEL(btnOK, 'click', ok_onClick); $AEL(btnCancel, 'click', cancel_onClick); $AEL(btnClose, 'click', close_onClick); header.appendChild(btnClose); footer.appendChild(btnCancel); footer.appendChild(btnOK); elements.button = {btnOK, btnCancel, btnClose}; const cssParent = useWrapper ? wrapper : document.head; const css = '.itemselector-container {display: none;position: fixed;position: fixed;width: 60vw;height: 60vh;left: 20vw;top: 20vh;border-radius: 1em;padding: 2em;user-select: none;font-family: -apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol!important;}.itemselector-container.itemselector-show {display: block;}.itemselector-header {position: absolute;width: calc(100% - 4em);padding-bottom: 0.3em;}.itemselector-title {position: relative;font-size: 1.3em;}.itemselector-body {position: absolute;top: calc(2em + 20px * 1.3 + 20px * 0.3 + 1px + 0.3em);bottom: calc(2em + 20px + 20px + calc(60vw - 4em) * 2 / 100 + 0.3em);overflow: auto;width: calc(100% - 4em);z-index: -2;}.itemselector-bglist {position: absolute;left: 0;width: 100%;z-index: -1;}.itemselector-footer {position: absolute;bottom: 2em;width: calc(100% - 4em);}.itemselector-button {font-size: 20px;width: 48%;margin: 1%;border: none;border-radius: 3px;padding: 0.5em;font-weight: 500;}.itemselector-button.itemselector-button-close {position: relative;float: right;margin: 0;padding: 0;width: 1.3em;height: 1.3em;text-align: center;font-size: 20px;}.itemselector-list {margin: 0;pointer-events: none;}.itemselector-item {margin: 0;margin-left: 1em;}.itemselector-item-root {margin-left: 0;}.itemselector-item-background {width: 100%;height: 49px;}.itemselector-item-background:first-child {border-top: none;}.itemselector-item-background.itemselector-hide {display: none;}.itemselector-item-self {font-size: 14px;line-height: 34px;padding: 8px;background-color: rgba(0,0,0,0);pointer-events: auto;}.itemselector-toggle {position: relative;visibility: hidden;}.itemselector-toggle.itemselector-show {visibility: visible;}.itemselector-toggle:before {content: "\\25BC";width: 1em;display: inline-block;position: relative;}.itemselector-item-collapsed>.itemselector-item-self>.itemselector-toggle:before {content: "\\25B6";}.itemselector-item-collapsed>.itemselector-item-child>.itemselector-item {display: none;}.itemselector-text {pointer-events: none;margin-left: 0.5em;}.itemselector-container.light {--itemselector-color: #000;--itemselector-bgcolor-1: #dddddd;--itemselector-bgcolor-0: #e2e2e2;--itemselector-bgcolor-2: #cdcdcd;--itemselector-bgcolor-3: #bdbdbd;--itemselector-btnclose-bgcolor: #00bcd4;--itemselector-spliter-color: rgba(0,0,0,0.28);}.itemselector-container.dark {--itemselector-color: #fff;--itemselector-bgcolor-0: #1d1d1d;--itemselector-bgcolor-1: #222222;--itemselector-bgcolor-2: #323232;--itemselector-bgcolor-3: #424242;--itemselector-btnclose-bgcolor: #00bcd4;--itemselector-spliter-color: rgba(255,255,255,0.28);}.itemselector-container {box-shadow: 0 3px 15px rgb(0 0 0 / 20%), 0 6px 6px rgb(0 0 0 / 14%), 0 9px 3px -6px rgb(0 0 0 / 12%);color: var(--itemselector-color);background-color: var(--itemselector-bgcolor-0);}.itemselector-header {border-bottom: 1px solid var(--itemselector-spliter-color);}.itemselector-body {scrollbar-color: var(--itemselector-bgcolor-2) var(--itemselector-bgcolor-1);}.itemselector-body:hover {scrollbar-color: var(--itemselector-bgcolor-3) var(--itemselector-bgcolor-1);}.itemselector-body::-webkit-scrollbar {background-color: var(--itemselector-bgcolor-1);}.itemselector-body::-webkit-scrollbar-corner {background-color: var(--itemselector-bgcolor-1);}.itemselector-body::-webkit-scrollbar-thumb, .itemselector-body::-webkit-scrollbar-button {background-color: var(--itemselector-bgcolor-2);}.itemselector-body::-webkit-scrollbar-thumb:hover, .itemselector-body::-webkit-scrollbar-button:hover {background-color: var(--itemselector-bgcolor-3);}.itemselector-item-background {transition-duration: 0.3s;border-top: 1px solid var(--itemselector-spliter-color);}.itemselector-item-background.itemselector-item-hover {background-color: var(--itemselector-bgcolor-2);}.itemselector-button {background-color: var(--itemselector-btnclose-bgcolor);color: var(--itemselector-color);}.itemselector-button.itemselector-button-close {background-color: var(--itemselector-bgcolor-2);}.itemselector-button.itemselector-button-close:hover {background-color: var(--itemselector-bgcolor-3);}'; const style = $CrE('style'); style.innerHTML = css; cssParent.appendChild(style); function ok_onClick(e) { if (!DATA.showing) { DoLog(LogLevel.Warning, 'ok_onClick invoked when dialog is not showing'); return false; } if (!DATA.options) { DoLog(LogLevel.Warning, 'DATA.options missing while ok_onClick invoked'); return false; } typeof DATA.options.onok === 'function' && DATA.options.onok.call(this, e, getSelectedItems()); close(); } function cancel_onClick(e) { if (!DATA.showing) { DoLog(LogLevel.Warning, 'cancel_onClick invoked when dialog is not showing'); return false; } if (!DATA.options) { DoLog(LogLevel.Warning, 'DATA.options missing while cancel_onClick invoked'); return false; } typeof DATA.options.oncancel === 'function' && DATA.options.oncancel.call(this, e, getSelectedItems()); close(); } function close_onClick(e) { if (!DATA.showing) { DoLog(LogLevel.Warning, 'close_onClick invoked when dialog is not showing'); return false; } if (!DATA.options) { DoLog(LogLevel.Warning, 'DATA.options missing while close_onClick invoked'); return false; } typeof DATA.options.onclose === 'function' && DATA.options.onclose.call(this, e, getSelectedItems()); close(); } } function show(json, options={title: ''}) { // Status check & update if (!json) { DoLog(LogLevel.Error, 'json missing'); return false; } if (DATA.showing) { DoLog(LogLevel.Error, 'show invoked while DATA.showing === true'); return false; } DATA.showing = true; DATA.options = options; DATA.json = structuredClone(json); DATA.data = makeData(json); // elements const {container, header, title, body, footer, bglist, list} = elements; // cleanings [...list.children].forEach(c => c.remove()); [...bglist.children].forEach(c => c.remove()); // make new <ul> const ul = makeListItem(json); ul.classList.add('itemselector-item-root'); list.appendChild(ul); // configure with options options.hasOwnProperty('title') && (title.innerText = options.title); // display container updateElementSelect(); container.classList.add('itemselector-show'); return IS; function makeListItem(json_item, path=[]) { const item = pathItem(path); const hasChild = Array.isArray(item.children); // create new div const div = item.elements.div = $CrE('div'); const self_container = item.elements.self_container = $CrE('div'); const child_container = item.elements.child_container = $CrE('div'); const background = item.elements.background = $CrE('div'); div.classList.add('itemselector-item'); self_container.classList.add('itemselector-item-self'); child_container.classList.add('itemselector-item-child'); background.classList.add('itemselector-item-background'); hasChild && div.classList.add('itemselector-item-parent'); $AEL(background, 'mouseenter', e => background.classList.add('itemselector-item-hover')); $AEL(background, 'mouseleave', e => background.classList.remove('itemselector-item-hover')); $AEL(self_container, 'mouseenter', e => background.classList.add('itemselector-item-hover')); $AEL(self_container, 'mouseleave', e => background.classList.remove('itemselector-item-hover')); bglist.appendChild(background); div.appendChild(self_container); div.appendChild(child_container); // triangle toggle for folder items const toggle = item.elements.toggle = $CrE('a'); toggle.classList.add('itemselector-toggle'); hasChild && toggle.classList.add('itemselector-show'); $AEL(toggle, 'click', e => { destroyEvent(e); const collapsed = [...div.classList].includes('itemselector-item-collapsed'); div.classList[collapsed ? 'remove' : 'add']('itemselector-item-collapsed'); toggleBackground(item); function toggleBackground(item) { if (Array.isArray(item.children)) { for (const child of item.children) { child.elements.background.classList[collapsed ? 'remove' : 'add']('itemselector-hide'); toggleBackground(child); } } } }); self_container.appendChild(toggle); // checkbox for selecting const checkbox = item.elements.checkbox = $CrE('input'); checkbox.type = 'checkbox'; checkbox.classList.add('itemselector-checker'); $AEL(checkbox, 'change', checkbox_onChange); self_container.appendChild(checkbox); // check checkbox when self_container or background block onclick const clickTargets = [self_container, background] clickTargets.forEach(elm => $AEL(elm, 'click', function(e) { if (clickTargets.includes(e.target)) { checkbox.checked = !checkbox.checked; checkbox_onChange(); } })); // item text const text = item.elements.text = $CrE('span'); text.classList.add('itemselector-text'); text.innerText = json_item.text; self_container.appendChild(text); // make child items if (hasChild) { item.elements.children = []; for (let i = 0; i < json_item.children.length; i++) { const childItem = makeListItem(json_item.children[i], [...path, i]); item.elements.children.push(childItem); child_container.appendChild(childItem); } } return div; function checkbox_onChange(e) { // set select status item.selected = checkbox.checked; // update element updateElementSelect(); } } } function close() { if (!DATA.showing) { DoLog(LogLevel.Error, 'show invoked while DATA.showing === false'); return false; } DATA.showing = false; DATA.options = null; elements.container.classList.remove('itemselector-show'); } function setTheme(theme='light') { const THEMES = ['light', 'dark']; const root = elements.container; if (THEMES.includes(theme)) { THEMES.filter(t => t !== theme).forEach(t => root.classList.remove(t)); root.classList.add(theme); return true; } else { return false; } } function updateElementSelect() { //const data = DATA.data; update(DATA.data); function update(item) { // item elements const elements = item.elements; const checkbox = elements.checkbox; // props checkbox.checked = item.selected; checkbox.indeterminate = item.childSelected && !item.selected; // update children if (Array.isArray(item.children)) { for (const child of item.children) { update(child); } } } } function getSelectedItems() { const json = structuredClone(DATA.json); const data = DATA.data; const MARK = Symbol('cut-mark'); mark(json, data); return cut(json); function mark(json_item, data_item) { if (!data_item.selected && !data_item.childSelected) { json_item[MARK] = true; } else if (Array.isArray(data_item.children)) { for (let i = 0; i < data_item.children.length; i++) { mark(json_item.children[i], data_item.children[i]); } } } function cut(json_item) { if (json_item[MARK]) { return null; } else { const children = json_item.children; if (Array.isArray(children)) { for (const cutchild of children.filter(child => child[MARK])) { children.splice(children.indexOf(cutchild), 1); } children.forEach((child, i) => { children[i] = cut(child); }); } return json_item; } } } function pathItem(path) { return pathObj(DATA.data, path); } function pathObj(obj, path) { let target = obj; const _path = [...path]; while (_path.length) { target = target.children[_path.shift()]; } return target; } function makeData(json) { return proxyItemData(makeItemData(json)); function proxyItemData(data) { return typeof data === 'object' && data !== null ? new Proxy(data, { get: function(target, property, receiver) { const value = target[property]; const noproxy = typeof value === 'object' && value !== null && value['__NOPROXY__'] === true; return noproxy ? value : proxyItemData(value); }, set: function(target, property, value, receiver) { switch (property) { case 'selected': // set item and its children's selected status by rule select(target, value, !value); break; default: // setting other props are not allowed break; } return true; function select(item, selected) { // write item item.selected = selected; // write children selected select_children(item) // write parent selected select_parent(item); // calculate children childSelected childSelected_children(item); // calculate parent childSelected childSelected_parent(item); function select_children(item) { if (Array.isArray(item.children)) { for (const child of item.children) { if (child.selected !== selected) { child.selected = selected; select_children(child, selected); } } } } function select_parent(item) { if (item.parent) { const parent = item.parent; const selected = parent.children.every(child => child.selected); if (parent.selected !== selected) { parent.selected = selected; select_parent(parent); } } } function childSelected_children(item) { if (Array.isArray(item.children)) { for (const child of item.children) { childSelected_children(child); } item.childSelected = item.children.some(child => child.selected || child.childSelected); } else { item.childSelected = false; } } function childSelected_parent(item) { if (item.parent) { const parent = item.parent; const childSelected = parent.children.some(child => child.selected || child.childSelected); if (parent.childSelected !== childSelected) { parent.childSelected = childSelected; childSelected_parent(parent); } } } } } }) : data; } function makeItemData(json, parent=null) { const hasChild = Array.isArray(json.children); const item = {}; item.elements = {__NOPROXY__:true}; item.selected = true; item.childSelected = hasChild && json.children.length > 0; item.parent = parent !== null && typeof parent === 'object' ? parent : null; if (hasChild) { item.children = json.children.map(child => makeItemData(child, item)); } return item; } } function defineGetter(obj, prop, getter) { Object.defineProperty(obj, prop, { get: getter, set: v => true, configurable: false, enumerable: true, }); } } // Basic functions // querySelector function $() { switch(arguments.length) { case 2: return arguments[0].querySelector(arguments[1]); break; default: return document.querySelector(arguments[0]); } } // querySelectorAll function $All() { switch(arguments.length) { case 2: return arguments[0].querySelectorAll(arguments[1]); break; default: return document.querySelectorAll(arguments[0]); } } // createElement function $CrE() { switch(arguments.length) { case 2: return arguments[0].createElement(arguments[1]); break; default: return document.createElement(arguments[0]); } } // addEventListener function $AEL(...args) { const target = args.shift(); return target.addEventListener.apply(target, args); } // Just stopPropagation and preventDefault function destroyEvent(e) { if (!e) {return false;}; if (!e instanceof Event) {return false;}; e.stopPropagation(); e.preventDefault(); } function parseArgs(args, rules, defaultValues=[]) { // args and rules should be array, but not just iterable (string is also iterable) if (!Array.isArray(args) || !Array.isArray(rules)) { throw new TypeError('parseArgs: args and rules should be array') } // fill rules[0] (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []); // max arguments length const count = rules.length - 1; // args.length must <= count if (args.length > count) { throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`); } // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function for (let i = 1; i <= count; i++) { const rule = rules[i]; if (Array.isArray(rule)) { if (rule.length !== i) { throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`); } if (!rule.every((num) => (typeof num === 'number' && num <= count))) { throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`); } } else if (typeof rule !== 'function') { throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`) } } // Parse const rule = rules[args.length]; let parsed; if (Array.isArray(rule)) { parsed = [...defaultValues]; for (let i = 0; i < rule.length; i++) { parsed[rule[i]-1] = args[i]; } } else { parsed = rule(args, defaultValues); } return parsed; } function MakeReadonlyObj(val) { return isObject(val) ? new Proxy(val, { get: function(target, property, receiver) { return MakeReadonlyObj(target[property]); }, set: function(target, property, value, receiver) { return true; } }) : val; function isObject(value) { return ['object', 'function'].includes(typeof value) && value !== null; } } // Returns a random string function randstr(length=16, nums=true, cases=true) { const all = 'abcdefghijklmnopqrstuvwxyz' + (nums ? '0123456789' : '') + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : ''); return Array(length).fill(0).reduce(pre => (pre += all.charAt(randint(0, all.length-1))), ''); } function randint(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } }) ();