├── .gitignore ├── .npmignore ├── src ├── copyright-header.txt └── modal.js ├── .editorconfig ├── scripts ├── postinstall.js ├── build-gh-pages.js └── build-all.js ├── LICENSE.txt ├── package.json ├── test ├── index.html └── test.js ├── .github └── workflows │ └── build-testbed.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .gh-build/ 4 | package-lock.json 5 | src/external 6 | test/src 7 | test/dist 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .npmignore 3 | .gitignore 4 | .editorconfig 5 | node_modules/ 6 | .gh-build/ 7 | test/src 8 | test/dist 9 | -------------------------------------------------------------------------------- /src/copyright-header.txt: -------------------------------------------------------------------------------- 1 | /*! byojs/Modal: #FILENAME# 2 | v#VERSION# (c) #YEAR# Kyle Simpson 3 | MIT License: http://getify.mit-license.org 4 | */ 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = tab 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.yml] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | var path = require("path"); 6 | var fs = require("fs"); 7 | 8 | const PKG_ROOT_DIR = path.join(__dirname,".."); 9 | const SRC_DIR = path.join(PKG_ROOT_DIR,"src"); 10 | const TEST_DIR = path.join(PKG_ROOT_DIR,"test"); 11 | 12 | try { fs.symlinkSync(path.join("..","src"),path.join(TEST_DIR,"src"),"dir"); } catch (err) {} 13 | try { fs.symlinkSync(path.join("..","dist"),path.join(TEST_DIR,"dist"),"dir"); } catch (err) {} 14 | try { fs.symlinkSync(path.join("..","dist","external"),path.join(SRC_DIR,"external"),"dir"); } catch (err) {} 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Kyle Simpson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@byojs/modal", 3 | "description": "Simple wrapper around SweetAlert2", 4 | "version": "0.2.6", 5 | "exports": { 6 | ".": "./dist/modal.mjs" 7 | }, 8 | "browser": { 9 | "@byojs/modal": "./dist/modal.mjs" 10 | }, 11 | "scripts": { 12 | "build:all": "node scripts/build-all.js", 13 | "build:gh-pages": "npm run build:all && node scripts/build-gh-pages.js", 14 | "build": "npm run build:all", 15 | "test:start": "npx http-server test/ -p 8080", 16 | "test": "npm run test:start", 17 | "postinstall": "node scripts/postinstall.js", 18 | "prepublishOnly": "npm run build:all" 19 | }, 20 | "dependencies": { 21 | "@byojs/toggler": "~0.1.3", 22 | "sweetalert2": "~11.16.1" 23 | }, 24 | "devDependencies": { 25 | "micromatch": "~4.0.8", 26 | "recursive-readdir-sync": "~1.0.6", 27 | "terser": "~5.39.0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/byojs/modal.git" 32 | }, 33 | "keywords": [ 34 | "storage" 35 | ], 36 | "bugs": { 37 | "url": "https://github.com/byojs/modal/issues", 38 | "email": "getify@gmail.com" 39 | }, 40 | "homepage": "https://github.com/byojs/modal", 41 | "author": "Kyle Simpson ", 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Modal: Tests 7 | 8 | 9 | 10 |
11 |

Modal: Tests

12 | 13 |

Github

14 | 15 |
16 | 17 |

18 | 19 |

20 |

21 | 22 | 23 | 24 | 25 | 26 |

