CKUIToolkit

A simple settings modal framework.

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/441653/1034229/CKUIToolkit.js

  1. // ==UserScript==
  2. // @name CKUIToolkit
  3. // @namespace ckylin-script-lib-combined-ui-components
  4. // @version 1.2.1
  5. // @match http://*
  6. // @match https://*
  7. // // @require https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1029952
  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/FloatWindow.js@main/floatwin.js
  11. // @resource fpcss https://cdn.jsdelivr.net/gh/CKylinMC/FloatWindow.js@main/floatwin.modal.css
  12. // @resource cktools https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1029952
  13. // @author CKylinMC
  14. // @license GPL-3.0-only
  15. // @grant GM_getResourceText
  16. // @grant unsafeWindow
  17. // ==/UserScript==
  18.  
  19. (function () {
  20. if (typeof (unsafeWindow) == 'undefined') {
  21. unsafeWindow = window;
  22. }
  23. if (typeof (GM_getResourceText) != 'function') {
  24. GM_getResourceText = () => null;
  25. }
  26. unsafeWindow.CKUIToolkit_loaded = false;
  27. //======[Apply all resources]
  28. const resourceList = [
  29. { name: 'popjs', type: 'js', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.js'},
  30. { name: 'popcss', type: 'css', source:'https://cdn.jsdelivr.net/gh/CKylinMC/PopNotify.js@master/PopNotify.css' },
  31. { name: 'fpjs', type: 'js', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/FloatWindow.js@main/floatwin.js' },
  32. { name: 'fpcss', type: 'css', source: 'https://cdn.jsdelivr.net/gh/CKylinMC/FloatWindow.js@main/floatwin.modal.css' },
  33. { name: 'cktools', type: 'js', source: 'https://greasyfork.org/scripts/429720-cktools/code/CKTools.js?version=1029952' },
  34. { name: 'popcsspatch', type: 'rawcss', content: "div.popNotifyUnitFrame{z-index:110000!important;}.CKTOOLS-modal-content{color: #616161!important;max-height: 80vh;overflow: auto;}" },
  35. { name: 'settingscss', type: 'rawcss', content: `
  36. .ckui-base .ckui-text{
  37. font-size: 14px;
  38. line-height: 1.428571429;
  39. }
  40. .ckui-base label{
  41. display: block;
  42. color: rgb(16, 140, 255);
  43. padding-top: 12px;
  44. }
  45. .ckui-base .ckui-texttoggle{
  46. display: block;
  47. color: rgb(16, 140, 255);
  48. padding-top: 12px;
  49. }
  50. .ckui-base .ckui-texttoggle-container::before{
  51. display: inline;
  52. content: "🔹";
  53. }
  54. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value{
  55. padding: 0px 6px;
  56. font-weight: bold;
  57. -webkit-user-select: none;
  58. -moz-user-select: none;
  59. -ms-user-select: none;
  60. user-select: none;
  61. cursor: pointer;
  62. }
  63. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::before{
  64. content: "["
  65. }
  66. .ckui-base .ckui-texttoggle-container .ckui-texttoggle-value::after{
  67. content: "]"
  68. }
  69. .ckui-base .ckui-texttoggle-container::after{
  70. display: inline;
  71. content: "(点击切换)";
  72. color: gray;
  73. font-size: 12px;
  74. font-style: italic;
  75. padding-left: 12px;
  76. }
  77. .ckui-base .ckui-description{
  78. display: block;
  79. color: rgb(92, 92, 92);
  80. font-size: small;
  81. font-style: italic;
  82. }
  83. .ckui-base .ckui-toggle{
  84. padding-top: 12px;
  85. }
  86. .ckui-base label.ckui-inline-label{
  87. display: inline;
  88. line-height: 18px;
  89. }
  90. .ckui-base .ckui-input input{
  91. display: block;
  92. width: calc(100% - 28px);
  93. height: 34px;
  94. padding: 1px 3px;
  95. margin: 3px 6px;
  96. font-size: 14px;
  97. line-height: 1.428571429;
  98. color: rgb(51, 51, 51);
  99. background-color: rgb(255, 255, 255);
  100. border: 1px solid rgb(204, 204, 204);
  101. border-radius: 4px;
  102. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  103. }
  104. .ckui-base .ckui-inputnumber input{
  105. display: block;
  106. width: calc(100% - 36px);
  107. height: 34px;
  108. padding: 1px 12px;
  109. margin: 3px 6px;
  110. font-size: 14px;
  111. line-height: 1.428571429;
  112. color: rgb(51, 51, 51);
  113. background-color: rgb(255, 255, 255);
  114. border: 1px solid rgb(204, 204, 204);
  115. border-radius: 4px;
  116. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  117. }
  118. .ckui-base .ckui-inputarea textarea{
  119. display: block;
  120. width: calc(100% - 28px);
  121. height: 100px;
  122. padding: 6px 6px;
  123. margin: 3px 6px;
  124. font-size: 14px;
  125. line-height: 1.428571429;
  126. color: rgb(51, 51, 51);
  127. background-color: rgb(255, 255, 255);
  128. border: 1px solid rgb(204, 204, 204);
  129. border-radius: 4px;
  130. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  131. }
  132. .ckui-base .ckui-select select{
  133. display: block;
  134. width: calc(100% - 28px);
  135. height: 34px;
  136. padding: 6px 6px;
  137. margin: 3px 6px;
  138. font-size: 14px;
  139. line-height: 1.428571429;
  140. color: rgb(51, 51, 51);
  141. background-color: rgb(255, 255, 255);
  142. border: 1px solid rgb(204, 204, 204);
  143. border-radius: 4px;
  144. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  145. }
  146. .ckui-base .ckui-select select option{
  147. font-size: 16px;
  148. line-height: 1.428571429;
  149. color: rgb(51, 51, 51);
  150. }
  151. .ckui-base .ckui-header::before{
  152. content: "💠";
  153. }
  154. .ckui-base .ckui-header{
  155. width: calc(100% - 8px);
  156. display: block;
  157. color: rgb(16, 140, 255);
  158. padding: 12px 3px;
  159. border-bottom: 2px solid rgb(16, 140, 255);
  160. margin: 12px 0px 12px 0px;
  161. }
  162. .ckui-base .ckui-btns{
  163. display: flex;
  164. flex-wrap: wrap;
  165. flex-direction: row;
  166. }
  167. .ckui-base .ckui-btn{
  168. display: block;
  169. width: calc(50% - 8px);
  170. height: 40px;
  171. padding: 6px 12px;
  172. margin: 6px;
  173. font-size: 14px;
  174. line-height: 1.428571429;
  175. color: rgb(255, 255, 255);
  176. background-color: rgb(16, 140, 255);
  177. border: 1px solid rgb(16, 140, 255);
  178. border-radius: 4px;
  179. box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
  180. }
  181. .ckui-base .ckui-btn:hover{
  182. background-color: rgb(0, 122, 255);
  183. }
  184. .ckui-base .ckui-btns .ckui-btn{
  185. flex: 1;
  186. }` }
  187. ]
  188. async function applyResource() {
  189. resloop: for (let res of resourceList) {
  190. if (true||!document.querySelector("#" + res.name)) {
  191. let el;
  192. switch (res.type) {
  193. case 'js':
  194. case 'rawjs':
  195. el = document.createElement("script");
  196. break;
  197. case 'css':
  198. case 'rawcss':
  199. el = document.createElement("style");
  200. break;
  201. default:
  202. console.error('[CKUI]','Err:unknown type', res);
  203. continue resloop;
  204. }
  205. el.id = res.name;
  206. let result = res.type.startsWith('raw') ? res.content : GM_getResourceText(res.name);
  207. if (result == null || result == 'null' || typeof result === 'undefined') {
  208. console.info('[CKUI]', 'Alternative method is using:',res.type,res.name);
  209. if (!res.source) {
  210. console.info('[CKUI]', 'Failed to apply:',res.type,res.name);
  211. continue resloop;
  212. }
  213. try {
  214. let response = await fetch(res.source);
  215. if (!response.ok) {
  216. console.info('[CKUI]', 'Failed to apply:',res.type,res.name,response.statusText);
  217. continue resloop;
  218. }
  219. result = await response.text();
  220. } catch (e) {
  221. console.info('[CKUI]', 'Failed to apply:',res.type,res.name,e);
  222. continue resloop;
  223. }
  224. }
  225. el.appendChild(document.createTextNode(result));
  226. document.head.appendChild(el);
  227. console.info('[CKUI]', 'Applied:',res.type,res.name);
  228. }
  229. }
  230. console.info('[CKUI]', 'Resources all applied');
  231. }
  232. applyResource().then(() => unsafeWindow.CKUIToolkit_loaded = true);
  233. let deepClone = (obj)=>{
  234. let newObject = {};
  235. if (Array.isArray(obj)) {
  236. newObject = [];
  237. for (let i = 0; i < obj.length; i++) {
  238. newObject.push(deepClone(obj[i]));
  239. }
  240. return newObject;
  241. }
  242. Object.keys(obj).map(key => {
  243. if (typeof obj[key] === 'object') {
  244. newObject[key] = deepClone(obj[key]);
  245. } else {
  246. newObject[key] = obj[key];
  247. }
  248. });
  249. return newObject;
  250. };
  251.  
  252. let domHelper = (...args) => {
  253. return CKTools.domHelper(...args);
  254. }
  255.  
  256. const CKUIToolkit = {};
  257. class CompUtils{
  258. static getId(name) {
  259. return 'ckui-settings-' + name;
  260. }
  261. static getClass(type) {
  262. return 'ckui-' + type;
  263. }
  264. static runChecker(checker = null,...args) {
  265. if (checker && typeof (checker) == 'function') {
  266. try {
  267. const result = checker(...args);
  268. return !!result;
  269. } catch (e) {
  270. console.log('checker errored', e);
  271. return false;
  272. }
  273. } else return true;
  274. }
  275. static cfgValidator(cfg, keystr='') {
  276. let keys = keystr.split(',').map(el => el.trim()).filter(el => el.length > 0);
  277. keys.concat(['name', 'type']);
  278. if (!cfg) return false;
  279. for (let key of keys) {
  280. if (cfg[key]===undefined) return false;
  281. }
  282. return true;
  283. }
  284. }
  285. class Components{
  286. static text(cfg) {
  287. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  288. return domHelper('div', {
  289. id: CompUtils.getId(cfg.name),
  290. classlist: CompUtils.getClass(cfg.type),
  291. html: cfg.label
  292. });
  293. }
  294. static header(cfg) {
  295. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  296. return domHelper('div', {
  297. id: CompUtils.getId(cfg.name),
  298. classlist: CompUtils.getClass(cfg.type),
  299. html: cfg.label
  300. });
  301. }
  302. static toggle(cfg) {
  303. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  304. const customId = 'toggle'+CKTools.GUID.getShort();
  305. return domHelper('div', {
  306. id: CompUtils.getId(cfg.name),
  307. classlist: CompUtils.getClass(cfg.type),
  308. childs: [
  309. domHelper('input', {
  310. id: customId,
  311. attr: {
  312. type: 'checkbox',
  313. checked: cfg.value??false
  314. },
  315. on: {
  316. change: (e) => {
  317. const value = !!(e.target?.checked);
  318. if (CompUtils.runChecker(cfg.checker,value)) cfg.value = value;
  319. // else TODO: error tip
  320. }
  321. }
  322. }),
  323. domHelper('label', {
  324. attr: {
  325. for: customId
  326. },
  327. classlist:'ckui-inline-label',
  328. html: cfg.label
  329. }),
  330. domHelper('span', {
  331. classList: 'ckui-description',
  332. html: cfg.description??''
  333. }),
  334. ]
  335. });
  336. }
  337. static input(cfg) {
  338. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  339. return domHelper('div', {
  340. id: CompUtils.getId(cfg.name),
  341. classlist: CompUtils.getClass(cfg.type),
  342. childs: [
  343. domHelper('label', {
  344. html: cfg.label
  345. }),
  346. domHelper('input', {
  347. attr: {
  348. value: cfg.value??''
  349. },
  350. on: {
  351. keyup: CKTools.debounce((e) => {
  352. const value = e.target?.value;
  353. if (CompUtils.runChecker(cfg.checker,value)) cfg.value = value;
  354. // else TODO: error tip
  355. })
  356. }
  357. }),
  358. domHelper('span', {
  359. classList: 'ckui-description',
  360. html: cfg.description??''
  361. }),
  362. ]
  363. });
  364. }
  365. static inputarea(cfg) {
  366. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  367. return domHelper('div', {
  368. id: CompUtils.getId(cfg.name),
  369. classlist: CompUtils.getClass(cfg.type),
  370. childs: [
  371. domHelper('label', {
  372. html: cfg.label
  373. }),
  374. domHelper('textarea', {
  375. attr: {
  376. value: cfg.value??''//not work until it put into dom
  377. },
  378. html:cfg.value??'',
  379. on: {
  380. keyup: CKTools.debounce((e) => {
  381. const value = e.target?.value ?? e.target.innerHTMl;
  382. console.log('inputarea', value,e.target?.value,e.target.innerHTMl);
  383. if (CompUtils.runChecker(cfg.checker, value)) cfg.value = value;
  384. else console.warn('checker refused');
  385. // else TODO: error tip
  386. })
  387. }
  388. }),
  389. domHelper('span', {
  390. classList: 'ckui-description',
  391. html: cfg.description??''
  392. }),
  393. ]
  394. });
  395. }
  396. static inputnumber(cfg) {
  397. if (!CompUtils.cfgValidator(cfg, 'label,min,max,step')) return;
  398. return domHelper('div', {
  399. id: CompUtils.getId(cfg.name),
  400. classlist: CompUtils.getClass(cfg.type),
  401. childs: [
  402. domHelper('label', {
  403. html: cfg.label
  404. }),
  405. domHelper('input', {
  406. attr: {
  407. type: 'number',
  408. value: isNaN(cfg.value)?undefined:+cfg.value,
  409. min: cfg.min,
  410. max: cfg.max,
  411. step: cfg.step
  412. },
  413. on: {
  414. change: (e) => {
  415. let value = e.target?.value;
  416. if (!isNaN(value)) {
  417. value = +value;
  418. }
  419. if (CompUtils.runChecker(cfg.checker, value)) {
  420. console.log('updated:', value);
  421. cfg.value = value;
  422. } else {
  423. console.log('refused to update value', value);
  424. }
  425. // else TODO: error tip
  426. }
  427. }
  428. }),
  429. domHelper('span', {
  430. classList: 'ckui-description',
  431. html: cfg.description??''
  432. }),
  433. ]
  434. });
  435. }
  436. static select(cfg) {
  437. if (!CompUtils.cfgValidator(cfg, 'label,options')) return;
  438. return domHelper('div', {
  439. id: CompUtils.getId(cfg.name),
  440. classlist: CompUtils.getClass(cfg.type),
  441. childs: [
  442. domHelper('label', {
  443. html: cfg.label
  444. }),
  445. domHelper('select', {
  446. init: select => {
  447. for (let option of cfg.options) {
  448. console.log('select option', option.opt,option.value,cfg.value == option.value);
  449. select.appendChild(domHelper('option', {
  450. attr: {
  451. value: option.value,
  452. },
  453. html: option.opt,
  454. init: optionel => {
  455. if (cfg.value == option.value) optionel.setAttribute('selected', true);
  456. }
  457. }));
  458. }
  459. },
  460. on: {
  461. change: (e) => {
  462. const value = e.target?.value??'';
  463. if (CompUtils.runChecker(cfg.checker,value)) cfg.value = value;
  464. // else TODO: error tip
  465. }
  466. }
  467. }),
  468. domHelper('span', {
  469. classList: 'ckui-description',
  470. html: cfg.description??''
  471. }),
  472. ]
  473. });
  474. }
  475. static texttoggle(cfg) {
  476. if (!CompUtils.cfgValidator(cfg, 'before,on,off,after')) return;
  477. return domHelper('div', {
  478. id: CompUtils.getId(cfg.name),
  479. classlist: CompUtils.getClass(cfg.type),
  480. childs: [
  481. domHelper('div', {
  482. classList:'ckui-texttoggle-container',
  483. childs: [
  484. domHelper('span', { text: cfg.before }),
  485. domHelper('span', {classList:'ckui-texttoggle-value',text:'...'}),
  486. domHelper('span', {text:cfg.after}),
  487. ],
  488. init: div => {
  489. const getText = () => cfg.value ? cfg.on : cfg.off;
  490. const setValue = (value) => {
  491. cfg.value = !!value;
  492. div.querySelector('.ckui-texttoggle-value').innerText = getText();
  493. }
  494. const toggleValue = () => setValue(!cfg.value);
  495. div.addEventListener('click', toggleValue);
  496. setTimeout(()=>setValue(cfg.value),50);
  497. }
  498. }),
  499. domHelper('span', {
  500. classList: 'ckui-description',
  501. html: cfg.description??''
  502. }),
  503. ]
  504. });
  505. }
  506. static raw(cfg) {
  507. if (!CompUtils.cfgValidator(cfg, 'contents')) return;
  508. return domHelper('div', {
  509. id: CompUtils.getId(cfg.name),
  510. classlist: CompUtils.getClass(cfg.type),
  511. childs:domHelper(cfg.contents)
  512. })
  513. }
  514. static window(cfg,type) {
  515. if (!CompUtils.cfgValidator(cfg, 'label,config')) return;
  516. return domHelper('div', {
  517. id: CompUtils.getId(cfg.name),
  518. classlist: CompUtils.getClass(cfg.type),
  519. childs: [
  520. domHelper('button', {
  521. classList: 'ckui-btn',
  522. html: cfg.label,
  523. on: {
  524. click: async (e) => {
  525. let subres = {};
  526. if (type == 'confirm') subres = await SettingsBuilder.open(cfg.config);
  527. else if (type == 'modal') subres = await SettingsBuilder.modal(cfg.config);
  528. else return;
  529. console.log('subres:', subres)
  530. Object.assign(cfg.config, subres);
  531. }
  532. }
  533. })
  534. ]
  535. })
  536. }
  537. static btns(cfg) {
  538. if (!CompUtils.cfgValidator(cfg, 'btns')) return;
  539. return domHelper('div', {
  540. id: CompUtils.getId(cfg.name),
  541. classlist: CompUtils.getClass(cfg.type),
  542. init: el => {
  543. for(let btn of cfg.btns) {
  544. el.appendChild(domHelper('button', {
  545. classList: 'ckui-btn',
  546. html: btn.label,
  547. on: {
  548. click: async (e) => {
  549. await btn.onclick();
  550. }
  551. }
  552. }));
  553. }
  554. }
  555. })
  556. }
  557. }
  558. class SettingsBuilder{
  559. static builder(cfg) {
  560. return new SettingsBuilder(cfg);
  561. }
  562. static async open(cfg, values = null) {
  563. const s = new SettingsBuilder(cfg);
  564. if(values) s.setValues(values);
  565. const result = await s.showWindow();
  566. return result;
  567. }
  568. static async modal(cfg, values = null) {
  569. const s = new SettingsBuilder(cfg);
  570. if(values) s.setValues(values);
  571. const result = await s.showAlertWindow();
  572. return result;
  573. }
  574. constructor(config) {
  575. this.config = Object.assign({
  576. title: '设置',
  577. settings:[]
  578. },config);
  579. }
  580. findSettingObjectByName(key = '',cfg = this.config) {
  581. const settings = cfg.settings;
  582. for (const setting of settings) {
  583. if(setting.name == key) return setting;
  584. }
  585. const subSettings = settings.filter(el => el.type == 'window');
  586. if (!subSettings.length) return null;
  587. for (const setting of subSettings) {
  588. const subresult = this.findSettingObjectByName(key, setting);
  589. if(subresult) return subresult;
  590. }
  591. return null;
  592. }
  593. setValues(settingsValues = {}) {
  594. for (let vk of Object.keys(settingsValues)) {
  595. const setting = this.findSettingObjectByName(vk);
  596. if (setting) setting.value = settingsValues[vk];
  597. else console.warn('[CKUI]', `${vk} not found in scheme`);
  598. }
  599. }
  600. flatValues(cfg) {
  601. const cfgs = {};
  602. if(cfg.settings){
  603. cfg = cfg.settings;
  604. }
  605. if(!Array.isArray(cfg)){
  606. return cfgs;
  607. }
  608. for(const s of cfg){
  609. if(!s.name || !s.type) {
  610. console.warn('[CKUI]', 'missing name or type',s);
  611. continue;
  612. }
  613. switch(s.type){
  614. case "toggle":
  615. case "texttoggle":
  616. cfgs[s.name] = !!s.value;
  617. break;
  618. case "select":
  619. case "input":
  620. case "inputarea":
  621. cfgs[s.name] = s.value?s.value+"":"";
  622. break;
  623. case "inputnumber":
  624. if(!isNaN(s.value)) cfgs[s.name] = +s.value;
  625. break;
  626. case "window":
  627. {
  628. const sub = Settings.flatValues(s.config);
  629. Object.assign(cfgs,sub);
  630. }
  631. break;
  632. default:
  633. console.log('[CKUI]','unrecognized type',s.type);
  634. }
  635. }
  636. return cfgs;
  637. }
  638. async showAlertWindow(config = this.config) {
  639. const copiedConfig = deepClone(config);
  640. return new Promise(r => {
  641. FloatWindow.alert(copiedConfig.title, domHelper('div', {
  642. classlist:'ckui-base',
  643. init: el => {
  644. for (const comp of copiedConfig.settings) {
  645. const r = this.makeComponent(comp,'alert');
  646. r&&el.appendChild(r);
  647. }
  648. }
  649. }), copiedConfig.btnName ?? "确定").then(() => {
  650. r(this.flatValues(copiedConfig));
  651. });
  652. })
  653. }
  654. async showWindow(config = this.config) {
  655. const copiedConfig = deepClone(config);
  656. return new Promise(r => {
  657. FloatWindow.confirm(copiedConfig.title, domHelper('div', {
  658. classlist:'ckui-base',
  659. init: el => {
  660. for (const comp of copiedConfig.settings) {
  661. const r = this.makeComponent(comp);
  662. r&&el.appendChild(r);
  663. }
  664. }
  665. }), copiedConfig.saveBtn??"保存", copiedConfig.cancelBtn??"取消").then(result => {
  666. console.log('[CKUI]', 'Save?', result);
  667. result ? r(this.flatValues(copiedConfig)) : r({});
  668. });
  669. })
  670. }
  671. makeComponent(cfg, type='confirm') {
  672. if (Components.hasOwnProperty(cfg.type)) {
  673. return Components[cfg.type](cfg,type);
  674. }
  675. }
  676. }
  677. CKUIToolkit.showSettings = SettingsBuilder.open;
  678. CKUIToolkit.showModal = SettingsBuilder.modal;
  679. CKUIToolkit.builder = SettingsBuilder.builder;
  680.  
  681. unsafeWindow.CKUIToolkit = CKUIToolkit;
  682. })();