usergui.js

usergui

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/460400/1157130/userguijs.js

  1. /*
  2. * usergui.js
  3. * v1.0.1
  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" : "Smart Chess Bot",
  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.  
  525. const guiObserver = new MutationObserver(adjustFrameSize);
  526. const guiElement = this.document.querySelector("#gui");
  527.  
  528. guiObserver.observe(guiElement, {
  529. childList: true,
  530. subtree: true,
  531. attributes: true
  532. });
  533. adjustFrameSize();
  534. }
  535.  
  536. async #openExternalGui(readyFunction) {
  537. const noWindow = this.window?.closed;
  538.  
  539. if(noWindow || this.window == undefined) {
  540. let pos = "";
  541. let windowSettings = "";
  542.  
  543. if(this.settings.window.centered && this.settings.gui.external.popup) {
  544. const centerPos = this.#getCenterScreenPosition();
  545. pos = `left=${centerPos.x}, top=${centerPos.y}`;
  546. }
  547.  
  548. if(this.settings.gui.external.popup) {
  549. windowSettings = `width=${this.settings.window.size.width}, height=${this.settings.window.size.height}, ${pos}`;
  550. }
  551.  
  552. // Create a new window for the GUI
  553. this.window = window.open("", this.settings.windowName, windowSettings);
  554.  
  555. if(!this.window) {
  556. this.settings.messages.blockedPopups();
  557. return;
  558. }
  559.  
  560. // Write the document to the new window
  561. this.window.document.open();
  562. this.window.document.write(await this.#createDocument());
  563. this.window.document.close();
  564.  
  565. if(!this.settings.gui.external.popup) {
  566. this.window.document.body.style.width = `${this.settings.window.size.width}px`;
  567.  
  568. if(this.settings.window.centered) {
  569. const centerPos = this.#getCenterScreenPosition();
  570.  
  571. this.window.document.body.style.position = "absolute";
  572. this.window.document.body.style.left = `${centerPos.x}px`;
  573. this.window.document.body.style.top = `${centerPos.y}px`;
  574. }
  575. }
  576.  
  577. // Dynamic sizing (only height & window.outerHeight no longer works on some browsers...)
  578. this.window.resizeTo(
  579. this.settings.window.size.width,
  580. this.settings.window.size.dynamicSize
  581. ? this.window.document.body.offsetHeight + (this.window.outerHeight - this.window.innerHeight)
  582. : this.settings.window.size.height
  583. );
  584.  
  585. this.document = this.window.document;
  586.  
  587. this.#initializeTabs();
  588.  
  589. // Call user's function
  590. if(typeof readyFunction == "function") {
  591. readyFunction();
  592. }
  593.  
  594. window.onbeforeunload = () => {
  595. // Close the GUI if parent window closes
  596. this.close();
  597. }
  598. }
  599. else {
  600. // Window was already opened, bring the window back to focus
  601. this.window.focus();
  602. }
  603. }
  604.  
  605. async #openInternalGui(readyFunction) {
  606. if(this.iFrame) {
  607. return;
  608. }
  609.  
  610. const fadeInSpeedMs = 250;
  611.  
  612. let left = 0, top = 0;
  613.  
  614. if(this.settings.window.centered) {
  615. const centerPos = this.#getCenterWindowPosition();
  616.  
  617. left = centerPos.x;
  618. top = centerPos.y;
  619. }
  620.  
  621. const iframe = document.createElement("iframe");
  622. iframe.srcdoc = await this.#createDocument();
  623. iframe.style = `
  624. position: fixed;
  625. top: ${top}px;
  626. left: ${left}px;
  627. width: ${this.settings.window.size.width};
  628. height: ${this.settings.window.size.height};
  629. border: 0;
  630. opacity: 0;
  631. transition: all ${fadeInSpeedMs/1000}s;
  632. border-radius: 5px;
  633. box-shadow: rgb(0 0 0 / 6%) 10px 10px 10px;
  634. z-index: 2147483647;
  635. `;
  636.  
  637. const waitForBody = setInterval(() => {
  638. if(document?.body) {
  639. clearInterval(waitForBody);
  640.  
  641. // Prepend the GUI to the document's body
  642. document.body.prepend(iframe);
  643.  
  644. iframe.contentWindow.onload = () => {
  645. // Fade-in implementation
  646. setTimeout(() => iframe.style["opacity"] = "1", fadeInSpeedMs/2);
  647. setTimeout(() => iframe.style["transition"] = "none", fadeInSpeedMs + 500);
  648. this.window = iframe.contentWindow;
  649. this.document = iframe.contentDocument;
  650. this.iFrame = iframe;
  651. this.#initializeInternalGuiEvents(iframe);
  652. this.#initializeTabs();
  653. readyFunction();
  654. }
  655. }
  656. }, 100);
  657. }
  658.  
  659. // Determines if the window is to be opened externally or internally
  660. open(readyFunction) {
  661. if(this.settings.window.external) {
  662. this.#openExternalGui(readyFunction);
  663. } else {
  664. this.#openInternalGui(readyFunction);
  665. }
  666. }
  667.  
  668. // Closes the GUI if it exists
  669. close() {
  670. if(this.settings.window.external) {
  671. if(this.window) {
  672. this.window.close();
  673. }
  674. } else {
  675. if(this.iFrame) {
  676. this.iFrame.remove();
  677. this.iFrame = undefined;
  678. }
  679. }
  680. }
  681.  
  682. saveConfig() {
  683. let config = [];
  684.  
  685. if(this.document) {
  686. [...this.document.querySelectorAll(".form-group")].forEach(elem => {
  687. const inputElem = elem.querySelector("[name]");
  688. const name = inputElem.getAttribute("name"),
  689. data = this.getData(name);
  690. if(data) {
  691. config.push({ "name" : name, "value" : data });
  692. }
  693. });
  694. }
  695.  
  696. GM_setValue("config", config);
  697. }
  698.  
  699. loadConfig() {
  700. const config = this.getConfig();
  701.  
  702. if(this.document && config) {
  703. config.forEach(elemConfig => {
  704. this.setData(elemConfig.name, elemConfig.value);
  705. })
  706. }
  707. }
  708.  
  709. getConfig() {
  710. return GM_getValue("config");
  711. }
  712.  
  713. resetConfig() {
  714. const config = this.getConfig();
  715.  
  716. if(config) {
  717. GM_setValue("config", []);
  718. }
  719. }
  720.  
  721. dispatchFormEvent(name) {
  722. const type = name.split("-")[0].toLowerCase();
  723. const properties = this.#typeProperties.find(x => type == x.type);
  724. const event = new Event(properties.event);
  725.  
  726. const field = this.document.querySelector(`.field-${name}`);
  727. field.dispatchEvent(event);
  728. }
  729.  
  730. setPrimaryColor(hex) {
  731. const styles = `
  732. #header {
  733. background-color: ${hex} !important;
  734. }
  735. .nav-link {
  736. color: ${hex} !important;
  737. }
  738. .text-primary {
  739. color: ${hex} !important;
  740. }
  741. `;
  742. const styleSheet = document.createElement("style")
  743. styleSheet.innerText = styles;
  744. this.document.head.appendChild(styleSheet);
  745. }
  746.  
  747. // Creates an event listener a GUI element
  748. event(name, event, eventFunction) {
  749. this.document.querySelector(`.field-${name}`).addEventListener(event, eventFunction);
  750. }
  751.  
  752. // Disables a GUI element
  753. disable(name) {
  754. [...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
  755. childElem.setAttribute("disabled", "true");
  756. });
  757. }
  758.  
  759. // Enables a GUI element
  760. enable(name) {
  761. [...this.document.querySelector(`.field-${name}`).children].forEach(childElem => {
  762. if(childElem.getAttribute("disabled")) {
  763. childElem.removeAttribute("disabled");
  764. }
  765. });
  766. }
  767.  
  768. // Gets data from types: TEXT FIELD, TEXTAREA, DATE FIELD & NUMBER
  769. getValue(name) {
  770. return this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value;
  771. }
  772.  
  773. // Sets data to types: TEXT FIELD, TEXT AREA, DATE FIELD & NUMBER
  774. setValue(name, newValue) {
  775. this.document.querySelector(`.field-${name}`).querySelector(`[id=${name}]`).value = newValue;
  776.  
  777. this.dispatchFormEvent(name);
  778. }
  779.  
  780. // Gets data from types: RADIO GROUP
  781. getSelection(name) {
  782. return this.document.querySelector(`.field-${name}`).querySelector(`input[name=${name}]:checked`).value;
  783. }
  784.  
  785. // Sets data to types: RADIO GROUP
  786. setSelection(name, newOptionsValue) {
  787. this.document.querySelector(`.field-${name}`).querySelector(`input[value=${newOptionsValue}]`).checked = true;
  788.  
  789. this.dispatchFormEvent(name);
  790. }
  791.  
  792. // Gets data from types: CHECKBOX GROUP
  793. getChecked(name) {
  794. return [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]:checked`)]
  795. .map(checkbox => checkbox.value);
  796. }
  797.  
  798. // Sets data to types: CHECKBOX GROUP
  799. setChecked(name, checkedArr) {
  800. const checkboxes = [...this.document.querySelector(`.field-${name}`).querySelectorAll(`input[name*=${name}]`)]
  801. checkboxes.forEach(checkbox => {
  802. if(checkedArr.includes(checkbox.value)) {
  803. checkbox.checked = true;
  804. }
  805. });
  806.  
  807. this.dispatchFormEvent(name);
  808. }
  809. // Gets data from types: FILE UPLOAD
  810. getFiles(name) {
  811. return this.document.querySelector(`.field-${name}`).querySelector(`input[id=${name}]`).files;
  812. }
  813. // Gets data from types: SELECT
  814. getOption(name) {
  815. const selectedArr = [...this.document.querySelector(`.field-${name} #${name}`).selectedOptions].map(({value}) => value);
  816.  
  817. return selectedArr.length == 1 ? selectedArr[0] : selectedArr;
  818. }
  819.  
  820. // Sets data to types: SELECT
  821. setOption(name, newOptionsValue) {
  822. if(typeof newOptionsValue == 'object') {
  823. newOptionsValue.forEach(optionVal => {
  824. this.document.querySelector(`.field-${name}`).querySelector(`option[value=${optionVal}]`).selected = true;
  825. });
  826. } else {
  827. this.document.querySelector(`.field-${name}`).querySelector(`option[value=${newOptionsValue}]`).selected = true;
  828. }
  829.  
  830. this.dispatchFormEvent(name);
  831. }
  832.  
  833. #typeProperties = [
  834. {
  835. "type": "button",
  836. "event": "click",
  837. "function": {
  838. "get" : null,
  839. "set" : null
  840. }
  841. },
  842. {
  843. "type": "radio",
  844. "event": "change",
  845. "function": {
  846. "get" : n => this.getSelection(n),
  847. "set" : (n, nV) => this.setSelection(n, nV)
  848. }
  849. },
  850. {
  851. "type": "checkbox",
  852. "event": "change",
  853. "function": {
  854. "get" : n => this.getChecked(n),
  855. "set" : (n, nV) => this.setChecked(n, nV)
  856. }
  857. },
  858. {
  859. "type": "date",
  860. "event": "change",
  861. "function": {
  862. "get" : n => this.getValue(n),
  863. "set" : (n, nV) => this.setValue(n, nV)
  864. }
  865. },
  866. {
  867. "type": "file",
  868. "event": "change",
  869. "function": {
  870. "get" : n => this.getFiles(n),
  871. "set" : null
  872. }
  873. },
  874. {
  875. "type": "number",
  876. "event": "input",
  877. "function": {
  878. "get" : n => this.getValue(n),
  879. "set" : (n, nV) => this.setValue(n, nV)
  880. }
  881. },
  882. {
  883. "type": "select",
  884. "event": "change",
  885. "function": {
  886. "get" : n => this.getOption(n),
  887. "set" : (n, nV) => this.setOption(n, nV)
  888. }
  889. },
  890. {
  891. "type": "text",
  892. "event": "input",
  893. "function": {
  894. "get" : n => this.getValue(n),
  895. "set" : (n, nV) => this.setValue(n, nV)
  896. }
  897. },
  898. {
  899. "type": "textarea",
  900. "event": "input",
  901. "function": {
  902. "get" : n => this.getValue(n),
  903. "set" : (n, nV) => this.setValue(n, nV)
  904. }
  905. },
  906. ];
  907.  
  908. // The same as the event() function, but automatically determines the best listener type for the element
  909. // (e.g. button -> listen for "click", textarea -> listen for "input")
  910. smartEvent(name, eventFunction) {
  911. if(name.includes("-")) {
  912. const type = name.split("-")[0].toLowerCase();
  913. const properties = this.#typeProperties.find(x => type == x.type);
  914.  
  915. if(typeof properties == "object") {
  916. this.event(name, properties.event, eventFunction);
  917.  
  918. } else {
  919. console.warn(`${this.#projectName}'s smartEvent function did not find any matches for the type "${type}". The event could not be made.`);
  920. }
  921.  
  922. } else {
  923. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s smartEvent. The event could not be made.`);
  924. }
  925. }
  926.  
  927. // Will automatically determine the suitable function for data retrivial
  928. // (e.g. file select -> use getFiles() function)
  929. getData(name) {
  930. if(name.includes("-")) {
  931. const type = name.split("-")[0].toLowerCase();
  932. const properties = this.#typeProperties.find(x => type == x.type);
  933.  
  934. if(typeof properties == "object") {
  935. const getFunction = properties.function.get;
  936.  
  937. if(typeof getFunction == "function") {
  938. return getFunction(name);
  939.  
  940. } else {
  941. console.error(`${this.#projectName}'s getData function can't be used for the type "${type}". The data can't be taken.`);
  942. }
  943.  
  944. } else {
  945. console.warn(`${this.#projectName}'s getData function did not find any matches for the type "${type}". The event could not be made.`);
  946. }
  947.  
  948. } else {
  949. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s getData function. The event could not be made.`);
  950. }
  951. }
  952.  
  953. // Will automatically determine the suitable function for data retrivial (e.g. checkbox -> use setChecked() function)
  954. setData(name, newData) {
  955. if(name.includes("-")) {
  956. const type = name.split("-")[0].toLowerCase();
  957. const properties = this.#typeProperties.find(x => type == x.type);
  958.  
  959. if(typeof properties == "object") {
  960. const setFunction = properties.function.set;
  961.  
  962. if(typeof setFunction == "function") {
  963. return setFunction(name, newData);
  964.  
  965. } else {
  966. console.error(`${this.#projectName}'s setData function can't be used for the type "${type}". The data can't be taken.`);
  967. }
  968.  
  969. } else {
  970. console.warn(`${this.#projectName}'s setData function did not find any matches for the type "${type}". The event could not be made.`);
  971. }
  972.  
  973. } else {
  974. console.warn(`The input name "${name}" is invalid for ${this.#projectName}'s setData function. The event could not be made.`);
  975. }
  976. }
  977. };