diff --git a/README.md b/README.md index 1ab27b7..9894cd8 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# tp-element +# tp-toaster diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c329ad0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,179 @@ +{ + "name": "@tp/tp-toaster", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@tp/tp-toaster", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^1.2.1", + "@tp/tp-icon": "^1.0.1", + "@tp/tp-media-query": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", + "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" + }, + "node_modules/@lit/reactive-element": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz", + "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/@tp/helpers": { + "version": "1.2.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.2.1/helpers-1.2.1.tgz", + "integrity": "sha512-ukcITyYE10lFT9lTOuu8UQM0s/qKIfw6ihYHVZbUViKyMgGNihWPIQSTZJS5UV2bVbBtQdwQJ+91WcO79ymk3g==", + "license": "Apache-2.0" + }, + "node_modules/@tp/tp-icon": { + "version": "1.0.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-icon/-/1.0.1/tp-icon-1.0.1.tgz", + "integrity": "sha512-rBbQoXZ5t35F7yIbPAEGAlDscZhxLZ5/o229kyiBBrXvCrc+aVOsetSwF1jPeBSmb57h2PfinIvQhtMARwWHoA==", + "license": "Apache-2.0", + "dependencies": { + "@tp/tp-tooltip": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-media-query": { + "version": "1.0.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-media-query/-/1.0.0/tp-media-query-1.0.0.tgz", + "integrity": "sha512-JanA89HDvkn6mw/A9eM9d7Awb5lTZwpmweiEAYjwq0TZaGRzjiOVvLjQnyx+VFqtk9o6E5eQwuKJzLlHExQO+Q==", + "license": "Apache-2.0", + "dependencies": { + "lit": "^2.2.0" + } + }, + "node_modules/@tp/tp-tooltip": { + "version": "1.0.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-tooltip/-/1.0.0/tp-tooltip-1.0.0.tgz", + "integrity": "sha512-wal/DPJH73rz9RbHg66ZciZUyjqfeTKMSImEVWczwjXGoPTG9n5FL5+tPyikpgFr5KDhDKlW8/Q0niBbGnc5KA==", + "license": "Apache-2.0", + "dependencies": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + }, + "node_modules/lit": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.6.tgz", + "integrity": "sha512-1amFHA7t4VaaDe+vdQejSVBklwtH9svGoG6/dZi9JhxtJBBlqY5D1RV7iLUYY0trCqQc4NfhYYZilZiVHt7Hxg==", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.7.0" + } + }, + "node_modules/lit-element": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz", + "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.7.0" + } + }, + "node_modules/lit-html": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.5.tgz", + "integrity": "sha512-YqUzpisJodwKIlbMFCtyrp58oLloKGnnPLMJ1t23cbfIJjg/H9pvLWK4XS69YeubK5HUs1UE4ys9w5dP1zg6IA==", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + } + }, + "dependencies": { + "@lit-labs/ssr-dom-shim": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz", + "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ==" + }, + "@lit/reactive-element": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz", + "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "@tp/helpers": { + "version": "1.2.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Fhelpers/-/1.2.1/helpers-1.2.1.tgz", + "integrity": "sha512-ukcITyYE10lFT9lTOuu8UQM0s/qKIfw6ihYHVZbUViKyMgGNihWPIQSTZJS5UV2bVbBtQdwQJ+91WcO79ymk3g==" + }, + "@tp/tp-icon": { + "version": "1.0.1", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-icon/-/1.0.1/tp-icon-1.0.1.tgz", + "integrity": "sha512-rBbQoXZ5t35F7yIbPAEGAlDscZhxLZ5/o229kyiBBrXvCrc+aVOsetSwF1jPeBSmb57h2PfinIvQhtMARwWHoA==", + "requires": { + "@tp/tp-tooltip": "^1.0.0", + "lit": "^2.2.0" + } + }, + "@tp/tp-media-query": { + "version": "1.0.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-media-query/-/1.0.0/tp-media-query-1.0.0.tgz", + "integrity": "sha512-JanA89HDvkn6mw/A9eM9d7Awb5lTZwpmweiEAYjwq0TZaGRzjiOVvLjQnyx+VFqtk9o6E5eQwuKJzLlHExQO+Q==", + "requires": { + "lit": "^2.2.0" + } + }, + "@tp/tp-tooltip": { + "version": "1.0.0", + "resolved": "https://gitea.codeblob.work/api/packages/tp-elements/npm/%40tp%2Ftp-tooltip/-/1.0.0/tp-tooltip-1.0.0.tgz", + "integrity": "sha512-wal/DPJH73rz9RbHg66ZciZUyjqfeTKMSImEVWczwjXGoPTG9n5FL5+tPyikpgFr5KDhDKlW8/Q0niBbGnc5KA==", + "requires": { + "@tp/helpers": "^1.0.0", + "lit": "^2.2.0" + } + }, + "@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" + }, + "lit": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.6.tgz", + "integrity": "sha512-1amFHA7t4VaaDe+vdQejSVBklwtH9svGoG6/dZi9JhxtJBBlqY5D1RV7iLUYY0trCqQc4NfhYYZilZiVHt7Hxg==", + "requires": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.7.0" + } + }, + "lit-element": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz", + "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.7.0" + } + }, + "lit-html": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.5.tgz", + "integrity": "sha512-YqUzpisJodwKIlbMFCtyrp58oLloKGnnPLMJ1t23cbfIJjg/H9pvLWK4XS69YeubK5HUs1UE4ys9w5dP1zg6IA==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + } + } +} diff --git a/package.json b/package.json index c39fdff..6c362c0 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,21 @@ { - "name": "@tp/tp-element", + "name": "@tp/tp-toaster", "version": "0.0.1", "description": "", - "main": "tp-element.js", + "main": "tp-toaster.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "https://gitea.codeblob.work/tp-elements/tp-element.git" + "url": "https://gitea.codeblob.work/tp-elements/tp-toaster.git" }, "author": "trading_peter", "license": "Apache-2.0", "dependencies": { + "@tp/helpers": "^1.2.1", + "@tp/tp-icon": "^1.0.1", + "@tp/tp-media-query": "^1.0.0", "lit": "^2.2.0" } } diff --git a/tp-element.js b/tp-element.js deleted file mode 100644 index 6a92a2f..0000000 --- a/tp-element.js +++ /dev/null @@ -1,35 +0,0 @@ -/** -@license -Copyright (c) 2022 trading_peter -This program is available under Apache License Version 2.0 -*/ - -import { LitElement, html, css } from 'lit'; - -class TpElement extends LitElement { - static get styles() { - return [ - css` - :host { - display: block; - } - ` - ]; - } - - render() { - const { } = this; - - return html` - - `; - } - - static get properties() { - return { }; - } - - -} - -window.customElements.define('tp-element', TpElement); diff --git a/tp-toast.js b/tp-toast.js new file mode 100644 index 0000000..2dc0c30 --- /dev/null +++ b/tp-toast.js @@ -0,0 +1,214 @@ +/** +@license +Copyright (c) 2023 EDV Wasmeier +*/ + +import { EventHelpers } from '@tp/helpers/event-helpers.js'; +import { LitElement, html, css, svg } from 'lit'; + +class TpToast extends EventHelpers(LitElement) { + static get styles() { + return [ + css` + :host { + --tp-toast-info-icon-color: #fff; + --tp-toast-success-icon-color: #fff; + --tp-toast-error-icon-color: #fff; + --tp-toast-warning-icon-color: #fff; + transition: transform 0.5s, opacity 0.3s; + will-change: transform, opacity; + display: inline-block; + border-radius: 2px; + background: #FAFAFA; + font-size: 0.8em; + cursor: pointer; + opacity: 1; + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), + 0 1px 8px 0 rgba(0, 0, 0, 0.12), + 0 3px 3px -2px rgba(0, 0, 0, 0.4); + } + + .wrap { + display: flex; + flex-direction: row; + } + + .icon { + padding: 10px; + border-right: 1px #fff; + color: #ffffff; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + + :host([type="info"]) .icon { + background: #039BE5; + } + + :host([type="success"]) .icon { + background: #558B2F; + color: #fff; + } + + :host([type="warning"]) .icon { + background: #FFCA28; + } + + :host([type="error"]) .icon { + background: #B71C1C; + } + + .content { + padding: 10px 10px 10px 10px; + line-height: 24px; + } + + .dismiss { + display: flex; + align-items: center; + justify-content: center; + padding: 10px; + } + + .dismiss tp-icon { + --tp-icon-width: 18px; + --tp-icon-height: 18px; + } + ` + ]; + } + + render() { + const { sticky, dismissIcon, icon } = this; + + return html` +
+
+ +
+
+ +
+ ${!sticky ? html` +
+ +
+ ` : null} +
+ `; + } + + static get properties() { + return { + icon: { type: Object }, + dismissIcon: { type: Object }, + + /** + * Configures what kind of toast this is. + * Comes with the variants `info`, `warning`, `error`, `success`. + * Each type comes with a pre-defined icon, but via the `icon` property a custom icon can also be used. + */ + type: { type: String, reflect: true }, + + /** + * Delay till the toast dismisses itself. + * Set in milliseconds. + */ + delay: { type: Number }, + + /** + * If true, the toast cant be dismissed manually. + */ + sticky: { type: Boolean }, + + isDismissed: { type: Boolean }, + + translateY: { type: Number }, + }; + } + + constructor() { + super(); + this.type = 'info'; + this.delay = 5000; + this.translateY = -150; + this.dismiss = this.dismiss.bind(this); + } + + static get defaultDismissIcon() { + return svg``; + } + + static get infoIcon() { + return svg``; + } + + static get successIcon() { + return svg``; + } + + static get errorIcon() { + return svg``; + } + + static get warningIcon() { + return svg``; + } + + connectedCallback() { + super.connectedCallback(); + this.listen(this, 'transitionend', '_afterTransitioned'); + } + + shouldUpdate(changes) { + if (changes.has('translateY')) { + this._translateYChanged(); + } + return true; + } + + /** + * Dismiss the toast right now. + */ + dismiss() { + this.isDismissed = true; + this.style.transform = `translate3d(200px, ${this.translateY}px, 0px)`; + this.style.opacity = '0'; + } + + /** + * Starts the delay timeout to dismiss the toast automatically. + */ + activateDelay() { + if (this.delay > 0) { + this.stopDelay(); + this._delayJob = setTimeout(this.dismiss, this.delay); + } + } + + stopDelay() { + if (this._delayJob) { + clearTimeout(this._delayJob); + this._delayJob = null; + } + } + + _afterTransitioned(e) { + if (this.isDismissed) { + if (e.propertyName === 'opacity') { + this.dispatchEvent(new CustomEvent('toast-dismissed', { detail: { toast: this } , bubbles: true, composed: true })); + } + } + } + + _translateYChanged() { + if (this.isDismissed) { + return; + } + this.style.transform = `translate3d(0px, ${this.translateY}px, 0px)`; + } +} + +window.customElements.define('tp-toast', TpToast); diff --git a/tp-toaster.js b/tp-toaster.js new file mode 100644 index 0000000..1404721 --- /dev/null +++ b/tp-toaster.js @@ -0,0 +1,259 @@ +/** +@license +Copyright (c) 2022 trading_peter +This program is available under Apache License Version 2.0 +*/ + +import './tp-toast.js'; +import '@tp/helpers/debounce.js'; +import '@tp/tp-icon/tp-icon.js'; +import '@tp/tp-media-query/tp-media-query.js'; +import { debounce } from '@tp/helpers/debounce.js'; +import { closest } from '@tp/helpers/closest.js'; +import { EventHelpers } from '@tp/helpers/event-helpers.js'; +import { LitElement, html, css, svg } from 'lit'; + +export default class TpToaster extends EventHelpers(LitElement) { + static get styles() { + return [ + css` + :host { + display: inline-block; + position: fixed; + top: 15px; + left: 50%; + width: 50%; + z-index: 1009; + transform: translateY(-60px); + } + + :host [hidden] { + display: none; + } + + #infos { + display: inline-block; + font-size: 0.7em; + border-radius: 4px; + background: #FAFAFA; + transition: transform 0.3s; + will-change: transform; + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), + 0 1px 8px 0 rgba(0, 0, 0, 0.12), + 0 3px 3px -2px rgba(0, 0, 0, 0.4); + } + + #infos .info-wrap { + display: flex; + flex-direction: row; + align-items: center; + } + + #infos[show-infos] { + transform: translateY(60px); + } + + #infos .info-wrap tp-icon { + padding: 5px; + --tp-icon-width: 18px; + --tp-icon-height: 18px; + } + + #infos .info-wrap > div { + padding: 0 5px; + } + + #wrap { + transition: transform 0.3s; + will-change: transform; + transform: translateY(30px); + } + + #wrap[show-infos] { + transform: translateY(70px); + } + + :host ::slotted(tp-toast) { + position: absolute; + } + + @media all and (min-width: 0) and (max-width: 480px) { + :host { + left: 2%; + width: 96% + } + } + ` + ]; + } + + render() { + const { hiddenCount, dismissLabel, moreLabel } = this; + + const show = this.toasts.length > 1; + + return html` + + +
+
+ ${dismissLabel} +
${hiddenCount} ${moreLabel}
+
+
+
+ +
+ `; + } + + static get properties() { + return { + maxVisible: { type: Number }, + count: { type: Number }, + hiddenCount: { type: Number }, + dismissLabel: { type: String }, + moreLabel: { type: String }, + }; + } + + constructor() { + super(); + this.updateList = debounce(this.updateList.bind(this), 50); + window.TpToaster = this; + this.moreLabel = 'more'; + this.hiddenCount = 0; + this.maxVisible = 4; + } + + static get clearAllIcon() { + return svg``; + } + + get toasts() { + const slot = this.shadowRoot.querySelector('slot'); + + if (!slot) return []; + + return slot + .assignedNodes({ flatten:true }) + .filter(n => n.nodeType === Node.ELEMENT_NODE); + } + + connectedCallback() { + super.connectedCallback(); + + this.listen(this, 'click', 'onClick'); + this.listen(this, 'toast-dismissed', 'onToastDismissed'); + } + + /** + * Adds a new toast. This can be a `tp-toast` element or a object with the properties: + * ```json + * { + * "content": "The message to display", + * "type": "warning", + * "delay": "5000" + * } + * ``` + * If the value of `content` starts with `i18n.`, the toaster tries to translate the string. + * + * @param toast + */ + add(toast) { + toast = Object.assign({ + content: '', + type: 'info', + delay: 5000 + }, toast); + + const newContent = document.createElement('div'); + newContent.textContent = toast.content; + + const newToast = document.createElement('tp-toast'); + newToast.type = toast.type; + newToast.delay = toast.delay; + newToast.sticky = toast.sticky; + newToast.icon = toast.icon; + newToast.appendChild(newContent); + + this.appendChild(newToast); + this.updateList(); + } + + /** + * Dismiss all non-sticky toasts. + */ + dismissAll() { + this.toasts.forEach(toast => { + if (!toast.sticky) { + toast.dismiss(); + } + }); + } + + /** + * Dismiss all sticky toasts. + */ + dismissSticky() { + this.toasts.forEach(toast => { + if (toast.sticky) { + toast.dismiss(); + } + }); + } + + onClick(e) { + const target = closest(e.target, 'tp-toast', true); + if (target && !target.isDismissed) { + target.parentNode.appendChild(target); + setTimeout(() => { + this.updateList(); + }, 50); + } + } + + onToastDismissed(e) { + this.removeChild(e.detail.toast); + this.updateList(); + } + + updateList() { + const toasts = this.toasts; + this.count = toasts.length; + + const hiddenToasts = toasts.slice(0, toasts.length - this.maxVisible); + + for (let i = 0, li = hiddenToasts.length; i < li; ++i) { + hiddenToasts[i].translateY = -150; + hiddenToasts[i].stopDelay(); + } + + const visibleToasts = toasts.slice(Math.max(0, toasts.length - this.maxVisible)); + + for (let i = 0, li = visibleToasts.length; i < li; ++i) { + visibleToasts[i].translateY = 30 * i; + + if (i === li - 1) { + visibleToasts[i].activateDelay(); + } else { + visibleToasts[i].stopDelay(); + } + } + + this.hiddenCount = Math.max(0, this.count - this.maxVisible); + } + + shouldShowMoreLabel(hiddenCount) { + return hiddenCount > 0; + } + + queryMatchChanged(e) { + this.maxVisible = e.detail.value ? 1 : 4; + if (this.isConnected) { + this.updateList(); + } + } +} + +window.customElements.define('tp-toaster', TpToaster);