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.

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