diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d2b47d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 214294d..5311f34 100644 --- a/README.md +++ b/README.md @@ -1 +1,317 @@ -# bs5-utils \ No newline at end of file +# Bs5Utils - A JavaScript utility package for Bootstrap 5 components + +--- + +A simple package to make the usage of various components within Bootstrap 5 easier to use. + +If this package has helped you, and you're feeling particularly generous: +- **ETH/MATIC:** 0x6515654c8e931052ab17a63311411D475D503e59 +- **ADA:** addr1qxaqvghsr8lu3wrmql4fcvg6txj5083s2a9rr5dmrrtjt0yn8t0x4yav3ma2flg3tzcu9767s7senydcumnf6c4krnnspn949q + +--- + +Contents +- + +- [Configuration](#configuration) +- [Theming](#theming) +- [API](#api) +- [Support & Contribute](#support--contribute) + +Configuration +- + +There are several defaults which you can customize: + +```javascript +Bs5Utils.defaults.toasts.position = 'top-right'; +Bs5Utils.defaults.toasts.container = 'toast-container'; +Bs5Utils.defaults.toasts.stacking = false; +``` + +As `bs5Utils.Snack` is a subset of `bs5Utils.Toast`, the configuration for toasts will also apply to `bs5Utils.Sanck`. + +Theming +- + +You can register your own custom styles by passing classes to specific components by using the static +method `Bs5Utils.registerStyle`. The components you can customise are: + +- `btnClose` - The dismiss button +- `main` - The area of the toast, snack, or modal which will display the `type` color +- `border` - The border of the component + +These components have been clearly illustrated below. For the time being, the `border` style for `bs5Utils.Snack` cannot +be overridden. + +**Note:** All of these keys _must_ be passed in the `styles` parameter object. + +**Method Overview** + +```javascript +/** + * Register a style for the components + * @param key - To reference your style + * @param styles - The style object + */ +Bs5Utils.registerStyle(key, styles) +``` + +**Usage** + +You first define your CSS classes: + +```css +.bg-pink { + background-color: pink; +} + +.text-purple { + color: purple; +} + +.border-pink { + border-color: pink !important; +} +``` + +Then you register the style: + +```javascript +Bs5Utils.registerStyle('pink', { + btnClose: ['btn-close-white'], + main: ['bg-pink', 'text-purple'], + border: ['border-pink'] +}); +``` + +Pass empty arrays if you wish to leave the default styles e.g. + +```javascript +Bs5Utils.registerStyle('pink', { + btnClose: [], + main: ['bg-pink', 'text-purple'], + border: ['border-pink'] +}); +``` + +Now, `pink` can be used as a `type` when displaying snacks, toasts, or modals e.g. + +**Snack** + +![Theming Snack](example/img/theming-snack.png) + +**Toast** + +![Theming Snack](example/img/theming-toast.png) + +**Modal** + +![Theming Modal](example/img/theming-modal.png) + +API +- + +This package is based around the `Bs5Utils` class, so first things first, construct the object: + +```javascript +const bs5Utils = new Bs5Utils(); +``` + +Thereafter you'll be able to use the methods outlined below. + +### Snacks + +**Method Overview** + +```javascript +/** + * Display a lightweight toast for simple alerts + * @param - type the theme of the snack + * @param - title the title of the of the snack + * @param - delay in ms, if specified the snack will autohide after the specified amount + * @param - dismissible set whether the dismiss button should show + */ +bs5Utils.Snack.show( + type, + title, + delay = 0, + dismissible = true +); +``` + +**Usage** + +```javascript +bs5Utils.Snack.show('secondary', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('light', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('white', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('dark', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('info', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('primary', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('success', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('warning', 'Hello World!', delay = 0, dismissible = true); +bs5Utils.Snack.show('danger', 'Hello World!', delay = 0, dismissible = true); +``` + +**Example** + +![img.png](example/img/api-snacks.png) + +### Toasts + +**Method Overview** + +```javascript +/** + * Display a toast for alerts + * @param type - the theme of the snack + * @param icon - Set an icon in the top-left corner, you can pass HTML directly + * @param title - the title of the of the toast + * @param subtitle - the subtitle of the toast + * @param content - the content of the toast + * @param buttons - the action buttons of the toast + * @param delay - in ms, if specified the snack will autohide after the specified amount + * @param dismissible - set whether the dismiss button should show + */ +bs5Utils.Toast.show({ + type, + icon = '', + title, + subtitle = '', + content = '', + buttons = [], + delay = 0, + dismissible = true, +}); +``` + +**Usage** + +```javascript +bs5Utils.Toast.show({ + type: 'primary', + icon: ``, + title: 'Notification!', + subtitle: '23 secs ago', + content: 'Hello World!', + buttons: [ + { + text: 'Click Me!', + class: 'btn btn-sm btn-primary', + handler: () => { + alert(`Button #1 has been clicked!`); + } + }, + { + text: 'Click Me Too!', + class: 'btn btn-sm btn-warning', + handler: () => { + alert(`You clicked me too!`); + } + }, + { + type: 'dismiss', + text: 'Hide', + class: 'btn btn-sm btn-secondary' + } + ], + delay: 0, + dismissible: true +}); +``` + +**Example** + +![img.png](example/img/api-toasts.png) + +### Modals + +**Method Overview** + +```javascript +/** + * Display a modal + * @param type - the theme of the snack + * @param title - the title of the modal, if omitted, the modal-header element is removed + * @param content - the content of the modal, if omitted, the modal-body element is removed + * @param buttons - any action buttons, if omitted, the the modal-footer element is removed + * @param centered - set whether the modal is centered + * @param dismissible - set whether the dismiss button should show + * @param backdrop - set the type of backdrop: true, false, static + * @param keyboard - set whether the escape key closes the modal + * @param focus - set whether the modal is autofocussed when initialized + * @param fullscreen - set whether the modal is fullscreen + * @param modalSize - set the size of the modal: sm, lg, xl by default, it's an empty string + */ +bs5Utils.Modal.show({ + type, + title = '', + content = '', + buttons = [], + centered = false, + dismissible = true, + backdrop = dismissible ? true : 'static', + keyboard = dismissible, + focus = true, + fullscreen = false, + size = '' +}) +``` + +**Usage** + +```javascript +bs5Utils.Modal.show({ + type: 'primary', + title: `Hello World!`, + content: `

