diff --git a/css/backoffice/components/_all.scss b/css/backoffice/components/_all.scss index efb91bdb1..5237baeba 100644 --- a/css/backoffice/components/_all.scss +++ b/css/backoffice/components/_all.scss @@ -32,4 +32,5 @@ @import "search-form"; @import "field-badge"; @import "file-select"; -@import "medallion-icon"; \ No newline at end of file +@import "medallion-icon"; +@import "toast"; \ No newline at end of file diff --git a/css/backoffice/components/_toast.scss b/css/backoffice/components/_toast.scss new file mode 100644 index 000000000..4f9ddf66a --- /dev/null +++ b/css/backoffice/components/_toast.scss @@ -0,0 +1,62 @@ +/* + * @copyright Copyright (C) 2010-2024 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/* SCSS variables */ +$ibo-toast--padding-y: $ibo-spacing-400 !default; +$ibo-toast--padding-right: $ibo-spacing-300 !default; +$ibo-toast--padding-left: $ibo-spacing-500 !default; +$ibo-toast--border-radius: $ibo-border-radius-300 !default; +$ibo-toast--box-shadow: $ibo-elevation-200 !default; +$ibo-toast--max-width: calc(50% - 20px) !default; + +@keyframes decreaseHighlight { + 0% { + height: 100%; + } + 8%{ + border-radius: 0 0 0 3px; + } + 100% { + height: 0; + } +} + +.ibo-toast { + display: inline-flex; + position: fixed; + align-items: center; + + max-width: $ibo-toast--max-width ; + padding: $ibo-toast--padding-y $ibo-toast--padding-right $ibo-toast--padding-y $ibo-toast--padding-left; + border-radius: $ibo-toast--border-radius; + + box-shadow: $ibo-toast--box-shadow; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + z-index: 2147483647; + &::before { + @include ibo-vertical-highlight; + top: initial; + bottom: 0; + border-radius: $ibo-toast--border-radius 0 0 $ibo-toast--border-radius; + } + &.ibo-is-auto-closeable::before{ + animation: decreaseHighlight 5s linear forwards; + } + &:hover::before { + animation: none; /* Pause animation on hover */ + } + &.ibo-is-error{ + @extend .ibo-alert.ibo-is-danger; + } + &.ibo-is-warning{ + @extend .ibo-alert.ibo-is-warning; + } + &.ibo-is-success{ + @extend .ibo-alert.ibo-is-success; + } + &.ibo-is-information{ + @extend .ibo-alert.ibo-is-information; + } +} \ No newline at end of file diff --git a/css/backoffice/vendors/_all.scss b/css/backoffice/vendors/_all.scss index 81603f943..58e188c19 100644 --- a/css/backoffice/vendors/_all.scss +++ b/css/backoffice/vendors/_all.scss @@ -17,4 +17,5 @@ @import "jquery-treeview"; @import "jquery-blockui"; @import "magnific-popup"; -@import "selectize"; \ No newline at end of file +@import "selectize"; +@import "toastify"; \ No newline at end of file diff --git a/css/backoffice/vendors/_toastify.scss b/css/backoffice/vendors/_toastify.scss new file mode 100644 index 000000000..b3678af4d --- /dev/null +++ b/css/backoffice/vendors/_toastify.scss @@ -0,0 +1,76 @@ +/* + * @copyright Copyright (C) 2010-2024 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +/* SCSS variables */ + +$ibo-vendors-toastify--right--right: $ibo-spacing-500 !default; +$ibo-vendors-toastify--left--left: $ibo-spacing-500 !default; +$ibo-vendors-toastify--top--top: -150px !default; +$ibo-vendors-toastify--bottom--bottom: -150px !default; + +$ibo-vendors-toastify--close--background: transparent !default; +$ibo-vendors-toastify--close--padding: 0 !default; +$ibo-vendors-toastify--close--margin-left: $ibo-spacing-300 !default; + +.toastify.on { + opacity: 1; +} + +.toast-close { + background: $ibo-vendors-toastify--close--background; + border: 0; + color: inherit; + cursor: pointer; + font-family: inherit; + padding: $ibo-vendors-toastify--close--padding; + margin-left: $ibo-vendors-toastify--close--margin-left; +} + +.toastify-right { + right: $ibo-vendors-toastify--right--right; +} + +.toastify-left { + left: $ibo-vendors-toastify--left--left; +} + +.toastify-top { + top: $ibo-vendors-toastify--top--top; +} + +.toastify-bottom { + bottom: $ibo-vendors-toastify--bottom--bottom; +} + +.toastify-rounded { + border-radius: 25px; +} + +.toastify-avatar { + width: 1.5em; + height: 1.5em; + margin: -7px 5px; + border-radius: 2px; +} + +.toastify-center { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + max-width: -moz-fit-content; +} + +@media only screen and (max-width: 360px) { + .toastify-right, .toastify-left { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + } +} + diff --git a/dictionaries/ui/pages/preferences/en.dictionary.itop.preferences.php b/dictionaries/ui/pages/preferences/en.dictionary.itop.preferences.php index 3a552b8ba..6c24e4672 100644 --- a/dictionaries/ui/pages/preferences/en.dictionary.itop.preferences.php +++ b/dictionaries/ui/pages/preferences/en.dictionary.itop.preferences.php @@ -46,6 +46,9 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Preferences:Tabs:Scrollable:Label' => 'Navigation', 'UI:Preferences:Tabs:Scrollable:Classic' => 'Classic', 'UI:Preferences:Tabs:Scrollable:Scrollable' => 'Scrollable', + 'UI:Preferences:General:Toasts' => 'Toast notifications position', + 'UI:Preferences:General:Toasts:Bottom' => 'Bottom', + 'UI:Preferences:General:Toasts:Top' => 'Top', 'UI:Preferences:ChooseAPlaceholder' => 'User placeholder image', 'UI:Preferences:ChooseAPlaceholder+' => 'Choose a placeholder image that will be displayed if the contact linked to your user doesn\'t have one', )); diff --git a/js/pages/backoffice/toolbox.js b/js/pages/backoffice/toolbox.js index 41e8b4936..480793d62 100644 --- a/js/pages/backoffice/toolbox.js +++ b/js/pages/backoffice/toolbox.js @@ -458,6 +458,27 @@ CombodoModal.OpenInformativeModal = function(sMessage, sSeverity, oOptions) { // Open modal CombodoModal.OpenModal(oOptions); } +/** + * @override + * @inheritDoc + */ +CombodoToast.OpenToast = function(sMessage, sSeverity, aOptions) { + aOptions = $.extend({ + text: sMessage, + className: "ibo-toast ibo-is-" + sSeverity, + duration: 6000, + close: true, + gravity: GetUserPreference('toasts_vertical_position', 'bottom'), + position: "right", + stopOnFocus: true, + }, aOptions); + + if(aOptions.duration !== -1){ + aOptions.className += ' ibo-is-auto-closeable'; + } + + Toastify(aOptions).showToast(); +}; // Processing on each pages of the backoffice $(document).ready(function(){ diff --git a/js/utils.js b/js/utils.js index 5de41a519..9e8feeed2 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1574,4 +1574,63 @@ let CombodoModal = { OpenErrorModal: function(sMessage, oOptions) { CombodoModal.OpenInformativeModal(sMessage, CombodoModal.INFORMATIVE_MODAL_SEVERITY_ERROR, oOptions); }, +}; + +/** + * Abstract wrapper to manage toasts in iTop. + * Implementations for the various GUIs may vary but APIs are the same. + * + * @since 3.2.0 + */ +let CombodoToast = { + /** + * Open a standard toast and put the content into it. + * + * @param sMessage {String} Message to be displayed in the toast + * @param sSeverity {String} Severity of the information. Default values are success, information, warning, error. + * @param aOptions {Object} {@see CombodoModal.OpenModal + */ + OpenToast: function(sMessage, sSeverity, aOptions = {}) { + // Meant for overloading + CombodoJSConsole.Debug('CombodoToast.OpenToast not implemented'); + }, + /** + * Open a standard toast for success messages. + * + * @param sMessage {String} Success message to be displayed in the toast + * @param aOptions {Object} {@see CombodoModal.OpenModal + */ + OpenSuccessToast: function(sMessage, aOptions = {}) { + CombodoToast.OpenToast(sMessage, 'success', aOptions); + }, + + /** + * Open a standard toast for information messages. + * + * @param sMessage {String} Information message to be displayed in the toast + * @param aOptions {Object} {@see CombodoModal.OpenModal + */ + OpenInformationToast: function(sMessage, aOptions = {}) { + CombodoToast.OpenToast(sMessage, 'information', aOptions); + }, + + /** + * Open a standard toast for warning messages. + * + * @param sMessage {String} Warning message to be displayed in the toast + * @param aOptions {Object} {@see CombodoModal.OpenModal + */ + OpenWarningToast: function(sMessage, aOptions = {}) { + CombodoToast.OpenToast(sMessage, 'warning', aOptions); + }, + + /** + * Open a standard toast for error messages. + * + * @param sMessage {String} Error message to be displayed in the toast + * @param aOptions {Object} {@see CombodoModal.OpenModal + */ + OpenErrorToast: function(sMessage, aOptions = {}) { + CombodoToast.OpenToast(sMessage, 'error', aOptions); + } }; \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 513ea2659..d319d2387 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,6 +1,6 @@ { - "name": "dev-trunk", - "lockfileVersion": 2, + "name": "iTop", + "lockfileVersion": 3, "requires": true, "packages": { "node_modules/@fontsource/raleway": { @@ -88,6 +88,11 @@ "dependencies": { "@popperjs/core": "^2.4.4" } + }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" } } } diff --git a/node_modules/toastify-js/.gitattributes b/node_modules/toastify-js/.gitattributes new file mode 100644 index 000000000..bdb0cabc8 --- /dev/null +++ b/node_modules/toastify-js/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/node_modules/toastify-js/.prettierrc b/node_modules/toastify-js/.prettierrc new file mode 100644 index 000000000..b909d0a75 --- /dev/null +++ b/node_modules/toastify-js/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": false, + "printWidth": 80, + "tabWidth": 2, + "singleQuote": false, + "trailingComma": "es5", + "parser": "babylon", + "noSemi": false +} diff --git a/node_modules/toastify-js/.travis.yml b/node_modules/toastify-js/.travis.yml new file mode 100644 index 000000000..5b7005dc4 --- /dev/null +++ b/node_modules/toastify-js/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: + - "8" +script: +- echo "skipping tests" +deploy: + skip_cleanup: true + provider: npm + email: $NPM_USERNAME + api_key: $NPM_TOKEN + on: + branch: master \ No newline at end of file diff --git a/node_modules/toastify-js/CHANGELOG.md b/node_modules/toastify-js/CHANGELOG.md new file mode 100644 index 000000000..fcdc1f943 --- /dev/null +++ b/node_modules/toastify-js/CHANGELOG.md @@ -0,0 +1,133 @@ +# Changelog + +All the changes made to toastify-js library. + +## [1.12.0] - 2022-07-21 + +* Accessibility fix: Support aria-live for the toast +* Accessibility fix: Add aria-label for close icon + +## [1.11.2] - 2021-10-06 + +* Bugfix: Style Options: "backgroundColor" not working! (#81) +* Bugfix: "ShadowRoot is undefined" in older browsers (#83) + +## [1.11.1] - 2021-07-15 + +* Bugfix: IE11 support broke since style option #77 + +## [1.11.0] - 2021-04-25 + +* New property `oldestFirst` allows to set the order of adding new toasts to page (#70 and #71) + +## [1.10.0] - 2021-03-25 + +* `selector` now supports a DOM Node, along with ID string ([#65](https://github.com/apvarun/toastify-js/pull/65)) +* New property - `escapeMarkup` - Toggle the default behavior of escaping HTML markup +* New property - `style` - Use the HTML DOM Style properties to add any style directly to toast +* Adds `toastify-es.js`, to be used from node_modules until there are no compatibility issues + +### Deprecations: + +* `backgroundColor` is deprecated. Use `style.background` instead + +## [1.9.3] - 2020-10-10 + +* Offset IE11 compatibility #64 + +## [1.9.2] - 2020-09-24 + +* Bugfix: Max width problem for firefox browser #61 + +## [1.9.1] - 2020-08-13 + +* Bugfix: Avatar positioning based on toast position + +## [1.9.0] - 2020-07-22 + +* Add support for providing toast `offset` +* Updated docs: offset + +## [1.8.0] - 2020-05-29 + +* Add option to provide a node instead of text +* Updated docs: permanent toast duration + +## [1.7.0] - 2020-03-01 + +* To be able to set `stopOnFocus` for toasts without close icon +* Bugfix: `duration` can be infinite by setting as `0` +* Bugfix: Prevent errors when parent node is removed from DOM while using frameworks +* Bugfix: IE 9/10 compatibility fix + +## [1.6.2] - 2020-01-03 + +* Bugfix: Closing the toast when custom close icon from icon fonts are used + +## [1.6.1] - 2019-06-29 + +* Bugfix: Disabling `stopOnFocus` + +## [1.6.0] - 2019-06-29 + +* **Deprecation Warning**: Migrating from `positionLeft` property to `position` +* Property `position` to support `center` as a value along with `left` and `right` - Useful for centering toast messages in the page + +## [1.5.0] - 2019-05-30 + +* Added persistant toast option with ability to programatically close it + +## [1.4.0] - 2019-05-12 + +* **Breaking Change**: Manually import CSS while using as module in your modern JavaScript applications +* Ability to pause the toast dismiss timer on hover (Using `stopOnFocus` property) + +## [1.3.2] - 2018-12-6 + +* Added z-index attribute + +## [1.2.1] - 2018-05-31 + +* Added support for Classes. Now custom classes can be added to the toast while creating it + +## [1.2.0] - 2018-03-05 + +* Fix issue when `destination` and `close` options is used at the same time + +## [1.1.0] - 2018-02-18 + +* Browser support extended to IE10+ without any polyfills + +## [1.0.0] - 2018-02-17 + +* Support for modules + +## [0.0.6] - 2017-09-09 + +* Support for changing background [Options] +* Optimized toast positioning logic +* Added changelog for library update tracking + +## [0.0.5] - 2017-09-06 + +* Support for toast messages on mobile screens +* Tweaked close icon + +## [0.0.4] - 2017-09-05 + +* Support for positioning of toasts on the page + +## [0.0.3] - 2017-09-05 + +* Close buton for toasts [Options] + +## [0.0.2] - 2017-09-04 + +* Option to add on-click link for toasts +* Updated comments for code readability + +## [0.0.1] - 2017-09-02 + +* Initial Release +* Added Preview page +* Optimized function structure diff --git a/node_modules/toastify-js/LICENSE b/node_modules/toastify-js/LICENSE new file mode 100644 index 000000000..506b07264 --- /dev/null +++ b/node_modules/toastify-js/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 apvarun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/toastify-js/README.md b/node_modules/toastify-js/README.md new file mode 100644 index 000000000..19363956f --- /dev/null +++ b/node_modules/toastify-js/README.md @@ -0,0 +1,439 @@ + +# Toastify + +![Built with JavaScript](https://img.shields.io/badge/Built%20with-JavaScript-red?style=for-the-badge&logo=javascript) + +[![toastify-js](https://img.shields.io/badge/toastify--js-1.12.0-brightgreen.svg)](https://www.npmjs.com/package/toastify-js) +![MIT License](https://img.shields.io/npm/l/toastify-js) + +Toastify is a lightweight, vanilla JS toast notification library. + +## Demo + +[Click here](https://apvarun.github.io/toastify-js/) + +## Features + +* Multiple stacked notifications +* Customizable +* No blocking of execution thread + +### Customization options + +* Notification Text +* Duration +* Toast background color +* Close icon display +* Display position +* Offset position + +## Installation + +#### Toastify now supports installation via NPM + +* Run the below command to add toastify-js to your exisitng or new project. + +``` +npm install --save toastify-js +``` + +or + +``` +yarn add toastify-js -S +``` + +* Import toastify-js into your module to start using it. + +``` +import Toastify from 'toastify-js' +``` + +You can use the default CSS from Toastify as below and later override it or choose to write your own CSS. + +``` +import "toastify-js/src/toastify.css" +``` + +#### Adding ToastifyJs to HTML page using the traditional method + +To start using **Toastify**, add the following CSS on to your page. + +```html + +``` + +And the script at the bottom of the page + +```html + +``` + +> Files are delivered via the CDN service provided by [jsdeliver](https://www.jsdelivr.com/) + +## Documentation + +```javascript +Toastify({ + text: "This is a toast", + duration: 3000, + destination: "https://github.com/apvarun/toastify-js", + newWindow: true, + close: true, + gravity: "top", // `top` or `bottom` + position: "left", // `left`, `center` or `right` + stopOnFocus: true, // Prevents dismissing of toast on hover + style: { + background: "linear-gradient(to right, #00b09b, #96c93d)", + }, + onClick: function(){} // Callback after click +}).showToast(); +``` + +> Toast messages will be centered on devices with screen width less than 360px. + +* See the [changelog](https://github.com/apvarun/toastify-js/blob/master/CHANGELOG.md) + +### Add own custom classes + +If you want to use custom classes on the toast for customizing (like info or warning for example), you can do that as follows: + +```javascript +Toastify({ + text: "This is a toast", + className: "info", + style: { + background: "linear-gradient(to right, #00b09b, #96c93d)", + } +}).showToast(); +``` + +Multiple classes also can be assigned as a string, with spaces between class names. + +### Add some offset + +If you want to add offset to the toast, you can do that as follows: + +```javascript +Toastify({ + text: "This is a toast with offset", + offset: { + x: 50, // horizontal axis - can be a number or a string indicating unity. eg: '2em' + y: 10 // vertical axis - can be a number or a string indicating unity. eg: '2em' + }, +}).showToast(); +``` + +Toast will be pushed 50px from right in x axis and 10px from top in y axis. + +**Note:** + +If `position` is equals to `left`, it will be pushed from left. +If `gravity` is equals to `bottom`, it will be pushed from bottom. + +## API + +| Option Key | type | Usage | Defaults | +|-----------------|----------------------|----------------------------------------------------------------------------|-------------| +| text | string | Message to be displayed in the toast | "Hi there!" | +| node | ELEMENT_NODE | Provide a node to be mounted inside the toast. `node` takes higher precedence over `text` | | +| duration | number | Duration for which the toast should be displayed.
-1 for permanent toast | 3000 | +| selector | string \| ELEMENT_NODE | ShadowRoot | CSS Selector or Element Node on which the toast should be added | body | +| destination | URL string | URL to which the browser should be navigated on click of the toast | | +| newWindow | boolean | Decides whether the `destination` should be opened in a new window or not | false | +| close | boolean | To show the close icon or not | false | +| gravity | "top" or "bottom" | To show the toast from top or bottom | "top" | +| position | "left" or "right" | To show the toast on left or right | "right" | +| backgroundColor | CSS background value | To be deprecated, use `style.background` option instead. Sets the background color of the toast | | +| avatar | URL string | Image/icon to be shown before text | | +| className | string | Ability to provide custom class name for further customization | | +| stopOnFocus | boolean | To stop timer when hovered over the toast (Only if duration is set) | true | +| callback | Function | Invoked when the toast is dismissed | | +| onClick | Function | Invoked when the toast is clicked | | +| offset | Object | Ability to add some offset to axis | | +| escapeMarkup | boolean | Toggle the default behavior of escaping HTML markup | true | +| style | object | Use the HTML DOM Style properties to add any style directly to toast | | +| ariaLive | string | Announce the toast to screen readers, see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions for options | "polite" | +| oldestFirst | boolean | Set the order in which toasts are stacked in page | true | + +> Deprecated properties: `backgroundColor` - use `style.background` option instead + +## Browsers support + +| ![][ie]
IE / Edge | ![][firefox]
Firefox | ![][chrome]
Chrome | ![][safari]
Safari | ![][opera]
Opera | +| ---------------------- | ------------------------- | ----------------------- | ----------------------- | --------------------- | +| IE10, IE11, Edge | last 10 versions | last 10 versions | last 10 versions | last 10 versions | + +## Contributors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + AStoker +
+ AStoker +
+
+ + caiomoura1994 +
+ caiomoura1994 +
+
+ + rndevfx +
+ rndevfx +
+
+ + 1ess +
+ 1ess +
+
+ + d4rn0k +
+ d4rn0k +
+
+ + danielkaiser80 +
+ danielkaiser80 +
+
+ + skjnldsv +
+ skjnldsv +
+
+ + chasedeanda +
+ chasedeanda +
+
+ + chrisgraham +
+ chrisgraham +
+
+ + Wachiwi +
+ Wachiwi +
+
+ + FeixuRuins +
+ FeixuRuins +
+
+ + gavinhungry +
+ gavinhungry +
+
+ + haydster7 +
+ haydster7 +
+
+ + joaquinwojcik +
+ joaquinwojcik +
+
+ + juliushaertl +
+ juliushaertl +
+
+ + mort3za +
+ mort3za +
+
+ + Sandip124 +
+ Sandip124 +
+
+ + Tadaz +
+ Tadaz +
+
+ + t12ung +
+ t12ung +
+
+ + victorfeijo +
+ victorfeijo +
+
+ + fiatjaf +
+ fiatjaf +
+
+ + prousseau-korem +
+ prousseau-korem +
+
+ + + + +## License + +MIT © [Varun A P](https://github.com/apvarun) + +Buy Me A Coffee + +[ie]: https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/edge.png +[firefox]: https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/firefox.png +[chrome]: https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/chrome.png +[safari]: https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/safari.png +[opera]: https://raw.githubusercontent.com/godban/browsers-support-badges/master/src/images/opera.png + diff --git a/node_modules/toastify-js/example/pattern.png b/node_modules/toastify-js/example/pattern.png new file mode 100644 index 000000000..310e8868d Binary files /dev/null and b/node_modules/toastify-js/example/pattern.png differ diff --git a/node_modules/toastify-js/example/script.css b/node_modules/toastify-js/example/script.css new file mode 100644 index 000000000..8de04f531 --- /dev/null +++ b/node_modules/toastify-js/example/script.css @@ -0,0 +1,95 @@ +* { + box-sizing: border-box; +} + +html, body { + height: 100%; +} + +body { + font-family: Helvetica, Arial, sans-serif; + background-size: 100%; + margin: 0; + padding: 0; + color: #424242; +} + +.container { + overflow: hidden; + display: flex; + height: 100%; + justify-content: center; + flex-direction: column; + align-items: center; + background-color: whitesmoke; + background-image: url("./pattern.png"); +} + +.hidden { + display: none; +} + +.docs { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + background-color: white; + border: 1px solid #e3e3e3; + padding: 20px 20px; + width: 60%; + border-radius: 4px; +} + +.docs h2 { + margin-top: 0px; +} + +code p { + margin: 2px; +} + +.pad-left { + padding-left: 20px; +} + +.buttons { + margin: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.button { + overflow: hidden; + margin: 10px; + padding: 12px 12px; + cursor: pointer; + -webkit-transition: all 200ms ease-in-out; + transition: all 200ms ease-in-out; + text-align: center; + white-space: nowrap; + text-decoration: none; + text-transform: none; + text-transform: capitalize; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + line-height: 1.3; + min-width: 100px; + display: inline-block; + box-shadow: 0 5px 20px rgba(22, 22, 22, 0.15); + color: #5477f5; + background-color: Snow; + border: 1px solid #5477f5; +} + +.button:hover { + color: #FFFFFF; + background: linear-gradient(135deg, #73a5ff, #5477f5); + border: 1px solid transparent; +} + +.repo { + margin: 10px; +} \ No newline at end of file diff --git a/node_modules/toastify-js/example/script.js b/node_modules/toastify-js/example/script.js new file mode 100644 index 000000000..8d290ff9c --- /dev/null +++ b/node_modules/toastify-js/example/script.js @@ -0,0 +1,72 @@ +var bgColors = [ + "linear-gradient(to right, #00b09b, #96c93d)", + "linear-gradient(to right, #ff5f6d, #ffc371)", + ], + i = 0; + +Toastify({ + text: "Hi", + duration: 4500, + destination: "https://github.com/apvarun/toastify-js", + newWindow: true, + gravity: "top", + position: 'left', +}).showToast(); + +setTimeout(function() { + Toastify({ + text: "Simple JavaScript Toasts", + gravity: "top", + position: 'center', + style: { + background: '#0f3443' + } + }).showToast(); +}, 1000); + +// Options for the toast +var options = { + text: "Happy toasting!", + duration: 2500, + callback: function() { + console.log("Toast hidden"); + Toastify.reposition(); + }, + close: true, + style: { + background: "linear-gradient(to right, #00b09b, #96c93d)", + } +}; + +// Initializing the toast +var myToast = Toastify(options); + +// Toast after delay +setTimeout(function() { + myToast.showToast(); +}, 4500); + +setTimeout(function() { + Toastify({ + text: "Highly customizable", + gravity: "bottom", + position: 'left', + close: true, + style: { + background: "linear-gradient(to right, #ff5f6d, #ffc371)", + } + }).showToast(); +}, 3000); + +// Displaying toast on manual action `Try` +document.getElementById("new-toast").addEventListener("click", function() { + Toastify({ + text: "I am a toast", + duration: 3000, + close: i % 3 ? true : false, + style: { + background: bgColors[i % 2], + } + }).showToast(); + i++; +}); diff --git a/node_modules/toastify-js/index.html b/node_modules/toastify-js/index.html new file mode 100644 index 000000000..9d58b9b01 --- /dev/null +++ b/node_modules/toastify-js/index.html @@ -0,0 +1,59 @@ + + + + + + + + Toastify JS - Pure JavaScript Toast Notificaton Library + + + + + + + + +
+
+

Toastify JS

+
+

Better notification messages

+
+ Try + Docs + Tweet +
+
+

Usage

+ +

Toastify({

+

text: "This is a toast",

+

duration: 3000

+

}).showToast();

+
+
+
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/node_modules/toastify-js/package.json b/node_modules/toastify-js/package.json new file mode 100644 index 000000000..52dcf261b --- /dev/null +++ b/node_modules/toastify-js/package.json @@ -0,0 +1,22 @@ +{ + "name": "toastify-js", + "version": "1.12.0", + "description": "Toastify is a lightweight, vanilla JS toast notification library.", + "main": "./src/toastify.js", + "repository": { + "type": "git", + "url": "git+https://github.com/apvarun/toastify-js.git" + }, + "keywords": [ + "toastify", + "javascript", + "notifications", + "toast" + ], + "author": "Varun A P", + "license": "MIT", + "bugs": { + "url": "https://github.com/apvarun/toastify-js/issues" + }, + "homepage": "https://github.com/apvarun/toastify-js#readme" +} diff --git a/node_modules/toastify-js/src/toastify-es.js b/node_modules/toastify-js/src/toastify-es.js new file mode 100644 index 000000000..784cf59be --- /dev/null +++ b/node_modules/toastify-js/src/toastify-es.js @@ -0,0 +1,466 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ + +/** + * Options used for Toastify + * @typedef {Object} ToastifyConfigurationObject + * @property {string} text - Message to be displayed in the toast + * @property {Element} node - Provide a node to be mounted inside the toast. node takes higher precedence over text + * @property {number} duration - Duration for which the toast should be displayed. -1 for permanent toast + * @property {string|Element} selector - CSS ID Selector on which the toast should be added + * @property {url} destination - URL to which the browser should be navigated on click of the toast + * @property {boolean} newWindow - Decides whether the destination should be opened in a new window or not + * @property {boolean} close - To show the close icon or not + * @property {string} gravity - To show the toast from top or bottom + * @property {string} position - To show the toast on left or right + * @property {string} backgroundColor - Deprecated: Sets the background color of the toast + * @property {url} avatar - Image/icon to be shown before text + * @property {string} className - Ability to provide custom class name for further customization + * @property {boolean} stopOnFocus - To stop timer when hovered over the toast (Only if duration is set) + * @property {Function} callback - Invoked when the toast is dismissed + * @property {Function} onClick - Invoked when the toast is clicked + * @property {Object} offset - Ability to add some offset to axis + * @property {boolean} escapeMarkup - Toggle the default behavior of escaping HTML markup + * @property {string} ariaLive - Use the HTML DOM style property to add styles to toast + * @property {Object} style - Use the HTML DOM style property to add styles to toast + */ + + +class Toastify { + + defaults = { + oldestFirst: true, + text: "Toastify is awesome!", + node: undefined, + duration: 3000, + selector: undefined, + callback: function() {}, + destination: undefined, + newWindow: false, + close: false, + gravity: "toastify-top", + positionLeft: false, + position: "", + backgroundColor: "", + avatar: "", + className: "", + stopOnFocus: true, + onClick: function() {}, + offset: { x: 0, y: 0 }, + escapeMarkup: true, + ariaLive: "polite", + style: { background: "" }, + }; + + constructor(options) { + /** + * The version of Toastify + * @type {string} + * @public + */ + this.version = "1.12.0"; + + /** + * The configuration object to configure Toastify + * @type {ToastifyConfigurationObject} + * @public + */ + this.options = {}; + + /** + * The element that is the Toast + * @type {Element} + * @public + */ + this.toastElement = null; + + /** + * The root element that contains all the toasts + * @type {Element} + * @private + */ + this._rootElement = document.body; + + this._init(options); + } + + /** + * Display the toast + * @public + */ + showToast() { + // Creating the DOM object for the toast + this.toastElement = this._buildToast(); + + // Getting the root element to with the toast needs to be added + if (typeof this.options.selector === "string") { + this._rootElement = document.getElementById(this.options.selector); + } else if (this.options.selector instanceof HTMLElement || this.options.selector instanceof ShadowRoot) { + this._rootElement = this.options.selector; + } else { + this._rootElement = document.body; + } + + // Validating if root element is present in DOM + if (!this._rootElement) { + throw "Root element is not defined"; + } + + // Adding the DOM element + this._rootElement.insertBefore(this.toastElement, this._rootElement.firstChild); + + // Repositioning the toasts in case multiple toasts are present + this._reposition(); + + if (this.options.duration > 0) { + this.toastElement.timeOutValue = window.setTimeout( + () => { + // Remove the toast from DOM + this._removeElement(this.toastElement); + }, + this.options.duration + ); // Binding `this` for function invocation + } + + // Supporting function chaining + return this; + } + + /** + * Hide the toast + * @public + */ + hideToast() { + if (this.toastElement.timeOutValue) { + clearTimeout(this.toastElement.timeOutValue); + } + this._removeElement(this.toastElement); + } + + /** + * Init the Toastify class + * @param {ToastifyConfigurationObject} options - The configuration object to configure Toastify + * @param {string} [options.text=Hi there!] - Message to be displayed in the toast + * @param {Element} [options.node] - Provide a node to be mounted inside the toast. node takes higher precedence over text + * @param {number} [options.duration=3000] - Duration for which the toast should be displayed. -1 for permanent toast + * @param {string} [options.selector] - CSS Selector on which the toast should be added + * @param {url} [options.destination] - URL to which the browser should be navigated on click of the toast + * @param {boolean} [options.newWindow=false] - Decides whether the destination should be opened in a new window or not + * @param {boolean} [options.close=false] - To show the close icon or not + * @param {string} [options.gravity=toastify-top] - To show the toast from top or bottom + * @param {string} [options.position=right] - To show the toast on left or right + * @param {string} [options.backgroundColor] - Sets the background color of the toast (To be deprecated) + * @param {url} [options.avatar] - Image/icon to be shown before text + * @param {string} [options.className] - Ability to provide custom class name for further customization + * @param {boolean} [options.stopOnFocus] - To stop timer when hovered over the toast (Only if duration is set) + * @param {Function} [options.callback] - Invoked when the toast is dismissed + * @param {Function} [options.onClick] - Invoked when the toast is clicked + * @param {Object} [options.offset] - Ability to add some offset to axis + * @param {boolean} [options.escapeMarkup=true] - Toggle the default behavior of escaping HTML markup + * @param {string} [options.ariaLive] - Announce the toast to screen readers + * @param {Object} [options.style] - Use the HTML DOM style property to add styles to toast + * @private + */ + _init(options) { + + // Setting defaults + this.options = Object.assign(this.defaults, options); + + if (this.options.backgroundColor) { + // This is being deprecated in favor of using the style HTML DOM property + console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'); + } + + this.toastElement = null; + + this.options.gravity = options.gravity === "bottom" ? "toastify-bottom" : "toastify-top"; // toast position - top or bottom + this.options.stopOnFocus = options.stopOnFocus === undefined ? true : options.stopOnFocus; // stop timeout on focus + if(options.backgroundColor) { + this.options.style.background = options.backgroundColor; + } + } + + /** + * Build the Toastify element + * @returns {Element} + * @private + */ + _buildToast() { + // Validating if the options are defined + if (!this.options) { + throw "Toastify is not initialized"; + } + + // Creating the DOM object + let divElement = document.createElement("div"); + divElement.className = `toastify on ${this.options.className}`; + + // Positioning toast to left or right or center (default right) + divElement.className += ` toastify-${this.options.position}`; + + // Assigning gravity of element + divElement.className += ` ${this.options.gravity}`; + + // Loop through our style object and apply styles to divElement + for (const property in this.options.style) { + divElement.style[property] = this.options.style[property]; + } + + // Announce the toast to screen readers + if (this.options.ariaLive) { + divElement.setAttribute('aria-live', this.options.ariaLive) + } + + // Adding the toast message/node + if (this.options.node && this.options.node.nodeType === Node.ELEMENT_NODE) { + // If we have a valid node, we insert it + divElement.appendChild(this.options.node) + } else { + if (this.options.escapeMarkup) { + divElement.innerText = this.options.text; + } else { + divElement.innerHTML = this.options.text; + } + + if (this.options.avatar !== "") { + let avatarElement = document.createElement("img"); + avatarElement.src = this.options.avatar; + + avatarElement.className = "toastify-avatar"; + + if (this.options.position == "left") { + // Adding close icon on the left of content + divElement.appendChild(avatarElement); + } else { + // Adding close icon on the right of content + divElement.insertAdjacentElement("afterbegin", avatarElement); + } + } + } + + // Adding a close icon to the toast + if (this.options.close === true) { + // Create a span for close element + let closeElement = document.createElement("button"); + closeElement.type = "button"; + closeElement.setAttribute("aria-label", "Close"); + closeElement.className = "toast-close"; + closeElement.innerHTML = "✖"; + + // Triggering the removal of toast from DOM on close click + closeElement.addEventListener( + "click", + (event) => { + event.stopPropagation(); + this._removeElement(this.toastElement); + window.clearTimeout(this.toastElement.timeOutValue); + } + ); + + //Calculating screen width + const width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Adding the close icon to the toast element + // Display on the right if screen width is less than or equal to 360px + if ((this.options.position == "left") && width > 360) { + // Adding close icon on the left of content + divElement.insertAdjacentElement("afterbegin", closeElement); + } else { + // Adding close icon on the right of content + divElement.appendChild(closeElement); + } + } + + // Clear timeout while toast is focused + if (this.options.stopOnFocus && this.options.duration > 0) { + // stop countdown + divElement.addEventListener( + "mouseover", + (event) => { + window.clearTimeout(divElement.timeOutValue); + } + ) + // add back the timeout + divElement.addEventListener( + "mouseleave", + () => { + divElement.timeOutValue = window.setTimeout( + () => { + // Remove the toast from DOM + this._removeElement(divElement); + }, + this.options.duration + ) + } + ) + } + + // Adding an on-click destination path + if (typeof this.options.destination !== "undefined") { + divElement.addEventListener( + "click", + (event) => { + event.stopPropagation(); + if (this.options.newWindow === true) { + window.open(this.options.destination, "_blank"); + } else { + window.location = this.options.destination; + } + } + ); + } + + if (typeof this.options.onClick === "function" && typeof this.options.destination === "undefined") { + divElement.addEventListener( + "click", + (event) => { + event.stopPropagation(); + this.options.onClick(); + } + ); + } + + // Adding offset + if (typeof this.options.offset === "object") { + + const x = this._getAxisOffsetAValue("x", this.options); + const y = this._getAxisOffsetAValue("y", this.options); + + const xOffset = this.options.position == "left" ? x : `-${x}`; + const yOffset = this.options.gravity == "toastify-top" ? y : `-${y}`; + + divElement.style.transform = `translate(${xOffset},${yOffset})`; + + } + + // Returning the generated element + return divElement; + } + + /** + * Remove the toast from the DOM + * @param {Element} toastElement + */ + _removeElement(toastElement) { + // Hiding the element + toastElement.className = toastElement.className.replace(" on", ""); + + // Removing the element from DOM after transition end + window.setTimeout( + () => { + // remove options node if any + if (this.options.node && this.options.node.parentNode) { + this.options.node.parentNode.removeChild(this.options.node); + } + + // Remove the element from the DOM, only when the parent node was not removed before. + if (toastElement.parentNode) { + toastElement.parentNode.removeChild(toastElement); + } + + // Calling the callback function + this.options.callback.call(toastElement); + + // Repositioning the toasts again + this._reposition(); + }, + 400 + ); // Binding `this` for function invocation + } + + /** + * Position the toast on the DOM + * @private + */ + _reposition() { + + // Top margins with gravity + let topLeftOffsetSize = { + top: 15, + bottom: 15, + }; + let topRightOffsetSize = { + top: 15, + bottom: 15, + }; + let offsetSize = { + top: 15, + bottom: 15, + }; + + // Get all toast messages that have been added to the container (selector) + let allToasts = this._rootElement.querySelectorAll(".toastify"); + + let classUsed; + + // Modifying the position of each toast element + for (let i = 0; i < allToasts.length; i++) { + // Getting the applied gravity + if (allToasts[i].classList.contains("toastify-top") === true) { + classUsed = "toastify-top"; + } else { + classUsed = "toastify-bottom"; + } + + let height = allToasts[i].offsetHeight; + classUsed = classUsed.substr(9, classUsed.length - 1) + // Spacing between toasts + let offset = 15; + + let width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Show toast in center if screen with less than or equal to 360px + if (width <= 360) { + // Setting the position + allToasts[i].style[classUsed] = `${offsetSize[classUsed]}px`; + + offsetSize[classUsed] += height + offset; + } else { + if (allToasts[i].classList.contains("toastify-left") === true) { + // Setting the position + allToasts[i].style[classUsed] = `${topLeftOffsetSize[classUsed]}px`; + + topLeftOffsetSize[classUsed] += height + offset; + } else { + // Setting the position + allToasts[i].style[classUsed] = `${topRightOffsetSize[classUsed]}px`; + + topRightOffsetSize[classUsed] += height + offset; + } + } + } + } + + /** + * Helper function to get offset + * @param {string} axis - 'x' or 'y' + * @param {ToastifyConfigurationObject} options - The options object containing the offset object + */ + _getAxisOffsetAValue(axis, options) { + + if (options.offset[axis]) { + if (isNaN(options.offset[axis])) { + return options.offset[axis]; + } else { + return `${options.offset[axis]}px`; + } + } + + return '0px'; + + } + + } + + + // Returning the Toastify function to be assigned to the window object/module + function StartToastifyInstance(options) { + return new Toastify(options); + } + + export default StartToastifyInstance; diff --git a/node_modules/toastify-js/src/toastify.css b/node_modules/toastify-js/src/toastify.css new file mode 100644 index 000000000..ccd65c835 --- /dev/null +++ b/node_modules/toastify-js/src/toastify.css @@ -0,0 +1,85 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ + +.toastify { + padding: 12px 20px; + color: #ffffff; + display: inline-block; + box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3); + background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5); + background: linear-gradient(135deg, #73a5ff, #5477f5); + position: fixed; + opacity: 0; + transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1); + border-radius: 2px; + cursor: pointer; + text-decoration: none; + max-width: calc(50% - 20px); + z-index: 2147483647; +} + +.toastify.on { + opacity: 1; +} + +.toast-close { + background: transparent; + border: 0; + color: white; + cursor: pointer; + font-family: inherit; + font-size: 1em; + opacity: 0.4; + padding: 0 5px; +} + +.toastify-right { + right: 15px; +} + +.toastify-left { + left: 15px; +} + +.toastify-top { + top: -150px; +} + +.toastify-bottom { + bottom: -150px; +} + +.toastify-rounded { + border-radius: 25px; +} + +.toastify-avatar { + width: 1.5em; + height: 1.5em; + margin: -7px 5px; + border-radius: 2px; +} + +.toastify-center { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + max-width: -moz-fit-content; +} + +@media only screen and (max-width: 360px) { + .toastify-right, .toastify-left { + margin-left: auto; + margin-right: auto; + left: 0; + right: 0; + max-width: fit-content; + } +} diff --git a/node_modules/toastify-js/src/toastify.js b/node_modules/toastify-js/src/toastify.js new file mode 100644 index 000000000..5d9659cd3 --- /dev/null +++ b/node_modules/toastify-js/src/toastify.js @@ -0,0 +1,445 @@ +/*! + * Toastify js 1.12.0 + * https://github.com/apvarun/toastify-js + * @license MIT licensed + * + * Copyright (C) 2018 Varun A P + */ +(function(root, factory) { + if (typeof module === "object" && module.exports) { + module.exports = factory(); + } else { + root.Toastify = factory(); + } +})(this, function(global) { + // Object initialization + var Toastify = function(options) { + // Returning a new init object + return new Toastify.lib.init(options); + }, + // Library version + version = "1.12.0"; + + // Set the default global options + Toastify.defaults = { + oldestFirst: true, + text: "Toastify is awesome!", + node: undefined, + duration: 3000, + selector: undefined, + callback: function () { + }, + destination: undefined, + newWindow: false, + close: false, + gravity: "toastify-top", + positionLeft: false, + position: '', + backgroundColor: '', + avatar: "", + className: "", + stopOnFocus: true, + onClick: function () { + }, + offset: {x: 0, y: 0}, + escapeMarkup: true, + ariaLive: 'polite', + style: {background: ''} + }; + + // Defining the prototype of the object + Toastify.lib = Toastify.prototype = { + toastify: version, + + constructor: Toastify, + + // Initializing the object with required parameters + init: function(options) { + // Verifying and validating the input object + if (!options) { + options = {}; + } + + // Creating the options object + this.options = {}; + + this.toastElement = null; + + // Validating the options + this.options.text = options.text || Toastify.defaults.text; // Display message + this.options.node = options.node || Toastify.defaults.node; // Display content as node + this.options.duration = options.duration === 0 ? 0 : options.duration || Toastify.defaults.duration; // Display duration + this.options.selector = options.selector || Toastify.defaults.selector; // Parent selector + this.options.callback = options.callback || Toastify.defaults.callback; // Callback after display + this.options.destination = options.destination || Toastify.defaults.destination; // On-click destination + this.options.newWindow = options.newWindow || Toastify.defaults.newWindow; // Open destination in new window + this.options.close = options.close || Toastify.defaults.close; // Show toast close icon + this.options.gravity = options.gravity === "bottom" ? "toastify-bottom" : Toastify.defaults.gravity; // toast position - top or bottom + this.options.positionLeft = options.positionLeft || Toastify.defaults.positionLeft; // toast position - left or right + this.options.position = options.position || Toastify.defaults.position; // toast position - left or right + this.options.backgroundColor = options.backgroundColor || Toastify.defaults.backgroundColor; // toast background color + this.options.avatar = options.avatar || Toastify.defaults.avatar; // img element src - url or a path + this.options.className = options.className || Toastify.defaults.className; // additional class names for the toast + this.options.stopOnFocus = options.stopOnFocus === undefined ? Toastify.defaults.stopOnFocus : options.stopOnFocus; // stop timeout on focus + this.options.onClick = options.onClick || Toastify.defaults.onClick; // Callback after click + this.options.offset = options.offset || Toastify.defaults.offset; // toast offset + this.options.escapeMarkup = options.escapeMarkup !== undefined ? options.escapeMarkup : Toastify.defaults.escapeMarkup; + this.options.ariaLive = options.ariaLive || Toastify.defaults.ariaLive; + this.options.style = options.style || Toastify.defaults.style; + if(options.backgroundColor) { + this.options.style.background = options.backgroundColor; + } + + // Returning the current object for chaining functions + return this; + }, + + // Building the DOM element + buildToast: function() { + // Validating if the options are defined + if (!this.options) { + throw "Toastify is not initialized"; + } + + // Creating the DOM object + var divElement = document.createElement("div"); + divElement.className = "toastify on " + this.options.className; + + // Positioning toast to left or right or center + if (!!this.options.position) { + divElement.className += " toastify-" + this.options.position; + } else { + // To be depreciated in further versions + if (this.options.positionLeft === true) { + divElement.className += " toastify-left"; + console.warn('Property `positionLeft` will be depreciated in further versions. Please use `position` instead.') + } else { + // Default position + divElement.className += " toastify-right"; + } + } + + // Assigning gravity of element + divElement.className += " " + this.options.gravity; + + if (this.options.backgroundColor) { + // This is being deprecated in favor of using the style HTML DOM property + console.warn('DEPRECATION NOTICE: "backgroundColor" is being deprecated. Please use the "style.background" property.'); + } + + // Loop through our style object and apply styles to divElement + for (var property in this.options.style) { + divElement.style[property] = this.options.style[property]; + } + + // Announce the toast to screen readers + if (this.options.ariaLive) { + divElement.setAttribute('aria-live', this.options.ariaLive) + } + + // Adding the toast message/node + if (this.options.node && this.options.node.nodeType === Node.ELEMENT_NODE) { + // If we have a valid node, we insert it + divElement.appendChild(this.options.node) + } else { + if (this.options.escapeMarkup) { + divElement.innerText = this.options.text; + } else { + divElement.innerHTML = this.options.text; + } + + if (this.options.avatar !== "") { + var avatarElement = document.createElement("img"); + avatarElement.src = this.options.avatar; + + avatarElement.className = "toastify-avatar"; + + if (this.options.position == "left" || this.options.positionLeft === true) { + // Adding close icon on the left of content + divElement.appendChild(avatarElement); + } else { + // Adding close icon on the right of content + divElement.insertAdjacentElement("afterbegin", avatarElement); + } + } + } + + // Adding a close icon to the toast + if (this.options.close === true) { + // Create a span for close element + var closeElement = document.createElement("button"); + closeElement.type = "button"; + closeElement.setAttribute("aria-label", "Close"); + closeElement.className = "toast-close"; + closeElement.innerHTML = "✖"; + + // Triggering the removal of toast from DOM on close click + closeElement.addEventListener( + "click", + function(event) { + event.stopPropagation(); + this.removeElement(this.toastElement); + window.clearTimeout(this.toastElement.timeOutValue); + }.bind(this) + ); + + //Calculating screen width + var width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Adding the close icon to the toast element + // Display on the right if screen width is less than or equal to 360px + if ((this.options.position == "left" || this.options.positionLeft === true) && width > 360) { + // Adding close icon on the left of content + divElement.insertAdjacentElement("afterbegin", closeElement); + } else { + // Adding close icon on the right of content + divElement.appendChild(closeElement); + } + } + + // Clear timeout while toast is focused + if (this.options.stopOnFocus && this.options.duration > 0) { + var self = this; + // stop countdown + divElement.addEventListener( + "mouseover", + function(event) { + window.clearTimeout(divElement.timeOutValue); + } + ) + // add back the timeout + divElement.addEventListener( + "mouseleave", + function() { + divElement.timeOutValue = window.setTimeout( + function() { + // Remove the toast from DOM + self.removeElement(divElement); + }, + self.options.duration + ) + } + ) + } + + // Adding an on-click destination path + if (typeof this.options.destination !== "undefined") { + divElement.addEventListener( + "click", + function(event) { + event.stopPropagation(); + if (this.options.newWindow === true) { + window.open(this.options.destination, "_blank"); + } else { + window.location = this.options.destination; + } + }.bind(this) + ); + } + + if (typeof this.options.onClick === "function" && typeof this.options.destination === "undefined") { + divElement.addEventListener( + "click", + function(event) { + event.stopPropagation(); + this.options.onClick(); + }.bind(this) + ); + } + + // Adding offset + if(typeof this.options.offset === "object") { + + var x = getAxisOffsetAValue("x", this.options); + var y = getAxisOffsetAValue("y", this.options); + + var xOffset = this.options.position == "left" ? x : "-" + x; + var yOffset = this.options.gravity == "toastify-top" ? y : "-" + y; + + divElement.style.transform = "translate(" + xOffset + "," + yOffset + ")"; + + } + + // Returning the generated element + return divElement; + }, + + // Displaying the toast + showToast: function() { + // Creating the DOM object for the toast + this.toastElement = this.buildToast(); + + // Getting the root element to with the toast needs to be added + var rootElement; + if (typeof this.options.selector === "string") { + rootElement = document.getElementById(this.options.selector); + } else if (this.options.selector instanceof HTMLElement || (typeof ShadowRoot !== 'undefined' && this.options.selector instanceof ShadowRoot)) { + rootElement = this.options.selector; + } else { + rootElement = document.body; + } + + // Validating if root element is present in DOM + if (!rootElement) { + throw "Root element is not defined"; + } + + // Adding the DOM element + var elementToInsert = Toastify.defaults.oldestFirst ? rootElement.firstChild : rootElement.lastChild; + rootElement.insertBefore(this.toastElement, elementToInsert); + + // Repositioning the toasts in case multiple toasts are present + Toastify.reposition(); + + if (this.options.duration > 0) { + this.toastElement.timeOutValue = window.setTimeout( + function() { + // Remove the toast from DOM + this.removeElement(this.toastElement); + }.bind(this), + this.options.duration + ); // Binding `this` for function invocation + } + + // Supporting function chaining + return this; + }, + + hideToast: function() { + if (this.toastElement.timeOutValue) { + clearTimeout(this.toastElement.timeOutValue); + } + this.removeElement(this.toastElement); + }, + + // Removing the element from the DOM + removeElement: function(toastElement) { + // Hiding the element + // toastElement.classList.remove("on"); + toastElement.className = toastElement.className.replace(" on", ""); + + // Removing the element from DOM after transition end + window.setTimeout( + function() { + // remove options node if any + if (this.options.node && this.options.node.parentNode) { + this.options.node.parentNode.removeChild(this.options.node); + } + + // Remove the element from the DOM, only when the parent node was not removed before. + if (toastElement.parentNode) { + toastElement.parentNode.removeChild(toastElement); + } + + // Calling the callback function + this.options.callback.call(toastElement); + + // Repositioning the toasts again + Toastify.reposition(); + }.bind(this), + 400 + ); // Binding `this` for function invocation + }, + }; + + // Positioning the toasts on the DOM + Toastify.reposition = function() { + + // Top margins with gravity + var topLeftOffsetSize = { + top: 15, + bottom: 15, + }; + var topRightOffsetSize = { + top: 15, + bottom: 15, + }; + var offsetSize = { + top: 15, + bottom: 15, + }; + + // Get all toast messages on the DOM + var allToasts = document.getElementsByClassName("toastify"); + + var classUsed; + + // Modifying the position of each toast element + for (var i = 0; i < allToasts.length; i++) { + // Getting the applied gravity + if (containsClass(allToasts[i], "toastify-top") === true) { + classUsed = "toastify-top"; + } else { + classUsed = "toastify-bottom"; + } + + var height = allToasts[i].offsetHeight; + classUsed = classUsed.substr(9, classUsed.length-1) + // Spacing between toasts + var offset = 15; + + var width = window.innerWidth > 0 ? window.innerWidth : screen.width; + + // Show toast in center if screen with less than or equal to 360px + if (width <= 360) { + // Setting the position + allToasts[i].style[classUsed] = offsetSize[classUsed] + "px"; + + offsetSize[classUsed] += height + offset; + } else { + if (containsClass(allToasts[i], "toastify-left") === true) { + // Setting the position + allToasts[i].style[classUsed] = topLeftOffsetSize[classUsed] + "px"; + + topLeftOffsetSize[classUsed] += height + offset; + } else { + // Setting the position + allToasts[i].style[classUsed] = topRightOffsetSize[classUsed] + "px"; + + topRightOffsetSize[classUsed] += height + offset; + } + } + } + + // Supporting function chaining + return this; + }; + + // Helper function to get offset. + function getAxisOffsetAValue(axis, options) { + + if(options.offset[axis]) { + if(isNaN(options.offset[axis])) { + return options.offset[axis]; + } + else { + return options.offset[axis] + 'px'; + } + } + + return '0px'; + + } + + function containsClass(elem, yourClass) { + if (!elem || typeof yourClass !== "string") { + return false; + } else if ( + elem.className && + elem.className + .trim() + .split(/\s+/gi) + .indexOf(yourClass) > -1 + ) { + return true; + } else { + return false; + } + } + + // Setting up the prototype for the init object + Toastify.lib.init.prototype = Toastify.lib; + + // Returning the Toastify function to be assigned to the window object/module + return Toastify; +}); diff --git a/package-lock.json b/package-lock.json index 64a3eef24..3890d3a15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "dev-trunk", + "name": "iTop", "lockfileVersion": 2, "requires": true, "packages": { @@ -15,7 +15,8 @@ "datatables.net-select": "^1.2.0", "scrollmagic": "^2.0.8", "selectize-plugin-a11y": "^1.1.0", - "tippy.js": "^6.2.5" + "tippy.js": "^6.2.5", + "toastify-js": "^1.12.0" } }, "node_modules/@fontsource/raleway": { @@ -103,6 +104,11 @@ "dependencies": { "@popperjs/core": "^2.4.4" } + }, + "node_modules/toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" } }, "dependencies": { @@ -181,6 +187,11 @@ "requires": { "@popperjs/core": "^2.4.4" } + }, + "toastify-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", + "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" } } } diff --git a/package.json b/package.json index 3c1107b07..417b4e217 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "datatables.net-select": "^1.2.0", "scrollmagic": "^2.0.8", "selectize-plugin-a11y": "^1.1.0", - "tippy.js": "^6.2.5" + "tippy.js": "^6.2.5", + "toastify-js": "^1.12.0" } } diff --git a/pages/preferences.php b/pages/preferences.php index 4dce26961..5a24a2bb5 100644 --- a/pages/preferences.php +++ b/pages/preferences.php @@ -115,6 +115,7 @@ function DisplayPreferences($oP) $oSecondColumn->AddSubBlock($oMiscOptionsFieldset); $oMiscOptionsFieldset->AddSubBlock(GetObsoleteDataFieldBlock()); $oMiscOptionsFieldset->AddSubBlock(GetSummaryCardsFieldBlock()); + $oMiscOptionsFieldset->AddSubBlock(GetToastsPositionFieldBlock()); $oP->add_script( <<AddSubBlock(SelectOptionUIBlockFactory::MakeForSelectOption("bottom", Dict::S('UI:Preferences:General:Toasts:Bottom'), $sPosition === "bottom")); + $oSelect->AddSubBlock(SelectOptionUIBlockFactory::MakeForSelectOption("top", Dict::S('UI:Preferences:General:Toasts:Top'), $sPosition === "top")); + + return $oSelect; +} + ///////////////////////////////////////////////////////////////////////////// // // Main program @@ -805,6 +825,12 @@ try { $bShowSummaryCards = (bool)utils::ReadParam('show_summary_cards', 0); appUserPreferences::SetPref('show_summary_cards', $bShowSummaryCards); + // - Toast notifications + $sToastsVerticalPosition = utils::ReadParam('toasts_vertical_position', "bottom"); + if(utils::IsNotNullOrEmptyString($sToastsVerticalPosition) && in_array($sToastsVerticalPosition, ["bottom", "top"], true)) { + appUserPreferences::SetPref('toasts_vertical_position', $sToastsVerticalPosition); + } + // Redirect to force a reload/display of the page in case language has been changed $oAppContext = new ApplicationContext(); $sURL = utils::GetAbsoluteUrlAppRoot().'pages/preferences.php?'.$oAppContext->GetForLink(); diff --git a/sources/Application/WebPage/iTopWebPage.php b/sources/Application/WebPage/iTopWebPage.php index 3be89d4f6..6a3a10f82 100644 --- a/sources/Application/WebPage/iTopWebPage.php +++ b/sources/Application/WebPage/iTopWebPage.php @@ -198,6 +198,9 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage // Tooltips $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'node_modules/@popperjs/core/dist/umd/popper.min.js'); $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'node_modules/tippy.js/dist/tippy-bundle.umd.min.js'); + + // Toasts + $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'node_modules/toastify-js/src/toastify.js'); // Keyboard shortcuts $this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/mousetrap/mousetrap.min.js');