CKUIToolkit

A simple settings modal framework.

目前為 2022-03-17 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/441653/1029229/CKUIToolkit.js

  1. // ==UserScript==
  2. // @name CKUIToolkit
  3. // @namespace ckylin-script-lib-combined-ui-components
  4. // @version 1.0
  5. // @match http://*
  6. // @match https://*
  7. // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1028655
  8. // @resource popjs https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.js
  9. // @resource popcss https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.css
  10. // @resource fpjs https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.js
  11. // @resource fpcss https://cdn.jsdelivr.net/gh/CKylinMC/FloatPopup.js@main/floatpopup.modal.css
  12. // @author CKylinMC
  13. // @license GPL-3.0-only
  14. // @grant GM_getResourceText
  15. // @grant unsafeWindow
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. if (typeof (unsafeWindow) == 'undefined') {
  20. unsafeWindow = window;
  21. }
  22. if (typeof (GM_getResourceText) != 'undefined') {
  23. //======[Apply all resources]
  24. const resourceList = [
  25. { name: 'popjs', type: 'js' },
  26. { name: 'popcss', type: 'css' },
  27. { name: 'fpjs', type: 'js' },
  28. { name: 'fpcss', type: 'css' },
  29. { name: 'popcsspatch', type: 'rawcss', content: "div.popNotifyUnitFrame{z-index:110000!important;}.CKTOOLS-modal-content{color: #616161!important;max-height: 80vh;overflow: auto;}" },
  30. { name: 'settingscss', type: 'rawcss', content: `
  31. .ckui-base .ckui-text{
  32. font-size: 14px;
  33. line-height: 1.428571429;
  34. }
  35. .ckui-base label{
  36. display: block;
  37. color: rgb(16, 140, 255);
  38. padding-top: 12px;
  39. }
  40. .ckui-base .ckui-texttoggle{
  41. display: block;
  42. color: rgb(16, 140, 255);
  43. padding-top: 12px;
  44. }
  45. .ckui-base .ckui-texttoggle-container::before{
  46. display: inline;
  47. content: "🔹";
  48. }
  49. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value{
  50. padding: 0px 6px;
  51. font-weight: bold;
  52. -webkit-user-select: none;
  53. -moz-user-select: none;
  54. -ms-user-select: none;
  55. user-select: none;
  56. cursor: pointer;
  57. }
  58. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::before{
  59. content: "["
  60. }
  61. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::after{
  62. content: "]"
  63. }
  64. .ckui-base .ckui-texttoggle-container::after{
  65. display: inline;
  66. content: "(点击切换)";
  67. color: gray;
  68. font-size: 12px;
  69. font-style: italic;
  70. padding-left: 12px;
  71. }
  72. .ckui-base .ckui-description{
  73. display: block;
  74. color: rgb(92, 92, 92);
  75. font-size: small;
  76. font-style: italic;
  77. }
  78. .ckui-base .ckui-toggle{
  79. padding-top: 12px;
  80. }
  81. .ckui-base label.ckui-inline-label{
  82. display: inline;
  83. line-height: 18px;
  84. }
  85. .ckui-base .ckui-input input{
  86. display: block;
  87. width: calc(100% - 28px);
  88. height: 34px;
  89. padding: 1px 3px;
  90. margin: 3px 6px;
  91. font-size: 14px;
  92. line-height: 1.428571429;
  93. color: rgb(51, 51, 51);
  94. background-color: rgb(255, 255, 255);
  95. border: 1px solid rgb(204, 204, 204);
  96. border-radius: 4px;
  97. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  98. }
  99. .ckui-base .ckui-inputnumber input{
  100. display: block;
  101. width: calc(100% - 36px);
  102. height: 34px;
  103. padding: 1px 12px;
  104. margin: 3px 6px;
  105. font-size: 14px;
  106. line-height: 1.428571429;
  107. color: rgb(51, 51, 51);
  108. background-color: rgb(255, 255, 255);
  109. border: 1px solid rgb(204, 204, 204);
  110. border-radius: 4px;
  111. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  112. }
  113. .ckui-base .ckui-inputarea textarea{
  114. display: block;
  115. width: calc(100% - 28px);
  116. height: 100px;
  117. padding: 6px 6px;
  118. margin: 3px 6px;
  119. font-size: 14px;
  120. line-height: 1.428571429;
  121. color: rgb(51, 51, 51);
  122. background-color: rgb(255, 255, 255);
  123. border: 1px solid rgb(204, 204, 204);
  124. border-radius: 4px;
  125. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  126. }
  127. .ckui-base .ckui-select select{
  128. display: block;
  129. width: calc(100% - 28px);
  130. height: 34px;
  131. padding: 6px 6px;
  132. margin: 3px 6px;
  133. font-size: 14px;
  134. line-height: 1.428571429;
  135. color: rgb(51, 51, 51);
  136. background-color: rgb(255, 255, 255);
  137. border: 1px solid rgb(204, 204, 204);
  138. border-radius: 4px;
  139. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  140. }
  141. .ckui-base .ckui-select select option{
  142. font-size: 16px;
  143. line-height: 1.428571429;
  144. color: rgb(51, 51, 51);
  145. }
  146. .ckui-base .ckui-header::before{
  147. content: "💠";
  148. }
  149. .ckui-base .ckui-header{
  150. width: calc(100% - 8px);
  151. display: block;
  152. color: rgb(16, 140, 255);
  153. padding: 12px 3px;
  154. border-bottom: 2px solid rgb(16, 140, 255);
  155. margin: 12px 0px 12px 0px;
  156. }
  157. .ckui-base .ckui-btns{
  158. display: flex;
  159. flex-wrap: wrap;
  160. flex-direction: row;
  161. }
  162. .ckui-base .ckui-btn{
  163. display: block;
  164. width: calc(50% - 8px);
  165. height: 40px;
  166. padding: 6px 12px;
  167. margin: 6px;
  168. font-size: 14px;
  169. line-height: 1.428571429;
  170. color: rgb(255, 255, 255);
  171. background-color: rgb(16, 140, 255);
  172. border: 1px solid rgb(16, 140, 255);
  173. border-radius: 4px;
  174. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  175. }
  176. .ckui-base .ckui-btn:hover{
  177. background-color: rgb(0, 122, 255);
  178. }
  179. .ckui-base .ckui-btns .ckui-btn{
  180. flex: 1;
  181. }` }
  182. ]
  183. function applyResource() {
  184. resloop: for (let res of resourceList) {
  185. if (!document.querySelector("#" + res.name)) {
  186. let el;
  187. switch (res.type) {
  188. case 'js':
  189. case 'rawjs':
  190. el = document.createElement("script");
  191. break;
  192. case 'css':
  193. case 'rawcss':
  194. el = document.createElement("style");
  195. break;
  196. default:
  197. console.log('Err:unknown type', res);
  198. continue resloop;
  199. }
  200. el.id = res.name;
  201. el.innerHTML = res.type.startsWith('raw') ? res.content : GM_getResourceText(res.name);
  202. document.head.appendChild(el);
  203. }
  204. }
  205. }
  206. applyResource();
  207. }
  208. const {domHelper,deepClone} = CKTools;
  209. const CKUIToolkit = {};
  210. class CompUtils{
  211. static getId(name) {
  212. return 'ckui-settings-' + name;
  213. }
  214. static getClass(type) {
  215. return 'ckui-' + type;
  216. }
  217. static runChecker(checker = null,...args) {
  218. if (checker && typeof (checker) == 'function') {
  219. try {
  220. const result = checker(...args);
  221. return !!result;
  222. } catch (e) {
  223. return false;
  224. }
  225. } else return true;
  226. }
  227. static cfgValidator(cfg, keystr='') {
  228. let keys = keystr.split(',').map(el => el.trim()).filter(el => el.length > 0);
  229. keys.concat(['name', 'type']);
  230. if (!cfg) return false;
  231. for (let key of keys) {
  232. if (cfg[key]===undefined) return false;
  233. }
  234. return true;
  235. }
  236. }
  237. class Components{
  238. static text(cfg) {
  239. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  240. return domHelper('div', {
  241. id: CompUtils.getId(cfg.name),
  242. classlist: CompUtils.getClass(cfg.type),
  243. html: cfg.label
  244. });
  245. }
  246. static header(cfg) {
  247. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  248. return domHelper('div', {
  249. id: CompUtils.getId(cfg.name),
  250. classlist: CompUtils.getClass(cfg.type),
  251. html: cfg.label
  252. });
  253. }
  254. static toggle(cfg) {
  255. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  256. const customId = 'toggle'+CKTools.GUID.getShort();
  257. return domHelper('div', {
  258. id: CompUtils.getId(cfg.name),
  259. classlist: CompUtils.getClass(cfg.type),
  260. childs: [
  261. domHelper('input', {
  262. id: customId,
  263. attr: {
  264. type: 'checkbox',
  265. value: cfg.value??false
  266. },
  267. on: {
  268. change: (e) => {
  269. const value = !!(e.target?.value);
  270. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  271. // else TODO: error tip
  272. }
  273. }
  274. }),
  275. domHelper('label', {
  276. attr: {
  277. for: customId
  278. },
  279. classlist:'ckui-inline-label',
  280. html: cfg.label
  281. }),
  282. domHelper('span', {
  283. classList: 'ckui-description',
  284. html: cfg.description??''
  285. }),
  286. ]
  287. });
  288. }
  289. static input(cfg) {
  290. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  291. return domHelper('div', {
  292. id: CompUtils.getId(cfg.name),
  293. classlist: CompUtils.getClass(cfg.type),
  294. childs: [
  295. domHelper('label', {
  296. html: cfg.label
  297. }),
  298. domHelper('input', {
  299. attr:cfg.value,
  300. on: {
  301. keyup: CKTools.debounce((e) => {
  302. const value = e.target?.value;
  303. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  304. // else TODO: error tip
  305. })
  306. }
  307. }),
  308. domHelper('span', {
  309. classList: 'ckui-description',
  310. html: cfg.description??''
  311. }),
  312. ]
  313. });
  314. }
  315. static inputarea(cfg) {
  316. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  317. return domHelper('div', {
  318. id: CompUtils.getId(cfg.name),
  319. classlist: CompUtils.getClass(cfg.type),
  320. childs: [
  321. domHelper('label', {
  322. html: cfg.label
  323. }),
  324. domHelper('textarea', {
  325. attr: {
  326. value: cfg.value
  327. },
  328. on: {
  329. keyup: CKTools.debounce((e) => {
  330. const value = e.target?.value ?? e.target.innerHTMl;
  331. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  332. // else TODO: error tip
  333. })
  334. }
  335. }),
  336. domHelper('span', {
  337. classList: 'ckui-description',
  338. html: cfg.description??''
  339. }),
  340. ]
  341. });
  342. }
  343. static inputnumber(cfg) {
  344. if (!CompUtils.cfgValidator(cfg, 'label,min,max,step')) return;
  345. return domHelper('div', {
  346. id: CompUtils.getId(cfg.name),
  347. classlist: CompUtils.getClass(cfg.type),
  348. childs: [
  349. domHelper('label', {
  350. html: cfg.label
  351. }),
  352. domHelper('input', {
  353. attr: {
  354. type: 'number',
  355. value: cfg.value,
  356. min: cfg.min,
  357. max: cfg.max,
  358. step: cfg.step
  359. },
  360. on: {
  361. keyup: CKTools.debounce((e) => {
  362. const value = e.target?.value;
  363. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  364. // else TODO: error tip
  365. })
  366. }
  367. }),
  368. domHelper('span', {
  369. classList: 'ckui-description',
  370. html: cfg.description??''
  371. }),
  372. ]
  373. });
  374. }
  375. static select(cfg) {
  376. if (!CompUtils.cfgValidator(cfg, 'label,options')) return;
  377. return domHelper('div', {
  378. id: CompUtils.getId(cfg.name),
  379. classlist: CompUtils.getClass(cfg.type),
  380. childs: [
  381. domHelper('label', {
  382. html: cfg.label
  383. }),
  384. domHelper('select', {
  385. attr: {
  386. value: cfg.value??'',
  387. },
  388. init: select => {
  389. for (let option of cfg.options) {
  390. select.appendChild(domHelper('option', {
  391. attr: {
  392. value: option.value,
  393. selected: cfg.value == option.value
  394. },
  395. html: option.opt
  396. }));
  397. }
  398. },
  399. on: {
  400. change: CKTools.debounce((e) => {
  401. const value = !!(e.target?.value);
  402. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  403. // else TODO: error tip
  404. })
  405. }
  406. }),
  407. domHelper('span', {
  408. classList: 'ckui-description',
  409. html: cfg.description??''
  410. }),
  411. ]
  412. });
  413. }
  414. static texttoggle(cfg) {
  415. if (!CompUtils.cfgValidator(cfg, 'before,on,off,after')) return;
  416. return domHelper('div', {
  417. id: CompUtils.getId(cfg.name),
  418. classlist: CompUtils.getClass(cfg.type),
  419. childs: [
  420. domHelper('div', {
  421. classList:'ckui-texttoggle-container',
  422. childs: [
  423. domHelper('span', { text: cfg.before }),
  424. domHelper('span', {classList:'ckui-texttoggle-value',text:'...'}),
  425. domHelper('span', {text:cfg.after}),
  426. ],
  427. init: div => {
  428. const getText = () => cfg.value ? cfg.on : cfg.off;
  429. const setValue = (value) => {
  430. cfg.value = !!value;
  431. div.querySelector('.ckui-texttoggle-value').innerText = getText();
  432. }
  433. const toggleValue = () => setValue(!cfg.value);
  434. div.addEventListener('click', toggleValue);
  435. setValue(cfg.value);
  436. }
  437. }),
  438. domHelper('span', {
  439. classList: 'ckui-description',
  440. html: cfg.description??''
  441. }),
  442. ]
  443. });
  444. }
  445. static raw(cfg) {
  446. if (!CompUtils.cfgValidator(cfg, 'contents')) return;
  447. return domHelper('div', {
  448. id: CompUtils.getId(cfg.name),
  449. classlist: CompUtils.getClass(cfg.type),
  450. childs:domHelper(cfg.contents)
  451. })
  452. }
  453. static window(cfg) {
  454. if (!CompUtils.cfgValidator(cfg, 'label,config')) return;
  455. return domHelper('div', {
  456. id: CompUtils.getId(cfg.name),
  457. classlist: CompUtils.getClass(cfg.type),
  458. childs: [
  459. domHelper('button', {
  460. classList: 'ckui-btn',
  461. html: cfg.label,
  462. on: {
  463. click: async (e) => {
  464. cfg.config = await SettingsBuilder.open(cfg.config);
  465. }
  466. }
  467. })
  468. ]
  469. })
  470. }
  471. static btns(cfg) {
  472. if (!CompUtils.cfgValidator(cfg, 'btns')) return;
  473. return domHelper('div', {
  474. id: CompUtils.getId(cfg.name),
  475. classlist: CompUtils.getClass(cfg.type),
  476. init: el => {
  477. for(let btn of cfg.btns) {
  478. el.appendChild(domHelper('button', {
  479. classList: 'ckui-btn',
  480. html: btn.label,
  481. on: {
  482. click: async (e) => {
  483. await btn.onclick();
  484. }
  485. }
  486. }));
  487. }
  488. }
  489. })
  490. }
  491. }
  492. class SettingsBuilder{
  493. static async open(cfg) {
  494. const s = new SettingsBuilder(cfg);
  495. const result = await s.showWindow();
  496. return result;
  497. }
  498. constructor(config) {
  499. this.config = Object.assign({
  500. title: '设置',
  501. settings:[]
  502. },config);
  503. }
  504. async showWindow(config = this.config) {
  505. const copiedConfig = deepClone(config);
  506. console.log('Cloned:',config,copiedConfig)
  507. return new Promise(r => {
  508. FloatPopup.confirm(copiedConfig.title, domHelper('div', {
  509. classlist:'ckui-base',
  510. init: el => {
  511. for (const comp of copiedConfig.settings) {
  512. const r = this.makeComponent(comp);
  513. r&&el.appendChild(r);
  514. }
  515. }
  516. }), "保存", "取消").then(result => result?r(copiedConfig):r(config));
  517. })
  518. }
  519. makeComponent(cfg) {
  520. if (Components.hasOwnProperty(cfg.type)) {
  521. return Components[cfg.type](cfg);
  522. }
  523. }
  524. }
  525. CKUIToolkit.showSettings = SettingsBuilder.open;
  526.  
  527. unsafeWindow.CKUIToolkit = CKUIToolkit;
  528. })();