🏠 Home 

UserGui

A Graphical user interface for userscripts. Creating user-friendly userscripts can be a bit challenging, as the majority of regular users are scared to touch your code. UserGui allows you to unlock your userscript to more people, and create a more pleasing experience.

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/459136/1143683/UserGui.js

  1. /*
  2. * usergui.js
  3. * v1.0.0
  4. * https://github.com/AugmentedWeb/UserGui
  5. * Apache 2.0 licensed
  6. */
  7. class UserGui {
  8. constructor() {
  9. const grantArr = GM_info?.script?.grant;
  10. if(typeof grantArr == "object") {
  11. if(!grantArr.includes("GM_xmlhttpRequest")) {
  12. prompt(`${this.#projectName} needs GM_xmlhttpRequest!\n\nPlease add this to your userscript's header...`, "// @grant GM_xmlhttpRequest");
  13. }
  14. if(!grantArr.includes("GM_getValue")) {
  15. prompt(`${this.#projectName} needs GM_getValue!\n\nPlease add this to your userscript's header...`, "// @grant GM_getValue");
  16. }
  17. if(!grantArr.includes("GM_setValue")) {
  18. prompt(`${this.#projectName} needs GM_setValue!\n\nPlease add this to your userscript's header...`, "// @grant GM_setValue");
  19. }
  20. }
  21. }
  22. #projectName = "UserGui";
  23. window = undefined;
  24. document = undefined;
  25. iFrame = undefined;
  26. settings = {
  27. "window" : {
  28. "title" : "No title set",
  29. "name" : "userscript-gui",
  30. "external" : false,
  31. "centered" : false,
  32. "size" : {
  33. "width" : 300,
  34. "height" : 500,
  35. "dynamicSize" : true
  36. }
  37. },
  38. "gui" : {
  39. "centeredItems" : false,
  40. "internal" : {
  41. "darkCloseButton" : false,
  42. "style" : `
  43. body {
  44. background-color: #ffffff;
  45. overflow: hidden;
  46. width: 100% !important;
  47. }
  48. form {
  49. padding: 10px;
  50. }
  51. #gui {
  52. height: fit-content;
  53. }
  54. .rendered-form {
  55. padding: 10px;
  56. }
  57. #header {
  58. padding: 10px;
  59. cursor: move;
  60. z-index: 10;
  61. background-color: #2196F3;
  62. color: #fff;
  63. height: fit-content;
  64. }
  65. .header-item-container {
  66. display: flex;
  67. justify-content: space-between;
  68. align-items: center;
  69. }
  70. .left-title {
  71. font-size: 14px;
  72. font-weight: bold;
  73. padding: 0;
  74. margin: 0;
  75. }
  76. #button-close-gui {
  77. vertical-align: middle;
  78. }
  79. div .form-group {
  80. margin-bottom: 15px;
  81. }
  82. #resizer {
  83. width: 10px;
  84. height: 10px;
  85. cursor: se-resize;
  86. position: absolute;
  87. bottom: 0;
  88. right: 0;
  89. }
  90. .formbuilder-button {
  91. width: fit-content;
  92. }
  93. `
  94. },
  95. "external" : {
  96. "popup" : true,
  97. "style" : `
  98. .rendered-form {
  99. padding: 10px;
  100. }
  101. div .form-group {
  102. margin-bottom: 15px;
  103. }
  104. `
  105. }
  106. },
  107. "messages" : {
  108. "blockedPopups" : () => alert(`The GUI (graphical user interface) failed to open!\n\nPossible reason: The popups are blocked.\n\nPlease allow popups for this site. (${window.location.hostname})`)
  109. }
  110. };
  111. // This error page will be shown if the user has not added any pages
  112. #errorPage = (title, code) => `
  113. <style>
  114. .error-page {
  115. width: 100%;
  116. height: fit-content;
  117. background-color: black;
  118. display: flex;
  119. justify-content: center;
  120. align-items: center;
  121. text-align: center;
  122. padding: 25px
  123. }
  124. .error-page-text {
  125. font-family: monospace;
  126. font-size: x-large;
  127. color: white;
  128. }
  129. .error-page-tag {
  130. margin-top: 20px;
  131. font-size: 10px;
  132. color: #4a4a4a;
  133. font-style: italic;
  134. margin-bottom: 0px;
  135. }
  136. </style>
  137. <div class="error-page">
  138. <div>
  139. <p class="error-page-text">${title}</p>
  140. <code>${code}</code>
  141. <p class="error-page-tag">${this.#projectName} error message</p>
  142. </div>
  143. </div>`;
  144. // The user can add multiple pages to their GUI. The pages are stored in this array.
  145. #guiPages = [
  146. {
  147. "name" : "default_no_content_set",
  148. "content" : this.#errorPage("Content missing", "Gui.setContent(html, tabName);")
  149. }
  150. ];
  151. // The userscript manager's xmlHttpRequest is used to bypass CORS limitations (To load Bootstrap)
  152. async #bypassCors(externalFile) {
  153. const res = await new Promise(resolve => {
  154. GM_xmlhttpRequest({
  155. method: "GET",
  156. url: externalFile,
  157. onload: resolve
  158. });
  159. });
  160. return res.responseText;
  161. }
  162. // Returns one tab (as HTML) for the navigation tabs
  163. #createNavigationTab(page) {
  164. const name = page.name;
  165. if(name == undefined) {
  166. console.error(`[${this.#projectName}] Gui.addPage(html, name) <- name missing!`);
  167. return undefined;
  168. } else {
  169. const modifiedName = name.toLowerCase().replaceAll(' ', '').replace(/[^a-zA-Z0-9]/g, '') + Math.floor(Math.random() * 1000000000);
  170. const content = page.content;
  171. const indexOnArray = this.#guiPages.map(x => x.name).indexOf(name);
  172. const firstItem = indexOnArray == 0 ? true : false;
  173. return {
  174. "listItem" : `
  175. <li class="nav-item" role="presentation">
  176. <button class="nav-link ${firstItem ? 'active' : ''}" id="${modifiedName}-tab" data-bs-toggle="tab" data-bs-target="#${modifiedName}" type="button" role="tab" aria-controls="${modifiedName}" aria-selected="${firstItem}">${name}</button>
  177. </li>
  178. `,
  179. "panelItem" : `
  180. <div class="tab-pane ${firstItem ? 'active' : ''}" id="${modifiedName}" role="tabpanel" aria-labelledby="${modifiedName}-tab">${content}</div>
  181. `
  182. };
  183. }
  184. }
  185. // Make tabs function without bootstrap.js (CSP might block bootstrap and make the GUI nonfunctional)
  186. #initializeTabs() {
  187. const handleTabClick = e => {
  188. const target = e.target;
  189. const contentID = target.getAttribute("data-bs-target");
  190. target.classList.add("active");
  191. this.document.querySelector(contentID).classList.add("active");
  192. [...this.document.querySelectorAll(".nav-link")].forEach(tab => {
  193. if(tab != target) {
  194. const contentID = tab.getAttribute("data-bs-target");
  195. tab.classList.remove("active");
  196. this.document.querySelector(contentID).classList.remove("active");
  197. }
  198. });
  199. }
  200. [...this.document.querySelectorAll(".nav-link")].forEach(tab => {
  201. tab.addEventListener("click", handleTabClick);
  202. });
  203. }
  204. // Will determine if a navbar is needed, returns either a regular GUI, or a GUI with a navbar
  205. #getContent() {
  206. // Only one page has been set, no navigation tabs will be created
  207. if(this.#guiPages.length == 1) {
  208. return this.#guiPages[0].content;
  209. }
  210. // Multiple pages has been set, dynamically creating the navigation tabs
  211. else if(this.#guiPages.length > 1) {
  212. const tabs = (list, panels) => `
  213. <ul class="nav nav-tabs" id="userscript-tab" role="tablist">
  214. ${list}
  215. </ul>
  216. <div class="tab-content">
  217. ${panels}
  218. </div>
  219. `;
  220. let list = ``;
  221. let panels = ``;
  222. this.#guiPages.forEach(page => {
  223. const data = this.#createNavigationTab(page);
  224. if(data != undefined) {
  225. list += data.listItem + '\n';
  226. panels += data.panelItem + '\n';
  227. }
  228. });
  229. return tabs(list, panels);
  230. }
  231. }
  232. // Returns the GUI's whole document as string
  233. async #createDocument() {
  234. const bootstrapStyling = await this.#bypassCors("https://raw.githubusercontent.com/AugmentedWeb/UserGui/main/resources/bootstrap.css");
  235. const externalDocument = `
  236. <!DOCTYPE html>
  237. <html>
  238. <head>
  239. <title>${this.settings.window.title}</title>
  240. <style>
  241. ${bootstrapStyling}
  242. ${this.settings.gui.external.style}
  243. ${
  244. this.settings.gui.centeredItems
  245. ? `.form-group {
  246. display: flex;
  247. justify-content: center;
  248. }`
  249. : ""
  250. }
  251. </style>
  252. </head>
  253. <body>
  254. ${this.#getContent()}
  255. </body>
  256. </html>
  257. `;
  258. const internalDocument = `
  259. <!doctype html>
  260. <html lang="en">
  261. <head>
  262. <style>
  263. ${bootstrapStyling}
  264. ${this.settings.gui.internal.style}
  265. ${
  266. this.settings.gui.centeredItems
  267. ? `.form-group {
  268. display: flex;
  269. justify-content: center;
  270. }`
  271. : ""
  272. }
  273. </style>
  274. </head>
  275. <body>
  276. <div id="gui">
  277. <div id="header">
  278. <div class="header-item-container">
  279. <h1 class="left-title">${this.settings.window.title}</h1>
  280. <div class="right-buttons">
  281. <button type="button" class="${this.settings.gui.internal.darkCloseButton ? "btn-close" : "btn-close btn-close-white"}" aria-label="Close" id="button-close-gui"></button>
  282. </div>
  283. </div>
  284. </div>
  285. <div id="content">
  286. ${this.#getContent()}
  287. </div>
  288. <div id="resizer"></div>
  289. </div>
  290. </body>
  291. </html>
  292. `;
  293. if(this.settings.window.external) {
  294. return externalDocument;
  295. } else {
  296. return internalDocument;
  297. }
  298. }
  299. // The user will use this function to add a page to their GUI, with their own HTML (Bootstrap 5)
  300. addPage(tabName, htmlString) {
  301. if(this.#guiPages[0].name == "default_no_content_set") {
  302. this.#guiPages = [];
  303. }
  304. this.#guiPages.push({
  305. "name" : tabName,
  306. "content" : htmlString
  307. });
  308. }
  309. #getCenterScreenPosition() {
  310. const guiWidth = this.settings.window.size.width;
  311. const guiHeight = this.settings.window.size.height;
  312. const x = (screen.width - guiWidth) / 2;
  313. const y = (screen.height - guiHeight) / 2;
  314. return { "x" : x, "y": y };
  315. }
  316. #getCenterWindowPosition() {
  317. const guiWidth = this.settings.window.size.width;
  318. const guiHeight = this.settings.window.size.height;
  319. const x = (window.innerWidth - guiWidth) / 2;
  320. const y = (window.innerHeight - guiHeight) / 2;
  321. return { "x" : x, "y": y };
  322. }
  323. #initializeInternalGuiEvents(iFrame) {
  324. // - The code below will consist mostly of drag and resize implementations
  325. // - iFrame window <-> Main window interaction requires these to be done
  326. // - Basically, iFrame document's event listeners make the whole iFrame move on the main window
  327. // Sets the iFrame's size
  328. function setFrameSize(x, y) {
  329. iFrame.style.width = `${x}px`;
  330. iFrame.style.height = `${y}px`;
  331. }
  332. // Gets the iFrame's size
  333. function getFrameSize() {
  334. const frameBounds = iFrame.getBoundingClientRect();
  335. return { "width" : frameBounds.width, "height" : frameBounds.height };
  336. }
  337. // Sets the iFrame's position relative to the main window's document
  338. function setFramePos(x, y) {
  339. iFrame.style.left = `${x}px`;
  340. iFrame.style.top = `${y}px`;
  341. }
  342. // Gets the iFrame's position relative to the main document
  343. function getFramePos() {
  344. const frameBounds = iFrame.getBoundingClientRect();
  345. return { "x": frameBounds.x, "y" : frameBounds.y };
  346. }
  347. // Gets the frame body's offsetHeight
  348. function getInnerFrameSize() {
  349. const innerFrameElem = iFrame.contentDocument.querySelector("#gui");
  350. return { "x": innerFrameElem.offsetWidth, "y" : innerFrameElem.offsetHeight };
  351. }
  352. // Sets the frame's size to the innerframe's size
  353. const adjustFrameSize = () => {
  354. const innerFrameSize = getInnerFrameSize();
  355. setFrameSize(innerFrameSize.x, innerFrameSize.y);
  356. }
  357. // Variables for draggable header
  358. let dragging = false,
  359. dragStartPos = { "x" : 0, "y" : 0 };
  360. // Variables for resizer
  361. let resizing = false,
  362. mousePos = { "x" : undefined, "y" : undefined },
  363. lastFrame;
  364. function handleResize(isInsideFrame, e) {
  365. if(mousePos.x == undefined && mousePos.y == undefined) {
  366. mousePos.x = e.clientX;
  367. mousePos.y = e.clientY;
  368. lastFrame = isInsideFrame;
  369. }
  370. const deltaX = mousePos.x - e.clientX,
  371. deltaY = mousePos.y - e.clientY;
  372. const frameSize = getFrameSize();
  373. const allowedSize = frameSize.width - deltaX > 160 && frameSize.height - deltaY > 90;
  374. if(isInsideFrame == lastFrame && allowedSize) {
  375. setFrameSize(frameSize.width - deltaX, frameSize.height - deltaY);
  376. }
  377. mousePos.x = e.clientX;
  378. mousePos.y = e.clientY;
  379. lastFrame = isInsideFrame;
  380. }
  381. function handleDrag(isInsideFrame, e) {
  382. const bR = iFrame.getBoundingClientRect();
  383. const windowWidth = window.innerWidth,
  384. windowHeight = window.innerHeight;
  385. let x, y;
  386. if(isInsideFrame) {
  387. x = getFramePos().x += e.clientX - dragStartPos.x;
  388. y = getFramePos().y += e.clientY - dragStartPos.y;
  389. } else {
  390. x = e.clientX - dragStartPos.x;
  391. y = e.clientY - dragStartPos.y;
  392. }
  393. // Check out of bounds: left
  394. if(x <= 0) {
  395. x = 0
  396. }
  397. // Check out of bounds: right
  398. if(x + bR.width >= windowWidth) {
  399. x = windowWidth - bR.width;
  400. }
  401. // Check out of bounds: top
  402. if(y <= 0) {
  403. y = 0;
  404. }
  405. // Check out of bounds: bottom
  406. if(y + bR.height >= windowHeight) {
  407. y = windowHeight - bR.height;
  408. }
  409. setFramePos(x, y);
  410. }
  411. // Dragging start (iFrame)
  412. this.document.querySelector("#header").addEventListener('mousedown', e => {
  413. e.preventDefault();
  414. dragging = true;
  415. dragStartPos.x = e.clientX;
  416. dragStartPos.y = e.clientY;
  417. });
  418. // Resizing start
  419. this.document.querySelector("#resizer").addEventListener('mousedown', e => {
  420. e.preventDefault();
  421. resizing = true;
  422. });
  423. // While dragging or resizing (iFrame)
  424. this.document.addEventListener('mousemove', e => {
  425. if(dragging)
  426. handleDrag(true, e);
  427. if(resizing)
  428. handleResize(true, e);
  429. });
  430. // While dragging or resizing (Main window)
  431. document.addEventListener('mousemove', e => {
  432. if(dragging)
  433. handleDrag(false, e);
  434. if(resizing)
  435. handleResize(false, e);
  436. });
  437. // Stop dragging and resizing (iFrame)
  438. this.document.addEventListener('mouseup', e => {
  439. e.preventDefault();
  440. dragging = false;
  441. resizing = false;
  442. });
  443. // Stop dragging and resizing (Main window)
  444. document.addEventListener('mouseup', e => {
  445. dragging = false;
  446. resizing = false;
  447. });
  448. // Listener for the close button, closes the internal GUI
  449. this.document.querySelector("#button-close-gui").addEventListener('click', e => {
  450. e.preventDefault();
  451. this.close();
  452. });
  453. const guiObserver = new MutationObserver(adjustFrameSize);
  454. const guiElement = this.document.querySelector("#gui");
  455. guiObserver.observe(guiElement, {
  456. childList: true,
  457. subtree: true,
  458. attributes: true
  459. });
  460. adjustFrameSize();
  461. }
  462. async #openExternalGui(readyFunction) {
  463. const noWindow = this.window?.closed;
  464. if(noWindow || this.window == undefined) {
  465. let pos = "";
  466. let windowSettings = "";
  467. if(this.settings.window.centered && this.settings.gui.external.popup) {
  468. const centerPos = this.#getCenterScreenPosition();
  469. pos = `left=${centerPos.x}, top=${centerPos.y}`;
  470. }
  471. if(this.settings.gui.external.popup) {
  472. windowSettings = `width=${this.settings.window.size.width}, height=${this.settings.window.size.height}, ${pos}`;
  473. }
  474. // Create a new window for the GUI
  475. this.window = window.open("", this.settings.windowName, windowSettings);
  476. if(!this.window) {
  477. this.settings.messages.blockedPopups();
  478. return;
  479. }
  480. // Write the document to the new window
  481. this.window.document.open();
  482. this.window.document.write(await this.#createDocument());
  483. this.window.document.close();
  484. if(!this.settings.gui.external.popup) {
  485. this.window.document.body.style.width = `${this.settings.window.size.width}px`;
  486. if(this.settings.window.centered) {
  487. const centerPos = this.#getCenterScreenPosition();
  488. this.window.document.body.style.position = "absolute";
  489. this.window.document.body.style.left = `${centerPos.x}px`;
  490. this.window.document.body.style.top = `${centerPos.y}px`;
  491. }
  492. }
  493. // Dynamic sizing (only height & window.outerHeight no longer works on some browsers...)
  494. this.window.resizeTo(
  495. this.settings.window.size.width,
  496. this.settings.window.size.dynamicSize
  497. ? this.window.document.body.offsetHeight + (this.window.outerHeight - this.window.innerHeight)
  498. : this.settings.window.size.height
  499. );
  500. this.document = this.window.document;
  501. this.#initializeTabs();
  502. // Call user's function
  503. if(typeof readyFunction == "function") {
  504. readyFunction();
  505. }
  506. window.onbeforeunload = () => {
  507. // Close the GUI if parent window closes
  508. this.close();
  509. }
  510. }
  511. else {
  512. // Window was already opened, bring the window back to focus
  513. this.window.focus();
  514. }
  515. }
  516. async #openInternalGui(readyFunction) {
  517. if(this.iFrame) {
  518. return;
  519. }
  520. const fadeInSpeedMs = 250;
  521. let left = 0, top = 0;
  522. if(this.settings.window.centered) {
  523. const centerPos = this.#getCenterWindowPosition();
  524. left = centerPos.x;
  525. top = centerPos.y;
  526. }
  527. const iframe = document.createElement("iframe");
  528. iframe.srcdoc = await this.#createDocument();
  529. iframe.style = `
  530. position: fixed;
  531. top: ${top}px;
  532. left: ${left}px;
  533. width: ${this.settings.window.size.width};
  534. height: ${this.settings.window.size.height};
  535. border: 0;
  536. opacity: 0;
  537. transition: all ${fadeInSpeedMs/1000}s;
  538. border-radius: 5px;
  539. box-shadow: rgb(0 0 0 / 6%) 10px 10px 10px;
  540. z-index: 2147483647;
  541. `;
  542. const waitForBody = setInterval(() => {
  543. if(document?.body) {
  544. clearInterval(waitForBody);
  545. // Prepend the GUI to the document's body
  546. document.body.prepend(iframe);
  547. iframe.contentWindow.onload = () => {
  548. // Fade-in implementation
  549. setTimeout(() => iframe.style["opacity"] = "1", fadeInSpeedMs/2);
  550. setTimeout(() => iframe.style["transition"] = "none", fadeInSpeedMs + 500);
  551. this.window = iframe.contentWindow;
  552. this.document = iframe.contentDocument;
  553. this.iFrame = iframe;
  554. this.#initializeInternalGuiEvents(iframe);
  555. this.#initializeTabs();
  556. readyFunction();
  557. }
  558. }
  559. }, 100);
  560. }
  561. // Determines if the window is to be opened externally or internally
  562. open(readyFunction) {
  563. if(this.settings.window.external) {
  564. this.#openExternalGui(readyFunction);
  565. } else {
  566. this.#openInternalGui(readyFunction);
  567. }
  568. }
  569. // Closes the GUI if it exists
  570. close() {
  571. if(this.settings.window.external) {
  572. if(this.window) {
  573. this.window.close();
  574. }
  575. } else {
  576. if(this.iFrame) {
  577. this.iFrame.remove();
  578. this.iFrame = undefined;
  579. }
  580. }
  581. }
  582. saveConfig() {
  583. let config = [];
  584. if(this.document) {
  585. [...this.document.querySelectorAll(".form-group")].forEach(elem => {
  586. const inputElem = elem.querySelector("[name]");
  587. const name = inputElem.getAttribute("name"),
  588. data = this.getData(name);
  589. if(data) {
  590. config.push({ "name" : name, "value" : data });
  591. }
  592. });
  593. }
  594. GM_setValue("config", config);
  595. }
  596. loadConfig() {
  597. const config = this.getConfig();
  598. if(this.document && config) {
  599. config.forEach(elemConfig => {
  600. this.setData(elemConfig.name, elemConfig.value);
  601. })
  602. }
  603. }
  604. getConfig() {
  605. return GM_getValue("config");
  606. }
  607. resetConfig() {
  608. const config = this.getConfig();
  609. if(config) {
  610. GM_setValue("config", []);
  611. }
  612. }
  613. dispatchFormEvent(name) {
  614. const type = name.split("-")[0].toLowerCase();
  615. const properties = this.#typeProperties.find(x => type == x.type);
  616. const event = new Event(properties.event);
  617. const field = this.document.querySelector(`.field-${name}`);
  618. field.dispatchEvent(event);
  619. }
  620. setPrimaryColor(hex) {
  621. const styles = `
  622. #header {
  623. background-color: ${hex} !important;
  624. }
  625. .nav-link {
  626. color: ${hex} !important;
  627. }
  628. .text-primary {
  629. color: ${hex} !important;
  630. }
  631. `;
  632. const styleSheet = document.createElement("style")
  633. styleSheet.innerText = styles;
  634. this.document.head.appendChild(styleSheet);
  635. }
  636. // Creates an event listener a GUI element
  637. event(name, event, eventFunction) {
  638. this.document.querySelector(`.field-${name}`).addEventListener(event, eventFunction);
  639. }
  640. // Disables a GUI element
  641. disable(name) {
  642. [...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
  643. childElem.setAttribute("disabled", "true");
  644. });
  645. }
  646. // Enables a GUI element
  647. enable(name) {
  648. [...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
  649. if(childElem.getAttribute("disabled")) {
  650. childElem.removeAttribute("disabled");
  651. }
  652. });
  653. }
  654. // Gets data from types: TEXT FIELD, TEXTAREA, DATE FIELD & NUMBER
  655. getValue(name) {
  656. return this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value;
  657. }
  658. // Sets data to types: TEXT FIELD, TEXT AREA, DATE FIELD & NUMBER
  659. setValue(name, newValue) {
  660. this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value = newValue;
  661. this.dispatchFormEvent(name);
  662. }
  663. // Gets data from types: RADIO GROUP
  664. getSelection(name) {
  665. return this.document.querySelector(`.field-${name}`).querySelector(`input[name=${name}]:checked`).value;
  666. }
  667. // Sets data to types: RADIO GROUP
  668. setSelection(name, newOptionsValue) {
  669. this.document.querySelector(`.field-${name}`).querySelector(`input[value=${newOptionsValue}]`).checked = true;
  670. this.dispatchFormEvent(name);
  671. }
  672. // Gets data from types: CHECKBOX GROUP
  673. getChecked(name) {
  674. return [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]:checked`)]
  675. .map(checkbox => checkbox.value);
  676. }
  677. // Sets data to types: CHECKBOX GROUP
  678. setChecked(name, checkedArr) {
  679. const checkboxes = [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]`)]
  680. checkboxes.forEach(checkbox => {
  681. if(checkedArr.includes(checkbox.value)) {
  682. checkbox.checked = true;
  683. }
  684. });
  685. this.dispatchFormEvent(name);
  686. }
  687. // Gets data from types: FILE UPLOAD
  688. getFiles(name) {
  689. return this.document.querySelector(`.field-${name}`).querySelector(`input[id=${name}]`).files;
  690. }
  691. // Gets data from types: SELECT
  692. getOption(name) {
  693. const selectedArr = [...this.document.querySelector(`.field-${name} #${name}`).selectedOptions].map(({value}) => value);
  694. return selectedArr.length == 1 ? selectedArr[0] : selectedArr;
  695. }
  696. // Sets data to types: SELECT
  697. setOption(name, newOptionsValue) {
  698. if(typeof newOptionsValue == 'object') {
  699. newOptionsValue.forEach(optionVal => {
  700. this.document.querySelector(`.field-${name}`).querySelector(`option[value=${optionVal}]`).selected = true;
  701. });
  702. } else {
  703. this.document.querySelector(`.field-${name}`).querySelector(`option[value=${newOptionsValue}]`).selected = true;
  704. }
  705. this.dispatchFormEvent(name);
  706. }
  707. #typeProperties = [
  708. {
  709. "type": "button",
  710. "event": "click",
  711. "function": {
  712. "get" : null,
  713. "set" : null
  714. }
  715. },
  716. {
  717. "type": "radio",
  718. "event": "change",
  719. "function": {
  720. "get" : n => this.getSelection(n),
  721. "set" : (n, nV) => this.setSelection(n, nV)
  722. }
  723. },
  724. {
  725. "type": "checkbox",
  726. "event": "change",
  727. "function": {
  728. "get" : n => this.getChecked(n),
  729. "set" : (n, nV) => this.setChecked(n, nV)
  730. }
  731. },
  732. {
  733. "type": "date",
  734. "event": "change",
  735. "function": {
  736. "get" : n => this.getValue(n),
  737. "set" : (n, nV) => this.setValue(n, nV)
  738. }
  739. },
  740. {
  741. "type": "file",
  742. "event": "change",
  743. "function": {
  744. "get" : n => this.getFiles(n),
  745. "set" : null
  746. }
  747. },
  748. {
  749. "type": "number",
  750. "event": "input",
  751. "function": {
  752. "get" : n => this.getValue(n),
  753. "set" : (n, nV) => this.setValue(n, nV)
  754. }
  755. },
  756. {
  757. "type": "select",
  758. "event": "change",
  759. "function": {
  760. "get" : n => this.getOption(n),
  761. "set" : (n, nV) => this.setOption(n, nV)
  762. }
  763. },
  764. {
  765. "type": "text",
  766. "event": "input",
  767. "function": {
  768. "get" : n => this.getValue(n),
  769. "set" : (n, nV) => this.setValue(n, nV)
  770. }
  771. },
  772. {
  773. "type": "textarea",
  774. "event": "input",
  775. "function": {
  776. "get" : n => this.getValue(n),
  777. "set" : (n, nV) => this.setValue(n, nV)
  778. }
  779. },
  780. ];
  781. // The same as the event() function, but automatically determines the best listener type for the element
  782. // (e.g. button -> listen for "click", textarea -> listen for "input")
  783. smartEvent(name, eventFunction) {
  784. if(name.includes("-")) {
  785. const type = name.split("-")[0].toLowerCase();
  786. const properties = this.#typeProperties.find(x => type == x.type);
  787. if(typeof properties == "object") {
  788. this.event(name, properties.event, eventFunction);
  789. } else {
  790. console.warn(`${this.#projectName}'s smartEvent function did not find any matches for the type "${type}". The event could not be made.`);
  791. }
  792. } else {
  793. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s smartEvent. The event could not be made.`);
  794. }
  795. }
  796. // Will automatically determine the suitable function for data retrivial
  797. // (e.g. file select -> use getFiles() function)
  798. getData(name) {
  799. if(name.includes("-")) {
  800. const type = name.split("-")[0].toLowerCase();
  801. const properties = this.#typeProperties.find(x => type == x.type);
  802. if(typeof properties == "object") {
  803. const getFunction = properties.function.get;
  804. if(typeof getFunction == "function") {
  805. return getFunction(name);
  806. } else {
  807. console.error(`${this.#projectName}'s getData function can't be used for the type "${type}". The data can't be taken.`);
  808. }
  809. } else {
  810. console.warn(`${this.#projectName}'s getData function did not find any matches for the type "${type}". The event could not be made.`);
  811. }
  812. } else {
  813. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s getData function. The event could not be made.`);
  814. }
  815. }
  816. // Will automatically determine the suitable function for data retrivial (e.g. checkbox -> use setChecked() function)
  817. setData(name, newData) {
  818. if(name.includes("-")) {
  819. const type = name.split("-")[0].toLowerCase();
  820. const properties = this.#typeProperties.find(x => type == x.type);
  821. if(typeof properties == "object") {
  822. const setFunction = properties.function.set;
  823. if(typeof setFunction == "function") {
  824. return setFunction(name, newData);
  825. } else {
  826. console.error(`${this.#projectName}'s setData function can't be used for the type "${type}". The data can't be taken.`);
  827. }
  828. } else {
  829. console.warn(`${this.#projectName}'s setData function did not find any matches for the type "${type}". The event could not be made.`);
  830. }
  831. } else {
  832. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s setData function. The event could not be made.`);
  833. }
  834. }
  835. };