├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── dependabot.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── v-click-outside.umd.js └── v-click-outside.umd.js.map ├── example ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── main.js │ ├── router │ └── index.js │ └── views │ ├── About.vue │ └── Home.vue ├── package-lock.json ├── package.json ├── src ├── index.js └── v-click-outside.js └── test └── v-click-outside.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "no-param-reassign": 0, 7 | "no-shadow": 0 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018 11 | }, 12 | "env": { 13 | "browser": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.log 4 | .DS_Store 5 | jest_* 6 | yarn.lock 7 | .idea 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | coverage 3 | test 4 | yarn.lock 5 | example 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "singleQuote": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: '12' 4 | 5 | cache: npm 6 | 7 | before_script: 8 | - npm install coveralls 9 | 10 | script: 11 | - npm test 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Nicolas Del Valle 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 | # click-outside-vue3 2 | 3 | Vue 3 directive to react on clicks outside an element without stopping the event propagation. 4 | Great for closing dialogues and menus among other things. 5 | 6 | ## Install 7 | ```bash 8 | $ npm install --save click-outside-vue3 9 | ``` 10 | 11 | ```bash 12 | $ yarn add click-outside-vue3 13 | ``` 14 | 15 | ## Use 16 | 17 | ```js 18 | import { createApp } from "vue" 19 | import App from "./App.vue" 20 | import vClickOutside from "click-outside-vue3" 21 | 22 | const app = createApp(App) 23 | app.use(vClickOutside) 24 | ``` 25 | 26 | ```js 27 | 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | Or use it as a directive 69 | 70 | ```js 71 | import vClickOutside from 'click-outside-vue3' 72 | 73 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | ## Detecting Iframe Clicks 92 | 93 | To our knowledge, there isn't an idiomatic way to detect a click on a `` (`HTMLIFrameElement`). 94 | Clicks on iframes moves `focus` to its contents’ `window` but don't `bubble` up to main `window`, therefore not triggering our `document.documentElement` listeners. On the other hand, the abovementioned `focus` event does trigger a `window.blur` event on main `window` that we use in conjunction with `document.activeElement` to detect if it came from an ``, and execute the provided `handler`. 95 | 96 | **As with any workaround, this also has its caveats:** 97 | 98 | - Click outside will be triggered once on iframe. Subsequent clicks on iframe will not execute the handler **until focus has been moved back to main window** — as in by clicking anywhere outside the iframe. This is the "expected" behaviour since, as mentioned before, by clicking the iframe focus will move to iframe contents — a different window, so subsequent clicks are inside its frame. There might be way to workaround this such as calling window.focus() at the end of the provided handler but that will break normal tab/focus flow; 99 | - Moving focus to `iframe` via `keyboard` navigation also triggers `window.blur` consequently the handler - no workaround found ATM; 100 | 101 | Because of these reasons, the detection mechansim is behind the `detectIframe` flag that you can optionally set to `false` if you find it conflicting with your use-case. 102 | Any improvements or suggestions to this are welcomed. 103 | 104 | 105 | ## License 106 | 107 | [MIT License](https://github.com/andymark-by/vue3-click-outside/blob/master/LICENSE) 108 | -------------------------------------------------------------------------------- /dist/v-click-outside.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e||self)["v-click-outside"]=n()}(this,function(){var e="__v-click-outside",n="undefined"!=typeof window,t="undefined"!=typeof navigator,r=n&&("ontouchstart"in window||t&&navigator.msMaxTouchPoints>0)?["touchstart"]:["click"],i=function(e){var n=e.event,t=e.handler;(0,e.middleware)(n)&&t(n)},a=function(n,t){var a=function(e){var n="function"==typeof e;if(!n&&"object"!=typeof e)throw new Error("v-click-outside: Binding value must be a function or an object");return{handler:n?e:e.handler,middleware:e.middleware||function(e){return e},events:e.events||r,isActive:!(!1===e.isActive),detectIframe:!(!1===e.detectIframe),capture:Boolean(e.capture)}}(t.value),o=a.handler,d=a.middleware,c=a.detectIframe,u=a.capture;if(a.isActive){if(n[e]=a.events.map(function(e){return{event:e,srcTarget:document.documentElement,handler:function(e){return function(e){var n=e.el,t=e.event,r=e.handler,a=e.middleware,o=t.path||t.composedPath&&t.composedPath();(o?o.indexOf(n)<0:!n.contains(t.target))&&i({event:t,handler:r,middleware:a})}({el:n,event:e,handler:o,middleware:d})},capture:u}}),c){var l={event:"blur",srcTarget:window,handler:function(e){return function(e){var n=e.el,t=e.event,r=e.handler,a=e.middleware;setTimeout(function(){var e=document.activeElement;e&&"IFRAME"===e.tagName&&!n.contains(e)&&i({event:t,handler:r,middleware:a})},0)}({el:n,event:e,handler:o,middleware:d})},capture:u};n[e]=[].concat(n[e],[l])}n[e].forEach(function(t){var r=t.event,i=t.srcTarget,a=t.handler;return setTimeout(function(){n[e]&&i.addEventListener(r,a,u)},0)})}},o=function(n){(n[e]||[]).forEach(function(e){return e.srcTarget.removeEventListener(e.event,e.handler,e.capture)}),delete n[e]},d=n?{beforeMount:a,updated:function(e,n){var t=n.value,r=n.oldValue;JSON.stringify(t)!==JSON.stringify(r)&&(o(e),a(e,{value:t}))},unmounted:o}:{};return{install:function(e){e.directive("click-outside",d)},directive:d}}); 2 | //# sourceMappingURL=v-click-outside.umd.js.map 3 | -------------------------------------------------------------------------------- /dist/v-click-outside.umd.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"v-click-outside.umd.js","sources":["../src/v-click-outside.js","../src/index.js"],"sourcesContent":["/**\n * Modified for Vue 3 from https://github.com/ndelvalle/v-click-outside\n * Cf. https://github.com/ndelvalle/v-click-outside/issues/238\n */\nconst HANDLERS_PROPERTY = \"__v-click-outside\";\nconst HAS_WINDOWS = typeof window !== \"undefined\";\nconst HAS_NAVIGATOR = typeof navigator !== \"undefined\";\nconst IS_TOUCH =\n HAS_WINDOWS &&\n (\"ontouchstart\" in window ||\n (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0));\nconst EVENTS = IS_TOUCH ? [\"touchstart\"] : [\"click\"];\nconst processDirectiveArguments = (bindingValue) => {\n const isFunction = typeof bindingValue === \"function\";\n if (!isFunction && typeof bindingValue !== \"object\") {\n throw new Error(\n \"v-click-outside: Binding value must be a function or an object\"\n );\n }\n return {\n handler: isFunction ? bindingValue : bindingValue.handler,\n middleware: bindingValue.middleware || ((item) => item),\n events: bindingValue.events || EVENTS,\n isActive: !(bindingValue.isActive === false),\n detectIframe: !(bindingValue.detectIframe === false),\n capture: Boolean(bindingValue.capture),\n };\n};\nconst execHandler = ({ event, handler, middleware }) => {\n if (middleware(event)) {\n handler(event);\n }\n};\nconst onFauxIframeClick = ({ el, event, handler, middleware }) => {\n // Note: on firefox clicking on iframe triggers blur, but only on\n // next event loop it becomes document.activeElement\n // https://stackoverflow.com/q/2381336#comment61192398_23231136\n setTimeout(() => {\n const { activeElement } = document;\n if (\n activeElement &&\n activeElement.tagName === \"IFRAME\" &&\n !el.contains(activeElement)\n ) {\n execHandler({ event, handler, middleware });\n }\n }, 0);\n};\nconst onEvent = ({ el, event, handler, middleware }) => {\n // Note: composedPath is not supported on IE and Edge, more information here:\n // https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath\n // In the meanwhile, we are using el.contains for those browsers, not\n // the ideal solution, but using IE or EDGE is not ideal either.\n const path = event.path || (event.composedPath && event.composedPath());\n const isClickOutside = path\n ? path.indexOf(el) < 0\n : !el.contains(event.target);\n if (!isClickOutside) {\n return;\n }\n execHandler({ event, handler, middleware });\n};\nconst beforeMount = (el, { value }) => {\n const {\n events,\n handler,\n middleware,\n isActive,\n detectIframe,\n capture,\n } = processDirectiveArguments(value);\n if (!isActive) {\n return;\n }\n el[HANDLERS_PROPERTY] = events.map((eventName) => ({\n event: eventName,\n srcTarget: document.documentElement,\n handler: (event) => onEvent({ el, event, handler, middleware }),\n capture,\n }));\n if (detectIframe) {\n const detectIframeEvent = {\n event: \"blur\",\n srcTarget: window,\n handler: (event) => onFauxIframeClick({ el, event, handler, middleware }),\n capture,\n };\n el[HANDLERS_PROPERTY] = [...el[HANDLERS_PROPERTY], detectIframeEvent];\n }\n el[HANDLERS_PROPERTY].forEach(({ event, srcTarget, handler: thisHandler }) =>\n setTimeout(() => {\n // Note: More info about this implementation can be found here:\n // https://github.com/ndelvalle/v-click-outside/issues/137\n if (!el[HANDLERS_PROPERTY]) {\n return;\n }\n srcTarget.addEventListener(event, thisHandler, capture);\n }, 0)\n );\n};\nconst unmounted = (el) => {\n const handlers = el[HANDLERS_PROPERTY] || [];\n handlers.forEach(({ event, srcTarget, handler, capture }) =>\n srcTarget.removeEventListener(event, handler, capture)\n );\n delete el[HANDLERS_PROPERTY];\n};\nconst updated = (el, { value, oldValue }) => {\n if (JSON.stringify(value) === JSON.stringify(oldValue)) {\n return;\n }\n unmounted(el);\n beforeMount(el, { value });\n};\nconst directive = {\n beforeMount,\n updated,\n unmounted,\n};\nexport default HAS_WINDOWS ? directive : {};\n","import directive from './v-click-outside'\n\nconst plugin = {\n install(Vue) {\n Vue.directive('click-outside', directive)\n },\n directive,\n}\n\nexport default plugin\n"],"names":["HANDLERS_PROPERTY","HAS_WINDOWS","window","HAS_NAVIGATOR","navigator","EVENTS","msMaxTouchPoints","execHandler","event","handler","middleware","beforeMount","el","bindingValue","isFunction","Error","item","events","isActive","detectIframe","capture","Boolean","processDirectiveArguments","value","map","eventName","srcTarget","document","documentElement","path","composedPath","indexOf","contains","target","onEvent","detectIframeEvent","setTimeout","activeElement","tagName","onFauxIframeClick","forEach","thisHandler","addEventListener","unmounted","removeEventListener","updated","oldValue","JSON","stringify","install","Vue","directive"],"mappings":"qOAIA,IAAMA,EAAoB,oBACpBC,EAAgC,oBAAXC,OACrBC,EAAqC,oBAAdC,UAKvBC,EAHJJ,IACC,iBAAkBC,QAChBC,GAAiBC,UAAUE,iBAAmB,GACzB,CAAC,cAAgB,CAAC,SAiBtCC,EAAc,gBAAGC,IAAAA,MAAOC,IAAAA,SACxBC,IADiCA,YACtBF,IACbC,EAAQD,IAgCNG,EAAc,SAACC,WAlDa,SAACC,GACjC,IAAMC,EAAqC,mBAAjBD,EAC1B,IAAKC,GAAsC,iBAAjBD,EACxB,UAAUE,MACR,kEAGJ,MAAO,CACLN,QAASK,EAAaD,EAAeA,EAAaJ,QAClDC,WAAYG,EAAaH,YAAe,SAACM,UAASA,GAClDC,OAAQJ,EAAaI,QAAUZ,EAC/Ba,YAAsC,IAA1BL,EAAaK,UACzBC,gBAA8C,IAA9BN,EAAaM,cAC7BC,QAASC,QAAQR,EAAaO,UA6C5BE,GARqBC,OAGvBd,IAAAA,QACAC,IAAAA,WAEAS,IAAAA,aACAC,IAAAA,QAEF,KAJEF,SAIF,CASA,GANAN,EAAGZ,KAVDiB,OAU6BO,IAAI,SAACC,SAAe,CACjDjB,MAAOiB,EACPC,UAAWC,SAASC,gBACpBnB,QAAS,SAACD,UA7BE,gBAAGI,IAAAA,GAAIJ,IAAAA,MAAOC,IAAAA,QAASC,IAAAA,WAK/BmB,EAAOrB,EAAMqB,MAASrB,EAAMsB,cAAgBtB,EAAMsB,gBACjCD,EACnBA,EAAKE,QAAQnB,GAAM,GAClBA,EAAGoB,SAASxB,EAAMyB,UAIvB1B,EAAY,CAAEC,MAAAA,EAAOC,QAAAA,EAASC,WAAAA,IAiBRwB,CAAQ,CAAEtB,GAAAA,EAAIJ,MAAAA,EAAOC,QAAAA,EAASC,WAAAA,KAClDU,QAAAA,KAEED,EAAc,CAChB,IAAMgB,EAAoB,CACxB3B,MAAO,OACPkB,UAAWxB,OACXO,QAAS,SAACD,UAnDU,gBAAGI,IAAAA,GAAIJ,IAAAA,MAAOC,IAAAA,QAASC,IAAAA,WAI/C0B,WAAW,eACDC,EAAkBV,SAAlBU,cAENA,GAC0B,WAA1BA,EAAcC,UACb1B,EAAGoB,SAASK,IAEb9B,EAAY,CAAEC,MAAAA,EAAOC,QAAAA,EAASC,WAAAA,KAE/B,GAsCqB6B,CAAkB,CAAE3B,GAAAA,EAAIJ,MAAAA,EAAOC,QAAAA,EAASC,WAAAA,KAC5DU,QAAAA,GAEFR,EAAGZ,aAAyBY,EAAGZ,IAAoBmC,IAErDvB,EAAGZ,GAAmBwC,QAAQ,gBAAGhC,IAAAA,MAAOkB,IAAAA,UAAoBe,IAAThC,eACjD2B,WAAW,WAGJxB,EAAGZ,IAGR0B,EAAUgB,iBAAiBlC,EAAOiC,EAAarB,IAC9C,OAGDuB,EAAY,SAAC/B,IACAA,EAAGZ,IAAsB,IACjCwC,QAAQ,qBAAUd,UACfkB,sBADQpC,QAAkBC,UAASW,kBAGxCR,EAAGZ,MAcGC,EALG,CAChBU,YAAAA,EACAkC,QATc,SAACjC,SAAMW,IAAAA,MAAOuB,IAAAA,SACxBC,KAAKC,UAAUzB,KAAWwB,KAAKC,UAAUF,KAG7CH,EAAU/B,GACVD,EAAYC,EAAI,CAAEW,MAAAA,MAKlBoB,UAAAA,GAEuC,SCrH1B,CACbM,iBAAQC,GACNA,EAAIC,UAAU,gBAAiBA,IAEjCA,UAAAA"} -------------------------------------------------------------------------------- /example/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/prettier'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # v-click-outside-example 2 | 3 | ## Project setup 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | npm run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ``` 24 | npm run lint 25 | ``` 26 | 27 | ### Customize configuration 28 | 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'], 3 | } 4 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v-click-outside-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.3.2", 12 | "v-click-outside": "^3.0.1", 13 | "vue": "^2.6.10", 14 | "vue-router": "^3.1.3" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "^4.0.0", 18 | "@vue/cli-plugin-eslint": "^4.0.0", 19 | "@vue/cli-plugin-router": "^4.0.0", 20 | "@vue/cli-service": "^4.0.0", 21 | "@vue/eslint-config-prettier": "^5.0.0", 22 | "babel-eslint": "^10.0.3", 23 | "eslint": "^5.16.0", 24 | "eslint-plugin-prettier": "^3.1.1", 25 | "eslint-plugin-vue": "^5.0.0", 26 | "prettier": "^1.18.2", 27 | "vue-template-compiler": "^2.6.10" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andymark-by/click-outside-vue3/b06a3254984ef5eba908a3c930c6d4f147bb947e/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | v-click-outside-example 9 | 10 | 11 | 12 | We're sorry but v-click-outside-example doesn't work properly without JavaScript enabled. Please enable it to continue. 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Home | 5 | About 6 | 7 | 8 | 9 | 10 | 11 | 33 | -------------------------------------------------------------------------------- /example/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andymark-by/click-outside-vue3/b06a3254984ef5eba908a3c930c6d4f147bb947e/example/src/assets/logo.png -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | // import vClickOutside from 'v-click-outside' 6 | import vClickOutside from '../../src' 7 | 8 | Vue.config.productionTip = false 9 | 10 | Vue.use(vClickOutside) 11 | 12 | new Vue({ 13 | router, 14 | render: (h) => h(App), 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /example/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Home from '../views/Home.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'home', 11 | component: Home, 12 | }, 13 | { 14 | path: '/about', 15 | name: 'about', 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => 20 | import(/* webpackChunkName: "about" */ '../views/About.vue'), 21 | }, 22 | ] 23 | 24 | const router = new VueRouter({ 25 | mode: 'history', 26 | base: process.env.BASE_URL, 27 | routes, 28 | }) 29 | 30 | export default router 31 | -------------------------------------------------------------------------------- /example/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is an about page 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to v-click-outside example 6 | 7 | 8 | Click Outside Yellow box 9 | 10 | 11 | 12 | Click Outside Red box 13 | 14 | 15 | 16 | Click Outside Lime box 17 | 18 | 19 | 20 | Click Outside blue box 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 83 | 84 | 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "click-outside-vue3", 3 | "version": "4.0.1", 4 | "description": "Vue 3 directive to react on clicks outside an element", 5 | "main": "dist/v-click-outside.umd.js", 6 | "umd:main": "dist/v-click-outside.umd.js", 7 | "source": "src/index.js", 8 | "scripts": { 9 | "test": "jest --coverage", 10 | "test:watch": "jest --colors --watch", 11 | "coverage": "open coverage/lcov-report/index.html", 12 | "build": "microbundle --entry src/index.js --target browser --format umd --name v-click-outside", 13 | "lint": "eslint . --ext .js", 14 | "format": "prettier --write '**/*.{js,json,md}'", 15 | "format:changed": "pretty-quick", 16 | "format:staged": "pretty-quick --staged", 17 | "release": "np", 18 | "version": "npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/andymark-by/click-outside-vue3.git" 23 | }, 24 | "author": "ndelvalle ", 25 | "keywords": [ 26 | "vue3", 27 | "click outside", 28 | "click" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/andymark-by/click-outside-vue3/issues" 33 | }, 34 | "homepage": "https://github.com/andymark-by/click-outside-vue3#readme", 35 | "devDependencies": { 36 | "@babel/preset-env": "^7.11.0", 37 | "babel-core": "^6.26.3", 38 | "babel-jest": "^26.3.0", 39 | "babel-preset-es2015": "^6.24.1", 40 | "eslint": "^7.7.0", 41 | "eslint-config-airbnb-base": "^14.2.0", 42 | "eslint-config-prettier": "^7.1.0", 43 | "eslint-plugin-import": "^2.22.0", 44 | "eslint-plugin-prettier": "^3.1.4", 45 | "husky": "^4.2.5", 46 | "jest": "^26.4.2", 47 | "jest-cli": "^26.4.2", 48 | "lodash": "^4.17.20", 49 | "microbundle": "^0.13.0", 50 | "np": "^7.2.0", 51 | "prettier": "^2.1.1", 52 | "pretty-quick": "^3.0.0" 53 | }, 54 | "jest": { 55 | "collectCoverage": true, 56 | "collectCoverageFrom": [ 57 | "src/**" 58 | ], 59 | "roots": [ 60 | "test/" 61 | ] 62 | }, 63 | "engines": { 64 | "node": ">=6" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import directive from './v-click-outside' 2 | 3 | const plugin = { 4 | install(Vue) { 5 | Vue.directive('click-outside', directive) 6 | }, 7 | directive, 8 | } 9 | 10 | export default plugin 11 | -------------------------------------------------------------------------------- /src/v-click-outside.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Modified for Vue 3 from https://github.com/ndelvalle/v-click-outside 3 | * Cf. https://github.com/ndelvalle/v-click-outside/issues/238 4 | */ 5 | const HANDLERS_PROPERTY = "__v-click-outside"; 6 | const HAS_WINDOWS = typeof window !== "undefined"; 7 | const HAS_NAVIGATOR = typeof navigator !== "undefined"; 8 | const IS_TOUCH = 9 | HAS_WINDOWS && 10 | ("ontouchstart" in window || 11 | (HAS_NAVIGATOR && navigator.msMaxTouchPoints > 0)); 12 | const EVENTS = IS_TOUCH ? ["touchstart"] : ["click"]; 13 | const processDirectiveArguments = (bindingValue) => { 14 | const isFunction = typeof bindingValue === "function"; 15 | if (!isFunction && typeof bindingValue !== "object") { 16 | throw new Error( 17 | "v-click-outside: Binding value must be a function or an object" 18 | ); 19 | } 20 | return { 21 | handler: isFunction ? bindingValue : bindingValue.handler, 22 | middleware: bindingValue.middleware || ((item) => item), 23 | events: bindingValue.events || EVENTS, 24 | isActive: !(bindingValue.isActive === false), 25 | detectIframe: !(bindingValue.detectIframe === false), 26 | capture: Boolean(bindingValue.capture), 27 | }; 28 | }; 29 | const execHandler = ({ event, handler, middleware }) => { 30 | if (middleware(event)) { 31 | handler(event); 32 | } 33 | }; 34 | const onFauxIframeClick = ({ el, event, handler, middleware }) => { 35 | // Note: on firefox clicking on iframe triggers blur, but only on 36 | // next event loop it becomes document.activeElement 37 | // https://stackoverflow.com/q/2381336#comment61192398_23231136 38 | setTimeout(() => { 39 | const { activeElement } = document; 40 | if ( 41 | activeElement && 42 | activeElement.tagName === "IFRAME" && 43 | !el.contains(activeElement) 44 | ) { 45 | execHandler({ event, handler, middleware }); 46 | } 47 | }, 0); 48 | }; 49 | const onEvent = ({ el, event, handler, middleware }) => { 50 | // Note: composedPath is not supported on IE and Edge, more information here: 51 | // https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath 52 | // In the meanwhile, we are using el.contains for those browsers, not 53 | // the ideal solution, but using IE or EDGE is not ideal either. 54 | const path = event.path || (event.composedPath && event.composedPath()); 55 | const isClickOutside = path 56 | ? path.indexOf(el) < 0 57 | : !el.contains(event.target); 58 | if (!isClickOutside) { 59 | return; 60 | } 61 | execHandler({ event, handler, middleware }); 62 | }; 63 | const beforeMount = (el, { value }) => { 64 | const { 65 | events, 66 | handler, 67 | middleware, 68 | isActive, 69 | detectIframe, 70 | capture, 71 | } = processDirectiveArguments(value); 72 | if (!isActive) { 73 | return; 74 | } 75 | el[HANDLERS_PROPERTY] = events.map((eventName) => ({ 76 | event: eventName, 77 | srcTarget: document.documentElement, 78 | handler: (event) => onEvent({ el, event, handler, middleware }), 79 | capture, 80 | })); 81 | if (detectIframe) { 82 | const detectIframeEvent = { 83 | event: "blur", 84 | srcTarget: window, 85 | handler: (event) => onFauxIframeClick({ el, event, handler, middleware }), 86 | capture, 87 | }; 88 | el[HANDLERS_PROPERTY] = [...el[HANDLERS_PROPERTY], detectIframeEvent]; 89 | } 90 | el[HANDLERS_PROPERTY].forEach(({ event, srcTarget, handler: thisHandler }) => 91 | setTimeout(() => { 92 | // Note: More info about this implementation can be found here: 93 | // https://github.com/ndelvalle/v-click-outside/issues/137 94 | if (!el[HANDLERS_PROPERTY]) { 95 | return; 96 | } 97 | srcTarget.addEventListener(event, thisHandler, capture); 98 | }, 0) 99 | ); 100 | }; 101 | const unmounted = (el) => { 102 | const handlers = el[HANDLERS_PROPERTY] || []; 103 | handlers.forEach(({ event, srcTarget, handler, capture }) => 104 | srcTarget.removeEventListener(event, handler, capture) 105 | ); 106 | delete el[HANDLERS_PROPERTY]; 107 | }; 108 | const updated = (el, { value, oldValue }) => { 109 | if (JSON.stringify(value) === JSON.stringify(oldValue)) { 110 | return; 111 | } 112 | unmounted(el); 113 | beforeMount(el, { value }); 114 | }; 115 | const directive = { 116 | beforeMount, 117 | updated, 118 | unmounted, 119 | }; 120 | export default HAS_WINDOWS ? directive : {}; 121 | -------------------------------------------------------------------------------- /test/v-click-outside.test.js: -------------------------------------------------------------------------------- 1 | /* global jest describe it expect beforeEach afterEach beforeAll */ 2 | 3 | import { merge } from 'lodash' 4 | import clickOutside from '../src/index' 5 | 6 | const HANDLERS_PROPERTY = '__v-click-outside' 7 | const { directive } = clickOutside 8 | 9 | const createDirective = () => merge({}, directive) 10 | 11 | function createHookArguments(el = document.createElement('div'), binding = {}) { 12 | return [ 13 | el, 14 | merge( 15 | { 16 | value: { 17 | handler: () => jest.fn(), 18 | events: ['dblclick'], 19 | middleware: () => jest.fn(), 20 | isActive: undefined, 21 | detectIframe: undefined, 22 | }, 23 | }, 24 | binding, 25 | ), 26 | ] 27 | } 28 | 29 | describe('v-click-outside -> plugin', () => { 30 | it('install the directive into the vue instance', () => { 31 | const vue = { 32 | directive: jest.fn(), 33 | } 34 | clickOutside.install(vue) 35 | expect(vue.directive).toHaveBeenCalledWith( 36 | 'click-outside', 37 | clickOutside.directive, 38 | ) 39 | expect(vue.directive).toHaveBeenCalledTimes(1) 40 | }) 41 | }) 42 | 43 | describe('v-click-outside -> directive', () => { 44 | it('it has bind, update and unbind methods available', () => { 45 | expect(typeof clickOutside.directive.bind).toBe('function') 46 | expect(typeof clickOutside.directive.update).toBe('function') 47 | expect(typeof clickOutside.directive.unbind).toBe('function') 48 | }) 49 | 50 | describe('bind', () => { 51 | beforeEach(() => { 52 | document.documentElement.addEventListener = jest.fn() 53 | window.addEventListener = jest.fn() 54 | jest.useFakeTimers() 55 | }) 56 | 57 | it('throws an error if the binding value is not a function or an object', () => { 58 | expect(() => 59 | directive.bind(document.createElement('div'), {}), 60 | ).toThrowError( 61 | /v-click-outside: Binding value must be a function or an object/, 62 | ) 63 | }) 64 | 65 | it('adds an event listener to the element and stores the handlers on the element', () => { 66 | const directive = createDirective() 67 | const [el, binding] = createHookArguments() 68 | 69 | directive.bind(el, binding) 70 | jest.runOnlyPendingTimers() 71 | 72 | expect(el[HANDLERS_PROPERTY].length).toEqual( 73 | binding.value.events.length + 1, // [vco:faux-iframe-click] 74 | ) 75 | 76 | el[HANDLERS_PROPERTY].forEach((eventHandler) => 77 | expect(typeof eventHandler.handler).toEqual('function'), 78 | ) 79 | 80 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 81 | binding.value.events.length, 82 | ) 83 | 84 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 85 | }) 86 | 87 | it("doesn't do anything when binding value isActive attribute is false", () => { 88 | const directive = createDirective() 89 | const [el, binding] = createHookArguments() 90 | binding.value.isActive = false 91 | 92 | directive.bind(el, binding) 93 | jest.runOnlyPendingTimers() 94 | 95 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes(0) 96 | expect(window.addEventListener).toHaveBeenCalledTimes(0) 97 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 98 | }) 99 | 100 | it('detects iframe clicks, if bindingValue.detectIframe attribute is not false', () => { 101 | const directive = createDirective() 102 | const [el, binding] = createHookArguments() 103 | 104 | directive.bind(el, binding) 105 | jest.runOnlyPendingTimers() 106 | 107 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 108 | 109 | const directive2 = createDirective() 110 | const [el2, binding2] = createHookArguments(undefined, { 111 | value: { detectIframe: false }, 112 | }) 113 | 114 | directive2.bind(el2, binding2) 115 | jest.runOnlyPendingTimers() 116 | 117 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 118 | }) 119 | 120 | it('checks that event listener is set correctly with capture option passed', () => { 121 | const directive = createDirective() 122 | const [el, binding] = createHookArguments() 123 | binding.value.capture = true 124 | 125 | directive.bind(el, binding) 126 | jest.runOnlyPendingTimers() 127 | 128 | el[HANDLERS_PROPERTY].forEach(({ event, srcTarget, handler }) => 129 | expect(srcTarget.addEventListener).toHaveBeenCalledWith( 130 | event, 131 | handler, 132 | true, 133 | ), 134 | ) 135 | }) 136 | }) 137 | 138 | describe('unbind', () => { 139 | beforeAll(() => { 140 | jest.useFakeTimers() 141 | document.documentElement.removeEventListener = jest.fn() 142 | window.removeEventListener = jest.fn() 143 | }) 144 | 145 | afterEach(() => { 146 | jest.clearAllMocks() 147 | }) 148 | 149 | it('removes event listeners attached to the element', () => { 150 | const directive = createDirective() 151 | 152 | const [el1, binding1] = createHookArguments() 153 | directive.bind(el1, binding1) 154 | jest.runOnlyPendingTimers() 155 | expect(el1[HANDLERS_PROPERTY].length).toEqual(2) 156 | 157 | const [el2, binding2] = createHookArguments() 158 | directive.bind(el2, binding2) 159 | jest.runOnlyPendingTimers() 160 | expect(el2[HANDLERS_PROPERTY].length).toEqual(2) 161 | 162 | const [el3, binding3] = createHookArguments() 163 | directive.bind(el3, binding3) 164 | jest.runOnlyPendingTimers() 165 | expect(el3[HANDLERS_PROPERTY].length).toEqual(2) 166 | 167 | const els = [el1, el2, el3] 168 | 169 | els.forEach((el) => { 170 | directive.unbind(el) 171 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 172 | }) 173 | expect( 174 | document.documentElement.removeEventListener, 175 | ).toHaveBeenCalledTimes(3) 176 | }) 177 | 178 | it('removes event listener attached to window', () => { 179 | const directive = createDirective() 180 | const [el, bindingValue] = createHookArguments() 181 | 182 | directive.bind(el, bindingValue) 183 | jest.runOnlyPendingTimers() 184 | 185 | directive.unbind(el) 186 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 187 | }) 188 | 189 | it('removes event listeners with capture option passed', () => { 190 | const directive = createDirective() 191 | 192 | const [el, binding] = createHookArguments() 193 | binding.value.capture = true 194 | directive.bind(el, binding) 195 | jest.runOnlyPendingTimers() 196 | 197 | const elSettings = el[HANDLERS_PROPERTY] 198 | directive.unbind(el) 199 | 200 | elSettings.forEach(({ event, srcTarget, handler }) => 201 | expect(srcTarget.removeEventListener).toHaveBeenCalledWith( 202 | event, 203 | handler, 204 | true, 205 | ), 206 | ) 207 | }) 208 | }) 209 | 210 | describe('update', () => { 211 | it('throws an error if the binding value is not a function or an object', () => { 212 | const directive = createDirective() 213 | 214 | expect(() => 215 | directive.update(document.createElement('div'), { value: 'no value' }), 216 | ).toThrowError( 217 | /v-click-outside: Binding value must be a function or an object/, 218 | ) 219 | }) 220 | 221 | describe('updates "isActive" binding value', () => { 222 | beforeEach(() => { 223 | jest.useFakeTimers() 224 | document.documentElement.addEventListener = jest.fn() 225 | document.documentElement.removeEventListener = jest.fn() 226 | window.addEventListener = jest.fn() 227 | window.removeEventListener = jest.fn() 228 | }) 229 | 230 | it('updates isActive binding value from true to true', () => { 231 | const directive = createDirective() 232 | const [el, binding] = createHookArguments() 233 | 234 | directive.bind(el, binding) 235 | jest.runOnlyPendingTimers() 236 | 237 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 238 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 239 | 1, 240 | ) 241 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 242 | 243 | binding.oldValue = binding.value 244 | directive.update(el, binding) 245 | jest.runOnlyPendingTimers() 246 | 247 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 248 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 249 | 1, 250 | ) 251 | expect( 252 | document.documentElement.removeEventListener, 253 | ).toHaveBeenCalledTimes(0) 254 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 255 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 256 | 257 | const [, newBinding] = createHookArguments(undefined, { 258 | value: { events: ['click'] }, 259 | oldValue: binding.oldValue, 260 | }) 261 | directive.update(el, newBinding) 262 | jest.runOnlyPendingTimers() 263 | 264 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 265 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 266 | 2, 267 | ) 268 | expect( 269 | document.documentElement.removeEventListener, 270 | ).toHaveBeenCalledTimes(binding.value.events.length) 271 | expect(window.addEventListener).toHaveBeenCalledTimes(2) 272 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 273 | }) 274 | 275 | it('updates is active binding value from true to false', () => { 276 | const directive = createDirective() 277 | const [el, binding] = createHookArguments() 278 | 279 | directive.bind(el, binding) 280 | jest.runOnlyPendingTimers() 281 | 282 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 283 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 284 | 1, 285 | ) 286 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 287 | 288 | binding.value.isActive = false 289 | directive.update(el, binding) 290 | jest.runOnlyPendingTimers() 291 | 292 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 293 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 294 | 1, 295 | ) 296 | expect( 297 | document.documentElement.removeEventListener, 298 | ).toHaveBeenCalledTimes(1) 299 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 300 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 301 | }) 302 | 303 | it('updates is active binding value from false to true', () => { 304 | const directive = createDirective() 305 | const [el, binding] = createHookArguments(undefined, { 306 | value: { isActive: false }, 307 | }) 308 | 309 | directive.bind(el, binding) 310 | jest.runOnlyPendingTimers() 311 | 312 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 313 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 314 | 0, 315 | ) 316 | expect( 317 | document.documentElement.removeEventListener, 318 | ).toHaveBeenCalledTimes(0) 319 | expect(window.addEventListener).toHaveBeenCalledTimes(0) 320 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 321 | 322 | binding.oldValue = { ...binding.value } 323 | binding.value.isActive = true 324 | directive.update(el, binding) 325 | jest.runOnlyPendingTimers() 326 | jest.runOnlyPendingTimers() 327 | 328 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 329 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 330 | 1, 331 | ) 332 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 333 | expect( 334 | document.documentElement.removeEventListener, 335 | ).toHaveBeenCalledTimes(0) 336 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 337 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 338 | 339 | const [, newBinding] = createHookArguments(undefined, { 340 | value: { events: ['click'] }, 341 | oldValue: binding.value, 342 | }) 343 | directive.update(el, newBinding) 344 | jest.runOnlyPendingTimers() 345 | 346 | expect(el[HANDLERS_PROPERTY].length).toEqual(2) 347 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 348 | 2, 349 | ) 350 | expect( 351 | document.documentElement.removeEventListener, 352 | ).toHaveBeenCalledTimes(binding.value.events.length) 353 | expect(window.addEventListener).toHaveBeenCalledTimes(2) 354 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 355 | }) 356 | 357 | it('updates is active binding value from false to false', () => { 358 | const directive = createDirective() 359 | const [el, binding] = createHookArguments(undefined, { 360 | value: { isActive: false }, 361 | }) 362 | 363 | directive.bind(el, binding) 364 | jest.runOnlyPendingTimers() 365 | 366 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 367 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 368 | 0, 369 | ) 370 | expect(window.addEventListener).toHaveBeenCalledTimes(0) 371 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 372 | 373 | directive.update(el, binding) 374 | jest.runOnlyPendingTimers() 375 | 376 | expect(el[HANDLERS_PROPERTY]).toBeUndefined() 377 | expect(document.documentElement.addEventListener).toHaveBeenCalledTimes( 378 | 0, 379 | ) 380 | expect( 381 | document.documentElement.removeEventListener, 382 | ).toHaveBeenCalledTimes(0) 383 | expect(window.addEventListener).toHaveBeenCalledTimes(0) 384 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 385 | }) 386 | }) 387 | 388 | describe('updates "detectIframe" binding value', () => { 389 | beforeEach(() => { 390 | jest.useFakeTimers() 391 | window.addEventListener = jest.fn() 392 | window.removeEventListener = jest.fn() 393 | }) 394 | 395 | it('works', () => { 396 | const directive = createDirective() 397 | const [el, binding] = createHookArguments() 398 | 399 | directive.bind(el, binding) 400 | jest.runOnlyPendingTimers() 401 | 402 | // starts true by default 403 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 404 | expect(window.removeEventListener).toHaveBeenCalledTimes(0) 405 | 406 | // TRUE TO FALSE 407 | binding.oldValue = { ...binding.value } 408 | binding.value.detectIframe = false 409 | directive.update(el, binding) 410 | jest.runOnlyPendingTimers() 411 | 412 | // Same count 413 | expect(window.addEventListener).toHaveBeenCalledTimes(1) 414 | // Event remove 415 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 416 | 417 | // FALSE TO TRUE 418 | binding.oldValue = { ...binding.value } 419 | binding.value.detectIframe = true 420 | directive.update(el, binding) 421 | jest.runOnlyPendingTimers() 422 | 423 | expect(window.addEventListener).toHaveBeenCalledTimes(2) 424 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 425 | 426 | // TRUE TO TRUE 427 | binding.oldValue = { ...binding.value } 428 | binding.value.detectIframe = true 429 | directive.update(el, binding) 430 | jest.runOnlyPendingTimers() 431 | 432 | expect(window.addEventListener).toHaveBeenCalledTimes(2) 433 | expect(window.removeEventListener).toHaveBeenCalledTimes(1) 434 | }) 435 | }) 436 | }) 437 | }) 438 | --------------------------------------------------------------------------------
Click Outside Yellow box
Click Outside Red box
Click Outside Lime box
Click Outside blue box