Hello World!

`, + buttons: [ + { + text: 'Click Me!', + class: 'btn btn-sm btn-primary', + handler: () => { + alert(`Button #1 has been clicked!`); + } + }, + { + text: 'Click Me Too!', + class: 'btn btn-sm btn-warning', + handler: () => { + alert(`You clicked me too!`); + } + }, + { + type: 'dismiss', + text: 'Hide', + class: 'btn btn-sm btn-secondary' + } + ], + centered: true, + dismissible: true, + backdrop: 'static', + keyboard: false, + focus: false +}); +``` + +**Example** + +![img.png](example/img/api-modal-1.png) + +![img.png](example/img/api-modal-2.png) + +![img_1.png](example/img/api-modal-3.png) + +![img.png](example/img/api-modal-4.png) + +Support & Contribute +- + +- Use: [Babel Repl](https://babeljs.io/repl) and [JavaScript Minifier](https://javascript-minifier.com/) to build the + app to transpile and minify your changes +- Submit issues and PRs +- Let's know how you're using this package in your project +- If this package has helped you, and you're feeling particularly generous: + - **ETH/MATIC:** 0x6515654c8e931052ab17a63311411D475D503e59 + - **ADA:** addr1qxaqvghsr8lu3wrmql4fcvg6txj5083s2a9rr5dmrrtjt0yn8t0x4yav3ma2flg3tzcu9767s7senydcumnf6c4krnnspn949q \ No newline at end of file diff --git a/dist/js/Bs5Utils.js b/dist/js/Bs5Utils.js new file mode 100644 index 0000000..8343842 --- /dev/null +++ b/dist/js/Bs5Utils.js @@ -0,0 +1 @@ +"use strict";function _classPrivateMethodInitSpec(t,e){_checkPrivateRedeclaration(t,e),e.add(t)}function _defineProperty(t,e,s){return e in t?Object.defineProperty(t,e,{value:s,enumerable:!0,configurable:!0,writable:!0}):t[e]=s,t}function _classPrivateMethodGet(t,e,s){if(!e.has(t))throw new TypeError("attempted to get private field on non-instance");return s}function _classPrivateFieldInitSpec(t,e,s){_checkPrivateRedeclaration(t,e),e.set(t,s)}function _checkPrivateRedeclaration(t,e){if(e.has(t))throw new TypeError("Cannot initialize the same private elements twice on an object")}function _classPrivateFieldSet(t,e,s){return _classApplyDescriptorSet(t,_classExtractFieldDescriptor(t,e,"set"),s),s}function _classApplyDescriptorSet(t,e,s){if(e.set)e.set.call(t,s);else{if(!e.writable)throw new TypeError("attempted to set read only private field");e.value=s}}function _classPrivateFieldGet(t,e){return _classApplyDescriptorGet(t,_classExtractFieldDescriptor(t,e,"get"))}function _classExtractFieldDescriptor(t,e,s){if(!e.has(t))throw new TypeError("attempted to "+s+" private field on non-instance");return e.get(t)}function _classApplyDescriptorGet(t,e){return e.get?e.get.call(t):e.value}var _count=new WeakMap;class Modal{constructor(){_classPrivateFieldInitSpec(this,_count,{writable:!0,value:0})}show({type:t,title:e="",content:s="",buttons:a=[],centered:o=!1,dismissible:i=!0,backdrop:n=!!i||"static",keyboard:r=i,focus:l=!0,fullscreen:d=!1,size:c=""}){_classPrivateFieldSet(this,_count,1+ +_classPrivateFieldGet(this,_count)),c=["sm","lg","xl"].includes(c)?`modal-${c}`:"",d=d?"modal-fullscreen":"",o=o?"modal-dialog-centered modal-dialog-scrollable":"";const b=Bs5Utils.defaults.styles[t],u=b.btnClose.join(" "),m=b.border,h=document.createElement("div");h.setAttribute("id",`modal-${_classPrivateFieldGet(this,_count)}`),h.setAttribute("tabindex","-1"),h.classList.add("modal");let p="",v=[];Array.isArray(a)&&a.length&&(p+=`"),h.innerHTML=` `,document.body.appendChild(h),h.addEventListener("hidden.bs.modal",function(t){t.target.remove()}),v.forEach(t=>{document.getElementById(t.id).addEventListener("click",t.handler)});const f={backdrop:n,keyboard:r,focus:l},y=new bootstrap.Modal(h,f);return y.show(),y}}var _count2=new WeakMap;class Snack{constructor(){_classPrivateFieldInitSpec(this,_count2,{writable:!0,value:0})}show(t,e,s=0,a=!0){_classPrivateFieldSet(this,_count2,1+ +_classPrivateFieldGet(this,_count2));const o=Bs5Utils.defaults.styles[t],i=o.btnClose.join(" "),n=document.createElement("div");n.classList.add("toast","align-items-center","border-1","border-dark"),o.main.forEach(t=>{n.classList.add(t)}),n.setAttribute("id",`snack-${_classPrivateFieldGet(this,_count2)}`),n.setAttribute("role","alert"),n.setAttribute("aria-live","assertive"),n.setAttribute("aria-atomic","true"),n.innerHTML=`
\n
${e}
\n ${a?``:""}\n
`,Bs5Utils.defaults.toasts.stacking||document.querySelectorAll(`#${Bs5Utils.defaults.toasts.container} .toast`).forEach(t=>{t.remove()}),document.querySelector(`#${Bs5Utils.defaults.toasts.container}`).appendChild(n),n.addEventListener("hidden.bs.toast",function(t){t.target.remove()});const r={autohide:s>0&&"number"==typeof s};s>0&&"number"==typeof s&&(r.delay=s);const l=new bootstrap.Toast(n,r);return l.show(),l}}var _count3=new WeakMap;class Toast{constructor(){_classPrivateFieldInitSpec(this,_count3,{writable:!0,value:0})}show({type:t,icon:e="",title:s,subtitle:a="",content:o="",buttons:i=[],delay:n=0,dismissible:r=!0}){_classPrivateFieldSet(this,_count3,1+ +_classPrivateFieldGet(this,_count3));const l=Bs5Utils.defaults.styles[t],d=l.btnClose.join(" "),c=l.border,b=document.createElement("div");b.setAttribute("id",`toast-${_classPrivateFieldGet(this,_count3)}`),b.setAttribute("role","alert"),b.setAttribute("aria-live","assertive"),b.setAttribute("aria-atomic","true"),b.classList.add("toast","align-items-center"),c.forEach(t=>{b.classList.add(t)});let u="",m=[];Array.isArray(i)&&i.length&&(u+=`
`,i.forEach((t,e)=>{switch(t.type||"button"){case"dismiss":u+=` `;break;default:let s=`toast-${_classPrivateFieldGet(this,_count3)}-button-${e}`;u+=` `,t.hasOwnProperty("handler")&&"function"==typeof t.handler&&m.push({id:s,handler:t.handler})}}),u+="
"),b.innerHTML=`
\n ${e}\n ${s}\n ${a}\n ${r?``:""}\n
\n
\n ${o}\n ${u}\n
`,Bs5Utils.defaults.toasts.stacking||document.querySelectorAll(`#${Bs5Utils.defaults.toasts.container} .toast`).forEach(t=>{t.remove()}),document.querySelector(`#${Bs5Utils.defaults.toasts.container}`).appendChild(b),b.addEventListener("hidden.bs.toast",function(t){t.target.remove()}),m.forEach(t=>{document.getElementById(t.id).addEventListener("click",t.handler)});const h={autohide:n>0&&"number"==typeof n};n>0&&"number"==typeof n&&(h.delay=n);const p=new bootstrap.Toast(b,h);return p.show(),p}}var _createToastContainer=new WeakSet;class Bs5Utils{constructor(){_classPrivateMethodInitSpec(this,_createToastContainer),_classPrivateMethodGet(this,_createToastContainer,_createToastContainer2).call(this),this.Toast=new Toast,this.Snack=new Snack,this.Modal=new Modal}static registerStyle(t,e){if("object"!=typeof e&&Array.isArray(e))throw"The styles parameter must be an object when you register component style.";Bs5Utils.defaults.styles[t]=e}}function _createToastContainer2(){let t=document.querySelector(`#${Bs5Utils.defaults.toasts.container}`);if(!t){const e={"top-left":"top-0 start-0 ms-1 mt-1","top-center":"top-0 start-50 translate-middle-x mt-1","top-right":"top-0 end-0 me-1 mt-1","middle-left":"top-50 start-0 translate-middle-y ms-1","middle-center":"top-50 start-50 translate-middle p-3","middle-right":"top-50 end-0 translate-middle-y me-1","bottom-left":"bottom-0 start-0 ms-1 mb-1","bottom-center":"bottom-0 start-50 translate-middle-x mb-1","bottom-right":"bottom-0 end-0 me-1 mb-1"};(t=document.createElement("div")).classList.add("position-relative"),t.setAttribute("aria-live","polite"),t.setAttribute("aria-atomic","true"),t.innerHTML=`
`,document.body.appendChild(t)}}_defineProperty(Bs5Utils,"defaults",{toasts:{position:"top-right",container:"toast-container",stacking:!0},styles:{secondary:{btnClose:["btn-close-white"],main:["text-white","bg-secondary"],border:["border-secondary"]},light:{btnClose:[],main:["text-dark","bg-light","border-bottom","border-dark"],border:["border-dark"]},white:{btnClose:[],main:["text-dark","bg-white","border-bottom","border-dark"],border:["border-dark"]},dark:{btnClose:["btn-close-white"],main:["text-white","bg-dark"],border:["border-dark"]},info:{btnClose:["btn-close-white"],main:["text-white","bg-info"],border:["border-info"]},primary:{btnClose:["btn-close-white"],main:["text-white","bg-primary"],border:["border-primary"]},success:{btnClose:["btn-close-white"],main:["text-white","bg-success"],border:["border-success"]},warning:{btnClose:["btn-close-white"],main:["text-white","bg-warning"],border:["border-warning"]},danger:{btnClose:["btn-close-white"],main:["text-white","bg-danger"],border:["border-danger"]}}}); \ No newline at end of file diff --git a/example/img/api-modal-1.png b/example/img/api-modal-1.png new file mode 100644 index 0000000..6b299a1 Binary files /dev/null and b/example/img/api-modal-1.png differ diff --git a/example/img/api-modal-2.png b/example/img/api-modal-2.png new file mode 100644 index 0000000..b16e5a9 Binary files /dev/null and b/example/img/api-modal-2.png differ diff --git a/example/img/api-modal-3.png b/example/img/api-modal-3.png new file mode 100644 index 0000000..7c0b4ca Binary files /dev/null and b/example/img/api-modal-3.png differ diff --git a/example/img/api-modal-4.png b/example/img/api-modal-4.png new file mode 100644 index 0000000..de6e270 Binary files /dev/null and b/example/img/api-modal-4.png differ diff --git a/example/img/api-snacks.png b/example/img/api-snacks.png new file mode 100644 index 0000000..1b831d9 Binary files /dev/null and b/example/img/api-snacks.png differ diff --git a/example/img/api-toasts.png b/example/img/api-toasts.png new file mode 100644 index 0000000..6e84d9a Binary files /dev/null and b/example/img/api-toasts.png differ diff --git a/example/img/theming-modal.png b/example/img/theming-modal.png new file mode 100644 index 0000000..87cefbc Binary files /dev/null and b/example/img/theming-modal.png differ diff --git a/example/img/theming-snack.png b/example/img/theming-snack.png new file mode 100644 index 0000000..4f66036 Binary files /dev/null and b/example/img/theming-snack.png differ diff --git a/example/img/theming-toast.png b/example/img/theming-toast.png new file mode 100644 index 0000000..42fd468 Binary files /dev/null and b/example/img/theming-toast.png differ diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..327d787 --- /dev/null +++ b/example/index.html @@ -0,0 +1,284 @@ + + + + + + + + + + + + Hello, world! + + + +
+

bs5-utils

+
+ +
+
+
+
+
Bs5Utils.Snack.show
+
+
bs5Utils.Snack.show(
+    type,
+    title,
+    delay = 0,
+    dismissible = true
+);
+
+ +
+ +
+
Bs5Utils.Toast.show
+
+
bs5Utils.Toast.show({
+     type,
+     icon = '',
+     title,
+     subtitle = '',
+     content = '',
+     buttons = [],
+     delay = 0,
+     dismissible = true
+});
+
+ +
+ +
+
Bs5Utils.Modal.show
+
+
bs5Utils.Modal.show({
+     type,
+     title = '',
+     content = '',
+     buttons = [],
+     centered = false,
+     dismissible = true,
+     backdrop = dismissible ? true : 'static',
+     keyboard = dismissible,
+     focus = true,
+     fullscreen = false,
+     size = ''
+});
+
+ +
+
+
+
+ + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fe1b3da --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "bs5-utils", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bootstrap": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.0.tgz", + "integrity": "sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..60afbaa --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "bs5-utils", + "version": "1.0.0", + "description": "A JavaScript utility package for Bootstrap 5 components.", + "main": "src/js/Bs5Utils.js", + "repository": { + "type": "git", + "url": "git+https://github.com/Script47/Toast.git" + }, + "keywords": [ + "bootstrap-5", + "bootstrap" + ], + "author": "Script47 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/Script47/bs5-utils/issues" + }, + "homepage": "https://github.com/Script47/bs5-utils", + "scripts": { + "build": "webpack --config webpack.config.js" + }, + "dependencies": { + "bootstrap": ">=5.0" + }, + "devDependencies": {} +} diff --git a/src/js/Bs5Utils.js b/src/js/Bs5Utils.js new file mode 100644 index 0000000..f65f814 --- /dev/null +++ b/src/js/Bs5Utils.js @@ -0,0 +1,108 @@ +class Bs5Utils { + /** + * Default config options + * @type {{toasts: {container: string, position: string, stacking: boolean}}} + */ + static defaults = { + toasts: { + position: 'top-right', + container: 'toast-container', + stacking: true + }, + + styles: { + secondary: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-secondary'], + border: ['border-secondary'] + }, + light: { + btnClose: [], + main: ['text-dark', 'bg-light', 'border-bottom', 'border-dark'], + border: ['border-dark'] + }, + white: { + btnClose: [], + main: ['text-dark', 'bg-white', 'border-bottom', 'border-dark'], + border: ['border-dark'] + }, + dark: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-dark'], + border: ['border-dark'] + }, + info: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-info'], + border: ['border-info'] + }, + primary: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-primary'], + border: ['border-primary'] + }, + success: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-success'], + border: ['border-success'] + }, + warning: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-warning'], + border: ['border-warning'] + }, + danger: { + btnClose: ['btn-close-white'], + main: ['text-white', 'bg-danger'], + border: ['border-danger'] + } + } + } + + constructor() { + this.#createToastContainer(); + + this.Toast = new Toast(); + this.Snack = new Snack(); + this.Modal = new Modal(); + } + + #createToastContainer() { + let containerEl = document.querySelector(`#${Bs5Utils.defaults.toasts.container}`); + + if (!containerEl) { + const positionToClass = { + 'top-left': 'top-0 start-0 ms-1 mt-1', + 'top-center': 'top-0 start-50 translate-middle-x mt-1', + 'top-right': 'top-0 end-0 me-1 mt-1', + 'middle-left': 'top-50 start-0 translate-middle-y ms-1', + 'middle-center': 'top-50 start-50 translate-middle p-3', + 'middle-right': 'top-50 end-0 translate-middle-y me-1', + 'bottom-left': 'bottom-0 start-0 ms-1 mb-1', + 'bottom-center': 'bottom-0 start-50 translate-middle-x mb-1', + 'bottom-right': 'bottom-0 end-0 me-1 mb-1' + }; + + containerEl = document.createElement('div'); + containerEl.classList.add('position-relative'); + containerEl.setAttribute('aria-live', 'polite'); + containerEl.setAttribute('aria-atomic', 'true'); + containerEl.innerHTML = `
`; + + document.body.appendChild(containerEl); + } + } + + /** + * Register a style for the components + * @param key - To reference your style + * @param styles - The style object + */ + static registerStyle(key, styles) { + if (typeof styles !== 'object' && Array.isArray(styles)) { + throw 'The styles parameter must be an object when you register component style.' + } + + Bs5Utils.defaults.styles[key] = styles; + } +} \ No newline at end of file diff --git a/src/js/components/Modal.js b/src/js/components/Modal.js new file mode 100644 index 0000000..fedf714 --- /dev/null +++ b/src/js/components/Modal.js @@ -0,0 +1,114 @@ +class Modal { + /** + * A counter for the Modals + * @type {number} + */ + #count = 0; + + /** + * Display a modal + * @param type - the theme of the snack + * @param title - the title of the modal, if omitted, the modal-header element is removed + * @param content - the content of the modal, if omitted, the modal-body element is removed + * @param buttons - any action buttons, if omitted, the the modal-footer element is removed + * @param centered - set whether the modal is centered + * @param dismissible - set whether the dismiss button should show + * @param backdrop - set the type of backdrop: true, false, static + * @param keyboard - set whether the escape key closes the modal + * @param focus - set whether the modal is autofocussed when initialized + * @param fullscreen - set whether the modal is fullscreen + * @param modalSize - set the size of the modal: sm, lg, xl by default, it's an empty string + */ + show({ + type, + title = '', + content = '', + buttons = [], + centered = false, + dismissible = true, + backdrop = dismissible ? true : 'static', + keyboard = dismissible, + focus = true, + fullscreen = false, + size = '' + }) { + this.#count++; + + size = ['sm', 'lg', 'xl'].includes(size) ? `modal-${size}` : ''; + fullscreen = fullscreen ? 'modal-fullscreen' : ''; + centered = centered ? 'modal-dialog-centered modal-dialog-scrollable' : ''; + + const style = Bs5Utils.defaults.styles[type], + btnCloseStyles = style.btnClose.join(' '), + borderStyles = style.border, + modal = document.createElement('div'); + + modal.setAttribute('id', `modal-${this.#count}`) + modal.setAttribute('tabindex', '-1'); + modal.classList.add('modal'); + + let footerHtml = '', + buttonIds = []; + + if (Array.isArray(buttons) && buttons.length) { + footerHtml += ``; + } + + modal.innerHTML = ` `; + + document.body.appendChild(modal); + + modal.addEventListener('hidden.bs.modal', function (e) { + e.target.remove(); + }); + + buttonIds.forEach(value => { + document.getElementById(value.id).addEventListener('click', value.handler) + }); + + const opts = { + backdrop, + keyboard, + focus + }; + + const bsModal = new bootstrap.Modal(modal, opts); + + bsModal.show(); + + return bsModal; + } +} \ No newline at end of file diff --git a/src/js/components/Snack.js b/src/js/components/Snack.js new file mode 100644 index 0000000..0c76f78 --- /dev/null +++ b/src/js/components/Snack.js @@ -0,0 +1,61 @@ +class Snack { + /** + * A counter for the Snacks + * @type {number} + */ + #count = 0; + + /** + * Display a lightweight toast + * @param type - the theme of the snack + * @param title - the title of the of the snack + * @param delay - in ms, if specified the snack will autohide after the specified amount + * @param dismissible - set whether the dismiss button should show + */ + show(type, title, delay = 0, dismissible = true) { + this.#count++; + + const style = Bs5Utils.defaults.styles[type], + btnCloseStyle = style.btnClose.join(' '), + snack = document.createElement('div'); + + snack.classList.add('toast', 'align-items-center', 'border-1', 'border-dark'); + style.main.forEach(value => { + snack.classList.add(value); + }); + snack.setAttribute('id', `snack-${this.#count}`); + snack.setAttribute('role', 'alert'); + snack.setAttribute('aria-live', 'assertive'); + snack.setAttribute('aria-atomic', 'true'); + snack.innerHTML = `
+
${title}
+ ${dismissible ? `` : ''} +
`; + + if (!Bs5Utils.defaults.toasts.stacking) { + document.querySelectorAll(`#${Bs5Utils.defaults.toasts.container} .toast`).forEach((toast) => { + toast.remove(); + }); + } + + document.querySelector(`#${Bs5Utils.defaults.toasts.container}`).appendChild(snack); + + snack.addEventListener('hidden.bs.toast', function (e) { + e.target.remove(); + }); + + const opts = { + autohide: (delay > 0 && typeof delay === 'number'), + }; + + if (delay > 0 && typeof delay === 'number') { + opts['delay'] = delay; + } + + const bsSnack = new bootstrap.Toast(snack, opts); + + bsSnack.show(); + + return bsSnack; + } +} \ No newline at end of file diff --git a/src/js/components/Toast.js b/src/js/components/Toast.js new file mode 100644 index 0000000..490d346 --- /dev/null +++ b/src/js/components/Toast.js @@ -0,0 +1,118 @@ +class Toast { + /** + * A counter for the Toasts + * @type {number} + */ + #count = 0; + + /** + * Display a toast for alerts + * @param type - the theme of the snack + * @param icon - Set an icon in the top-left corner, you can pass HTML directly + * @param title - the title of the of the toast + * @param subtitle - the subtitle of the toast + * @param content - the content of the toast + * @param buttons - the action buttons of the toast + * @param delay - in ms, if specified the snack will autohide after the specified amount + * @param dismissible - set whether the dismiss button should show + */ + show({ + type, + icon = '', + title, + subtitle = '', + content = '', + buttons = [], + delay = 0, + dismissible = true + }) { + this.#count++; + + const style = Bs5Utils.defaults.styles[type], + btnCloseStyles = style.btnClose.join(' '), + borderStyles = style.border, + toast = document.createElement('div'); + + toast.setAttribute('id', `toast-${this.#count}`); + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.classList.add('toast', 'align-items-center'); + borderStyles.forEach(value => { + toast.classList.add(value); + }); + + let buttonsHtml = ``, + buttonIds = []; + + if (Array.isArray(buttons) && buttons.length) { + buttonsHtml += `
`; + + buttons.forEach((button, key) => { + const type = button.type || 'button'; + + switch (type) { + case 'dismiss': + buttonsHtml += ` `; + break; + + default: + let id = `toast-${this.#count}-button-${key}`; + + buttonsHtml += ` `; + + if (button.hasOwnProperty('handler') && typeof button.handler === 'function') { + buttonIds.push({ + id, + handler: button.handler + }); + } + } + }); + + buttonsHtml += `
`; + } + + toast.innerHTML = `
+ ${icon} + ${title} + ${subtitle} + ${dismissible ? `` : ''} +
+
+ ${content} + ${buttonsHtml} +
`; + + if (!Bs5Utils.defaults.toasts.stacking) { + document.querySelectorAll(`#${Bs5Utils.defaults.toasts.container} .toast`).forEach((toast) => { + toast.remove(); + }); + } + + document.querySelector(`#${Bs5Utils.defaults.toasts.container}`).appendChild(toast); + + toast.addEventListener('hidden.bs.toast', function (e) { + e.target.remove(); + }); + + buttonIds.forEach(value => { + document.getElementById(value.id).addEventListener('click', value.handler) + }); + + const opts = { + autohide: (delay > 0 && typeof delay === 'number'), + }; + + if (delay > 0 && typeof delay === 'number') { + opts['delay'] = delay; + } + + const bsToast = new bootstrap.Toast(toast, opts); + + bsToast.show(); + + return bsToast; + } +} \ No newline at end of file