├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── build.js ├── utils │ ├── index.js │ ├── log.js │ ├── style.js │ └── write.js ├── webpack.config.base.js ├── webpack.config.dev.js └── webpack.config.dll.js ├── package.json ├── src ├── IntersectionObserver.js ├── MutationObserver.js ├── ResizeObserver.js └── index.js ├── test ├── .eslintrc ├── helpers │ ├── Test.vue │ ├── index.js │ ├── utils.js │ └── wait-for-update.js ├── index.js ├── karma.conf.js ├── specs │ ├── Hello.spec.js │ ├── IntersectionObserver.spec.js │ ├── MutationObserver.spec.js │ └── ResizeObserver.spec.js └── visual.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions" 9 | ] 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-vue-jsx", 16 | "transform-object-rest-spread" 17 | ], 18 | "env": { 19 | "test": { 20 | "plugins": [ 21 | "istanbul" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'vue', 8 | // add your custom rules here 9 | 'rules': { 10 | // allow async-await 11 | 'generator-star-spacing': 0, 12 | // allow debugger during development 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 14 | }, 15 | globals: { 16 | requestAnimationFrame: true, 17 | performance: true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | dist 6 | yarn-error.log 7 | reports 8 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-html"], 3 | "extends": "stylelint-config-standard", 4 | "rules": { 5 | "no-empty-source": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.1.4](https://github.com/apertureless/vue-observable/compare/v0.1.2...v0.1.4) (2019-05-15) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * Remove console.logs, update Resize Observer warnings, update doc links ([#4](https://github.com/apertureless/vue-observable/issues/4)) ([f143f1f](https://github.com/apertureless/vue-observable/commit/f143f1f)) 11 | 12 | 13 | 14 | ### [0.1.2](https://github.com/apertureless/vue-observable/compare/v0.1.1...v0.1.2) (2019-05-15) 15 | 16 | 17 | 18 | 19 | ## 0.1.1 (2018-04-25) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **intersection-observer:** Remove prop type validation of root prop ([e2434a3](https://github.com/apertureless/vue-observable/commit/e2434a3)) 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/apertureless/vue-observable). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `npm run lint:fix` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch visual tests and watch the components at the same time 30 | 31 | ``` bash 32 | $ npm run dev 33 | ``` 34 | 35 | 36 | **Happy coding**! 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jakub Juszczak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-observable 2 | 3 | [![npm](https://img.shields.io/npm/v/vue-observable.svg)](https://www.npmjs.com/package/vue-observable) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) 4 | 5 | > Abstract Vue Components to utilizing the IntersectionObserver, MutationObserver and ResizeObserver Browser APIs 6 | 7 | 🔥 Please keep in mind that you will need polyfills 8 | 9 | ## 🔧 Installation 10 | 11 | ```bash 12 | npm install --save vue-observable 13 | ``` 14 | 15 | ## 👈 Usage 16 | 17 | ### Componentlist 18 | 19 | - [``](./src/IntersectionObserver.js) 20 | - [``](./src/MutationObserver.js) 21 | - [``](./src/ResizeObserver.js) 22 | 23 | ### Bundler (Webpack, Rollup) 24 | 25 | ```js 26 | import Vue from 'vue' 27 | import VueObservable from 'vue-observable' 28 | 29 | Vue.use(VueObservable) 30 | ``` 31 | 32 | or 33 | 34 | ```js 35 | import {Intersect, Mutation, Resize} from 'vue-observable' 36 | 37 | export default { 38 | components: {Intersect, Mutation, Resize} 39 | } 40 | ``` 41 | 42 | ### Browser 43 | 44 | ```html 45 | 46 | 47 | 48 | 49 | 50 | 51 | ``` 52 | 53 | ## 📒 Components 54 | 55 | ### IntersectionObserver - [``](/src/IntersectionnObserver.js) 56 | 57 | The `` component will detect if a given element is in the viewport. And emit an event. 58 | 59 | #### Props 60 | 61 | | Prop | Required | Default | 62 | | --- | --- | --- | 63 | | [root](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root) | no | null 64 | | [rootMargin](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) | no | `0px 0px 0px 0px`| 65 | | [threshold](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds) | no | `[0, 0.2]`| 66 | 67 | #### Events 68 | 69 | - `enter` 70 | - `leave` 71 | - `change` 72 | 73 | #### Usage 74 | 75 | ```html 76 | 77 | 78 | 79 | ``` 80 | 81 | ### MutationObserver - [``](/src/MutationObserver.js) 82 | 83 | #### Props 84 | 85 | | Prop | Required | Default | 86 | | --- | --- | --- | 87 | | [attributeFilter](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/attributeFilter) | no | null 88 | | [attributeOldValue](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/attributeOldValue) | no | null 89 | | [attributes](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/attributes) | no | false 90 | | [characterData](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/characterData) | no |null 91 | | [characterDataOldValue](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/characterDataOldValue) | no |null 92 | | [childList](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/childList) | no |false 93 | | [subtree](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit/subtree) | no |false 94 | 95 | #### Events 96 | 97 | - `mutation` 98 | 99 | #### Usage 100 | 101 | ```html 102 | 103 | 104 | 105 | ``` 106 | 107 | ### ResizeObserver - [``](/src/ResizeObserver.js) 108 | 109 | #### Props 110 | 111 | N/A 112 | 113 | #### Events 114 | 115 | - `resize` 116 | 117 | #### Usage 118 | 119 | ```html 120 | 121 | 122 | 123 | ``` 124 | 125 | ## Development 126 | 127 | ### Launch visual tests 128 | 129 | ```bash 130 | npm run dev 131 | ``` 132 | 133 | ### Launch Karma with coverage 134 | 135 | ```bash 136 | npm run dev:coverage 137 | ``` 138 | 139 | ### Build 140 | 141 | Bundle the js and css of to the `dist` folder: 142 | 143 | ```bash 144 | npm run build 145 | ``` 146 | 147 | 148 | ## Publishing 149 | 150 | The `prepublish` hook will ensure dist files are created before publishing. This 151 | way you don't need to commit them in your repository. 152 | 153 | ```bash 154 | # Bump the version first 155 | # It'll also commit it and create a tag 156 | npm version 157 | # Push the bumped package and tags 158 | git push --follow-tags 159 | # Ship it 🚀 160 | npm publish 161 | ``` 162 | 163 | ## License 164 | 165 | [MIT](http://opensource.org/licenses/MIT) 166 | 167 | 168 | ## Support 169 | 170 | Buy Me A Coffee 171 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require('mkdirp') 2 | const rollup = require('rollup').rollup 3 | const vue = require('rollup-plugin-vue') 4 | const jsx = require('rollup-plugin-jsx') 5 | const buble = require('rollup-plugin-buble') 6 | const replace = require('rollup-plugin-replace') 7 | const cjs = require('rollup-plugin-commonjs') 8 | const node = require('rollup-plugin-node-resolve') 9 | const uglify = require('uglify-js') 10 | const CleanCSS = require('clean-css') 11 | 12 | // Make sure dist dir exists 13 | mkdirp('dist') 14 | 15 | const { 16 | logError, 17 | write, 18 | banner, 19 | name, 20 | moduleName, 21 | version, 22 | processStyle 23 | } = require('./utils') 24 | 25 | function rollupBundle ({ env }) { 26 | return rollup({ 27 | entry: 'src/index.js', 28 | plugins: [ 29 | node({ 30 | extensions: ['.js', '.jsx', '.vue'] 31 | }), 32 | cjs(), 33 | vue({ 34 | compileTemplate: true, 35 | css (styles, stylesNodes) { 36 | // Only generate the styles once 37 | if (env['process.env.NODE_ENV'] === '"production"') { 38 | Promise.all( 39 | stylesNodes.map(processStyle) 40 | ).then(css => { 41 | const result = css.map(c => c.css).join('') 42 | // write the css for every component 43 | // TODO add it back if we extract all components to individual js 44 | // files too 45 | // css.forEach(writeCss) 46 | write(`dist/${name}.css`, result) 47 | write(`dist/${name}.min.css`, new CleanCSS().minify(result).styles) 48 | }).catch(logError) 49 | } 50 | } 51 | }), 52 | jsx({ factory: 'h' }), 53 | replace(Object.assign({ 54 | __VERSION__: version 55 | }, env)), 56 | buble({ 57 | objectAssign: 'Object.assign' 58 | }) 59 | ] 60 | }) 61 | } 62 | 63 | const bundleOptions = { 64 | banner, 65 | exports: 'named', 66 | format: 'umd', 67 | moduleName 68 | } 69 | 70 | function createBundle ({ name, env, format }) { 71 | return rollupBundle({ 72 | env 73 | }).then(function (bundle) { 74 | const options = Object.assign({}, bundleOptions) 75 | if (format) options.format = format 76 | const code = bundle.generate(options).code 77 | if (/min$/.test(name)) { 78 | const minified = uglify.minify(code, { 79 | output: { 80 | preamble: banner, 81 | ascii_only: true // eslint-disable-line camelcase 82 | } 83 | }).code 84 | return write(`dist/${name}.js`, minified) 85 | } else { 86 | return write(`dist/${name}.js`, code) 87 | } 88 | }).catch(logError) 89 | } 90 | 91 | // Browser bundle (can be used with script) 92 | createBundle({ 93 | name: `${name}`, 94 | env: { 95 | 'process.env.NODE_ENV': '"development"' 96 | } 97 | }) 98 | 99 | // Commonjs bundle (preserves process.env.NODE_ENV) so 100 | // the user can replace it in dev and prod mode 101 | createBundle({ 102 | name: `${name}.common`, 103 | env: {}, 104 | format: 'cjs' 105 | }) 106 | 107 | // uses export and import syntax. Should be used with modern bundlers 108 | // like rollup and webpack 2 109 | createBundle({ 110 | name: `${name}.esm`, 111 | env: {}, 112 | format: 'es' 113 | }) 114 | 115 | // Minified version for browser 116 | createBundle({ 117 | name: `${name}.min`, 118 | env: { 119 | 'process.env.NODE_ENV': '"production"' 120 | } 121 | }) 122 | -------------------------------------------------------------------------------- /build/utils/index.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | const { join } = require('path') 3 | 4 | const { 5 | red, 6 | logError 7 | } = require('./log') 8 | 9 | const { 10 | processStyle 11 | } = require('./style') 12 | 13 | const uppercamelcase = require('uppercamelcase') 14 | 15 | exports.write = require('./write') 16 | 17 | const { 18 | author, 19 | name, 20 | version, 21 | dllPlugin 22 | } = require('../../package.json') 23 | 24 | const authorName = author.replace(/\s+<.*/, '') 25 | const minExt = process.env.NODE_ENV === 'production' ? '.min' : '' 26 | 27 | exports.author = authorName 28 | exports.version = version 29 | exports.dllName = dllPlugin.name 30 | exports.moduleName = uppercamelcase(name) 31 | exports.name = name 32 | exports.filename = name + minExt 33 | exports.banner = `/*! 34 | * ${name} v${version} 35 | * (c) ${new Date().getFullYear()} ${authorName} 36 | * Released under the MIT License. 37 | */ 38 | ` 39 | 40 | // log.js 41 | exports.red = red 42 | exports.logError = logError 43 | 44 | // It'd be better to add a sass property to the vue-loader options 45 | // but it simply don't work 46 | const sassOptions = { 47 | includePaths: [ 48 | join(__dirname, '../../node_modules') 49 | ] 50 | } 51 | 52 | // don't extract css in test mode 53 | const nullLoader = process.env.NODE_ENV === 'common' ? 'null-loader!' : '' 54 | exports.vueLoaders = 55 | process.env.BABEL_ENV === 'test' ? { 56 | css: 'css-loader', 57 | scss: `css-loader!sass-loader?${JSON.stringify(sassOptions)}` 58 | } : { 59 | css: ExtractTextPlugin.extract(`${nullLoader}css-loader`), 60 | scss: ExtractTextPlugin.extract( 61 | `${nullLoader}css-loader!sass-loader?${JSON.stringify(sassOptions)}` 62 | ) 63 | } 64 | 65 | // style.js 66 | exports.processStyle = processStyle 67 | -------------------------------------------------------------------------------- /build/utils/log.js: -------------------------------------------------------------------------------- 1 | function logError (e) { 2 | console.log(e) 3 | } 4 | 5 | function blue (str) { 6 | return `\x1b[1m\x1b[34m${str}\x1b[39m\x1b[22m` 7 | } 8 | 9 | function green (str) { 10 | return `\x1b[1m\x1b[32m${str}\x1b[39m\x1b[22m` 11 | } 12 | 13 | function red (str) { 14 | return `\x1b[1m\x1b[31m${str}\x1b[39m\x1b[22m` 15 | } 16 | 17 | function yellow (str) { 18 | return `\x1b[1m\x1b[33m${str}\x1b[39m\x1b[22m` 19 | } 20 | 21 | module.exports = { 22 | blue, 23 | green, 24 | red, 25 | yellow, 26 | logError 27 | } 28 | -------------------------------------------------------------------------------- /build/utils/style.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const postcss = require('postcss') 3 | const cssnext = require('postcss-cssnext') 4 | const CleanCSS = require('clean-css') 5 | const { logError } = require('./log.js') 6 | const write = require('./write.js') 7 | 8 | function processCss (style) { 9 | const componentName = path.basename(style.id, '.vue') 10 | return postcss([cssnext()]) 11 | .process(style.code, {}) 12 | .then(result => { 13 | return { 14 | name: componentName, 15 | css: result.css, 16 | map: result.map 17 | } 18 | }) 19 | } 20 | 21 | let stylus 22 | function processStylus (style) { 23 | try { 24 | stylus = stylus || require('stylus') 25 | } catch (e) { 26 | logError(e) 27 | } 28 | const componentName = path.basename(style.id, '.vue') 29 | return new Promise((resolve, reject) => { 30 | stylus.render(style.code, function (err, css) { 31 | if (err) return reject(err) 32 | resolve({ 33 | original: { 34 | code: style.code, 35 | ext: 'styl' 36 | }, 37 | name: componentName, 38 | css 39 | }) 40 | }) 41 | }) 42 | } 43 | 44 | function processStyle (style) { 45 | if (style.lang === 'css') { 46 | return processCss(style) 47 | } else if (style.lang === 'stylus') { 48 | return processStylus(style) 49 | } else { 50 | throw new Error(`Unknown style language '${style.lang}'`) 51 | } 52 | } 53 | 54 | function writeCss (style) { 55 | write(`dist/${style.name}.css`, style.css) 56 | if (style.original) { 57 | write(`dist/${style.name}.${style.original.ext}`, style.original.code) 58 | } 59 | if (style.map) write(`dist/${style.name}.css.map`, style.map) 60 | write(`dist/${style.name}.min.css`, new CleanCSS().minify(style.css).styles) 61 | } 62 | 63 | module.exports = { 64 | writeCss, 65 | processStyle 66 | } 67 | -------------------------------------------------------------------------------- /build/utils/write.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const { blue } = require('./log.js') 4 | 5 | function write (dest, code) { 6 | return new Promise(function (resolve, reject) { 7 | fs.writeFile(dest, code, function (err) { 8 | if (err) return reject(err) 9 | console.log(blue(dest) + ' ' + getSize(code)) 10 | resolve(code) 11 | }) 12 | }) 13 | } 14 | 15 | function getSize (code) { 16 | return (code.length / 1024).toFixed(2) + 'kb' 17 | } 18 | 19 | module.exports = write 20 | -------------------------------------------------------------------------------- /build/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | const { resolve } = require('path') 4 | 5 | const { 6 | banner, 7 | filename, 8 | version, 9 | vueLoaders 10 | } = require('./utils') 11 | 12 | const plugins = [ 13 | new webpack.DefinePlugin({ 14 | '__VERSION__': JSON.stringify(version), 15 | 'process.env.NODE_ENV': '"test"' 16 | }), 17 | new webpack.BannerPlugin({ banner, raw: true, entryOnly: true }), 18 | new ExtractTextPlugin({ 19 | filename: `${filename}.css`, 20 | // Don't extract css in test mode 21 | disable: /^(common|test)$/.test(process.env.NODE_ENV) 22 | }) 23 | ] 24 | 25 | module.exports = { 26 | output: { 27 | path: resolve(__dirname, '../dist'), 28 | filename: `${filename}.common.js` 29 | }, 30 | entry: './src/index.js', 31 | resolve: { 32 | extensions: ['.js', '.vue', '.jsx', 'css'], 33 | alias: { 34 | 'src': resolve(__dirname, '../src') 35 | } 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /.jsx?$/, 41 | use: 'babel-loader', 42 | include: [ 43 | resolve(__dirname, '../node_modules/@material'), 44 | resolve(__dirname, '../src'), 45 | resolve(__dirname, '../test') 46 | ] 47 | }, 48 | { 49 | test: /\.vue$/, 50 | loader: 'vue-loader', 51 | options: { 52 | loaders: vueLoaders, 53 | postcss: [require('postcss-cssnext')()] 54 | } 55 | } 56 | ] 57 | }, 58 | plugins 59 | } 60 | -------------------------------------------------------------------------------- /build/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin') 5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 6 | const DashboardPlugin = require('webpack-dashboard/plugin') 7 | const base = require('./webpack.config.base') 8 | const { resolve, join } = require('path') 9 | const { existsSync } = require('fs') 10 | const { 11 | dllName, 12 | logError, 13 | red, 14 | vueLoaders 15 | } = require('./utils') 16 | 17 | const rootDir = resolve(__dirname, '../test') 18 | const buildPath = resolve(rootDir, 'dist') 19 | 20 | if (!existsSync(join(buildPath, dllName) + '.dll.js')) { 21 | logError(red('The DLL manifest is missing. Please run `npm run build:dll` (Quit this with `q`)')) 22 | process.exit(1) 23 | } 24 | 25 | const dllManifest = require( 26 | join(buildPath, dllName) + '.json' 27 | ) 28 | 29 | module.exports = merge(base, { 30 | entry: { 31 | tests: resolve(rootDir, 'visual.js') 32 | }, 33 | output: { 34 | path: buildPath, 35 | filename: '[name].js', 36 | chunkFilename: '[id].js' 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /.scss$/, 42 | loader: vueLoaders.scss, 43 | include: [ 44 | resolve(__dirname, '../node_modules/@material'), 45 | resolve(__dirname, '../src') 46 | ] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new webpack.DllReferencePlugin({ 52 | context: join(__dirname, '..'), 53 | manifest: dllManifest 54 | }), 55 | new HtmlWebpackPlugin({ 56 | chunkSortMode: 'dependency' 57 | }), 58 | new AddAssetHtmlPlugin({ 59 | filepath: require.resolve( 60 | join(buildPath, dllName) + '.dll.js' 61 | ) 62 | }), 63 | new webpack.optimize.CommonsChunkPlugin({ 64 | name: 'vendor', 65 | minChunks (module, count) { 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf(join(__dirname, '../node_modules/')) === 0 70 | ) 71 | } 72 | }), 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'manifest', 75 | chunks: ['vendor'] 76 | }), 77 | new DashboardPlugin(), 78 | new BundleAnalyzerPlugin({ 79 | analyzerMode: 'static', 80 | openAnalyzer: false, 81 | reportFilename: resolve(__dirname, `../reports/${process.env.NODE_ENV}.html`) 82 | }) 83 | ], 84 | devtool: '#eval-source-map', 85 | devServer: { 86 | inline: true, 87 | stats: { 88 | colors: true, 89 | chunks: false, 90 | cached: false 91 | }, 92 | contentBase: buildPath 93 | }, 94 | performance: { 95 | hints: false 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /build/webpack.config.dll.js: -------------------------------------------------------------------------------- 1 | const { resolve, join } = require('path') 2 | const webpack = require('webpack') 3 | const pkg = require('../package.json') 4 | 5 | const rootDir = resolve(__dirname, '../test') 6 | const buildPath = resolve(rootDir, 'dist') 7 | 8 | const entry = {} 9 | entry[pkg.dllPlugin.name] = pkg.dllPlugin.include 10 | 11 | module.exports = { 12 | devtool: '#source-map', 13 | entry, 14 | output: { 15 | path: buildPath, 16 | filename: '[name].dll.js', 17 | library: '[name]' 18 | }, 19 | plugins: [ 20 | new webpack.DllPlugin({ 21 | name: '[name]', 22 | path: join(buildPath, '[name].json') 23 | }) 24 | ], 25 | performance: { 26 | hints: false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-observable", 3 | "version": "0.1.4", 4 | "description": "Vue Components for the IntersectionObserver, MutationObserver and ResizeObserver APIs", 5 | "author": "Jakub Juszczak ", 6 | "main": "dist/vue-observable.common.js", 7 | "module": "dist/vue-observable.esm.js", 8 | "browser": "dist/vue-observable.js", 9 | "unpkg": "dist/vue-observable.js", 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "scripts": { 15 | "clean": "rimraf dist", 16 | "build": "node build/build.js", 17 | "build:dll": "webpack --progress --config build/webpack.config.dll.js", 18 | "lint": "yon run lint:js && yon run lint:css", 19 | "lint:js": "eslint --ext js --ext jsx --ext vue src test/**/*.spec.js test/*.js build", 20 | "lint:js:fix": "yon run lint:js -- --fix", 21 | "lint:css": "stylelint src/**/*.{vue,css}", 22 | "lint:staged": "lint-staged", 23 | "pretest": "yon run lint", 24 | "test": "cross-env BABEL_ENV=test karma start test/karma.conf.js --single-run", 25 | "dev": "webpack-dashboard -- webpack-dev-server --config build/webpack.config.dev.js --open", 26 | "dev:coverage": "cross-env BABEL_ENV=test karma start test/karma.conf.js", 27 | "prepublish": "yon run build" 28 | }, 29 | "lint-staged": { 30 | "*.{vue,jsx,js}": [ 31 | "eslint --fix" 32 | ], 33 | "*.{vue,css}": [ 34 | "stylefmt", 35 | "stylelint" 36 | ] 37 | }, 38 | "pre-commit": "lint:staged", 39 | "devDependencies": { 40 | "buble": "^0.15.2", 41 | "clean-css": "^4.0.0", 42 | "mkdirp": "^0.5.1", 43 | "rollup": "^0.41.6", 44 | "rollup-plugin-buble": "^0.15.0", 45 | "rollup-plugin-commonjs": "^8.0.0", 46 | "rollup-plugin-jsx": "^1.0.0", 47 | "rollup-plugin-node-resolve": "^3.0.0", 48 | "rollup-plugin-postcss": "^0.4.1", 49 | "rollup-plugin-replace": "^1.1.0", 50 | "rollup-plugin-vue": "^2.3.0", 51 | "uglify-js": "^3.0.0", 52 | "add-asset-html-webpack-plugin": "^2.0.0", 53 | "babel-core": "^6.24.0", 54 | "babel-eslint": "^7.2.0", 55 | "babel-helper-vue-jsx-merge-props": "^2.0.0", 56 | "babel-loader": "^7.0.0", 57 | "babel-plugin-istanbul": "^4.1.0", 58 | "babel-plugin-syntax-jsx": "^6.18.0", 59 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 60 | "babel-plugin-transform-runtime": "^6.23.0", 61 | "babel-plugin-transform-vue-jsx": "^3.4.0", 62 | "babel-preset-env": "^1.4.0", 63 | "chai": "^3.5.0", 64 | "chai-dom": "^1.4.0", 65 | "cross-env": "^4.0.0", 66 | "css-loader": "^0.28.0", 67 | "eslint": "^3.19.0", 68 | "eslint-config-vue": "^2.0.0", 69 | "eslint-plugin-vue": "^2.0.0", 70 | "extract-text-webpack-plugin": "^2.1.0", 71 | "html-webpack-plugin": "^2.28.0", 72 | "karma": "^1.7.0", 73 | "karma-chai-dom": "^1.1.0", 74 | "karma-chrome-launcher": "^2.1.0", 75 | "karma-coverage": "^1.1.0", 76 | "karma-mocha": "^1.3.0", 77 | "karma-sinon-chai": "^1.3.0", 78 | "karma-sourcemap-loader": "^0.3.7", 79 | "karma-spec-reporter": "^0.0.31", 80 | "karma-webpack": "^2.0.0", 81 | "lint-staged": "^3.4.0", 82 | "mocha": "^3.3.0", 83 | "mocha-css": "^1.0.1", 84 | "postcss": "^6.0.0", 85 | "postcss-cssnext": "^2.10.0", 86 | "pre-commit": "^1.2.0", 87 | "rimraf": "^2.6.0", 88 | "sinon": "2.2.0", 89 | "sinon-chai": "^2.10.0", 90 | "style-loader": "^0.17.0", 91 | "stylefmt": "^5.3.0", 92 | "stylelint": "^7.10.0", 93 | "stylelint-config-standard": "^16.0.0", 94 | "stylelint-processor-html": "^1.0.0", 95 | "uppercamelcase": "^3.0.0", 96 | "vue": "^2.3.0", 97 | "vue-loader": "^12.0.0", 98 | "vue-template-compiler": "^2.3.0", 99 | "webpack": "^2.5.0", 100 | "webpack-bundle-analyzer": "^2.4.0", 101 | "webpack-dashboard": "^0.4.0", 102 | "webpack-dev-server": "^2.4.0", 103 | "webpack-merge": "^4.0.0", 104 | "yarn-or-npm": "^2.0.0" 105 | }, 106 | "peerDependencies": { 107 | "vue": "^2.3.0" 108 | }, 109 | "dllPlugin": { 110 | "name": "vuePluginTemplateDeps", 111 | "include": [ 112 | "mocha/mocha.js", 113 | "style-loader!css-loader!mocha-css", 114 | "html-entities", 115 | "vue/dist/vue.js", 116 | "chai", 117 | "core-js/library", 118 | "url", 119 | "sockjs-client", 120 | "vue-style-loader/lib/addStylesClient.js", 121 | "events", 122 | "ansi-html", 123 | "style-loader/addStyles.js" 124 | ] 125 | }, 126 | "repository": { 127 | "type": "git", 128 | "url": "git+https://github.com/apertureless/vue-observable.git" 129 | }, 130 | "bugs": { 131 | "url": "https://github.com/apertureless/vue-observable/issues" 132 | }, 133 | "homepage": "https://github.com/apertureless/vue-observable#readme", 134 | "license": { 135 | "type": "MIT", 136 | "url": "http://www.opensource.org/licenses/mit-license.php" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/IntersectionObserver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'intersect', 3 | abstract: true, 4 | props: { 5 | /** 6 | * A specific ancestor of the target element being observed. 7 | * If no value was passed to the constructor or this is null, the 8 | * top-level document's viewport is used. 9 | * @url: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root 10 | */ 11 | root: { 12 | default: () => null 13 | }, 14 | /** 15 | * An offset rectangle applied to the root's bounding box when 16 | * calculating intersections, effectively shrinking or growing the 17 | * root for calculation purposes. The value returned by this property 18 | * may not be the same as the one specified when calling the constructor 19 | * as it may be changed to match internal requirements. Each offset can be 20 | * expressed in pixels (px) or as a percentage (%). 21 | * The default is "0px 0px 0px 0px". 22 | * @url: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin 23 | */ 24 | rootMargin: { 25 | type: String, 26 | default: () => '0px 0px 0px 0px' 27 | }, 28 | /** 29 | * A list of thresholds, sorted in increasing numeric order, where each threshold 30 | * is a ratio of intersection area to bounding box area of an observed target. Notifications 31 | * for a target are generated when any of the thresholds are crossed for that target. If no value 32 | * was passed to the constructor, 0 is used. 33 | * @url: https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/thresholds 34 | */ 35 | threshold: { 36 | type: Array, 37 | default: () => [0, 0.2] 38 | } 39 | }, 40 | data () { 41 | return { 42 | _observer: null 43 | } 44 | }, 45 | created () { 46 | if ('IntersectionObserver' in window) { 47 | this.$data._observer = new IntersectionObserver((entries) => { // eslint-disable-line no-undef 48 | if (!entries[0].isIntersecting) { 49 | this.$emit('leave', [entries[0]]) 50 | } else { 51 | this.$emit('enter', [entries[0]]) 52 | } 53 | this.$emit('change', [entries[0]]) 54 | }, { 55 | threshold: this.threshold, 56 | root: this.root, 57 | rootMargin: this.rootMargin 58 | }) 59 | } else { 60 | console.warn('[✋ VueObservables] You need to polyfill IntersectionObserver') 61 | } 62 | }, 63 | mounted () { 64 | this.$nextTick(() => { 65 | if (this.$slots.default && this.$slots.default.length > 1) { 66 | console.warn('[✋ VueObservables] You may only wrap one element in a component.') 67 | } else if (!this.$slots.default || this.$slots.default.length < 1) { 68 | console.warn('[✋ VueObservables] You must have one child inside a component.') 69 | return 70 | } 71 | this.$data._observer.observe(this.$slots.default[0].elm) 72 | }) 73 | }, 74 | destroyed () { 75 | this.$data._observer.disconnect() 76 | }, 77 | render () { 78 | return this.$slots.default ? this.$slots.default[0] : null 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/MutationObserver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'mutate', 3 | abstract: true, 4 | props: { 5 | /** 6 | * Set to true if additions and removals of the target 7 | * node's child elements (including text nodes) are to be observed. 8 | */ 9 | childList: { 10 | type: Boolean, 11 | default: true 12 | }, 13 | /** 14 | * Set to true if mutations to target's data are to be observed. 15 | */ 16 | characterData: { 17 | type: Boolean, 18 | default: true 19 | }, 20 | /** 21 | * Set to true if mutations to target's attributes are to be observed. 22 | */ 23 | attributes: { 24 | type: Boolean, 25 | default: true 26 | }, 27 | /** 28 | * Set to true if mutations to target and target's descendants 29 | * are to be observed. 30 | */ 31 | subtree: { 32 | type: Boolean, 33 | default: true 34 | }, 35 | /** 36 | * Set to true if attributes is set to true and 37 | * target's attribute value before the mutation needs to be recorded. 38 | */ 39 | attributeOldValue: { 40 | type: Boolean, 41 | default: false 42 | }, 43 | /** 44 | * Set to true if characterData is set to true and target's data 45 | * before the mutation needs to be recorded. 46 | */ 47 | characterDataOldValue: { 48 | type: Boolean, 49 | default: false 50 | }, 51 | /** 52 | * Set to an array of attribute local names 53 | * (without namespace) if not all attribute mutations need to be observed. 54 | */ 55 | attributeFilter: { 56 | type: Array, 57 | default: () => [] 58 | } 59 | }, 60 | data () { 61 | return { 62 | _observer: null 63 | } 64 | }, 65 | created () { 66 | if ('MutationObserver' in window) { 67 | this.$data._observer = new MutationObserver((mutations) => { // eslint-disable-line no-undef 68 | mutations.forEach((mutation) => { 69 | this.$emit('mutation', [mutation]) 70 | }) 71 | }) 72 | } else { 73 | console.warn('[✋ VueObservables] You need to polyfill MutationObserver') 74 | } 75 | }, 76 | mounted () { 77 | this.$nextTick(() => { 78 | if (this.$slots.default && this.$slots.default.length > 1) { 79 | console.warn('[✋ VueObservables] You may only wrap one element in a component.') 80 | } else if (!this.$slots.default || this.$slots.default.length < 1) { 81 | console.warn('[✋ VueObservables] You must have one child inside a component.') 82 | return 83 | } 84 | this.$data._observer.observe(this.$slots.default[0].elm, this.$props) 85 | }) 86 | }, 87 | destroyed () { 88 | this.$data._observer.disconnect() 89 | }, 90 | render () { 91 | return this.$slots.default ? this.$slots.default[0] : null 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ResizeObserver.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'resize', 3 | abstract: true, 4 | props: { 5 | 6 | }, 7 | data () { 8 | return { 9 | _observer: null 10 | } 11 | }, 12 | created () { 13 | if ('ResizeObserver' in window) { 14 | this.$data._observer = new ResizeObserver((entries) => { // eslint-disable-line no-undef 15 | this.$emit('resize', [entries[0]]) 16 | }) 17 | } else { 18 | console.warn('[✋ VueObservables] You need to polyfill ResizeObserver') 19 | } 20 | }, 21 | mounted () { 22 | this.$nextTick(() => { 23 | if (this.$slots.default && this.$slots.default.length > 1) { 24 | console.warn('[✋ VueObservables] You may only wrap one element in a component.') 25 | } else if (!this.$slots.default || this.$slots.default.length < 1) { 26 | console.warn('[✋ VueObservables] You must have one child inside a component.') 27 | return 28 | } 29 | this.$data._observer.observe(this.$slots.default[0].elm) 30 | }) 31 | }, 32 | destroyed () { 33 | this.$data._observer.disconnect() 34 | }, 35 | render () { 36 | return this.$slots.default ? this.$slots.default[0] : null 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Intersect from './IntersectionObserver' 2 | import Mutation from './MutationObserver' 3 | import Resize from './ResizeObserver' 4 | 5 | function plugin(Vue) { 6 | Vue.component('intersect', Intersect) 7 | Vue.component('mutate', Mutation) 8 | Vue.component('resize', Resize) 9 | } 10 | 11 | // Install by default if using the script tag 12 | if (typeof window !== 'undefined' && window.Vue) { 13 | window.Vue.use(plugin) 14 | } 15 | 16 | export default plugin 17 | const version = '__VERSION__' 18 | // Export all components too 19 | export { 20 | Intersect, 21 | Mutation, 22 | Resize, 23 | version 24 | } 25 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/Test.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 39 | 40 | 108 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import camelcase from 'camelcase' 2 | import { createVM, Vue } from './utils' 3 | import { nextTick } from './wait-for-update' 4 | 5 | export function dataPropagationTest (Component) { 6 | return function () { 7 | const spy = sinon.spy() 8 | const vm = createVM(this, function (h) { 9 | return ( 10 | Hello 11 | ) 12 | }) 13 | spy.should.have.not.been.called 14 | vm.$('.custom').should.exist 15 | vm.$('.custom').click() 16 | spy.should.have.been.calledOnce 17 | } 18 | } 19 | 20 | export function attrTest (it, base, Component, attr) { 21 | const attrs = Array.isArray(attr) ? attr : [attr] 22 | 23 | attrs.forEach(attr => { 24 | it(attr, function (done) { 25 | const vm = createVM(this, function (h) { 26 | const opts = { 27 | props: { 28 | [camelcase(attr)]: this.active 29 | } 30 | } 31 | return ( 32 | {attr} 33 | ) 34 | }, { 35 | data: { active: true } 36 | }) 37 | vm.$(`.${base}`).should.have.class(`${base}--${attr}`) 38 | vm.active = false 39 | nextTick().then(() => { 40 | vm.$(`.${base}`).should.not.have.class(`${base}--${attr}`) 41 | vm.active = true 42 | }).then(done) 43 | }) 44 | }) 45 | } 46 | 47 | export { 48 | createVM, 49 | Vue, 50 | nextTick 51 | } 52 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | import Test from './Test.vue' 3 | 4 | Vue.config.productionTip = false 5 | const isKarma = !!window.__karma__ 6 | 7 | export function createVM (context, template, opts = {}) { 8 | return isKarma 9 | ? createKarmaTest(context, template, opts) 10 | : createVisualTest(context, template, opts) 11 | } 12 | 13 | const emptyNodes = document.querySelectorAll('nonexistant') 14 | Vue.prototype.$$ = function $$ (selector) { 15 | const els = document.querySelectorAll(selector) 16 | const vmEls = this.$el.querySelectorAll(selector) 17 | const fn = vmEls.length 18 | ? el => vmEls.find(el) 19 | : el => this.$el === el 20 | const found = Array.from(els).filter(fn) 21 | return found.length 22 | ? found 23 | : emptyNodes 24 | } 25 | 26 | Vue.prototype.$ = function $ (selector) { 27 | const els = document.querySelectorAll(selector) 28 | const vmEl = this.$el.querySelector(selector) 29 | const fn = vmEl 30 | ? el => el === vmEl 31 | : el => el === this.$el 32 | // Allow should chaining for tests 33 | return Array.from(els).find(fn) || emptyNodes 34 | } 35 | 36 | export function createKarmaTest (context, template, opts) { 37 | const el = document.createElement('div') 38 | document.getElementById('tests').appendChild(el) 39 | const render = typeof template === 'string' 40 | ? { template: `
${template}
` } 41 | : { render: template } 42 | return new Vue({ 43 | el, 44 | name: 'Test', 45 | ...render, 46 | ...opts 47 | }) 48 | } 49 | 50 | export function createVisualTest (context, template, opts) { 51 | let vm 52 | if (typeof template === 'string') { 53 | opts.components = opts.components || {} 54 | // Let the user define a test component 55 | if (!opts.components.Test) { 56 | opts.components.Test = Test 57 | } 58 | vm = new Vue({ 59 | name: 'TestContainer', 60 | el: context.DOMElement, 61 | template: `${template}`, 62 | ...opts 63 | }) 64 | } else { 65 | // TODO allow redefinition of Test component 66 | vm = new Vue({ 67 | name: 'TestContainer', 68 | el: context.DOMElement, 69 | render (h) { 70 | return h(Test, { 71 | attrs: { 72 | id: context.DOMElement.id 73 | } 74 | // render the passed component with this scope 75 | }, [template.call(this, h)]) 76 | }, 77 | ...opts 78 | }) 79 | } 80 | 81 | context.DOMElement.vm = vm 82 | return vm 83 | } 84 | 85 | export function register (name, component) { 86 | Vue.component(name, component) 87 | } 88 | 89 | export { isKarma, Vue } 90 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue/dist/vue.js' 2 | 3 | // Testing helper 4 | // nextTick().then(() => { 5 | // 6 | // Automatically waits for nextTick 7 | // }).then(() => { 8 | // return a promise or value to skip the wait 9 | // }) 10 | function nextTick () { 11 | const jobs = [] 12 | let done 13 | 14 | const chainer = { 15 | then (cb) { 16 | jobs.push(cb) 17 | return chainer 18 | } 19 | } 20 | 21 | function shift (...args) { 22 | const job = jobs.shift() 23 | let result 24 | try { 25 | result = job(...args) 26 | } catch (e) { 27 | jobs.length = 0 28 | done(e) 29 | } 30 | 31 | // wait for nextTick 32 | if (result !== undefined) { 33 | if (result.then) { 34 | result.then(shift) 35 | } else { 36 | shift(result) 37 | } 38 | } else if (jobs.length) { 39 | requestAnimationFrame(() => Vue.nextTick(shift)) 40 | } 41 | } 42 | 43 | // First time 44 | Vue.nextTick(() => { 45 | done = jobs[jobs.length - 1] 46 | if (done.toString().slice(0, 14) !== 'function (err)') { 47 | throw new Error('waitForUpdate chain is missing .then(done)') 48 | } 49 | shift() 50 | }) 51 | 52 | return chainer 53 | } 54 | 55 | exports.nextTick = nextTick 56 | exports.delay = time => new Promise(resolve => setTimeout(resolve, time)) 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | import bind from 'function-bind' 3 | /* eslint-disable no-extend-native */ 4 | Function.prototype.bind = bind 5 | 6 | // Polyfill Object.assign for PhantomJS 7 | import objectAssign from 'object-assign' 8 | Object.assign = objectAssign 9 | 10 | // require all src files for coverage. 11 | // you can also change this to match only the subset of files that 12 | // you want coverage for. 13 | const srcContext = require.context('../src', true, /^\.\/(?!index(\.js)?$)/) 14 | srcContext.keys().forEach(srcContext) 15 | 16 | // Use a div to insert elements 17 | before(function () { 18 | const el = document.createElement('DIV') 19 | el.id = 'tests' 20 | document.body.appendChild(el) 21 | }) 22 | 23 | // Remove every test html scenario 24 | afterEach(function () { 25 | const el = document.getElementById('tests') 26 | for (let i = 0; i < el.children.length; ++i) { 27 | el.removeChild(el.children[i]) 28 | } 29 | }) 30 | 31 | const specsContext = require.context('./specs', true) 32 | specsContext.keys().forEach(specsContext) 33 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const baseConfig = require('../build/webpack.config.dev.js') 3 | 4 | const webpackConfig = merge(baseConfig, { 5 | // use inline sourcemap for karma-sourcemap-loader 6 | devtool: '#inline-source-map' 7 | }) 8 | 9 | webpackConfig.plugins = [] 10 | 11 | const vueRule = webpackConfig.module.rules.find(rule => rule.loader === 'vue-loader') 12 | vueRule.options = vueRule.options || {} 13 | vueRule.options.loaders = vueRule.options.loaders || {} 14 | vueRule.options.loaders.js = 'babel-loader' 15 | 16 | // no need for app entry during tests 17 | delete webpackConfig.entry 18 | 19 | module.exports = function (config) { 20 | config.set({ 21 | // to run in additional browsers: 22 | // 1. install corresponding karma launcher 23 | // http://karma-runner.github.io/0.13/config/browsers.html 24 | // 2. add it to the `browsers` array below. 25 | browsers: ['ChromeCanary'], 26 | frameworks: ['mocha', 'chai-dom', 'sinon-chai'], 27 | reporters: ['spec', 'coverage'], 28 | files: ['./index.js'], 29 | preprocessors: { 30 | './index.js': ['webpack', 'sourcemap'] 31 | }, 32 | webpack: webpackConfig, 33 | webpackMiddleware: { 34 | noInfo: true 35 | }, 36 | coverageReporter: { 37 | dir: './coverage', 38 | reporters: [ 39 | { type: 'lcov', subdir: '.' }, 40 | { type: 'text-summary' } 41 | ] 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /test/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | // import Hello from 'src/Hello.vue' 2 | // import { createVM } from '../helpers/utils.js' 3 | 4 | // describe('Hello.vue', function () { 5 | // it('should render correct contents', function () { 6 | // const vm = createVM(this, ` 7 | // 8 | // `, { components: { Hello }}) 9 | // vm.$el.querySelector('.hello h1').textContent.should.eql('Hello World!') 10 | // }) 11 | // }) 12 | -------------------------------------------------------------------------------- /test/specs/IntersectionObserver.spec.js: -------------------------------------------------------------------------------- 1 | import Intersect from 'src/IntersectionObserver' 2 | import { createVM } from '../helpers/utils.js' 3 | 4 | describe('Intersect', () => { 5 | it('should render correct contents', function () { 6 | const vm = createVM(this, ` 7 |

Hello World!

8 | `, { components: { Intersect }}) 9 | vm.$el.querySelector('h1').textContent.should.eql('Hello World!') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/specs/MutationObserver.spec.js: -------------------------------------------------------------------------------- 1 | import Mutate from 'src/MutationObserver' 2 | import { createVM } from '../helpers/utils.js' 3 | 4 | describe('Mutate', () => { 5 | it('should render correct contents', function () { 6 | const vm = createVM(this, ` 7 |

Hello World!

8 | `, { components: { Mutate }}) 9 | vm.$el.querySelector('h1').textContent.should.eql('Hello World!') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/specs/ResizeObserver.spec.js: -------------------------------------------------------------------------------- 1 | import Resize from 'src/ResizeObserver' 2 | import { createVM } from '../helpers/utils.js' 3 | 4 | describe('Resize', () => { 5 | it('should render correct contents', function () { 6 | const vm = createVM(this, ` 7 |

Hello World!

8 | `, { components: { Resize }}) 9 | vm.$el.querySelector('h1').textContent.should.eql('Hello World!') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/visual.js: -------------------------------------------------------------------------------- 1 | import 'style-loader!css-loader!mocha-css' 2 | 3 | // create a div where mocha can add its stuff 4 | const mochaDiv = document.createElement('DIV') 5 | mochaDiv.id = 'mocha' 6 | document.body.appendChild(mochaDiv) 7 | 8 | import 'mocha/mocha.js' 9 | import sinon from 'sinon' 10 | import chai from 'chai' 11 | window.mocha.setup({ 12 | ui: 'bdd', 13 | slow: 750, 14 | timeout: 5000, 15 | globals: [ 16 | '__VUE_DEVTOOLS_INSTANCE_MAP__', 17 | 'script', 18 | 'inject', 19 | 'originalOpenFunction' 20 | ] 21 | }) 22 | window.sinon = sinon 23 | chai.use(require('chai-dom')) 24 | chai.use(require('sinon-chai')) 25 | chai.should() 26 | 27 | let vms = [] 28 | let testId = 0 29 | 30 | beforeEach(function () { 31 | this.DOMElement = document.createElement('DIV') 32 | this.DOMElement.id = `test-${++testId}` 33 | document.body.appendChild(this.DOMElement) 34 | }) 35 | 36 | afterEach(function () { 37 | const testReportElements = document.getElementsByClassName('test') 38 | const lastReportElement = testReportElements[testReportElements.length - 1] 39 | 40 | if (!lastReportElement) return 41 | const el = document.getElementById(this.DOMElement.id) 42 | if (el) lastReportElement.appendChild(el) 43 | // Save the vm to hide it later 44 | if (this.DOMElement.vm) vms.push(this.DOMElement.vm) 45 | }) 46 | 47 | // Hide all tests at the end to prevent some weird bugs 48 | before(function () { 49 | vms = [] 50 | testId = 0 51 | }) 52 | after(function () { 53 | requestAnimationFrame(function () { 54 | setTimeout(function () { 55 | vms.forEach(vm => { 56 | // Hide if test passed 57 | if (!vm.$el.parentElement.classList.contains('fail')) { 58 | vm.$children[0].visible = false 59 | } 60 | }) 61 | }, 100) 62 | }) 63 | }) 64 | 65 | const specsContext = require.context('./specs', true) 66 | specsContext.keys().forEach(specsContext) 67 | 68 | window.mocha.checkLeaks() 69 | window.mocha.run() 70 | --------------------------------------------------------------------------------