27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/build-testbed.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build-TestBed 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "main" branch 8 | push: 9 | branches: [ "main" ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: true 23 | 24 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 25 | jobs: 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 33 | - uses: actions/checkout@v4 34 | 35 | # Runs a set of commands using the runners shell 36 | - name: install deps and build test bed 37 | run: | 38 | npm install 39 | npm run build:gh-pages 40 | - name: Setup Pages 41 | uses: actions/configure-pages@v5 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v3 44 | with: 45 | # Upload built files 46 | path: './.gh-build' 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v4 50 | -------------------------------------------------------------------------------- /scripts/build-gh-pages.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | var path = require("path"); 6 | var fs = require("fs"); 7 | var fsp = require("fs/promises"); 8 | 9 | var micromatch = require("micromatch"); 10 | var recursiveReadDir = require("recursive-readdir-sync"); 11 | 12 | const PKG_ROOT_DIR = path.join(__dirname,".."); 13 | const DIST_DIR = path.join(PKG_ROOT_DIR,"dist"); 14 | const TEST_DIR = path.join(PKG_ROOT_DIR,"test"); 15 | const BUILD_DIR = path.join(PKG_ROOT_DIR,".gh-build"); 16 | const BUILD_DIST_DIR = path.join(BUILD_DIR,"dist"); 17 | 18 | 19 | main().catch(console.error); 20 | 21 | 22 | // ********************** 23 | 24 | async function main() { 25 | console.log("*** Building GH-Pages Deployment ***"); 26 | 27 | // try to make various .gh-build/** directories, if needed 28 | for (let dir of [ BUILD_DIR, BUILD_DIST_DIR, ]) { 29 | if (!(await safeMkdir(dir))) { 30 | throw new Error(`Target directory (${dir}) does not exist and could not be created.`); 31 | } 32 | } 33 | 34 | // copy test/* files 35 | await copyFilesTo( 36 | recursiveReadDir(TEST_DIR), 37 | TEST_DIR, 38 | BUILD_DIR, 39 | /*skipPatterns=*/[ "**/src", "**/dist", ] 40 | ); 41 | 42 | // patch import reference in test.js to point to dist/ 43 | var testJSPath = path.join(BUILD_DIR,"test.js"); 44 | var testJSContents = await fsp.readFile(testJSPath,{ encoding: "utf8", }); 45 | testJSContents = testJSContents.replace(/(import[^;]+"modal\/)src([^"]*)"/g,"$1dist$2\""); 46 | await fsp.writeFile(testJSPath,testJSContents,{ encoding: "utf8", }); 47 | 48 | // copy dist/* files 49 | await copyFilesTo( 50 | recursiveReadDir(DIST_DIR), 51 | DIST_DIR, 52 | BUILD_DIST_DIR 53 | ); 54 | 55 | console.log("Complete."); 56 | } 57 | 58 | async function copyFilesTo(files,fromBasePath,toDir,skipPatterns) { 59 | for (let fromPath of files) { 60 | // should we skip copying this file? 61 | if (matchesSkipPattern(fromPath,skipPatterns)) { 62 | continue; 63 | } 64 | 65 | let relativePath = fromPath.slice(fromBasePath.length); 66 | let outputPath = path.join(toDir,relativePath); 67 | let outputDir = path.dirname(outputPath); 68 | 69 | if (!(fs.existsSync(outputDir))) { 70 | if (!(await safeMkdir(outputDir))) { 71 | throw new Error(`While copying files, directory (${outputDir}) could not be created.`); 72 | } 73 | } 74 | 75 | await fsp.copyFile(fromPath,outputPath); 76 | } 77 | } 78 | 79 | function matchesSkipPattern(pathStr,skipPatterns) { 80 | if (skipPatterns && skipPatterns.length > 0) { 81 | return (micromatch(pathStr,skipPatterns).length > 0); 82 | } 83 | } 84 | 85 | async function safeMkdir(pathStr) { 86 | if (!fs.existsSync(pathStr)) { 87 | try { 88 | await fsp.mkdir(pathStr,{ recursive: true, mode: 0o755, }); 89 | return true; 90 | } 91 | catch (err) {} 92 | return false; 93 | } 94 | return true; 95 | } 96 | -------------------------------------------------------------------------------- /scripts/build-all.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | var path = require("path"); 6 | var fs = require("fs"); 7 | var fsp = require("fs/promises"); 8 | 9 | var micromatch = require("micromatch"); 10 | var recursiveReadDir = require("recursive-readdir-sync"); 11 | var terser = require("terser"); 12 | 13 | const PKG_ROOT_DIR = path.join(__dirname,".."); 14 | const SRC_DIR = path.join(PKG_ROOT_DIR,"src"); 15 | const MAIN_COPYRIGHT_HEADER = path.join(SRC_DIR,"copyright-header.txt"); 16 | const NODE_MODULES_DIR = path.join(PKG_ROOT_DIR,"node_modules"); 17 | const BYOJS_TOGGLER_DIST_DIR = path.join(NODE_MODULES_DIR,"@byojs","toggler","dist"); 18 | const SWAL_DIST = path.join(NODE_MODULES_DIR,"sweetalert2","dist","sweetalert2.esm.all.min.js"); 19 | 20 | const DIST_DIR = path.join(PKG_ROOT_DIR,"dist"); 21 | const DIST_EXTERNAL_DIR = path.join(DIST_DIR,"external"); 22 | const DIST_EXTERNAL_BYOJS_DIR = path.join(DIST_EXTERNAL_DIR,"@byojs"); 23 | const DIST_EXTERNAL_BYOJS_TOGGLER_DIR = path.join(DIST_EXTERNAL_BYOJS_DIR,"toggler"); 24 | 25 | 26 | main().catch(console.error); 27 | 28 | 29 | // ********************** 30 | 31 | async function main() { 32 | console.log("*** Building JS ***"); 33 | 34 | // try to make various dist/ directories, if needed 35 | for (let dir of [ 36 | DIST_DIR, 37 | DIST_EXTERNAL_DIR, 38 | DIST_EXTERNAL_BYOJS_DIR, 39 | DIST_EXTERNAL_BYOJS_TOGGLER_DIR, 40 | ]) { 41 | if (!(await safeMkdir(dir))) { 42 | throw new Error(`Target directory (${dir}) does not exist and could not be created.`); 43 | } 44 | } 45 | 46 | // read package.json 47 | var packageJSON = require(path.join(PKG_ROOT_DIR,"package.json")); 48 | // read version number from package.json 49 | var version = packageJSON.version; 50 | // read main src copyright-header text 51 | var mainCopyrightHeader = await fsp.readFile(MAIN_COPYRIGHT_HEADER,{ encoding: "utf8", }); 52 | // render main copyright header with version and year 53 | mainCopyrightHeader = ( 54 | mainCopyrightHeader 55 | .replace(/#VERSION#/g,version) 56 | .replace(/#YEAR#/g,(new Date()).getFullYear()) 57 | ); 58 | 59 | // build src/* to bundlers/* 60 | await buildFiles( 61 | recursiveReadDir(SRC_DIR), 62 | SRC_DIR, 63 | DIST_DIR, 64 | (contents,outputPath,filename = path.basename(outputPath)) => prepareFileContents( 65 | contents, 66 | outputPath.replace(/\.js$/,".mjs"), 67 | filename.replace(/\.js$/,".mjs") 68 | ), 69 | /*skipPatterns=*/[ "**/*.txt", "**/*.json", "**/external" ] 70 | ); 71 | 72 | // build dist/external/* 73 | await buildFiles( 74 | [ SWAL_DIST, ], 75 | path.dirname(SWAL_DIST), 76 | DIST_EXTERNAL_DIR, 77 | (contents,outputPath) => ({ 78 | contents, 79 | 80 | // rename file 81 | outputPath: path.join(path.dirname(outputPath),"esm.swal.mjs"), 82 | }) 83 | ); 84 | await buildFiles( 85 | recursiveReadDir(BYOJS_TOGGLER_DIST_DIR), 86 | BYOJS_TOGGLER_DIST_DIR, 87 | DIST_EXTERNAL_BYOJS_TOGGLER_DIR, 88 | // simple copy as-is 89 | (contents,outputPath) => ({ contents, outputPath, }) 90 | ); 91 | 92 | console.log("Complete."); 93 | 94 | 95 | // **************************** 96 | 97 | async function prepareFileContents(contents,outputPath,filename = path.basename(outputPath)) { 98 | // JS file (to minify)? 99 | if (/\.[mc]?js$/i.test(filename)) { 100 | contents = await minifyJS(contents); 101 | } 102 | 103 | // add copyright header 104 | return { 105 | contents: `${ 106 | mainCopyrightHeader.replace(/#FILENAME#/g,filename) 107 | }\n${ 108 | contents 109 | }`, 110 | 111 | outputPath, 112 | }; 113 | } 114 | } 115 | 116 | async function buildFiles(files,fromBasePath,toDir,processFileContents,skipPatterns) { 117 | for (let fromPath of files) { 118 | // should we skip copying this file? 119 | if (matchesSkipPattern(fromPath,skipPatterns)) { 120 | continue; 121 | } 122 | let relativePath = fromPath.slice(fromBasePath.length); 123 | let outputPath = path.join(toDir,relativePath); 124 | let contents = await fsp.readFile(fromPath,{ encoding: "utf8", }); 125 | ({ contents, outputPath, } = await processFileContents(contents,outputPath)); 126 | let outputDir = path.dirname(outputPath); 127 | 128 | if (!(fs.existsSync(outputDir))) { 129 | if (!(await safeMkdir(outputDir))) { 130 | throw new Error(`While copying files, directory (${outputDir}) could not be created.`); 131 | } 132 | } 133 | 134 | await fsp.writeFile(outputPath,contents,{ encoding: "utf8", }); 135 | } 136 | } 137 | 138 | async function minifyJS(contents,esModuleFormat = true) { 139 | let result = await terser.minify(contents,{ 140 | mangle: { 141 | keep_fnames: true, 142 | }, 143 | compress: { 144 | keep_fnames: true, 145 | }, 146 | output: { 147 | comments: /^!/, 148 | }, 149 | module: esModuleFormat, 150 | }); 151 | if (!(result && result.code)) { 152 | if (result.error) throw result.error; 153 | else throw result; 154 | } 155 | return result.code; 156 | } 157 | 158 | function matchesSkipPattern(pathStr,skipPatterns) { 159 | if (skipPatterns && skipPatterns.length > 0) { 160 | return (micromatch(pathStr,skipPatterns).length > 0); 161 | } 162 | } 163 | 164 | async function safeMkdir(pathStr) { 165 | if (!fs.existsSync(pathStr)) { 166 | try { 167 | await fsp.mkdir(pathStr,{ recursive: true, mode: 0o755, }); 168 | return true; 169 | } 170 | catch (err) {} 171 | return false; 172 | } 173 | return true; 174 | } 175 | -------------------------------------------------------------------------------- /src/modal.js: -------------------------------------------------------------------------------- 1 | import Swal from "sweetalert2"; 2 | import Toggler from "@byojs/toggler"; 3 | 4 | 5 | // *********************** 6 | 7 | var modalType = null; 8 | var modalStatus = "closed"; 9 | var toggleStartDelay = 300; 10 | var toggleStopDelay = 500; 11 | var toggleSpinner = null; 12 | configSpinner(toggleStartDelay,toggleStopDelay).catch(()=>{}); 13 | 14 | 15 | // *********************** 16 | 17 | export { 18 | Swal, 19 | showToast, 20 | showNotice, 21 | showError, 22 | promptSimple, 23 | configSpinner, 24 | startSpinner, 25 | stopSpinner, 26 | close, 27 | }; 28 | var publicAPI = { 29 | Swal, 30 | showToast, 31 | showNotice, 32 | showError, 33 | promptSimple, 34 | configSpinner, 35 | startSpinner, 36 | stopSpinner, 37 | close, 38 | }; 39 | export default publicAPI; 40 | 41 | 42 | // *********************** 43 | 44 | async function showToast(toastMsg,hideDelay = 5000) { 45 | var check = checkCloseSpinner(); 46 | if (isPromise(check)) await check; 47 | 48 | modalType = "toast"; 49 | modalStatus = "opening"; 50 | 51 | return Swal.fire({ 52 | text: toastMsg, 53 | showConfirmButton: false, 54 | showCloseButton: true, 55 | timer: Math.max(Number(hideDelay) || 0,250), 56 | toast: true, 57 | position: "top-end", 58 | customClass: { 59 | popup: "toast-popup", 60 | }, 61 | didOpen: onModalOpen, 62 | didDestroy: onModalClose, 63 | }); 64 | } 65 | 66 | async function showNotice(noticeMsg) { 67 | var check = checkCloseSpinner(); 68 | if (isPromise(check)) await check; 69 | 70 | modalType = "notice"; 71 | modalStatus = "opening"; 72 | 73 | return Swal.fire({ 74 | text: noticeMsg, 75 | icon: "info", 76 | confirmButtonText: "OK", 77 | didOpen: onModalOpen, 78 | didDestroy: onModalClose, 79 | }); 80 | } 81 | 82 | async function showError(errMsg) { 83 | var check = checkCloseSpinner(); 84 | if (isPromise(check)) await check; 85 | 86 | modalType = "error"; 87 | modalStatus = "opening"; 88 | 89 | return Swal.fire({ 90 | title: "Error!", 91 | text: errMsg, 92 | icon: "error", 93 | confirmButtonText: "OK", 94 | didOpen: onModalOpen, 95 | didDestroy: onModalClose, 96 | }); 97 | } 98 | 99 | async function promptSimple({ 100 | title = "Enter Info", 101 | showConfirmButton = true, 102 | confirmButtonText = "Submit", 103 | confirmButtonColor = "darkslateblue", 104 | showCancelButton = true, 105 | cancelButtonColor = "darkslategray", 106 | allowOutsideClick = true, 107 | allowEscapeKey = true, 108 | icon = "question", 109 | didOpen = onModalOpen, 110 | didDestroy = onModalClose, 111 | ...swalOptions 112 | } = {}) { 113 | var check = checkCloseSpinner(); 114 | if (isPromise(check)) await check; 115 | 116 | modalType = "simple-prompt"; 117 | modalStatus = "opening"; 118 | 119 | var result = await Swal.fire({ 120 | title, 121 | showConfirmButton, 122 | confirmButtonText, 123 | confirmButtonColor, 124 | showCancelButton, 125 | cancelButtonColor, 126 | allowOutsideClick, 127 | allowEscapeKey, 128 | icon, 129 | didOpen: ( 130 | didOpen != onModalOpen ? 131 | (...args) => (onModalOpen(...args), didOpen(...args)) : 132 | didOpen 133 | ), 134 | didDestroy: ( 135 | didDestroy != onModalClose ? 136 | (...args) => (onModalClose(...args), didDestroy(...args)) : 137 | didDestroy 138 | ), 139 | ...swalOptions 140 | }); 141 | 142 | if (result.isConfirmed) { 143 | return result.value; 144 | } 145 | return result; 146 | } 147 | 148 | async function configSpinner( 149 | startDelay = toggleStartDelay, 150 | stopDelay = toggleStopDelay 151 | ) { 152 | var check = checkCloseSpinner(); 153 | if (isPromise(check)) await check; 154 | 155 | toggleStartDelay = startDelay; 156 | toggleStopDelay = stopDelay; 157 | toggleSpinner = Toggler(startDelay,stopDelay); 158 | } 159 | 160 | function startSpinner() { 161 | if (![ "opening", "open", ].includes(modalStatus)) { 162 | modalType = "spinner"; 163 | modalStatus = "opening"; 164 | toggleSpinner(showSpinner,hideSpinner); 165 | } 166 | } 167 | 168 | function stopSpinner() { 169 | if ( 170 | modalType == "spinner" && 171 | ![ "closing", "closed", ].includes(modalStatus) 172 | ) { 173 | modalStatus = "closing"; 174 | toggleSpinner(showSpinner,hideSpinner); 175 | } 176 | } 177 | 178 | function showSpinner() { 179 | // ensure we don't "re-open" an already-open spinner modal, 180 | // as this causes a flicker that is UX undesirable. 181 | if (!( 182 | Swal.isVisible() && 183 | Swal.getPopup().matches(".spinner-popup")) 184 | ) { 185 | Swal.fire({ 186 | position: "top", 187 | showConfirmButton: false, 188 | allowOutsideClick: false, 189 | allowEscapeKey: false, 190 | customClass: { 191 | // used purely for .matches(), not for CSS, 192 | // although you *can* add your own CSS 193 | // `.spinner-popup` class, to customize its 194 | // styling 195 | popup: "spinner-popup", 196 | }, 197 | didOpen: onModalOpen, 198 | didDestroy: onModalClose, 199 | }); 200 | Swal.showLoading(); 201 | } 202 | } 203 | 204 | function hideSpinner() { 205 | // ensure we only close an actually-open spinner 206 | // modal (and not some other valid modal) 207 | if ( 208 | Swal.isVisible() && 209 | Swal.getPopup().matches(".spinner-popup") 210 | ) { 211 | Swal.close(); 212 | } 213 | } 214 | 215 | function checkCloseSpinner(){ 216 | if (modalType == "spinner" && modalStatus != "closed") { 217 | return new Promise(res => { 218 | if ([ "opening", "open", ].includes(modalStatus)) { 219 | stopSpinner(); 220 | } 221 | modalStatus = "closing"; 222 | 223 | // make sure we wait for the spinner to fully close 224 | setTimeout(res,toggleStopDelay); 225 | }); 226 | } 227 | } 228 | 229 | async function close() { 230 | if (modalType == "spinner") { 231 | await checkCloseSpinner(); 232 | } 233 | // modal still visible? 234 | if (Swal.isVisible()) { 235 | modalStatus = "closing"; 236 | Swal.close(); 237 | } 238 | else { 239 | modalType = null; 240 | modalStatus = "closed"; 241 | } 242 | } 243 | 244 | function onModalOpen() { 245 | modalStatus = "open"; 246 | } 247 | 248 | function onModalClose() { 249 | modalType = null; 250 | modalStatus = "closed"; 251 | } 252 | 253 | function isPromise(v) { 254 | return v && typeof v == "object" && typeof v.then == "function"; 255 | } 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modal 2 | 3 | [![npm Module](https://badge.fury.io/js/@byojs%2Fmodal.svg)](https://www.npmjs.org/package/@byojs/modal) 4 | [![License](https://img.shields.io/badge/license-MIT-a1356a)](LICENSE.txt) 5 | 6 | **Modal** makes it easy to create nice, accessible modal dialogs (using the widely used and popular [SweetAlert 2](https://sweetalert2.github.io) library). 7 | 8 | ```js 9 | import { showNotice } from ".."; 10 | 11 | showNotice("Hello, friend."); 12 | ``` 13 | 14 | ---- 15 | 16 | [Library Tests (Demo)](https://byojs.dev/modal/) 17 | 18 | ---- 19 | 20 | ## Overview 21 | 22 | The main purpose of **Modal** is to provide a simple set of wrappers (and default behaviors) around [SweetAlert 2](https://sweetalert2.github.io). 23 | 24 | In addition to standard modals and prompt-dialogs, **Modal** also provides time-limited *Toast* popups (non-modal), as well as a debounced (UX-optimized) spinnner (modal) for use during blocking asynchronous operations in your UI. 25 | 26 | ## Deployment / Import 27 | 28 | ```cmd 29 | npm install @byojs/modal 30 | ``` 31 | 32 | The [**@byojs/modal** npm package](https://npmjs.com/package/@byojs/modal) includes a `dist/` directory with all files you need to deploy **Modal** (and its dependencies) into your application/project. 33 | 34 | **Note:** If you obtain this library via git instead of npm, you'll need to [build `dist/` manually](#re-building-dist) before deployment. 35 | 36 | ### Using a bundler 37 | 38 | If you are using a bundler (Astro, Vite, Webpack, etc) for your web application, you should not need to manually copy any files from `dist/`. 39 | 40 | Just `import` the methods of your choice, like so: 41 | 42 | ```js 43 | import { showNotice, showError } from "@byojs/modal"; 44 | ``` 45 | 46 | The bundler tool should pick up and find whatever files (and dependencies) are needed. 47 | 48 | ### Without using a bundler 49 | 50 | If you are not using a bundler (Astro, Vite, Webpack, etc) for your web application, and just deploying the contents of `dist/` as-is without changes (e.g., to `/path/to/js-assets/modal/`), you'll need an [Import Map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) in your app's HTML: 51 | 52 | ```html 53 | 60 | ``` 61 | 62 | Now, you'll be able to `import` the library in your app in a friendly/readable way: 63 | 64 | ```js 65 | import { showNotice, showError } from "modal"; 66 | ``` 67 | 68 | **Note:** If you omit the above *modal* import-map entry, you can still `import` **Modal** by specifying the proper full path to the `modal.mjs` file. 69 | 70 | ## Modal API 71 | 72 | The API provided by **Modal** includes a variety of methods for displaying different types of modals. 73 | 74 | **Note:** In addition to the wrappers provided by **Modal** as described below, you can access the `Swal` import directly, which is just a re-export of **SweetAlert**. 75 | 76 | ### Spinner 77 | 78 | When asynchronous (but "blocking") behavior is happening on a page -- such as loading important data or behavior -- it can be useful to show a modal spinner to the user to indicate they shouldn't try to interact with the page temporarily. 79 | 80 | But more importantly, spinners should be carefully managed to avoid detractive and distracting UX. For example, if an operation is going to complete pretty quickly, flashing up a spinner for only a fraction of a second might not be helpful. It's therefore preferable to [*debounce*](https://github.com/byojs/scheduler?tab=readme-ov-file#debouncing) (i.e., delaying briefly) the spinner being shown. 81 | 82 | On the other hand, once a spinner has been shown, the underlying operation might finish a few milliseconds later, which would then hide the spinner quickly, causing the same unfortunate UX flash. Even worse, a series of async operations could cause the spinner to flicker on and off repeatedly. So the closing of an open spinner *also* needs to be briefly debounced, even though that *technically* delays the user slightly longer than strictly required. 83 | 84 | **Note:** This extra *delay* is only UX perceptible, it's not actually a delay that the underlying code would experience. It's also possible to call another **Modal** method to popup another type of modal, which will immediately hide the spinner. 85 | 86 | Since all this spinner timing is highly UX dependent, the amount of delay is configurable, for both the debounce on showing the spinner (default: `300`ms) and the debounce on hiding the spinner (default: `500`ms). 87 | 88 | Importantly, **Modal** makes all this complicated spinner timing management easy. 89 | 90 | ```js 91 | import { 92 | configSpinner, 93 | startSpinner, 94 | stopSpinner 95 | } from ".."; 96 | 97 | // override the spinner timing defaults 98 | await configSpinner( 99 | /*startDelay=*/150, 100 | /*stopDelay=*/225 101 | ); 102 | 103 | // schedule the spinner to show up 104 | // (after its debounce delay) 105 | startSpinner(); 106 | 107 | // later, schedule the spinner to hide 108 | // (after its debounce delay) 109 | stopSpinner(); 110 | ``` 111 | 112 | **Warning:** You must make sure any other **Modal** dialogs are closed *before* calling `startSpinner()`; otherwise, its ignored. 113 | 114 | Even though the `startSpinner()` and `stopSpinner()` functions fire off asynchronous behavior (spinner modal, dependent on debounce delays), the function themselves are synchronous (not promise returning). 115 | 116 | Both `startSpinner()` and `stopSpinner()` are idempotently safe, meaning you *may* call `startSpinner()` twice before calling `stopSpinner()`, or vice versa, and you still just get the one scheduled action (showing or hiding). 117 | 118 | Also, if you call `stopSpinner()` after `startSpinner()` but *before* the start-delay has transpired, the spinner showing will be canceled. Likewise, if you call `showSpinner()` after `stopSpinner()` but *before* the stop-delay has transpired, the spinner hiding will be canceled. 119 | 120 | ### Toast 121 | 122 | A *toast* is a briefly displayed notice, in the upper right corner of the page, that only displays for a period of time then auto-hides itself. Toasts do have a "X" close button to dismiss them early. 123 | 124 | **Note:** Toasts are not *technically* "modal" -- they don't actually block the rest of the page. But they're included with this library since they're so closely related. 125 | 126 | To display a toast: 127 | 128 | ```js 129 | import { showToast } from ".."; 130 | 131 | // wait 5s (default) 132 | showToast("Quick heads-up!"); 133 | 134 | // wait 15s 135 | showToast("Longer message...",15000); 136 | ``` 137 | 138 | The minimum value for the delay is `250`ms (1/4 of a second). 139 | 140 | `showToast()` is an async (promise-returning) function; if desired, use `await` (or `.then()`) to wait for when the modal is closed -- **however, since toasts aren't technically UX modal, this is not recommended**. The resolved value is a [SweetAlert2 result object](https://sweetalert2.github.io/#handling-buttons), should you need it. 141 | 142 | ### Notice 143 | 144 | A *notice* is a standard modal that presents textual information. To display a notice modal: 145 | 146 | ```js 147 | import { showNotice } from ".."; 148 | 149 | await showNotice("This is important information."); 150 | ``` 151 | 152 | **Note:** This modal requires a user to click "OK", or dismiss the dialog with `` key or by clicking the "X" icon. 153 | 154 | `showNotice()` is an async (promise-returning) function; use `await` (or `.then()`) to wait for when the modal is closed. The resolved value is a [SweetAlert2 result object](https://sweetalert2.github.io/#handling-buttons), should you need it. 155 | 156 | ### Error 157 | 158 | An *error* is a standard modal that presents textual information that represents something that went wrong. To display an error modal: 159 | 160 | ```js 161 | import { showError } from ".."; 162 | 163 | await showError("Oops, that request failed. Please try again."); 164 | ``` 165 | 166 | **Note:** This modal requires a user to click "OK", or dismiss the dialog with `` key or by clicking the "X" icon. 167 | 168 | `showError()` is an async (promise-returning) function; use `await` (or `.then()`) to wait for when the modal is closed. The resolved value is a [SweetAlert2 result object](https://sweetalert2.github.io/#handling-buttons), should you need it. 169 | 170 | ### Simple Prompt 171 | 172 | An *simple prompt* is a standard modal asks the user to input one piece of information, such as an email address. To display a simple-prompt modal: 173 | 174 | ```js 175 | import { promptSimple } from ".."; 176 | 177 | var result = await promptSimple({ 178 | title: "We need your information...", 179 | text: "Please enter your email:", 180 | input: "email", 181 | inputPlaceholder: "someone@whatever.tld" 182 | }); 183 | if (typeof result == "string") { 184 | console.log(`Email entered: ${result}`); 185 | } 186 | else if (result.canceled) { 187 | console.log("No email provided."); 188 | } 189 | ``` 190 | 191 | The options available to `promptSimple()` are [passed directly through to SweetAlert2](https://sweetalert2.github.io/#configuration). **Modal** provides a few sensible defaults, but pretty much everything can be overridden here, as you see fit. 192 | 193 | **Note:** This modal requires a user to click the confirm button (default: "Submit"), or dismiss the dialog with `` key or by clicking the cancel button (default: "Cancel") or "X" icon. 194 | 195 | `promptSimple()` is an async (promise-returning) function; use `await` (or `.then()`) to wait for when the modal is closed. If the prompt is confirmed, the promise will resolve directly to the user input value (string *email address* above). 196 | 197 | Otherwise, if you need it, the resolved value is a [SweetAlert2 result object](https://sweetalert2.github.io/#handling-buttons), with `isDenied` or `isDismissed` properties set to `true` -- and if dismissed, [the `dismiss` property is also set, with the dismissal reason](https://sweetalert2.github.io/#handling-dismissals). Depending on configuration and user interaction, the `value` property may also be set. 198 | 199 | ## Closing an open modal 200 | 201 | To externally force-close any open modal immediately (except spinner): 202 | 203 | ```js 204 | import { close } from ".."; 205 | 206 | close(); 207 | ``` 208 | 209 | **Note:** It's strongly suggested you *do not* use this method to close the [spinner modal](#spinner); use `stopSpinner()` instead. 210 | 211 | ## Re-building `dist/*` 212 | 213 | If you need to rebuild the `dist/*` files for any reason, run: 214 | 215 | ```cmd 216 | # only needed one time 217 | npm install 218 | 219 | npm run build:all 220 | ``` 221 | 222 | ## Tests 223 | 224 | This library only works in a browser, so its test suite must also be run in a browser. 225 | 226 | Visit [`https://byojs.dev/modal/`](https://byojs.dev/modal/) and click the "run tests" button. 227 | 228 | ### Run Locally 229 | 230 | To instead run the tests locally, first make sure you've [already run the build](#re-building-dist), then: 231 | 232 | ```cmd 233 | npm test 234 | ``` 235 | 236 | This will start a static file webserver (no server logic), serving the interactive test page from `http://localhost:8080/`; visit this page in your browser and click the "run tests" button. 237 | 238 | By default, the `test/test.js` file imports the code from the `src/*` directly. However, to test against the `dist/*` files (as included in the npm package), you can modify `test/test.js`, updating the `/src` in its `import` statements to `/dist` (see the import-map in `test/index.html` for more details). 239 | 240 | ## License 241 | 242 | [![License](https://img.shields.io/badge/license-MIT-a1356a)](LICENSE.txt) 243 | 244 | All code and documentation are (c) 2024 Kyle Simpson and released under the [MIT License](http://getify.mit-license.org/). A copy of the MIT License [is also included](LICENSE.txt). 245 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // note: this module specifier comes from the import-map 2 | // in index.html; swap "src" for "dist" here to test 3 | // against the dist/* files 4 | import Modal from "modal/src"; 5 | import Swal from "sweetalert2"; 6 | 7 | 8 | // *********************** 9 | 10 | var testResultsEl; 11 | 12 | if (document.readyState == "loading") { 13 | document.addEventListener("DOMContentLoaded",ready,false); 14 | } 15 | else { 16 | ready(); 17 | } 18 | 19 | 20 | // *********************** 21 | 22 | async function ready() { 23 | var runAllTestsBtn = document.getElementById("run-all-tests-btn"); 24 | var runSpinnerModalTestsBtn = document.getElementById("run-spinner-tests-btn"); 25 | var runToastModalTestsBtn = document.getElementById("run-toast-modal-tests-btn"); 26 | var runNoticeModalTestsBtn = document.getElementById("run-notice-modal-tests-btn"); 27 | var runErrorModalTestsBtn = document.getElementById("run-error-modal-tests-btn"); 28 | var runSimplePromptModalTestsBtn = document.getElementById("run-simple-prompt-modal-tests-btn"); 29 | testResultsEl = document.getElementById("test-results"); 30 | 31 | runAllTestsBtn.addEventListener("click",runAllTests,false); 32 | runSpinnerModalTestsBtn.addEventListener("click",runSpinnerModalTests,false); 33 | runToastModalTestsBtn.addEventListener("click",runToastModalTests,false); 34 | runNoticeModalTestsBtn.addEventListener("click",runNoticeModalTests,false); 35 | runErrorModalTestsBtn.addEventListener("click",runErrorModalTests,false); 36 | runSimplePromptModalTestsBtn.addEventListener("click",runSimplePromptModalTests,false); 37 | } 38 | 39 | async function runAllTests() { 40 | testResultsEl.innerHTML = ""; 41 | 42 | for (let testFn of [ 43 | runSpinnerModalTests, runToastModalTests, runNoticeModalTests, 44 | runErrorModalTests, runSimplePromptModalTests, 45 | ]) { 46 | let result = await testFn(); 47 | if (result === false) { 48 | break; 49 | } 50 | await timeout(500); 51 | } 52 | } 53 | 54 | async function runSpinnerModalTests() { 55 | var results = []; 56 | var expected = [ "show", "hide", "show", "hide", "show", "hide", ]; 57 | testResultsEl.innerHTML += "Running spinner-modal tests... please wait.
"; 58 | 59 | try { 60 | Modal.configSpinner(100,100); 61 | 62 | var observer = new MutationObserver(mutationList => { 63 | results.push( 64 | ...( 65 | mutationList.filter(mutation => ( 66 | mutation.type == "attributes" && 67 | mutation.attributeName == "class" && 68 | mutation.target.matches(".spinner-popup.swal2-show, .spinner-popup.swal2-hide") 69 | )) 70 | .map(mutation => ( 71 | mutation.target.className.match(/\bswal2-(show|hide)\b/)[1] 72 | )) 73 | ) 74 | ); 75 | }); 76 | observer.observe(document.body,{ attributes: true, subtree: true, }); 77 | 78 | Modal.startSpinner(); 79 | await timeout(110); 80 | Modal.stopSpinner(); 81 | await timeout(500); 82 | Modal.startSpinner(); 83 | Modal.startSpinner(); 84 | Modal.startSpinner(); 85 | Modal.stopSpinner(); 86 | await timeout(110); 87 | Modal.startSpinner(); 88 | Modal.stopSpinner(); 89 | Modal.stopSpinner(); 90 | Modal.stopSpinner(); 91 | await timeout(110); 92 | Modal.startSpinner(); 93 | await timeout(50); 94 | Modal.stopSpinner(); 95 | await timeout(50); 96 | Modal.startSpinner(); 97 | await timeout(110); 98 | Modal.stopSpinner(); 99 | await timeout(500); 100 | Modal.startSpinner(); 101 | await timeout(110); 102 | Modal.stopSpinner(); 103 | await timeout(50); 104 | Modal.startSpinner(); 105 | await timeout(110); 106 | Modal.stopSpinner(); 107 | await timeout(500); 108 | 109 | // remove consecutive duplicates 110 | results = results.reduce((list,v) => ( 111 | list.length == 0 || list[list.length - 1] != v ? 112 | [ ...list, v ] : 113 | list 114 | ),[]); 115 | 116 | if (JSON.stringify(results) == JSON.stringify(expected)) { 117 | testResultsEl.innerHTML += "(Spinner Modal) PASSED.
"; 118 | return true; 119 | } 120 | else { 121 | testResultsEl.innerHTML += `(Spinner Modal) FAILED: expected '${expected.join(",")}', found '${results.join(",")}'
`; 122 | } 123 | } 124 | catch (err) { 125 | logError(err); 126 | testResultsEl.innerHTML += "(Spinner Modal) FAILED -- see console
"; 127 | } 128 | return false; 129 | } 130 | 131 | async function runToastModalTests() { 132 | var results = []; 133 | var expected = [ 134 | /*visible*/true, 135 | /*correct toast*/true, 136 | /*visible*/false, 137 | /*visible*/true, 138 | /*correct spinner*/true, 139 | /*visible*/true, 140 | /*correct toast*/true, 141 | /*visible*/false, 142 | ]; 143 | testResultsEl.innerHTML += "Running toast-modal tests... please wait.
"; 144 | 145 | try { 146 | let toastMsg = "Testing toasts..."; 147 | Modal.showToast(toastMsg,500); 148 | await timeout(250); 149 | let popup = Swal.getPopup(); 150 | results.push( 151 | Swal.isVisible(), 152 | ( 153 | popup.querySelector(".swal2-html-container").innerText == toastMsg 154 | ) 155 | ); 156 | await timeout(500); 157 | results.push( 158 | Swal.isVisible() || Swal.getContainer() != null 159 | ); 160 | 161 | // now popup spinner and make sure toast-dialog 162 | // then closes spinner before opening itself 163 | Modal.configSpinner(100,100); 164 | Modal.startSpinner(); 165 | await timeout(250); 166 | popup = Swal.getPopup(); 167 | results.push( 168 | Swal.isVisible(), 169 | popup.matches(".spinner-popup.swal2-show") 170 | ); 171 | Modal.showToast(toastMsg,500); 172 | await timeout(350); 173 | popup = Swal.getPopup(); 174 | results.push( 175 | Swal.isVisible(), 176 | ( 177 | popup.querySelector(".swal2-html-container").innerText == toastMsg 178 | ) 179 | ); 180 | await timeout(500); 181 | results.push( 182 | Swal.isVisible() || Swal.getContainer() != null 183 | ); 184 | 185 | if (JSON.stringify(results) == JSON.stringify(expected)) { 186 | testResultsEl.innerHTML += "(Toast Modal) PASSED.
"; 187 | return true; 188 | } 189 | else { 190 | testResultsEl.innerHTML += `(Toast Modal) FAILED: expected '${expected.join(",")}', found '${results.join(",")}'
`; 191 | } 192 | } 193 | catch (err) { 194 | logError(err); 195 | testResultsEl.innerHTML = "(Toast Modal) FAILED -- see console" 196 | } 197 | return false; 198 | } 199 | 200 | async function runNoticeModalTests() { 201 | var results = []; 202 | var expected = [ 203 | /*visible*/true, 204 | /*correct notice*/true, 205 | /*visible*/false, 206 | /*visible*/true, 207 | /*correct spinner*/true, 208 | /*visible*/true, 209 | /*correct notice*/true, 210 | /*visible*/false, 211 | ]; 212 | testResultsEl.innerHTML += "Running notice-modal tests... please wait.
"; 213 | 214 | try { 215 | let noticeMsg = "Testing notice modal."; 216 | Modal.showNotice(noticeMsg); 217 | await timeout(250); 218 | let popup = Swal.getPopup(); 219 | results.push( 220 | Swal.isVisible(), 221 | ( 222 | popup.querySelector(".swal2-icon.swal2-info.swal2-icon-show") != null && 223 | popup.querySelector(".swal2-html-container").innerText == noticeMsg 224 | ) 225 | ); 226 | Modal.close(); 227 | await timeout(250); 228 | results.push( 229 | Swal.isVisible() || Swal.getContainer() != null 230 | ); 231 | 232 | // now popup spinner and make sure notice-dialog 233 | // then closes spinner before opening itself 234 | Modal.configSpinner(100,100); 235 | Modal.startSpinner(); 236 | await timeout(250); 237 | popup = Swal.getPopup(); 238 | results.push( 239 | Swal.isVisible(), 240 | popup.matches(".spinner-popup.swal2-show") 241 | ); 242 | Modal.showNotice(noticeMsg); 243 | await timeout(350); 244 | popup = Swal.getPopup(); 245 | results.push( 246 | Swal.isVisible(), 247 | ( 248 | popup.querySelector(".swal2-icon.swal2-info.swal2-icon-show") != null && 249 | popup.querySelector(".swal2-html-container").innerText == noticeMsg 250 | ) 251 | ); 252 | Modal.close(); 253 | await timeout(250); 254 | results.push( 255 | Swal.isVisible() || Swal.getContainer() != null 256 | ); 257 | 258 | if (JSON.stringify(results) == JSON.stringify(expected)) { 259 | testResultsEl.innerHTML += "(Notice Modal) PASSED.
"; 260 | return true; 261 | } 262 | else { 263 | testResultsEl.innerHTML += `(Notice Modal) FAILED: expected '${expected.join(",")}', found '${results.join(",")}'
`; 264 | } 265 | } 266 | catch (err) { 267 | logError(err); 268 | testResultsEl.innerHTML = "(Notice Modal) FAILED -- see console" 269 | } 270 | return false; 271 | } 272 | 273 | async function runErrorModalTests() { 274 | var results = []; 275 | var expected = [ 276 | /*visible*/true, 277 | /*correct error*/true, 278 | /*visible*/false, 279 | /*visible*/true, 280 | /*correct spinner*/true, 281 | /*visible*/true, 282 | /*correct error*/true, 283 | /*visible*/false, 284 | ]; 285 | testResultsEl.innerHTML += "Running error-modal tests... please wait.
"; 286 | 287 | try { 288 | let errMsg = "Testing error modal."; 289 | Modal.showError(errMsg); 290 | await timeout(250); 291 | let popup = Swal.getPopup(); 292 | results.push( 293 | Swal.isVisible(), 294 | ( 295 | popup.querySelector(".swal2-icon.swal2-error.swal2-icon-show") != null && 296 | popup.querySelector(".swal2-html-container").innerText == errMsg 297 | ) 298 | ); 299 | Modal.close(); 300 | await timeout(250); 301 | results.push( 302 | Swal.isVisible() || Swal.getContainer() != null 303 | ); 304 | 305 | // now popup spinner and make sure error-dialog 306 | // then closes spinner before opening itself 307 | Modal.configSpinner(100,100); 308 | Modal.startSpinner(); 309 | await timeout(250); 310 | popup = Swal.getPopup(); 311 | results.push( 312 | Swal.isVisible(), 313 | popup.matches(".spinner-popup.swal2-show") 314 | ); 315 | Modal.showError(errMsg); 316 | await timeout(350); 317 | popup = Swal.getPopup(); 318 | results.push( 319 | Swal.isVisible(), 320 | ( 321 | popup.querySelector(".swal2-icon.swal2-error.swal2-icon-show") != null && 322 | popup.querySelector(".swal2-html-container").innerText == errMsg 323 | ) 324 | ); 325 | Modal.close(); 326 | await timeout(250); 327 | results.push( 328 | Swal.isVisible() || Swal.getContainer() != null 329 | ); 330 | 331 | if (JSON.stringify(results) == JSON.stringify(expected)) { 332 | testResultsEl.innerHTML += "(Error Modal) PASSED.
"; 333 | return true; 334 | } 335 | else { 336 | testResultsEl.innerHTML += `(Error Modal) FAILED: expected '${expected.join(",")}', found '${results.join(",")}'
`; 337 | } 338 | } 339 | catch (err) { 340 | logError(err); 341 | testResultsEl.innerHTML = "(Error Modal) FAILED -- see console" 342 | } 343 | return false; 344 | } 345 | 346 | async function runSimplePromptModalTests() { 347 | var results = []; 348 | var expected = [ 349 | /*visible*/true, 350 | /*correct prompt*/true, 351 | /*visible*/false, 352 | /*visible*/true, 353 | /*correct spinner*/true, 354 | /*visible*/true, 355 | /*correct prompt*/true, 356 | /*visible*/false, 357 | ]; 358 | testResultsEl.innerHTML += "Running prompt-modal tests... please wait.
"; 359 | 360 | try { 361 | let promptTitle = "Testing Prompt Modal"; 362 | let promptText = "Testing prompt modal."; 363 | let promptInputLabel = "good label"; 364 | let promptConfirmButtonText = "Yep"; 365 | let promptCancelButtonText = "Nope"; 366 | let now = new Date(); 367 | let currentDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`; 368 | Modal.promptSimple({ 369 | title: promptTitle, 370 | text: promptText, 371 | input: "date", 372 | inputLabel: promptInputLabel, 373 | inputValue: currentDate, 374 | confirmButtonText: promptConfirmButtonText, 375 | cancelButtonText: promptCancelButtonText, 376 | }); 377 | await timeout(250); 378 | 379 | let popup = Swal.getPopup(); 380 | results.push( 381 | Swal.isVisible(), 382 | ( 383 | popup.querySelector(".swal2-title").innerText == promptTitle && 384 | popup.querySelector(".swal2-icon.swal2-question.swal2-icon-show") != null && 385 | popup.querySelector(".swal2-html-container").innerText == promptText && 386 | popup.querySelector(".swal2-input-label").innerText == promptInputLabel && 387 | popup.querySelector(".swal2-input[type=date]").value == currentDate && 388 | popup.querySelector(".swal2-confirm").innerText == promptConfirmButtonText && 389 | popup.querySelector(".swal2-cancel").innerText == promptCancelButtonText 390 | ) 391 | ); 392 | Modal.close(); 393 | await timeout(250); 394 | results.push( 395 | Swal.isVisible() || Swal.getContainer() != null 396 | ); 397 | 398 | // now popup spinner and make sure prompt-dialog 399 | // then closes spinner before opening itself 400 | Modal.configSpinner(100,100); 401 | Modal.startSpinner(); 402 | await timeout(250); 403 | popup = Swal.getPopup(); 404 | results.push( 405 | Swal.isVisible(), 406 | popup.matches(".spinner-popup.swal2-show") 407 | ); 408 | Modal.promptSimple({ 409 | title: promptTitle, 410 | text: promptText, 411 | input: "date", 412 | inputLabel: promptInputLabel, 413 | inputValue: currentDate, 414 | confirmButtonText: promptConfirmButtonText, 415 | cancelButtonText: promptCancelButtonText, 416 | }); 417 | await timeout(350); 418 | popup = Swal.getPopup(); 419 | results.push( 420 | Swal.isVisible(), 421 | ( 422 | popup.querySelector(".swal2-title").innerText == promptTitle && 423 | popup.querySelector(".swal2-icon.swal2-question.swal2-icon-show") != null && 424 | popup.querySelector(".swal2-html-container").innerText == promptText && 425 | popup.querySelector(".swal2-input-label").innerText == promptInputLabel && 426 | popup.querySelector(".swal2-input[type=date]").value == currentDate && 427 | popup.querySelector(".swal2-confirm").innerText == promptConfirmButtonText && 428 | popup.querySelector(".swal2-cancel").innerText == promptCancelButtonText 429 | ) 430 | ); 431 | Modal.close(); 432 | await timeout(250); 433 | results.push( 434 | Swal.isVisible() || Swal.getContainer() != null 435 | ); 436 | 437 | if (JSON.stringify(results) == JSON.stringify(expected)) { 438 | testResultsEl.innerHTML += "(Prompt Modal) PASSED.
"; 439 | return true; 440 | } 441 | else { 442 | testResultsEl.innerHTML += `(Prompt Modal) FAILED: expected '${expected.join(",")}', found '${results.join(",")}'
`; 443 | } 444 | } 445 | catch (err) { 446 | logError(err); 447 | testResultsEl.innerHTML = "(Prompt Modal) FAILED -- see console" 448 | } 449 | return false; 450 | } 451 | 452 | function timeout(ms) { 453 | return new Promise(res => setTimeout(res,ms)); 454 | } 455 | 456 | function logError(err,returnLog = false) { 457 | var err = `${ 458 | err.stack ? err.stack : err.toString() 459 | }${ 460 | err.cause ? `\n${logError(err.cause,/*returnLog=*/true)}` : "" 461 | }`; 462 | if (returnLog) return err; 463 | else console.error(err); 464 | } 465 | --------------------------------------------------------------------------------