├── .editorconfig
├── .eslintrc.cjs
├── .github
├── FUNDING.yml
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── public
└── favicon.ico
├── src
├── App.vue
├── app-components
│ ├── AppInput.vue
│ ├── AppSelect.vue
│ ├── AppStatusDisplay.vue
│ ├── AppTextarea.vue
│ ├── AppThemeToggle.vue
│ └── AppToggle.vue
├── assets
│ ├── container.scss
│ ├── themes
│ │ ├── common.scss
│ │ ├── dark.scss
│ │ └── light.scss
│ └── toast.scss
├── components
│ ├── Node.ts
│ ├── ProgressBar.vue
│ ├── ToastContainer.vue
│ ├── VtIcon.vue
│ ├── VtToast.vue
│ └── VtTransition.vue
├── composables
│ ├── createVtTheme.ts
│ ├── useDraggable.ts
│ ├── useSettings.ts
│ ├── useToast.ts
│ └── useVtEvents.ts
├── index.ts
├── main.ts
├── plugin.ts
├── shims
│ └── vue.d.ts
├── type.ts
└── utils.ts
├── tailwind.config.js
├── tsconfig.build.json
├── tsconfig.json
└── vite.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | max_line_length = 120
8 | end_of_line = lf
9 | indent_style = space
10 | indent_size = 4
11 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:@typescript-eslint/recommended',
9 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
10 | 'plugin:vue/vue3-recommended',
11 | '@vue/typescript/recommended',
12 | ],
13 | plugins: [
14 | 'vue',
15 | '@stylistic/ts'
16 | ],
17 | ignorePatterns: [
18 | 'node_modules',
19 | '*.js'
20 | ],
21 | parserOptions: {
22 | ecmaVersion: 2020,
23 | parser: '@typescript-eslint/parser',
24 | sourceType: 'module',
25 | project: './tsconfig.json',
26 | tsconfigRootDir: __dirname,
27 | extraFileExtensions: ['.vue'],
28 | },
29 | rules: {
30 | // https://eslint.org/docs/rules/
31 | 'no-console': 'warn',
32 | 'no-debugger': 'warn',
33 | 'semi': 'off',
34 | 'indent': 'off',
35 | 'no-unused-expressions': 'off',
36 | 'space-before-function-paren': ['warn', {
37 | 'anonymous': 'never',
38 | 'named': 'never',
39 | 'asyncArrow': 'always'
40 | }],
41 | 'no-trailing-spaces': 'warn',
42 | 'no-any': 'off',
43 | 'no-prototype-builtins': 'off',
44 | 'no-unused-vars': 'off',
45 | 'prefer-rest-params': 'warn',
46 | 'no-extra-parens': 'off',
47 | 'quotes': 'off',
48 | 'func-call-spacing': 'off',
49 | 'comma-spacing': 'off',
50 | 'keyword-spacing': 'off',
51 | 'object-curly-spacing': ['warn', 'always'],
52 | 'comma-dangle': ['warn', 'never'],
53 | 'max-len': ['warn', 120],
54 | 'eqeqeq': 'error',
55 |
56 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules
57 | '@stylistic/ts/indent': ['warn', 4],
58 | '@stylistic/ts/semi': 'error',
59 | '@typescript-eslint/no-unused-expressions': ['error', {
60 | allowTernary: true,
61 | allowShortCircuit: true
62 | }],
63 | '@stylistic/ts/quotes': ['warn', 'single'],
64 | '@stylistic/ts/no-extra-parens': 'error',
65 | '@typescript-eslint/no-unused-vars': 'warn',
66 | '@typescript-eslint/no-useless-constructor': 'warn',
67 | '@typescript-eslint/no-explicit-any': 'off',
68 | '@typescript-eslint/ban-ts-comment': 'off',
69 | '@typescript-eslint/no-unsafe-return': 'off',
70 | '@typescript-eslint/no-unsafe-assignment': 'off',
71 | '@typescript-eslint/explicit-module-boundary-types': ['error', {
72 | allowArgumentsExplicitlyTypedAsAny: true,
73 | allowedNames: ['setup']
74 | }],
75 | '@typescript-eslint/prefer-nullish-coalescing': 'warn',
76 | '@typescript-eslint/prefer-optional-chain': 'warn',
77 | '@typescript-eslint/prefer-ts-expect-error': 'warn',
78 | '@typescript-eslint/promise-function-async': 'error',
79 | '@stylistic/ts/func-call-spacing': ['error', 'never'],
80 | '@stylistic/ts/comma-spacing': 'warn',
81 | '@stylistic/ts/keyword-spacing': 'warn',
82 | // '@typescript-eslint/consistent-indexed-object-style': ['error', 'record'], // waiting on dependency updates
83 | // '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
84 | '@stylistic/ts/member-delimiter-style': 'warn',
85 | '@stylistic/ts/type-annotation-spacing': 'warn',
86 | '@typescript-eslint/naming-convention': ['error',
87 | {
88 | selector: 'default',
89 | format: ['camelCase']
90 | },
91 | {
92 | selector: 'import',
93 | format: null
94 | },
95 | {
96 | selector: 'objectLiteralProperty',
97 | format: ['camelCase', 'PascalCase']
98 | },
99 | {
100 | selector: 'typeLike',
101 | format: ['PascalCase']
102 | },
103 | {
104 | selector: 'parameter',
105 | format: null,
106 | filter: {
107 | regex: '^_.*',
108 | match: true
109 | }
110 | }
111 | ],
112 | '@typescript-eslint/no-non-null-assertion': 'off',
113 |
114 | // https://eslint.vuejs.org/rules/
115 | // if unsure and eslint doesn't cover it please refer to https://v3.vuejs.org/style-guide/
116 | 'vue/html-indent': ['warn', 4],
117 | 'vue/component-name-in-template-casing': ['warn', 'PascalCase'],
118 | 'vue/match-component-file-name': ['off', { // until we have storybook working
119 | extensions: ['jsx', 'vue', 'tsx']
120 | }],
121 | 'vue/new-line-between-multi-line-property': 'warn',
122 | 'vue/max-attributes-per-line': [
123 | 'warn', {
124 | 'singleline': 3,
125 | 'multiline': 1
126 | }
127 | ],
128 | 'vue/first-attribute-linebreak': ['warn', {
129 | 'singleline': 'ignore',
130 | 'multiline': 'beside'
131 | }],
132 | 'vue/no-boolean-default': ['error', 'default-false'],
133 | 'vue/no-duplicate-attr-inheritance': 'error',
134 | 'vue/no-empty-component-block': 'warn',
135 | 'vue/no-multiple-objects-in-class': 'error',
136 | 'vue/no-potential-component-option-typo': ['error', {
137 | presets: ['vue', 'vue-router']
138 | }],
139 | 'vue/no-reserved-component-names': ['error', {
140 | 'disallowVueBuiltInComponents': true,
141 | 'disallowVue3BuiltInComponents': true
142 | }],
143 | 'vue/no-template-target-blank': 'error',
144 | 'vue/no-unsupported-features': ['error', {
145 | 'version': '^3.0.0'
146 | }],
147 | 'vue/no-useless-mustaches': 'warn',
148 | 'vue/no-useless-v-bind': 'error',
149 | 'vue/padding-line-between-blocks': 'warn',
150 | 'vue/require-name-property': 'error',
151 | 'vue/v-for-delimiter-style': 'error',
152 | 'vue/v-on-event-hyphenation': 'error',
153 | 'vue/v-on-function-call': ['error', 'never', {
154 | 'ignoreIncludesComment': true
155 | }],
156 | 'vue/eqeqeq': 'error',
157 | 'vue/no-extra-parens': 'warn',
158 | 'vue/html-closing-bracket-newline': ['warn', {
159 | 'multiline': 'never'
160 | }],
161 | 'vue/no-v-html': 'off',
162 | 'vue/require-default-prop': 'off',
163 | 'vue/no-v-text-v-html-on-component': 'off'
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: nandi95
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | - package-ecosystem: npm
5 | directory: '/'
6 | target-branch: release/0.x
7 | schedule:
8 | interval: monthly
9 | time: '00:00'
10 | open-pull-requests-limit: 10
11 | commit-message:
12 | prefix: fix
13 | prefix-development: chore
14 | include: scope
15 |
16 |
17 | - package-ecosystem: github-actions
18 | directory: '/'
19 | target-branch: release/0.x
20 | schedule:
21 | interval: monthly
22 | time: '00:00'
23 | open-pull-requests-limit: 10
24 | commit-message:
25 | prefix: fix
26 | prefix-development: chore
27 | include: scope
28 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | ### 🔗 Linked issue
9 |
10 |
11 |
12 | ### ❓ Type of change
13 |
14 |
15 |
16 | - [ ] 🧹 Updates to the build process or auxiliary tools and libraries
17 | - [ ] 📖 Documentation (updates to the documentation or readme)
18 | - [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
19 | - [ ] 🚀 Enhancement (improving an existing functionality like performance)
20 | - [ ] ✨ New feature (a non-breaking change that adds functionality)
21 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
22 |
23 | ### 📚 Description
24 |
25 |
26 |
27 |
28 |
29 | ### 📝 Checklist
30 |
31 |
32 |
33 |
34 |
35 | - [ ] I have linked an issue or discussion.
36 | - [ ] I have updated the documentation accordingly.
37 |
38 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | contents: read
10 | id-token: write
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: 'lts/*'
16 | registry-url: 'https://registry.npmjs.org'
17 | - run: npm ci
18 | - run: npm run build:lib
19 | - run: npm publish --provenance --access public
20 | env:
21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.local
4 | .idea
5 |
6 | # distribution files
7 | dist
8 | vue-toastify-*
9 |
10 | # Local Netlify folder
11 | .netlify
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | **v2.1.0** *15/03/2025*
2 |
3 | Chore:
4 | - Add a missing build step
5 | - Convert toast container to setup for better type inference
6 |
7 | Fix:
8 | - fix typings internal to the package/demo
9 | - fix toast sizing issue on chrome
10 | - fix logic for getting title - respect defaultTitle setting
11 | - added `enableHtmlInterpretation` flag enabling v-html input for the body and icon (xss risk)
12 | - fix text legibility issue on theme generator
13 | - fix centering issue on small screens
14 |
15 | Feature:
16 | - Add JSX support for body and icon
17 | - updated dependencies
18 | - added some a11y attributes
19 | - allow overwriting built in methods with `customNotifications`
20 |
21 | **v2.0.1** *11/09/2024*
22 |
23 | Chore:
24 | - Add a missing build step
25 |
26 | **v2.0.0** *10/09/2024*
27 |
28 | Performance:
29 | - ***BREAKING***: removed include of all styles by default. You now have to import the base styles **AND** the theme you want to use.
30 | - Add `will-change` for transitioning toasts too.
31 | - Added separate exports for composables.
32 |
33 | Fix:
34 | - Fixed toast might stay on screen if the duration is very low.
35 |
36 | Chore:
37 | - Fixed eslint issues.
38 | - Updated dependencies.
39 | - Updated demo app styling.
40 | - Documented nuxt usage.
41 |
42 | **v2.0.0-alpha** *13/04/2023*
43 |
44 | Refactor:
45 | - Rewritten codebase using v3 of Vue, and its composition api.
46 | - Removed support for passing in router to the plugin. (Developer can just use the callback option)
47 | - Added pauseOnFocusLoss option to pause the timer when the window loses focus.
48 | - Added dynamic theme creation
49 |
50 | **v1.8.1** *25/11/2021*
51 |
52 | Fix:
53 | - Resolved xss vulnerability stemming from an url option not being encoded (#26).
54 |
55 | ***
56 |
57 | **v1.8.0** *05/05/2020*
58 |
59 | Feature:
60 | - Added a new event for when the notification is being dragged.
61 | - Drag event payloads now also include the position of the notification.
62 | - Added toastify to the vue as a static method for easier handling in vuex.
63 |
64 | ***
65 |
66 | **v1.7.0** *28/03/2020*
67 |
68 | Feature:
69 | - Added vue router support to the `url` setting.
70 | - `getSettings()` can now return a single setting's value, given the key.
71 |
72 | Fix:
73 | - `changeToast` now returns `false` as expected when the toast isn't found.
74 |
75 | ***
76 |
77 | *v1.6.1* *11/03/2020*
78 |
79 | Fix:
80 | - Drag behavior fixed to the toast not only the parent element.
81 | - Added will change optimisation to dragging.
82 |
83 | ***
84 |
85 | **v1.6.0** *10/03/2020*
86 |
87 | Feature:
88 | - Added one type at a time option.
89 | - Added max number of notifications on screen option.
90 | - Added `getSettings()` function. (This is more verbose than `setSettings({})`)
91 |
92 | Fix:
93 | - Concatenated queue with toasts on `getToast` and added return value on not found.
94 | - Fixed positioning of the toast on singular dismiss
95 | - Fixed `stopLoader` when passed an array of ids
96 |
97 | ***
98 |
99 | *v1.5.2* *20/01/2020*
100 |
101 | Fix:
102 | - Fixed width issue with long content on leave transition.
103 |
104 | ***
105 |
106 | *v1.5.1* *20/01/2020*
107 |
108 | Fix:
109 | - Added extra check for icon selection.
110 |
111 | ***
112 |
113 | **v1.5.0** *19/01/2020*
114 |
115 | Fix:
116 | - Added the return statement to the custom notification methods
117 | - Fixed the setSettings when using with falsy value.
118 | - Added logic to delay the notification move on removal
119 | - Fixed transition positions
120 | - Added more style namespaces and a whitelist pattern
121 |
122 | Feature:
123 | - Added draggable option to dismiss toast by dragging.
124 | - Added function to listen for events emitted by the notifications.
125 |
126 | ***
127 |
128 | **v1.4.0** *03/01/2020*
129 |
130 | Fix:
131 | - Removed bodyMaxWidth from props and added to the styles
132 | - Updated adding feature to return id instead of currently showing toasts if it is in singular mode.
133 | - Removed prototype builtins.
134 | - Added missing checks for notification coming from server.
135 | - Added packages for testing.
136 | - Styling adjustment for the notification content
137 |
138 | Feature:
139 | - Added sass functions for easier theme creation
140 | - Added option for accepting icons in object format
141 |
142 | ***
143 |
144 | *v1.3.1* *27/12/2019*
145 |
146 | Fix:
147 | - Removed polyfills from bundle
148 |
149 | ***
150 |
151 | **v1.3.0** *27/12/2019*
152 |
153 | Fix:
154 | - Container now ignores clicks thanks to `pointer-events: none;`
155 | - Better responsiveness added to the toasts
156 | - Fixed notification timeout close logic
157 | - General refactor for maintainability
158 | - The status' title attribute will no longer get capitalised (Sorry I didn't see major bump version justified)
159 |
160 | Feature:
161 | - Moved transition into group transition for using the FLIP technique
162 | - Added notification display ordering option
163 | - Plugin now accepts custom transitions
164 |
165 | ***
166 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 nandi95
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ##
🔥Vue Toastify🔥
2 | Simple and dependency-free notification plugin.
3 |
4 | ## Installation
5 |
6 | ```bash
7 | npm i vue-toastify
8 | ```
9 |
10 | ```ts
11 | import { createApp } from 'vue';
12 | import plugin from 'vue-toastify';
13 | // base styles
14 | import 'vue-toastify/index.css';
15 | // theme styles
16 | import 'vue-toastify/themes/dark.css';
17 | import type { Settings } from 'vue-toastify';
18 |
19 | const app = createApp({ });
20 | app.use(plugin, { });
21 | app.mount('#app');
22 | ```
23 |
24 | [Usage with Nuxt](#usage-with-nuxt)
25 |
26 | ## Options:
27 | - [ToastOptions](src/type.ts#L195) - settings per toast
28 | - [Settings](src/type.ts#L113) - global settings (default settings [here](src/composables/useSettings.ts#L7))
29 | - [ToastPluginAPI](src/composables/useToast.ts#L13) - methods available on the `useToast()` composable
30 | - [Events](src/composables/useVtEvents.ts#L6) - events emitted by the plugin
31 |
32 | ## Migration Information
33 |
34 | In the future `enableHtmlInterpretation` setting will default to `false`. If you rely on that behavior, make sure to enable it in the settings. If you don't want user input treated as html, make sure to set it to `false`.
35 |
36 | ## Custom styling
37 | Styles include a `'dark'`(default) and a `'light'` theme. If you would like to create your own styles you may use the following helpers:
38 |
39 | ```ts
40 | import { createVtTheme, getCssRules } from 'vue-toastify';
41 |
42 | // this will create a stylesheet if doesn't exists and insert it into the head
43 | createVtTheme('myThemeName', '#8f6b42');
44 | // then you can set the theme of the status or the global settings
45 | // alternatively, you can get an array of css rules using getCssRules
46 | getCssRules('myThemeName', '#8f6b42').forEach(rule => {...});
47 | // this will give you a good starting point to customise the theme
48 | ```
49 |
50 | ## Custom notifications
51 | You may create some methods on the `useToast()` so it will shortcut any repetition you may have in your app. To register them add a `customNotifications` key to the settings when registering the plugin.
52 |
53 | ```ts
54 | app.use(plugin, {
55 | customNotifications: {
56 | authenticationError: {
57 | body: 'Authentication error',
58 | // ... rest of the toast options here
59 | }
60 | }
61 | });
62 | // then later you can use it as
63 | useToast().authenticationError();
64 | ```
65 |
66 | ## Ambient declaration for custom notifications
67 |
68 | ```ts
69 | import type { ToastPluginAPI, CustomMethods } from 'vue-toastify';
70 |
71 | declare module 'vue-toastify' {
72 | interface MyMethods extends CustomMethods {
73 | authenticationError(): string;
74 | }
75 |
76 | function useToast(): ToastPluginAPI & MyMethods;
77 | }
78 | ```
79 |
80 | ## Events
81 | The plugin emits events that you can listen to which allows for using callbacks at different points in the toast's lifecycle.
82 |
83 | ```ts
84 | import { useVtEvents, useToast } from 'vue-toastify';
85 |
86 | const toast = useToast().success({ body: 'Hello world', canTimeout: true });
87 |
88 | useVtEvents().once('vtPaused', payload => {
89 | if (payload.id === toast.id) {
90 | // do something
91 | }
92 | })
93 | ```
94 |
95 | ### Usage with Nuxt
96 | The recommended way to install is by creating a plugin. As notifications are expected to be responses to user actions, we can lazy load the plugin to reduce the initial bundle size.
97 |
98 | Be sure
99 | to familiarise yourself with the [Nuxt plugin documentation](https://nuxt.com/docs/guide/directory-structure/plugins).
100 |
101 | ```ts
102 | // plugins/toast.client.ts
103 | // .client will only run the plugin on the client side.
104 | import type { Settings } from 'vue-toastify';
105 |
106 | export default defineNuxtPlugin({
107 | name: 'toast',
108 | // can load the same time as the rest of the plugins
109 | parallel: true,
110 | setup: nuxt => {
111 | // this will lazy load the plugin therefore won't be included in the entry point
112 | void import('vue-toastify').then(exports => {
113 | nuxt.vueApp.use(exports.default, {
114 | pauseOnHover: true,
115 | theme: 'light',
116 | position: 'top-right'
117 | });
118 | });
119 | }
120 | });
121 |
122 | ```
123 | Then specify the auto-imported preset in your configuration.
124 | ```ts
125 | // nuxt.config.ts
126 | export default defineNuxtConfig({
127 | css: [
128 | // required base themes
129 | 'vue-toastify/index.css',
130 | // include the theme you want to use
131 | 'vue-toastify/themes/light.css'
132 | // or generate one of your own as described in the custom styling section
133 | ],
134 | imports: {
135 | // this will include the composables that the plugin provides
136 | // which is negligable in size compared to the plugin itself
137 | presets: [
138 | {
139 | from: 'vue-toastify',
140 | imports: [
141 | // include only the composables you need auto-imported
142 | 'useToast',
143 | // 'useVtEvents',
144 | // 'useVtSettings'
145 | ]
146 | }
147 | ]
148 | },
149 | })
150 | ```
151 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vue Toastify
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-toastify",
3 | "version": "2.1.0",
4 | "license": "MIT",
5 | "type": "module",
6 | "author": "Nandor Kraszlan",
7 | "scripts": {
8 | "dev": "vite",
9 | "build:demo": "vite build",
10 | "serve": "vite preview",
11 | "lint": "eslint . --fix --ext .ts,.vue,.tsx",
12 | "build:lib": "vite build --mode=library && tsc -p tsconfig.build.json"
13 | },
14 | "module": "dist/index.mjs",
15 | "main": "dist/index.cjs",
16 | "exports": {
17 | ".": {
18 | "types": "./dist/types/src/index.d.ts",
19 | "import": "./dist/index.mjs",
20 | "require": "./dist/index.cjs"
21 | },
22 | "./useToast": {
23 | "types": "./dist/types/src/composables/useToast.d.ts",
24 | "import": "./dist/useToast.mjs",
25 | "require": "./dist/useToast.cjs"
26 | },
27 | "./useVtEvents": {
28 | "types": "./dist/types/src/composables/useVtEvents.d.ts",
29 | "import": "./dist/useVtEvents.mjs",
30 | "require": "./dist/useVtEvents.cjs"
31 | },
32 | "./useVtSetting": {
33 | "types": "./dist/types/src/composables/useSetting.d.ts",
34 | "import": "./dist/useVtEvents.mjs",
35 | "require": "./dist/useVtEvents.cjs"
36 | },
37 | "./index.css": "./dist/index.css",
38 | "./themes/dark.css": "./dist/themes/dark.css",
39 | "./themes/light.css": "./dist/themes/light.css"
40 | },
41 | "types": "dist/types/src/index.d.ts",
42 | "files": [
43 | "dist"
44 | ],
45 | "repository": {
46 | "type": "git",
47 | "url": "git+https://github.com/nandi95/vue-toastify.git"
48 | },
49 | "dependencies": {
50 | "vue": "^3.0.11"
51 | },
52 | "devDependencies": {
53 | "@rollup/plugin-commonjs": "^28.0.3",
54 | "@rollup/plugin-typescript": "^12.1.2",
55 | "@stylistic/eslint-plugin-ts": "^2.8.0",
56 | "@tailwindcss/forms": "^0.5.3",
57 | "@typescript-eslint/eslint-plugin": "^8.5.0",
58 | "@typescript-eslint/parser": "^8.5.0",
59 | "@vitejs/plugin-vue": "^5.1.3",
60 | "@vue/compiler-sfc": "^3.0.5",
61 | "@vue/eslint-config-typescript": "^13.0.0",
62 | "autoprefixer": "^10.2.5",
63 | "daisyui": "^4.12.10",
64 | "eslint": "^8.24.0",
65 | "eslint-plugin-import": "^2.22.1",
66 | "eslint-plugin-vue": "^9.5.1",
67 | "postcss": "^8.2.8",
68 | "rollup": "^4.21.2",
69 | "rollup-plugin-vue": "^6.0.0",
70 | "sass": "^1.78.0",
71 | "tailwindcss": "^3.1.8",
72 | "typescript": "^5.0.2",
73 | "vite": "^6.2.2",
74 | "vite-plugin-static-copy": "^2.3.0",
75 | "vue-router": "^4.1.6"
76 | },
77 | "keywords": [
78 | "vue",
79 | "toast",
80 | "notification",
81 | "vue3",
82 | "alert",
83 | "vue-toastify",
84 | "vue-toast",
85 | "snackbar",
86 | "toast-notification",
87 | "notice",
88 | "toastify",
89 | "toast-component",
90 | "toast-plugin",
91 | "toastify-component",
92 | "vue-notification"
93 | ]
94 | }
95 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nandi95/vue-toastify/18c8daf2992c6b8f88f1a45a9f1f533573e562cf/public/favicon.ico
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
49 |
54 |
59 |
66 |
67 |
68 |
69 |
70 |
74 |
80 |
81 |
82 |
83 |
84 |
88 |
89 |
90 |
91 |
109 |
110 |
111 |
112 |
115 |
118 |
119 |
120 |
121 |
124 |
125 | With backdrop the rest of the page is inaccessible.
126 |
127 |
128 | Make sure to also cancel the loading if your process has failed.
129 |
130 |
133 |
134 |
135 |
136 |
137 |
138 |
342 |
343 |
368 |
--------------------------------------------------------------------------------
/src/app-components/AppInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
17 |
18 |
19 |
20 |
21 |
22 |
68 |
--------------------------------------------------------------------------------
/src/app-components/AppSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
21 |
22 |
23 |
24 |
25 |
76 |
--------------------------------------------------------------------------------
/src/app-components/AppStatusDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
// status object
4 |
{{ line }}
5 |
6 |
7 |
8 |
44 |
--------------------------------------------------------------------------------
/src/app-components/AppTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
66 |
--------------------------------------------------------------------------------
/src/app-components/AppThemeToggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
--------------------------------------------------------------------------------
/src/app-components/AppToggle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
53 |
--------------------------------------------------------------------------------
/src/assets/container.scss:
--------------------------------------------------------------------------------
1 | $tinyScreen: 268px;
2 |
3 | .vt-notification-container {
4 | pointer-events: none;
5 | box-sizing: border-box;
6 | position: fixed;
7 | display: flex;
8 | gap: 10px;
9 | width: auto;
10 | height: auto;
11 | z-index: 9999;
12 | }
13 |
14 | .vt-backdrop-hidden {
15 | transition: all 150ms ease-out;
16 | opacity: 0;
17 | visibility: hidden;
18 | z-index: 50;
19 | top: 0;
20 | left: 0;
21 | right: 0;
22 | bottom: 0;
23 | position: fixed;
24 | }
25 |
26 | .vt-backdrop-visible {
27 | opacity: 1;
28 | visibility: visible;
29 | }
30 |
31 | .vt-top {
32 | top: 10px;
33 | }
34 |
35 | .vt-centerY {
36 | top: 50%;
37 | transform: translateY(-50%);
38 | }
39 |
40 | .vt-bottom {
41 | bottom: 10px;
42 | }
43 |
44 | .vt-left {
45 | left: 10px;
46 | }
47 |
48 | .vt-centerX {
49 | left: 50%;
50 | transform: translateX(-50%);
51 | }
52 |
53 | .vt-right {
54 | right: 10px;
55 | }
56 |
57 | .vt-center-center {
58 | top: 50%;
59 | left: 50%;
60 | transform: translate(-50%, -50%);
61 | }
62 |
63 | @media (max-width: $tinyScreen) {
64 | .vt-notification-container {
65 | width: 100%;
66 | margin: 0;
67 | right: 0;
68 | &:not(.vt-centerX) {
69 | left: 0;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/assets/themes/common.scss:
--------------------------------------------------------------------------------
1 | @use "sass:color";
2 | @use "sass:map";
3 | @use "sass:math";
4 |
5 | @function luminance($color) {
6 | $colors: (
7 | 'red': color.channel($color, "red", $space: rgb),
8 | 'green': color.channel($color, "green", $space: rgb),
9 | 'blue': color.channel($color, "blue", $space: rgb)
10 | );
11 |
12 | @each $name, $value in $colors {
13 | $value: calc($value / 255);
14 |
15 | @if $value < 0.03928 {
16 | $value: calc($value / 12.92);
17 | } @else {
18 | $value: calc(($value + .055) / 1.055);
19 | $value: math.pow($value, 2); // todo exponent used to be 2.4
20 | }
21 |
22 | $colors: map.merge($colors, ($name: $value));
23 | }
24 |
25 | @return (map.get($colors, 'red') * .2126) + (map.get($colors, 'green') * .7152) + (map.get($colors, 'blue') * .0722);
26 | }
27 | @function color-change($color, $percentage) {
28 | @if (luminance($color) > 0.4) {
29 | $color: color.adjust($color, $lightness: -$percentage);
30 | } @else {
31 | $color: color.adjust($color, $lightness: $percentage);
32 | }
33 | @return $color;
34 | }
35 |
36 | @mixin generate-theme($name, $color) {
37 | .vt-theme-#{$name} {
38 | background-color: $color;
39 | & > .vt-progress-bar {
40 | background-color: color-change($color, 10%);
41 | & > .vt-progress {
42 | background-color: color-change($color, 30%);
43 | }
44 | }
45 | & > .vt-content {
46 | & > .vt-title {
47 | color: color-change($color, 75%);
48 | }
49 | & > .vt-paragraph {
50 | color: color-change($color, 75%);
51 | }
52 | }
53 | & > .vt-buttons {
54 | & > button {
55 | border: solid 1px color-change($color, 10%);
56 | background-color: color-change($color, 10%);
57 | color: color-change($color, 75%);
58 | transition: all 0.2s ease-out;
59 | &:hover {
60 | background-color: color-change($color, 65%);
61 | color: color-change($color, 5%);
62 | transition: all 0.2s ease-out;
63 | }
64 | }
65 | }
66 | & > .vt-prompt {
67 | & > .vt-icon > svg {
68 | fill: color-change($color, 70%);
69 | }
70 | & {
71 | border-color: color-change($color, 70%);
72 | }
73 | }
74 | & > .vt-icon-container > .vt-spinner {
75 | border: 2px solid color-change($color, 30%);
76 | border-top: 2px solid color-change($color, 90%);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/assets/themes/dark.scss:
--------------------------------------------------------------------------------
1 | @use "./common.scss";
2 |
3 | @include common.generate-theme('dark', #1d1d1d);
4 |
--------------------------------------------------------------------------------
/src/assets/themes/light.scss:
--------------------------------------------------------------------------------
1 | @use "./common";
2 |
3 | @include common.generate-theme('light', #f0f0f0);
4 |
--------------------------------------------------------------------------------
/src/assets/toast.scss:
--------------------------------------------------------------------------------
1 | @use "container";
2 | // This is all the styles the toast component uses.
3 | // You may change any of the default values
4 | // by appending the value with !important.
5 |
6 | .vt-success {
7 | & > .vt-icon > svg {
8 | fill: #199919;
9 | }
10 | & {
11 | border-color: #199919;
12 | }
13 | }
14 | .vt-info {
15 | & > .vt-icon > svg {
16 | fill: #003bc8;
17 | }
18 | & {
19 | border-color: #003bc8;
20 | }
21 | }
22 | .vt-warning {
23 | & > .vt-icon > svg {
24 | fill: #ffb300;
25 | }
26 | & {
27 | border-color: #ffb300;
28 | }
29 | }
30 | .vt-error {
31 | & > .vt-icon > svg {
32 | fill: #b11414;
33 | }
34 | & {
35 | border-color: #b11414;
36 | }
37 | }
38 | // You may reorganise the items with the order rule
39 | .vt-notification {
40 | transition: transform 100ms ease; // This ensures that on drag the notification slides back to its original position
41 | box-sizing: border-box;
42 | pointer-events: auto;
43 | box-shadow: 0 0 10px 0.5px rgba(0, 0, 0, 0.35);
44 | padding: 10px 20px;
45 | min-height: 100px;
46 | min-width: 250px;
47 | border-radius: 5px;
48 | margin-left: auto;
49 | margin-right: auto;
50 | z-index: 9999;
51 | display: flex;
52 | justify-content: space-around;
53 | align-items: center;
54 | flex-flow: wrap row;
55 | align-content: center;
56 | user-select: none;
57 | & > .vt-progress-bar {
58 | height: 3px;
59 | width: 100%;
60 | margin-bottom: 5px;
61 | & > .vt-progress {
62 | max-width: 100%;
63 | height: 3px;
64 | overflow: hidden;
65 | transition: max-width 1ms ease-in-out;
66 | }
67 | }
68 | & > .vt-content {
69 | width: auto;
70 | height: 100%;
71 | max-width: 250px;
72 | word-break: break-word;
73 | & > .vt-title {
74 | font-size: 1.4rem;
75 | margin: 0;
76 | }
77 | & > .vt-paragraph {
78 | font-size: 1rem;
79 | margin: 0.5rem 0;
80 | }
81 | }
82 | & > .vt-circle {
83 | border-style: solid;
84 | border-width: 2px;
85 | width: 65px;
86 | height: 65px;
87 | border-radius: 50%;
88 | margin: 5px !important;
89 | }
90 | & > .vt-icon-container {
91 | margin: 0 20px;
92 | position: relative;
93 | & > .vt-icon {
94 | position: absolute;
95 | top: 50%;
96 | left: 50%;
97 | transform: translate(-50%, -50%);
98 | }
99 | }
100 | & > .vt-buttons {
101 | flex-basis: 100%;
102 | display: flex;
103 | flex-flow: row wrap;
104 | align-content: center;
105 | align-items: center;
106 | justify-content: space-evenly;
107 | margin: 5px -23px 0;
108 | & > button {
109 | flex-basis: 48%;
110 | width: auto;
111 | margin-bottom: 4px;
112 | border-radius: 4px;
113 | }
114 | }
115 | }
116 | @media (max-width: container.$tinyScreen) {
117 | .vt-notification {
118 | min-height: auto;
119 | min-width: 100%;
120 | border-radius: 0;
121 | & > .vt-content {
122 | text-align: center;
123 | }
124 | }
125 | }
126 | .vt-will-change {
127 | will-change: transform, opacity;
128 | }
129 |
130 | // This controls how the notification should move when others are added/removed
131 | .vt-move {
132 | transition-timing-function: ease-in-out;
133 | transition-property: all;
134 | transition-duration: 200ms;
135 | &[data-delayed="true"] {
136 | transition-delay: 200ms !important; // this should be set to the same as the transition leave duration
137 | }
138 | }
139 | .vt-spinner {
140 | width: 60px;
141 | height: 60px;
142 | border-radius: 50%;
143 | background-color: transparent;
144 | animation: 1s spin linear infinite;
145 | }
146 | .vt-cursor-wait {
147 | cursor: wait;
148 | }
149 |
150 | @keyframes spin {
151 | from {
152 | -webkit-transform: rotate(0deg);
153 | transform: rotate(0deg);
154 | }
155 | to {
156 | -webkit-transform: rotate(360deg);
157 | transform: rotate(360deg);
158 | }
159 | }
160 |
161 | .vt-right-enter-active,
162 | .vt-left-enter-active,
163 | .vt-bottom-enter-active,
164 | .vt-top-enter-active,
165 | .vt-center-enter-active {
166 | transition: transform 200ms ease-out, opacity 200ms ease-out;
167 | }
168 |
169 | .vt-right-leave-active,
170 | .vt-left-leave-active,
171 | .vt-bottom-leave-active,
172 | .vt-top-leave-active,
173 | .vt-center-leave-active {
174 | transition: transform 200ms ease-in, opacity 200ms ease-in;
175 | }
176 |
177 | .vt-right-enter-from,
178 | .vt-right-leave-to {
179 | transform: translateX(50px);
180 | opacity: 0;
181 | }
182 |
183 | .vt-left-enter-from,
184 | .vt-left-leave-to {
185 | transform: translateX(-50px);
186 | opacity: 0;
187 | }
188 |
189 | .vt-bottom-enter-from,
190 | .vt-bottom-leave-to {
191 | transform: translateY(50px);
192 | opacity: 0;
193 | }
194 |
195 | .vt-top-enter-from,
196 | .vt-top-leave-to {
197 | transform: translateY(-50px);
198 | opacity: 0;
199 | }
200 |
201 | .vt-center-enter-from,
202 | .vt-center-leave-to {
203 | opacity: 0;
204 | }
205 |
--------------------------------------------------------------------------------
/src/components/Node.ts:
--------------------------------------------------------------------------------
1 | import type { VNode } from 'vue';
2 | // This is a JSX component - Pascal-case tends to be the norm
3 | // eslint-disable-next-line @typescript-eslint/naming-convention
4 | export default function Node(props: { node: VNode | string }): VNode|string {
5 | return props.node;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/ProgressBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
115 |
--------------------------------------------------------------------------------
/src/components/ToastContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
31 |
32 |
433 |
434 |
437 |
--------------------------------------------------------------------------------
/src/components/VtIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
31 |
42 |
52 |
63 |
64 |
65 |
66 |
138 |
--------------------------------------------------------------------------------
/src/components/VtToast.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ status.body }}
30 |
31 |
32 |
33 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
170 |
--------------------------------------------------------------------------------
/src/components/VtTransition.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
119 |
--------------------------------------------------------------------------------
/src/composables/createVtTheme.ts:
--------------------------------------------------------------------------------
1 | function hexToHSL(hex: string): { h: number; s: number; l: number } {
2 | // Remove # if present
3 | hex = hex.replace(/^#/, '');
4 |
5 | // Parse hex to RGB
6 | const r = parseInt(hex.slice(0, 2), 16) / 255;
7 | const g = parseInt(hex.slice(2, 4), 16) / 255;
8 | const b = parseInt(hex.slice(4, 6), 16) / 255;
9 |
10 | const max = Math.max(r, g, b);
11 | const min = Math.min(r, g, b);
12 | let h = 0, s = 0;
13 | const l = (max + min) / 2;
14 |
15 | if (max !== min) {
16 | const d = max - min;
17 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
18 |
19 | switch (max) {
20 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
21 | case g: h = (b - r) / d + 2; break;
22 | case b: h = (r - g) / d + 4; break;
23 | }
24 |
25 | h /= 6;
26 | }
27 |
28 | return { h, s, l };
29 | }
30 |
31 | function hslToHex(h: number, s: number, l: number): string {
32 | let r, g, b;
33 |
34 | if (s === 0) {
35 | r = g = b = l;
36 | } else {
37 | const hue2rgb = (p: number, q: number, t: number): number => {
38 | if (t < 0) t += 1;
39 | if (t > 1) t -= 1;
40 | if (t < 1/6) return p + (q - p) * 6 * t;
41 | if (t < 1/2) return q;
42 | if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
43 | return p;
44 | };
45 |
46 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
47 | const p = 2 * l - q;
48 |
49 | r = hue2rgb(p, q, h + 1/3);
50 | g = hue2rgb(p, q, h);
51 | b = hue2rgb(p, q, h - 1/3);
52 | }
53 |
54 | const toHex = (x: number): string => {
55 | const hex = Math.round(x * 255).toString(16);
56 | return hex.length === 1 ? '0' + hex : hex;
57 | };
58 |
59 | return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
60 | }
61 |
62 | function darken(color: string, percentage: number): string {
63 | const hsl = hexToHSL(color);
64 | hsl.l = Math.max(0, hsl.l - percentage / 100);
65 | return hslToHex(hsl.h, hsl.s, hsl.l);
66 | }
67 |
68 | function lighten(color: string, percentage: number): string {
69 | const hsl = hexToHSL(color);
70 | hsl.l = Math.min(1, hsl.l + percentage / 100);
71 | return hslToHex(hsl.h, hsl.s, hsl.l);
72 | }
73 |
74 | function luminance(color: string): number {
75 | const colors = {
76 | red: parseInt(color.slice(1, 3), 16),
77 | green: parseInt(color.slice(3, 5), 16),
78 | blue: parseInt(color.slice(5, 7), 16)
79 | };
80 |
81 | for (const [name, value] of Object.entries(colors)) {
82 | let normalizedValue = value / 255;
83 |
84 | if (normalizedValue < 0.03928) {
85 | normalizedValue /= 12.92;
86 | } else {
87 | // todo exponent used to be 2.4
88 | normalizedValue = ((normalizedValue + 0.055) / 1.055) ** 2;
89 | }
90 |
91 | colors[name as keyof typeof colors] = normalizedValue;
92 | }
93 |
94 | const redValue = colors['red'] * 0.2126;
95 | const greenValue = colors['green'] * 0.7152;
96 | const blueValue = colors['blue'] * 0.0722;
97 |
98 | return redValue + greenValue + blueValue;
99 | }
100 |
101 | function colorChange(color: string, amount: number): string {
102 | return luminance(color) > 0.4 ? darken(color, amount) : lighten(color, amount);
103 | }
104 |
105 | export function getCssRules(name: string, colour: string): string[] {
106 | return [
107 | `.vt-theme-${name} {
108 | background-color: ${colour};
109 | }`,
110 | `.vt-theme-${name} > .vt-progress-bar {
111 | background-color: ${colorChange(colour, 10)};
112 | }`,
113 | `.vt-theme-${name} > .vt-progress-bar > .vt-progress {
114 | background-color: ${colorChange(colour, 30)};
115 | }`,
116 | `.vt-theme-${name} > .vt-content > .vt-title {
117 | color: ${colorChange(colour, 75)};
118 | }`,
119 | `.vt-theme-${name} > .vt-content > .vt-paragraph {
120 | color: ${colorChange(colour, 75)};
121 | }`,
122 | `.vt-theme-${name} > .vt-buttons > .button {
123 | background-color: ${colorChange(colour, 10)};
124 | color: ${colorChange(colour, 75)};
125 | border: 1px solid ${colorChange(colour, 10)};
126 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
127 | }`,
128 | `.vt-theme-${name} > .vt-buttons > .button:hover {
129 | background-color: ${colorChange(colour, 65)};
130 | color: ${colorChange(colour, 5)};
131 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
132 | }`,
133 | `.vt-theme-${name} > .vt-prompt {
134 | border-color: ${colorChange(colour, 70)};
135 | }`,
136 | `.vt-theme-${name} > .vt-prompt > .vt-icon > .svg {
137 | fill: ${colorChange(colour, 70)};
138 | }`,
139 | `.vt-theme-${name} > .vt-icon-container > .vt-spinner {
140 | border: 2px solid ${colorChange(colour, 30)};
141 | border-top: 2px solid ${colorChange(colour, 90)};
142 | }`
143 | ];
144 | }
145 |
146 | const styleSheetId = Symbol('vue-toastify-themes');
147 |
148 | export default function createVtTheme(name: string, colour: string): void {
149 | let style = document.getElementById(styleSheetId.toString()) as HTMLStyleElement | null;
150 |
151 | if (!style) {
152 | style = document.createElement('style');
153 | style.id = styleSheetId.toString();
154 | document.head.appendChild(style);
155 | }
156 |
157 | getCssRules(name, colour)
158 | .forEach(rule => style.sheet!.insertRule(
159 | rule.replaceAll(' ', '').replaceAll('\n', ' ')
160 | ));
161 | }
162 |
--------------------------------------------------------------------------------
/src/composables/useDraggable.ts:
--------------------------------------------------------------------------------
1 | import { onMounted, onBeforeUnmount, getCurrentInstance, ref, computed } from 'vue';
2 | import type { Ref, ComputedRef, CSSProperties } from 'vue';
3 | import useVtEvents from './useVtEvents';
4 | import { Toast } from '../type';
5 |
6 | export function xPos(event: TouchEvent | MouseEvent): number {
7 | return 'targetTouches' in event && event.targetTouches.length > 0
8 | ? event.targetTouches[0].clientX
9 | : (event as MouseEvent).clientX;
10 | }
11 | export function yPos(event: TouchEvent | MouseEvent): number {
12 | return 'targetTouches' in event && event.targetTouches.length > 0
13 | ? event.targetTouches[0].clientY
14 | : (event as MouseEvent).clientY;
15 | }
16 |
17 | export type Coordinates = {
18 | x: number;
19 | y: number;
20 | };
21 |
22 | type Draggable = {
23 | hasMoved: ComputedRef;
24 | draggableStyles: ComputedRef;
25 | isDragged: Readonly[>;
26 | };
27 |
28 | export default function useDraggable(
29 | props: { status: Required> },
30 | close: () => void
31 | ): Draggable {
32 | const instance = getCurrentInstance()!;
33 | const dragStartPos = ref();
34 | const dragPos = ref();
35 | const isDragged = ref(false);
36 | const startingClientRect = ref();
37 | const events = useVtEvents();
38 |
39 | const element = computed(() => instance.proxy?.$el as HTMLDivElement);
40 | const hasMoved = computed(() => {
41 | return !!dragPos.value && dragStartPos.value?.x !== dragPos.value.x;
42 | });
43 | const dragXDistance = computed(() => {
44 | return isDragged.value ? dragPos.value!.x - dragStartPos.value!.x : 0;
45 | });
46 | const removalDistance = computed(() => {
47 | if (!startingClientRect.value) {
48 | return 15;
49 | }
50 |
51 | return startingClientRect.value.width * props.status.dragThreshold;
52 | });
53 | const draggableStyles = computed(() => {
54 | if (!isDragged.value
55 | || dragPos.value !== undefined
56 | && dragStartPos.value !== undefined
57 | && dragStartPos.value.x === dragPos.value.x
58 | || !hasMoved.value
59 | ) {
60 | return {};
61 | }
62 |
63 | let opacity = 1 - Math.abs(dragXDistance.value / removalDistance.value);
64 | opacity = isNaN(opacity) ? 1 : opacity;
65 |
66 | return {
67 | transform: `translateX(${dragXDistance.value}px)`,
68 | opacity: String(opacity),
69 | userSelect: 'none'
70 | };
71 | });
72 |
73 | const dragStarted = (event: TouchEvent | MouseEvent) => {
74 | element.value.classList.add('vt-will-change');
75 | isDragged.value = true;
76 | dragStartPos.value = { x: xPos(event), y: yPos(event) };
77 | startingClientRect.value = element.value.getBoundingClientRect();
78 | };
79 | const beingDragged = (event: TouchEvent | MouseEvent) => {
80 | // prevent page scroll
81 | event.preventDefault();
82 | if (isDragged.value) {
83 | dragPos.value = { x: xPos(event), y: yPos(event) };
84 | if (!hasMoved.value) {
85 | events.emit('vtDragStarted', {
86 | id: props.status.id,
87 | position: dragStartPos.value!
88 | });
89 | } else {
90 | events.emit('vtBeingDragged', {
91 | id: props.status.id,
92 | position: dragPos.value
93 | });
94 | }
95 | }
96 | };
97 | const dragFinished = () => {
98 | if (hasMoved.value) {
99 | events.emit('vtDragFinished', {
100 | id: props.status.id,
101 | position: dragPos.value!
102 | });
103 | // todo if at least 75% of the notification is out of the window (in case of mobile)
104 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
105 | const isAlmostOffRight =
106 | element.value.getBoundingClientRect().right > window.innerWidth &&
107 | element.value.getBoundingClientRect().right - window.innerWidth >
108 | startingClientRect.value!.width * 0.75;
109 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
110 | const isAlmostOffLeft =
111 | element.value.getBoundingClientRect().right < startingClientRect.value!.width * 0.25;
112 | // todo - if they dragging with high speed then the removal distance can be shortened somewhat
113 | if (
114 | Math.abs(startingClientRect.value!.left - element.value.getBoundingClientRect().left) >
115 | removalDistance.value
116 | ) {
117 | close();
118 | }
119 | isDragged.value = false;
120 | // execute after the next event cycle
121 | setTimeout(() => {
122 | dragPos.value = undefined;
123 | dragStartPos.value = undefined;
124 | startingClientRect.value = undefined;
125 | element.value.classList.remove('vt-will-change');
126 | });
127 | }
128 | };
129 |
130 | if (props.status.draggable) {
131 | onMounted(() => {
132 | element.value.addEventListener('touchstart', dragStarted);
133 | element.value.addEventListener('mousedown', dragStarted);
134 | addEventListener('touchmove', beingDragged);
135 | addEventListener('mousemove', beingDragged);
136 | addEventListener('touchend', dragFinished);
137 | addEventListener('mouseup', dragFinished);
138 | });
139 | onBeforeUnmount(() => {
140 | element.value.removeEventListener('touchstart', dragStarted);
141 | element.value.removeEventListener('mousedown', dragStarted);
142 | removeEventListener('touchmove', beingDragged);
143 | removeEventListener('mousemove', beingDragged);
144 | removeEventListener('touchend', dragFinished);
145 | removeEventListener('mouseup', dragFinished);
146 | });
147 | }
148 |
149 | return {
150 | hasMoved,
151 | draggableStyles,
152 | isDragged
153 | };
154 | }
155 |
--------------------------------------------------------------------------------
/src/composables/useSettings.ts:
--------------------------------------------------------------------------------
1 | import type { Settings } from '../type';
2 | import { reactive, readonly } from 'vue';
3 |
4 | type DefaultSettings = Omit, 'transition'> & Pick;
5 |
6 | const settings = reactive({
7 | singular: false,
8 | withBackdrop: false,
9 | backdrop: 'rgba(0, 0, 0, 0.2)',
10 | position: 'bottom-right',
11 | defaultTitle: true,
12 | canTimeout: true,
13 | pauseOnHover: false,
14 | pauseOnFocusLoss: true,
15 | iconEnabled: true,
16 | draggable: true,
17 | dragThreshold: 0.75,
18 | hideProgressbar: false,
19 | errorDuration: 8000,
20 | successDuration: 4000,
21 | warningInfoDuration: 6000,
22 | theme: 'dark',
23 | baseIconClass: '',
24 | orderLatest: true,
25 | transition: undefined,
26 | oneType: false,
27 | maxToasts: 6,
28 | customNotifications: {},
29 | enableHtmlInterpretation: true
30 | });
31 |
32 | type UseSettings = {
33 | settings: DefaultSettings;
34 | updateSettings: (
35 | key: T,
36 | value?: T extends keyof Settings ? Settings[T] : never
37 | ) => Settings;
38 | };
39 |
40 | /**
41 | * Base settings applying plugin wide.
42 | */
43 | export default function useSettings(): UseSettings {
44 | return {
45 | settings: readonly(settings) as DefaultSettings,
46 | updateSettings: (key, newSettings) => {
47 | let settingsNew = {} as Settings;
48 |
49 | if (typeof key === 'object' && newSettings === undefined) {
50 | settingsNew = key;
51 | } else if (typeof key === 'string') {
52 | settingsNew = { [key]: newSettings };
53 | }
54 |
55 | return readonly(Object.assign(settings, settingsNew)) as Settings;
56 | }
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/composables/useToast.ts:
--------------------------------------------------------------------------------
1 | import type { ContainerMethods, Settings, Status, Toast, ToastOptions } from '../type';
2 | import useSettings from './useSettings';
3 | import useVtEvents from './useVtEvents';
4 | import { isBody } from '../utils';
5 |
6 | export type CustomMethods = Record string>;
7 |
8 | export interface ToastPluginAPI {
9 | notify: (status: Status, title?: string) => Toast['id'];
10 | success: (status: Status, title?: string) => Toast['id'];
11 | info: (status: Status, title?: string) => Toast['id'];
12 | warning: (status: Status, title?: string) => Toast['id'];
13 | error: (status: Status, title?: string) => Toast['id'];
14 | loader: (status: Status, title?: string) => Toast['id'];
15 | prompt: (status: Omit & { answers: Record }) => Promise;
16 | updateToast: (id: Toast['id'], status: ToastOptions) => boolean;
17 | settings: (settings?: Settings) => Settings;
18 | findToast: (id: T) => Toast | undefined;
19 | getToasts: () => Toast[];
20 | stopLoader: (id?: Toast['id']) => number;
21 | remove: (id?: Toast['id']) => number;
22 | }
23 |
24 | // @ts-expect-error
25 | //eslint-disable-next-line prefer-const
26 | export let app: { container: ContainerMethods } = {};
27 |
28 | const notify = (status: Status, title?: string): ReturnType => {
29 | if (isBody(status)) {
30 | status = {
31 | body: status
32 | };
33 | }
34 | if (title) {
35 | status.title = title;
36 | }
37 | if (!status.type) {
38 | status.type = 'success';
39 | }
40 | return app.container.add(status);
41 | };
42 |
43 | export const toastMethods: ToastPluginAPI = {
44 | notify,
45 | success: (status: Status, title?: string) => notify(status, title),
46 | info: (status: Status, title?: string) => {
47 | if (isBody(status)) {
48 | status = {
49 | body: status
50 | };
51 | }
52 | if (title) {
53 | status.title = title;
54 | }
55 | status.type = 'info';
56 | return notify(status);
57 | },
58 | warning: (status: Status, title?: string) => {
59 | if (isBody(status)) {
60 | status = {
61 | body: status
62 | };
63 | }
64 | if (title) {
65 | status.title = title;
66 | }
67 | status.type = 'warning';
68 | return notify(status);
69 | },
70 | error: (status: Status, title?: string) => {
71 | const notification = {} as ToastOptions;
72 |
73 | if (isBody(status)) {
74 | notification.body = status;
75 | }
76 |
77 | if (title) {
78 | notification.title = title;
79 | }
80 |
81 | notification.type = 'error';
82 |
83 | return notify(notification);
84 | },
85 | loader: (status: Status, title?: string) => {
86 | if (isBody(status)) {
87 | status = {
88 | body: status
89 | };
90 | }
91 | if (title) {
92 | status.title = title;
93 | }
94 | status.mode = 'loader';
95 | return notify(status);
96 | },
97 | prompt: async (status: Omit & { answers: Record }) => {
98 | (status as ToastOptions).mode = 'prompt';
99 |
100 | const events = useVtEvents();
101 | const toastId = app.container.add(status);
102 |
103 | return new Promise(resolve => {
104 | events.once('vtPromptResponse', payload => {
105 | if (payload.id === toastId) {
106 | resolve(payload.response as T);
107 | }
108 | });
109 | });
110 | },
111 | stopLoader(id?: Toast['id']): number {
112 | return app.container.stopLoader(id);
113 | },
114 | findToast(id?: Toast['id']): Toast | undefined {
115 | return app.container.get(id);
116 | },
117 | getToasts(): Toast[] {
118 | return app.container.get() as unknown as Toast[];
119 | },
120 | updateToast(id: Toast['id'], status: ToastOptions): boolean {
121 | return app.container.set(id, status);
122 | },
123 | remove(id?: Toast['id']): number {
124 | return app.container.remove(id);
125 | },
126 | settings(settings?: Settings): Settings {
127 | const settingsComposable = useSettings();
128 |
129 | return settings ? settingsComposable.updateSettings(settings) : settingsComposable.settings;
130 | }
131 | };
132 |
133 | export default function useToast(): ToastPluginAPI {
134 | return toastMethods;
135 | }
136 |
--------------------------------------------------------------------------------
/src/composables/useVtEvents.ts:
--------------------------------------------------------------------------------
1 | import type { Coordinates } from './useDraggable';
2 |
3 | /**
4 | * Event name mapping to the payload of the type.
5 | */
6 | type EventMap = {
7 | vtDragFinished: { id: string; position: Coordinates };
8 | vtBeingDragged: { id: string; position: Coordinates };
9 | vtDragStarted: { id: string; position: Coordinates };
10 | vtDismissed: { id: string };
11 | vtStarted: { id: string };
12 | vtFinished: { id: string };
13 | vtLoadStop: { id: string };
14 | vtPromptResponse: { id: string; response: any };
15 | vtPaused: { id: string };
16 | vtResumed: { id: string };
17 | };
18 |
19 | export type EventName = keyof EventMap;
20 |
21 | type Events = {
22 | on: (event: T, callback: (payload: EventMap[T]) => void) => void;
23 | once: (event: T, callback: (payload: EventMap[T]) => void) => void;
24 | off: (event: T, callback?: (payload: EventMap[T]) => void) => void;
25 | emit: (event: T, payload: EventMap[T]) => void;
26 | };
27 |
28 | const events: Record = {
29 | vtDragFinished: [],
30 | vtBeingDragged: [],
31 | vtDragStarted: [],
32 | vtDismissed: [],
33 | vtStarted: [],
34 | vtFinished: [],
35 | vtLoadStop: [],
36 | vtPromptResponse: [],
37 | vtPaused: [],
38 | vtResumed: []
39 | };
40 |
41 | export default function useVtEvents(): Events {
42 | return {
43 | on(event, callback) {
44 | if (!events[event]) {
45 | events[event] = [];
46 | }
47 |
48 | events[event].push(callback);
49 | },
50 | once(event, callback) {
51 | const onceCallback = (payload: EventMap[typeof event]) => {
52 | callback(payload);
53 | this.off(event, onceCallback);
54 | };
55 |
56 | this.on(event, onceCallback);
57 | },
58 | off(event, callback) {
59 | if (!events[event]) {
60 | return;
61 | }
62 |
63 | if (callback) {
64 | events[event] = events[event].filter(cb => cb !== callback);
65 | } else {
66 | events[event] = [];
67 | }
68 | },
69 | emit(event, payload) {
70 | if (!events[event]) {
71 | return;
72 | }
73 |
74 | events[event].forEach(callback => callback(payload));
75 | }
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import useVtEvents from './composables/useVtEvents';
2 | import plugin from './plugin';
3 | import useToast from './composables/useToast';
4 | import { default as useVtSettings } from './composables/useSettings';
5 | import createVtTheme, { getCssRules } from './composables/createVtTheme';
6 |
7 | export type { Settings, ToastOptions, Status } from './type';
8 | export type { ToastPluginAPI, CustomMethods } from './composables/useToast';
9 |
10 | export {
11 | useVtEvents,
12 | useToast,
13 | useVtSettings,
14 | createVtTheme,
15 | getCssRules
16 | };
17 |
18 | export default plugin;
19 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './App.vue';
3 | import plugin from './index';
4 | import type { Settings } from './type';
5 | import './assets/toast.scss';
6 | import './assets/themes/dark.scss';
7 | import './assets/themes/light.scss';
8 |
9 | createApp(App).use(plugin, {}).mount('#app');
10 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import type { InjectionKey, Plugin } from 'vue';
2 | import type { ContainerMethods, Settings, Status, ToastOptions } from './type';
3 | import { createApp } from 'vue';
4 | // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
5 | // @ts-ignore - the shim is not being picked up by the build ts config, and we don't want to deliver the shim either
6 | import ToastContainer from './components/ToastContainer.vue';
7 | import { toastMethods, app } from './composables/useToast';
8 | import useSettings from './composables/useSettings';
9 | import { isBody } from './utils';
10 |
11 | const pluginInjectionKey: InjectionKey = Symbol('vue-toastify');
12 | const plugin: Plugin = (_, settings: Settings = {}) => {
13 | useSettings().updateSettings(settings);
14 |
15 | const mountPoint = document.createElement('div');
16 | mountPoint.setAttribute('id', pluginInjectionKey.toString());
17 | document.body.appendChild(mountPoint);
18 |
19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
20 | app.container = createApp(ToastContainer).mount(mountPoint) as unknown as ContainerMethods;
21 |
22 | if (
23 | settings.customNotifications &&
24 | Object.entries(settings.customNotifications).length > 0
25 | ) {
26 | Object.entries(settings.customNotifications).forEach(keyValArr => {
27 | Object.defineProperty(toastMethods, keyValArr[0], {
28 | get() {
29 | return (status: Status, title?: string) => {
30 | let toast: ToastOptions = Object.assign({}, keyValArr[1]);
31 |
32 | if (isBody(status)) {
33 | toast.body = status;
34 | } else {
35 | toast = { ...keyValArr[1], ...status };
36 | }
37 |
38 | if (title) {
39 | toast.title = title;
40 | }
41 |
42 | if (!toast.type) {
43 | toast.type = 'success';
44 | }
45 |
46 | return app.container.add(toast);
47 | };
48 | }
49 | });
50 | });
51 | }
52 | };
53 |
54 | export default plugin;
55 |
--------------------------------------------------------------------------------
/src/shims/vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import type { DefineComponent } from 'vue';
3 | const component: DefineComponent;
4 | export default component;
5 | }
6 |
--------------------------------------------------------------------------------
/src/type.ts:
--------------------------------------------------------------------------------
1 | import type { VNode } from 'vue';
2 |
3 | export type XPosition = 'left' | 'center' | 'right';
4 | export type YPosition = 'top' | 'center' | 'bottom';
5 | export type Position = `${YPosition}-${XPosition}`;
6 |
7 | export type MaybeArray = T | T[];
8 |
9 | export interface Icon {
10 | /**
11 | * The HTML element name to use.
12 | */
13 | tag?: keyof HTMLElementTagNameMap;
14 | /**
15 | * The HTML to use.
16 | */
17 | ligature?: string;
18 | }
19 |
20 | /**
21 | * Settings that can be applied to both toasts and global settings.
22 | */
23 | export interface BaseSettings {
24 | /**
25 | * Whether the toast disappears after a time.
26 | *
27 | * @default true
28 | */
29 | canTimeout?: boolean;
30 |
31 | /**
32 | * Whether the toast can be paused by hovering over the toast.
33 | *
34 | * @default true
35 | */
36 | pauseOnHover?: boolean;
37 |
38 | /**
39 | * Whether a default title should be shown if no title is supplied.
40 | *
41 | * @default true
42 | */
43 | defaultTitle?: boolean;
44 |
45 | /**
46 | * Whether the progressbar should be shown on the notification or not.
47 | *
48 | * @default false
49 | */
50 | hideProgressbar?: boolean;
51 |
52 | /**
53 | * The theme that should be displaying.
54 | * By default, there's `light` and `dark` theme.
55 | *
56 | * @default 'dark'
57 | */
58 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
59 | theme?: 'light' | 'dark' | string;
60 |
61 | /**
62 | * If string supplied this will apply the usual transition classes (e.g.: .name-enter-active).
63 | * If an object is supplied, it expects a `name` and optionally a `moveClass`
64 | * (this class has to use `!important` for its rules) attribute.
65 | * The name will be applied as above. The move class is applied when the notifications adjust their position.
66 | */
67 | transition?: string | { name: string; moveClass?: string };
68 |
69 | /**
70 | * If set to false, no icon will be shown on the notification.
71 | *
72 | * @default true
73 | */
74 | iconEnabled?: boolean;
75 |
76 | /**
77 | * Whether to enable dismissing the notification by dragging or not.
78 | *
79 | * @default true
80 | */
81 | draggable?: boolean;
82 |
83 | /**
84 | * A number between 0-5 representing how far the notification should be dragged to dismiss.
85 | *
86 | * @default 0.75
87 | */
88 | // todo - 5 is probably an option for too high value
89 | dragThreshold?: number;
90 |
91 | /**
92 | * Whether the toast should pause when the window loses focus.
93 | *
94 | * @default true
95 | */
96 | pauseOnFocusLoss?: boolean;
97 |
98 | /**
99 | * If set to true, the string body and string icon of a toast can be interpreted as HTML directly.
100 | *
101 | * **Warning**: This is a potential avenue for XSS issues,
102 | * so use sanitisers (e.g. dompurify) when putting user input in toasts if you have this enabled.
103 | *
104 | * - For the body, any string will be treated as HTML.
105 | * - For the icon, if the string has `]