CKUIToolkit

A simple settings modal framework.

当前为 2022-03-19 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/441653/1029955/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. let { domHelper, deepClone } = CKTools;
  209. if (!deepClone) {
  210. deepClone = (obj)=>{
  211. let newObject = {};
  212. if (Array.isArray(obj)) {
  213. newObject = [];
  214. for (let i = 0; i < obj.length; i++) {
  215. newObject.push(deepClone(obj[i]));
  216. }
  217. return newObject;
  218. }
  219. Object.keys(obj).map(key => {
  220. if (typeof obj[key] === 'object') {
  221. newObject[key] = deepClone(obj[key]);
  222. } else {
  223. newObject[key] = obj[key];
  224. }
  225. });
  226. return newObject;
  227. };
  228. }
  229. const CKUIToolkit = {};
  230. class CompUtils{
  231. static getId(name) {
  232. return 'ckui-settings-' + name;
  233. }
  234. static getClass(type) {
  235. return 'ckui-' + type;
  236. }
  237. static runChecker(checker = null,...args) {
  238. if (checker && typeof (checker) == 'function') {
  239. try {
  240. const result = checker(...args);
  241. return !!result;
  242. } catch (e) {
  243. return false;
  244. }
  245. } else return true;
  246. }
  247. static cfgValidator(cfg, keystr='') {
  248. let keys = keystr.split(',').map(el => el.trim()).filter(el => el.length > 0);
  249. keys.concat(['name', 'type']);
  250. if (!cfg) return false;
  251. for (let key of keys) {
  252. if (cfg[key]===undefined) return false;
  253. }
  254. return true;
  255. }
  256. }
  257. class Components{
  258. static text(cfg) {
  259. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  260. return domHelper('div', {
  261. id: CompUtils.getId(cfg.name),
  262. classlist: CompUtils.getClass(cfg.type),
  263. html: cfg.label
  264. });
  265. }
  266. static header(cfg) {
  267. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  268. return domHelper('div', {
  269. id: CompUtils.getId(cfg.name),
  270. classlist: CompUtils.getClass(cfg.type),
  271. html: cfg.label
  272. });
  273. }
  274. static toggle(cfg) {
  275. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  276. const customId = 'toggle'+CKTools.GUID.getShort();
  277. return domHelper('div', {
  278. id: CompUtils.getId(cfg.name),
  279. classlist: CompUtils.getClass(cfg.type),
  280. childs: [
  281. domHelper('input', {
  282. id: customId,
  283. attr: {
  284. type: 'checkbox',
  285. value: cfg.value??false
  286. },
  287. on: {
  288. change: (e) => {
  289. const value = !!(e.target?.value);
  290. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  291. // else TODO: error tip
  292. }
  293. }
  294. }),
  295. domHelper('label', {
  296. attr: {
  297. for: customId
  298. },
  299. classlist:'ckui-inline-label',
  300. html: cfg.label
  301. }),
  302. domHelper('span', {
  303. classList: 'ckui-description',
  304. html: cfg.description??''
  305. }),
  306. ]
  307. });
  308. }
  309. static input(cfg) {
  310. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  311. return domHelper('div', {
  312. id: CompUtils.getId(cfg.name),
  313. classlist: CompUtils.getClass(cfg.type),
  314. childs: [
  315. domHelper('label', {
  316. html: cfg.label
  317. }),
  318. domHelper('input', {
  319. attr:cfg.value,
  320. on: {
  321. keyup: CKTools.debounce((e) => {
  322. const value = e.target?.value;
  323. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  324. // else TODO: error tip
  325. })
  326. }
  327. }),
  328. domHelper('span', {
  329. classList: 'ckui-description',
  330. html: cfg.description??''
  331. }),
  332. ]
  333. });
  334. }
  335. static inputarea(cfg) {
  336. if (!CompUtils.cfgValidator(cfg, 'label')) return;
  337. return domHelper('div', {
  338. id: CompUtils.getId(cfg.name),
  339. classlist: CompUtils.getClass(cfg.type),
  340. childs: [
  341. domHelper('label', {
  342. html: cfg.label
  343. }),
  344. domHelper('textarea', {
  345. attr: {
  346. value: cfg.value
  347. },
  348. on: {
  349. keyup: CKTools.debounce((e) => {
  350. const value = e.target?.value ?? e.target.innerHTMl;
  351. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  352. // else TODO: error tip
  353. })
  354. }
  355. }),
  356. domHelper('span', {
  357. classList: 'ckui-description',
  358. html: cfg.description??''
  359. }),
  360. ]
  361. });
  362. }
  363. static inputnumber(cfg) {
  364. if (!CompUtils.cfgValidator(cfg, 'label,min,max,step')) return;
  365. return domHelper('div', {
  366. id: CompUtils.getId(cfg.name),
  367. classlist: CompUtils.getClass(cfg.type),
  368. childs: [
  369. domHelper('label', {
  370. html: cfg.label
  371. }),
  372. domHelper('input', {
  373. attr: {
  374. type: 'number',
  375. value: cfg.value,
  376. min: cfg.min,
  377. max: cfg.max,
  378. step: cfg.step
  379. },
  380. on: {
  381. keyup: CKTools.debounce((e) => {
  382. const value = e.target?.value;
  383. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  384. // else TODO: error tip
  385. })
  386. }
  387. }),
  388. domHelper('span', {
  389. classList: 'ckui-description',
  390. html: cfg.description??''
  391. }),
  392. ]
  393. });
  394. }
  395. static select(cfg) {
  396. if (!CompUtils.cfgValidator(cfg, 'label,options')) return;
  397. return domHelper('div', {
  398. id: CompUtils.getId(cfg.name),
  399. classlist: CompUtils.getClass(cfg.type),
  400. childs: [
  401. domHelper('label', {
  402. html: cfg.label
  403. }),
  404. domHelper('select', {
  405. attr: {
  406. value: cfg.value??'',
  407. },
  408. init: select => {
  409. for (let option of cfg.options) {
  410. select.appendChild(domHelper('option', {
  411. attr: {
  412. value: option.value,
  413. selected: cfg.value == option.value
  414. },
  415. html: option.opt
  416. }));
  417. }
  418. },
  419. on: {
  420. change: CKTools.debounce((e) => {
  421. const value = !!(e.target?.value);
  422. if (CompUtils.runChecker(cfg.checker)) cfg.value = value;
  423. // else TODO: error tip
  424. })
  425. }
  426. }),
  427. domHelper('span', {
  428. classList: 'ckui-description',
  429. html: cfg.description??''
  430. }),
  431. ]
  432. });
  433. }
  434. static texttoggle(cfg) {
  435. if (!CompUtils.cfgValidator(cfg, 'before,on,off,after')) return;
  436. return domHelper('div', {
  437. id: CompUtils.getId(cfg.name),
  438. classlist: CompUtils.getClass(cfg.type),
  439. childs: [
  440. domHelper('div', {
  441. classList:'ckui-texttoggle-container',
  442. childs: [
  443. domHelper('span', { text: cfg.before }),
  444. domHelper('span', {classList:'ckui-texttoggle-value',text:'...'}),
  445. domHelper('span', {text:cfg.after}),
  446. ],
  447. init: div => {
  448. const getText = () => cfg.value ? cfg.on : cfg.off;
  449. const setValue = (value) => {
  450. cfg.value = !!value;
  451. div.querySelector('.ckui-texttoggle-value').innerText = getText();
  452. }
  453. const toggleValue = () => setValue(!cfg.value);
  454. div.addEventListener('click', toggleValue);
  455. setValue(cfg.value);
  456. }
  457. }),
  458. domHelper('span', {
  459. classList: 'ckui-description',
  460. html: cfg.description??''
  461. }),
  462. ]
  463. });
  464. }
  465. static raw(cfg) {
  466. if (!CompUtils.cfgValidator(cfg, 'contents')) return;
  467. return domHelper('div', {
  468. id: CompUtils.getId(cfg.name),
  469. classlist: CompUtils.getClass(cfg.type),
  470. childs:domHelper(cfg.contents)
  471. })
  472. }
  473. static window(cfg) {
  474. if (!CompUtils.cfgValidator(cfg, 'label,config')) return;
  475. return domHelper('div', {
  476. id: CompUtils.getId(cfg.name),
  477. classlist: CompUtils.getClass(cfg.type),
  478. childs: [
  479. domHelper('button', {
  480. classList: 'ckui-btn',
  481. html: cfg.label,
  482. on: {
  483. click: async (e) => {
  484. cfg.config = await SettingsBuilder.open(cfg.config);
  485. }
  486. }
  487. })
  488. ]
  489. })
  490. }
  491. static btns(cfg) {
  492. if (!CompUtils.cfgValidator(cfg, 'btns')) return;
  493. return domHelper('div', {
  494. id: CompUtils.getId(cfg.name),
  495. classlist: CompUtils.getClass(cfg.type),
  496. init: el => {
  497. for(let btn of cfg.btns) {
  498. el.appendChild(domHelper('button', {
  499. classList: 'ckui-btn',
  500. html: btn.label,
  501. on: {
  502. click: async (e) => {
  503. await btn.onclick();
  504. }
  505. }
  506. }));
  507. }
  508. }
  509. })
  510. }
  511. }
  512. class SettingsBuilder{
  513. static async open(cfg) {
  514. const s = new SettingsBuilder(cfg);
  515. const result = await s.showWindow();
  516. return result;
  517. }
  518. constructor(config) {
  519. this.config = Object.assign({
  520. title: '设置',
  521. settings:[]
  522. },config);
  523. }
  524. async showWindow(config = this.config) {
  525. const copiedConfig = deepClone(config);
  526. console.log('Cloned:',config,copiedConfig)
  527. return new Promise(r => {
  528. FloatPopup.confirm(copiedConfig.title, domHelper('div', {
  529. classlist:'ckui-base',
  530. init: el => {
  531. for (const comp of copiedConfig.settings) {
  532. const r = this.makeComponent(comp);
  533. r&&el.appendChild(r);
  534. }
  535. }
  536. }), "保存", "取消").then(result => result?r(copiedConfig):r(config));
  537. })
  538. }
  539. makeComponent(cfg) {
  540. if (Components.hasOwnProperty(cfg.type)) {
  541. return Components[cfg.type](cfg);
  542. }
  543. }
  544. }
  545. CKUIToolkit.showSettings = SettingsBuilder.open;
  546.  
  547. unsafeWindow.CKUIToolkit = CKUIToolkit;
  548. })();