CKUIToolkit

A simple settings modal framework.

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

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