Skip to content

Commit

Permalink
Merge pull request #222 from renatodeleao/fix/iframe-click-detection
Browse files Browse the repository at this point in the history
fix: iframe click detection
  • Loading branch information
ndelvalle authored Aug 29, 2020
2 parents e56b509 + a7ff8d1 commit a6c7e1b
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 56 deletions.
84 changes: 50 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ Vue.use(vClickOutside)
events: ['dblclick', 'click'],
// Note: The default value is true, but in case you want to activate / deactivate
// this directive dynamically use this attribute.
isActive: true
isActive: true,
// Note: The default value is true. See detecting "Detecting Iframe Clicks" section
// to understand why this behaviour is behind a flag.
detectIFrame: true
}
},
methods: {
Expand Down Expand Up @@ -92,40 +95,40 @@ Or use directive‘s hooks programatically

```vue
<script>
import vClickOutside from 'v-click-outside'
const { bind, unbind } = vClickOutside.directive
export default {
name: 'RenderlessExample',
mounted() {
const this._el = document.querySelector('data-ref', 'some-uid')
// Note: v-click-outside config or handler needs to be passed to the
// "bind" function 2nd argument as object with a "value" key:
// same as Vue’s directives "binding" format.
// https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
bind(this._el, { value: this.onOutsideClick })
},
beforeDestroy() {
unbind(this._el)
},
methods: {
onClickOutside (event) {
console.log('Clicked outside. Event: ', event)
}
},
render() {
return this.$scopedSlots.default({
// Note: you can't pass vue's $refs (ref attribute) via slot-scope,
// and have this.$refs property populated as it will be
// interpreted as a regular html attribute. Workaround it
// with good old data-attr + querySelector combo.
props: { 'data-ref': 'some-uid' }
})
import vClickOutside from 'v-click-outside'
const { bind, unbind } = vClickOutside.directive
export default {
name: 'RenderlessExample',
mounted() {
const this._el = document.querySelector('data-ref', 'some-uid')
// Note: v-click-outside config or handler needs to be passed to the
// "bind" function 2nd argument as object with a "value" key:
// same as Vue’s directives "binding" format.
// https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
bind(this._el, { value: this.onOutsideClick })
},
beforeDestroy() {
unbind(this._el)
},
methods: {
onClickOutside (event) {
console.log('Clicked outside. Event: ', event)
}
};
},
render() {
return this.$scopedSlots.default({
// Note: you can't pass vue's $refs (ref attribute) via slot-scope,
// and have this.$refs property populated as it will be
// interpreted as a regular html attribute. Workaround it
// with good old data-attr + querySelector combo.
props: { 'data-ref': 'some-uid' }
})
}
};
</script>
```

Expand Down Expand Up @@ -154,6 +157,19 @@ The `notouch` modifier is no longer supported, same functionality can be achieve

The HTML `el` is not sent in the handler function argument any more. Review [this issue](https://github.com/ndelvalle/v-click-outside/issues/137) for more details.

## Detecting Iframe Clicks

To our knowledge, there isn't an idiomatic way to detect a click on a `<iframe>` (`HTMLIFrameElement`).
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 `<iframe>`, and execute the provided `handler`.

**As with any workaround, this also has its caveats:**

- 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;
- Moving focus to `iframe` via `keyboard` navigation also triggers `window.blur` consequently the handler - no workaround found ATM;

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.
Any improvements or suggestions to this are welcomed.

## License

[MIT License](https://github.com/ndelvalle/v-click-outside/blob/master/LICENSE)
7 changes: 6 additions & 1 deletion example/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<div class="lime-box" ref="limeEl">
<p>Click Outside #lime box</p>
</div>
<iframe class="iframe" src="/about" width="100%" />
</div>
</div>
</template>
Expand Down Expand Up @@ -77,5 +78,9 @@ export default {
background-color: lime;
height: 50px;
}
</style>
.iframe {
border: 1px solid lightgrey;
margin-top: 1em;
}
</style>
51 changes: 41 additions & 10 deletions src/v-click-outside.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ function processDirectiveArguments(bindingValue) {
middleware: bindingValue.middleware || ((item) => item),
events: bindingValue.events || EVENTS,
isActive: !(bindingValue.isActive === false),
detectIframe: !(bindingValue.detectIframe === false),
}
}

function execHandler({ event, handler, middleware }) {
if (middleware(event)) {
handler(event)
}
}

function onFauxIframeClick({ event, handler, middleware }) {
// Note: on firefox clicking on iframe triggers blur, but only on
// next event loop it becomes document.activeElement
// https://stackoverflow.com/q/2381336#comment61192398_23231136
setTimeout(() => {
if (document.activeElement.tagName === 'IFRAME') {
execHandler({ event, handler, middleware })
}
}, 0)
}

function onEvent({ el, event, handler, middleware }) {
// Note: composedPath is not supported on IE and Edge, more information here:
// https://developer.mozilla.org/en-US/docs/Web/API/Event/composedPath
Expand All @@ -37,40 +55,53 @@ function onEvent({ el, event, handler, middleware }) {
return
}

if (middleware(event)) {
handler(event)
}
execHandler({ event, handler, middleware })
}

function bind(el, { value }) {
const { events, handler, middleware, isActive } = processDirectiveArguments(
value,
)
const {
events,
handler,
middleware,
isActive,
detectIframe,
} = processDirectiveArguments(value)
if (!isActive) {
return
}

el[HANDLERS_PROPERTY] = events.map((eventName) => ({
event: eventName,
srcTarget: document.documentElement,
handler: (event) => onEvent({ event, el, handler, middleware }),
}))

el[HANDLERS_PROPERTY].forEach(({ event, handler }) =>
if (detectIframe) {
const detectIframeEvent = {
event: 'blur',
srcTarget: window,
handler: (event) => onFauxIframeClick({ event, handler, middleware }),
}

el[HANDLERS_PROPERTY] = [...el[HANDLERS_PROPERTY], detectIframeEvent]
}

el[HANDLERS_PROPERTY].forEach(({ event, srcTarget, handler }) =>
setTimeout(() => {
// Note: More info about this implementation can be found here:
// https://github.com/ndelvalle/v-click-outside/issues/137
if (!el[HANDLERS_PROPERTY]) {
return
}
document.documentElement.addEventListener(event, handler, false)
srcTarget.addEventListener(event, handler, false)
}, 0),
)
}

function unbind(el) {
const handlers = el[HANDLERS_PROPERTY] || []
handlers.forEach(({ event, handler }) =>
document.documentElement.removeEventListener(event, handler, false),
handlers.forEach(({ event, srcTarget, handler }) =>
srcTarget.removeEventListener(event, handler, false),
)
delete el[HANDLERS_PROPERTY]
}
Expand Down
Loading

0 comments on commit a6c7e1b

Please sign in to comment.