├── .npmignore ├── .gitattributes ├── .gitignore ├── .editorconfig ├── LICENSE ├── gulpfile.js ├── src ├── jade │ ├── github.html │ ├── site.css │ └── index.jade ├── css │ └── native-toast.styl └── js │ └── index.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | /site 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | /dist 5 | /site 6 | index.html 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (github.com/egoist) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const gulp = require('gulp') 3 | const stylus = require('gulp-stylus') 4 | const jade = require('gulp-jade') 5 | const serve = require('gulp-serve') 6 | const autoprefixer = require('gulp-autoprefixer') 7 | const spawnSync = require('child_process').spawnSync 8 | 9 | gulp.task('serve', serve({ 10 | hostname: 'localhost', 11 | port: 3099, 12 | root: './' 13 | })) 14 | 15 | gulp.task('js', () => { 16 | spawnSync('npm', ['run', 'build:js'], {stdio: 'inherit'}) 17 | }) 18 | 19 | gulp.task('css', () => { 20 | return gulp.src('./src/css/*.styl') 21 | .pipe(stylus()) 22 | .pipe(autoprefixer()) 23 | .pipe(gulp.dest('dist')) 24 | }) 25 | 26 | gulp.task('jade', () => { 27 | return gulp.src('./src/jade/index.jade') 28 | .pipe(jade({ 29 | locals: { 30 | title: 'Native Toast', 31 | time: Date.now() 32 | } 33 | })) 34 | .pipe(gulp.dest('.')) 35 | }) 36 | 37 | gulp.task('watch', () => { 38 | gulp.watch('./src/js/*.js', ['js']) 39 | gulp.watch('./src/css/*.styl', ['css']) 40 | gulp.watch('./src/jade/*', ['jade']) 41 | }) 42 | 43 | gulp.task('build', ['js', 'css', 'jade']) 44 | 45 | gulp.task('default', ['build', 'watch', 'serve']) 46 | -------------------------------------------------------------------------------- /src/jade/github.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "native-toast", 3 | "version": "2.0.0", 4 | "description": "Native-like toast notification but for the web.", 5 | "license": "MIT", 6 | "repository": "egoist/native-toast", 7 | "author": { 8 | "name": "EGOIST", 9 | "email": "0x142857@gmail.com", 10 | "url": "https://github.com/egoist" 11 | }, 12 | "engines": { 13 | "node": ">=4" 14 | }, 15 | "scripts": { 16 | "test": "eslint src/js/*.js", 17 | "prepublishOnly": "npm run build", 18 | "build": "gulp build", 19 | "build:js": "bili src/js/index.js --js buble --format cjs,umd,umd-min --module-name nativeToast", 20 | "dev": "gulp", 21 | "gh": "npm run build && npm run site && gh-pages -d ./site", 22 | "site": "mkdir -p site && cp -r dist site/ && cp index.html site/" 23 | }, 24 | "main": "dist/native-toast.cjs.js", 25 | "cdn": "dist/native-toast.js", 26 | "unpkg": "dist/native-toast.js", 27 | "jsdelivr": "dist/native-toast.js", 28 | "files": [ 29 | "dist" 30 | ], 31 | "keywords": [ 32 | "toast", 33 | "android", 34 | "notification" 35 | ], 36 | "devDependencies": { 37 | "bili": "^3.0.15", 38 | "eslint": "^3.13.1", 39 | "eslint-config-rem": "^2.0.2", 40 | "gh-pages": "^0.12.0", 41 | "gulp": "^3.9.1", 42 | "gulp-autoprefixer": "^5.0.0", 43 | "gulp-jade": "^1.1.0", 44 | "gulp-postcss": "^6.1.1", 45 | "gulp-serve": "^1.2.0", 46 | "postcss-cssnext": "^2.5.2", 47 | "postcss-mixins": "^6.2.0" 48 | }, 49 | "dependencies": { 50 | "gulp-stylus": "^2.7.0", 51 | "nano-assign": "^1.0.0" 52 | }, 53 | "eslintConfig": { 54 | "extends": "rem/esnext-browser" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/jade/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-size: 1rem; 8 | line-height: 1.5; 9 | font-family: -apple-system, BlinkMacSystemFont, 10 | 'avenir next', avenir, 11 | helvetica, 'helvetica neue', 12 | Ubuntu, 13 | 'segoe ui', arial, 14 | sans-serif; 15 | } 16 | 17 | .main { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | text-align: center; 22 | padding-top: 120px; 23 | flex-direction: column; 24 | } 25 | 26 | .buttons { 27 | max-width: 600px; 28 | display: flex; 29 | } 30 | 31 | .buttons > div:first-child { 32 | margin-right: 20px; 33 | } 34 | 35 | .buttons > div { 36 | padding: 20px; 37 | margin-bottom: 20px; 38 | background-color: #ddd; 39 | box-shadow: 0 2px 5px #ccc; 40 | border: 1px solid #ccc; 41 | border-radius: 4px; 42 | overflow: hidden; 43 | } 44 | 45 | .buttons .buttons-right { 46 | } 47 | 48 | .md-button { 49 | background: #3f51b5; 50 | border: none; 51 | border-radius: 2px; 52 | color: #fff; 53 | position: relative; 54 | height: 36px; 55 | margin: 0; 56 | padding: 0 16px; 57 | display: inline-block; 58 | font-family: "Roboto","Helvetica","Arial",sans-serif; 59 | font-size: 14px; 60 | font-weight: 500; 61 | text-transform: uppercase; 62 | letter-spacing: 0; 63 | overflow: hidden; 64 | will-change: box-shadow; 65 | transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1); 66 | outline: none; 67 | cursor: pointer; 68 | text-decoration: none; 69 | text-align: center; 70 | line-height: 36px; 71 | vertical-align: middle; 72 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); 73 | } 74 | 75 | .click-button { 76 | display: block; 77 | margin: 0 auto; 78 | } 79 | 80 | .click-button:not(:last-child) { 81 | margin-bottom: 20px; 82 | } 83 | 84 | .site-name { 85 | color: black; 86 | font-weight: 400; 87 | font-size: 2.4rem; 88 | margin: 0; 89 | margin-bottom: 30px; 90 | } 91 | 92 | .control { 93 | margin-bottom: 20px; 94 | } 95 | 96 | .control label { 97 | display: block; 98 | } 99 | -------------------------------------------------------------------------------- /src/css/native-toast.styl: -------------------------------------------------------------------------------- 1 | .native-toast 2 | position fixed 3 | background-color rgba(50, 50, 50, 0.8) 4 | border-radius 3px 5 | color #fff 6 | text-align center 7 | padding 10px 12px 8 | opacity 0 9 | z-index 99999 10 | transition transform 0.25s, opacity 0.25s, top 0.25s, bottom 0.25s 11 | box-sizing border-box 12 | max-width: 18rem 13 | 14 | toast-position(position) 15 | .native-toast-{position} 16 | if match('north', position) 17 | left: 50% 18 | transform: translateX(-50%) 19 | top: -50px 20 | if match('south', position) 21 | left: 50% 22 | transform: translateX(-50%) 23 | bottom: -50px 24 | if match('west', position) 25 | left: 50px 26 | transform: translateX(0) 27 | if match('east', position) 28 | left: auto 29 | right: 50px 30 | transform: translateX(0) 31 | if position is center 32 | left: 50% 33 | transform: translate(-50%, -50%) 34 | if match('^(east|west|center)$', position) 35 | bottom: -50px 36 | &.native-toast-shown 37 | opacity 1 38 | if match('north', position) 39 | top: 50px 40 | if match('south', position) 41 | bottom: 50px 42 | if match('^(east|west|center)$', position) 43 | bottom: 50% 44 | &.native-toast-edge 45 | if match('north', position) 46 | top: 0 47 | if match('south', position) 48 | bottom: 0 49 | 50 | toast-position('north') 51 | toast-position('north-west') 52 | toast-position('north-east') 53 | 54 | toast-position('south') 55 | toast-position('south-west') 56 | toast-position('south-east') 57 | 58 | toast-position('center') 59 | toast-position('west') 60 | toast-position('east') 61 | 62 | .native-toast-edge 63 | border-radius 0 64 | max-width 100% 65 | width 100% 66 | text-align left 67 | left: 0 68 | transform: none 69 | 70 | .native-toast-error 71 | background-color #ff3d3d 72 | color #fff 73 | 74 | .native-toast-success 75 | background-color #62a465 76 | color #fff 77 | 78 | .native-toast-warning 79 | background-color darken(#ffce65, 15) 80 | color #fff 81 | 82 | .native-toast-info 83 | background-color #67daff 84 | color #fff 85 | 86 | [class^='native-toast-icon-'] 87 | vertical-align middle 88 | margin-right 5px 89 | svg 90 | width 16px 91 | height 16px 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # native-toast [![NPM version](https://badgen.net/npm/v/native-toast)](https://npmjs.com/package/native-toast) [![NPM downloads](https://badgen.net/npm/dm/native-toast)](https://npmjs.com/package/native-toast) 2 | 3 | > Native-like toast notification but for the web. (JS + CSS ≈ 4KB) 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ npm install --save native-toast 9 | ``` 10 | 11 | NPMCDN: https://unpkg.com/native-toast/dist/ 12 | 13 | ## Usage 14 | 15 | First import `native-toast/dist/native-toast.css`, then: 16 | 17 | ```js 18 | import nativeToast from 'native-toast' 19 | 20 | nativeToast({ 21 | message: 'wait wait!', 22 | position: 'north-east', 23 | // Self destroy in 5 seconds 24 | timeout: 5000, 25 | type: 'warning' 26 | }) 27 | // or nativeToast.warning(options) 28 | ``` 29 | 30 | Four types: `success` `warning` `info` `error` 31 | 32 | ## API 33 | 34 | ### nativeToast(options) 35 | 36 | #### options 37 | 38 | ##### message 39 | 40 | Type: `string`
41 | Default: `''` 42 | 43 | Toast message. 44 | 45 | ##### position 46 | 47 | Type: `string`
48 | Default: `south-east`
49 | Values: `center` `west` `east` `south` `south-west` `south-east` `north` `north-west` `north-east` 50 | 51 | Toast position. 52 | 53 | ##### rounded 54 | 55 | Type: `boolean`
56 | Default: `false` 57 | 58 | Set `border-radius` to `33px` instead of `3px`. Has no effect when `edge === false`. 59 | 60 | ##### timeout 61 | 62 | Type: `number`
63 | Default: `3000` 64 | 65 | Self destroy in specfic timeout. If given `0` or `false then toast will never self destroy. 66 | 67 | ##### type 68 | 69 | Type: `string`
70 | Default: `undefined` 71 | 72 | One of `success` `warning` `info` `error`. 73 | 74 | A short-hand usage: `nativeToast.success(opts)` `nativeToast.error(opts)` and such. 75 | 76 | ##### icon 77 | 78 | Type: `boolean`
79 | Default: `true` 80 | 81 | Set to `false` to disable icon. 82 | 83 | ##### edge 84 | 85 | Type: `boolean`
86 | Default: `false` 87 | 88 | Show toast on the edge. 89 | 90 | ##### closeOnClick 91 | 92 | Type: `boolean`
93 | Default: `false` 94 | 95 | Close the toast when clicked. 96 | 97 | ##### elements 98 | 99 | Type: `HTMLElement[]` 100 | 101 | Optionally provide an array of HTML elements to insert after the `message`. 102 | 103 | ## License 104 | 105 | MIT © [EGOIST](https://github.com/egoist) 106 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import assign from 'nano-assign' 2 | 3 | let prevToast = null 4 | 5 | const icons = { 6 | warning: ``, 7 | success: ``, 8 | info: ``, 9 | error: `` 10 | } 11 | 12 | class Toast { 13 | constructor({ 14 | message = '', 15 | position = 'south-east', 16 | timeout = 3000, 17 | el = document.body, 18 | rounded = false, 19 | type = '', 20 | debug = false, 21 | edge = false, 22 | icon = true, 23 | closeOnClick = false, 24 | elements = [] 25 | } = {}) { 26 | if (prevToast) { 27 | prevToast.destroy() 28 | } 29 | 30 | this.message = message 31 | this.position = position 32 | this.el = el 33 | this.timeout = timeout 34 | this.closeOnClick = closeOnClick 35 | 36 | this.toast = document.createElement('div') 37 | this.toast.className = `native-toast native-toast-${this.position}` 38 | 39 | if (type) { 40 | this.toast.className += ` native-toast-${type}` 41 | 42 | if (icon) { 43 | this.message = `${icons[type] || ''}${this.message}` 44 | } 45 | } 46 | 47 | const messageElement = document.createElement('div') 48 | messageElement.className = 'native-toast-message' 49 | messageElement.innerHTML = this.message 50 | 51 | ;[messageElement, ...elements].forEach(el => { 52 | this.toast.appendChild(el) 53 | }) 54 | 55 | const isMobile = document.body.clientWidth < 768 56 | if (edge || isMobile) { 57 | this.toast.className += ' native-toast-edge' 58 | } else if (rounded) { 59 | this.toast.style.borderRadius = '33px' 60 | } 61 | 62 | this.el.appendChild(this.toast) 63 | 64 | prevToast = this 65 | 66 | this.show() 67 | if (!debug && timeout) { 68 | this.hide() 69 | } 70 | 71 | if (this.closeOnClick) { 72 | this.toast.addEventListener('click', () => { 73 | this.destroy() 74 | }) 75 | } 76 | } 77 | 78 | show() { 79 | setTimeout(() => { 80 | this.toast.classList.add('native-toast-shown') 81 | }, 300) 82 | } 83 | 84 | hide() { 85 | setTimeout(() => { 86 | this.destroy() 87 | }, this.timeout) 88 | } 89 | 90 | destroy() { 91 | if (!this.toast) return 92 | 93 | this.toast.classList.remove('native-toast-shown') 94 | 95 | setTimeout(() => { 96 | if (this.toast) { 97 | this.el.removeChild(this.toast) 98 | this.toast = null 99 | } 100 | }, 300) 101 | } 102 | } 103 | 104 | function toast(options) { 105 | return new Toast(options) 106 | } 107 | 108 | for (const type of ['success', 'info', 'warning', 'error']) { 109 | toast[type] = options => toast(assign({type}, options)) 110 | } 111 | 112 | export default toast 113 | 114 | -------------------------------------------------------------------------------- /src/jade/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset="utf-8") 5 | meta(name="viewport" content="width=device-width, initial-scale=1") 6 | meta(http-equiv="X-UA-Compatible" content="IE=edge,chrome=1") 7 | title=title 8 | style 9 | include site.css 10 | link(rel="stylesheet", href="./dist/native-toast.css?t=#{time}") 11 | body 12 | include github.html 13 | .main 14 | h1.site-name Native Toast 15 | .control 16 | label 17 | input(type="checkbox" id="switch-edge-mode") 18 | | Edge mode (show on the edge) 19 | label 20 | input(type="checkbox" id="switch-debug-mode") 21 | | debug mode (do not self-destruct) 22 | label 23 | input(type="checkbox" id="switch-close-on-click-mode") 24 | | Close on click 25 | .buttons 26 | .buttons-group 27 | button.md-button.click-button(onclick="fromPosition('north')") North 28 | button.md-button.click-button(onclick="fromPosition('north-west')") North West 29 | button.md-button.click-button(onclick="fromPosition('north-east')") North East 30 | button.md-button.click-button(onclick="fromPosition('south')") South 31 | button.md-button.click-button(onclick="fromPosition('south-west')") South West 32 | button.md-button.click-button(onclick="fromPosition('south-east')") South East 33 | button.md-button.click-button(onclick="fromPosition('center')") Center 34 | button.md-button.click-button(onclick="fromPosition('west')") West 35 | button.md-button.click-button(onclick="fromPosition('east')") East 36 | button.md-button.click-button(onclick="rounded()") Rounded 37 | button.md-button.click-button(onclick="largeText()") Large Text 38 | .buttons-group 39 | button.md-button.click-button(onclick="error()") Error 40 | button.md-button.click-button(onclick="warning()") Warning 41 | button.md-button.click-button(onclick="success()") Success 42 | button.md-button.click-button(onclick="info()") Info 43 | script(src="./dist/native-toast.js?t=#{time}") 44 | script. 45 | var useEdge = false 46 | var useDebug = false 47 | var closeOnClick = false 48 | function fromPosition(position) { 49 | nativeToast({ 50 | message: `No more posts`, 51 | position: position, 52 | edge: useEdge, 53 | debug: useDebug, 54 | closeOnClick: closeOnClick 55 | }) 56 | } 57 | function rounded() { 58 | nativeToast({ 59 | message: 'No more posts', 60 | rounded: true, 61 | edge: useEdge, 62 | debug: useDebug, 63 | closeOnClick: closeOnClick 64 | }) 65 | } 66 | function largeText() { 67 | nativeToast({ 68 | message: `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`, 69 | edge: useEdge, 70 | debug: useDebug, 71 | closeOnClick: closeOnClick 72 | }) 73 | } 74 | function error() { 75 | nativeToast({ 76 | message: 'Something bad happened!', 77 | type: 'error', 78 | edge: useEdge, 79 | debug: useDebug, 80 | closeOnClick: closeOnClick 81 | }) 82 | } 83 | function info() { 84 | nativeToast({ 85 | message: 'Some information!', 86 | type: 'info', 87 | edge: useEdge, 88 | debug: useDebug, 89 | closeOnClick: closeOnClick 90 | }) 91 | } 92 | function warning() { 93 | nativeToast.warning({ 94 | message: 'Something warning!', 95 | type: 'warning', 96 | edge: useEdge, 97 | debug: useDebug, 98 | closeOnClick: closeOnClick 99 | }) 100 | } 101 | function success() { 102 | nativeToast({ 103 | message: 'Success message!', 104 | type: 'success', 105 | edge: useEdge, 106 | debug: useDebug, 107 | closeOnClick: closeOnClick 108 | }) 109 | } 110 | function edge() { 111 | nativeToast({ 112 | message: 'Bottom edge!', 113 | edge: useEdge, 114 | debug: useDebug, 115 | closeOnClick: closeOnClick 116 | }) 117 | } 118 | document.getElementById('switch-edge-mode').addEventListener('change', function (e) { 119 | useEdge = e.target.checked 120 | }) 121 | document.getElementById('switch-debug-mode').addEventListener('change', function (e) { 122 | useDebug = e.target.checked 123 | }) 124 | document.getElementById('switch-close-on-click-mode').addEventListener('change', function (e) { 125 | closeOnClick = e.target.checked 126 | }) 127 | --------------------------------------------------------------------------------