Is it possible to disable the scroll wheel changing the number in an input number field?
I've messed with webkit-specific CSS to remove the spinner but I'd like to get rid of this behavior altogether. I like using type=number since it brings up a nice keyboard on iOS.
Prevent the default behavior of the mousewheel event on input-number elements like suggested by others (calling "blur()" would normally not be the preferred way to do it, because that wouldn't be, what the user wants).
BUT. I would avoid listening for the mousewheel event on all input-number elements all the time and only do it, when the element is in focus (that's when the problem exists). Otherwise the user cannot scroll the page when the mouse pointer is anywhere over a input-number element.
Solution for jQuery:
// disable mousewheel on a input number field when in focus
// (to prevent Chromium browsers change the value when scrolling)
$('form').on('focus', 'input[type=number]', function (e) {
$(this).on('wheel.disableScroll', function (e) {
e.preventDefault()
})
})
$('form').on('blur', 'input[type=number]', function (e) {
$(this).off('wheel.disableScroll')
})
(Delegate focus events to the surrounding form element - to avoid to many event listeners, which are bad for performance.)
One event listener to rule them all
This is similar to #Simon Perepelitsa's answer in pure js, but a bit simpler, as it puts one event listener on the document element for all inputs and checks if the focused element is a number input tpye:
document.addEventListener("wheel", function(event){
if(document.activeElement.type === "number"){
document.activeElement.blur();
}
});
If you want to turn off the value scrolling behaviour on some fields by class/id, but not others just add && and the corresponding document selector instead:
document.addEventListener("wheel", function(event){
if(document.activeElement.type === "number" &&
document.activeElement.classList.contains("noscroll"))
{
document.activeElement.blur();
}
});
with this:
<input type="number" class="noscroll"/>
If an input has the noscroll class it wont change on scroll, otherwise everything stays the same.
Test here with JSFiddle
$(document).on("wheel", "input[type=number]", function (e) {
$(this).blur();
});
You can simply use the HTML onwheel attribute.
This option have no effects on scrolling over other elements of the page.
And add a listener for all inputs don't work in inputs dynamically created posteriorly.
Aditionaly, you can remove the input arrows with CSS.
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
<input type="number" onwheel="this.blur()" />
I have an alternative suggestion. The problem I see with most of the common recommendation of firing a blur event is that it has unexpected side-effects. It's not always a good thing to remove a focus state unexpectedly.
Why not this instead?
<input type="number" onwheel="return false;" />
It's very simple and straight-forward, easy to implement, and no side-effects that I can think of.
input = document.getElementById("the_number_input")
input.addEventListener("mousewheel", function(event){ this.blur() })
http://jsfiddle.net/bQbDm/2/
For jQuery example and a cross-browser solution see related question:
HTML5 event listener for number input scroll - Chrome only
#Semyon Perepelitsa
There is a better solution for this. Blur removes the focus from the input and that is a side affect that you do not want. You should use evt.preventDefault instead. This prevents the default behavior of the input when the user scrolls. Here is the code:
input = document.getElementById("the_number_input")
input.addEventListener("mousewheel", function(evt){ evt.preventDefault(); })
For anyone working with React and looking for solution. I’ve found out that easiest way is to use onWheelCapture prop in Input component like this:
onWheelCapture={e => {
e.target.blur()
}}
ReactJS Solution
For those needing a React solution, here's an onWheel handler for your type="number" input to prevent the number from changing and prevent the page from scrolling while the user tries to wheel over the input. Finally, it'll refocus on the input so the user can keep editing as intended:
const numberInputOnWheelPreventChange = (e) => {
// Prevent the input value change
e.target.blur()
// Prevent the page/container scrolling
e.stopPropagation()
// Refocus immediately, on the next tick (after the current function is done)
setTimeout(() => {
e.target.focus()
}, 0)
}
return <input type="number" onWheel={numberInputOnWheelPreventChange}/>
First you must stop the mousewheel event by either:
Disabling it with mousewheel.disableScroll
Intercepting it with e.preventDefault();
By removing focus from the element el.blur();
The first two approaches both stop the window from scrolling and the last removes focus from the element; both of which are undesirable outcomes.
One workaround is to use el.blur() and refocus the element after a delay:
$('input[type=number]').on('mousewheel', function(){
var el = $(this);
el.blur();
setTimeout(function(){
el.focus();
}, 10);
});
Easiest solution is to add onWheel={ event => event.currentTarget.blur() }} on input itself.
Typescript Variation
Typescript needs to know that you're working with an HTMLElement for type safety, else you'll see lots of Property 'type' does not exist on type 'Element' type of errors.
document.addEventListener("wheel", function(event){
const numberInput = (<HTMLInputElement>document.activeElement);
if (numberInput.type === "number") {
numberInput.blur();
}
});
The provided answers do not work in Firefox (Quantum). The event listener needs to be changed from mousewheel to wheel:
$(':input[type=number]').on('wheel',function(e){ $(this).blur(); });
This code works on Firefox Quantum and Chrome.
If you want a solution that doesn’t need JavaScript, combining some HTML functionality with a CSS pseudo-element does the trick:
span {
position: relative;
display: inline-block; /* Fit around contents */
}
span::after {
content: "";
position: absolute;
top: 0; right: 0; bottom: 0; left: 0; /* Stretch over containing block */
cursor: text; /* restore I-beam cursor */
}
/* Restore context menu while editing */
input:focus {
position: relative;
z-index: 1;
}
<label>How many javascripts can u fit in u mouth?
<span><input type="number" min="0" max="99" value="1"></span>
</label>
This works because clicking on the contents of a <label> that’s associated with a form field will focus the field. However, the “windowpane” of the pseudo-element over the field will block mousewheel events from reaching it.
The drawback is that the up/down spinner buttons no longer work, but you said you had removed those anyway.
In theory, one could restore the context menu without requiring the input to be focused first: :hover styles shouldn’t fire when the user scrolls, since browsers avoid recalculating them during scrolling for performance reasons, but I haven’t thoroughly cross-browser/device tested it.
In my case, I needed to maintain focus and still apply the scroll. None of the solutions above can handle that and doing blur/focus feels a bit hacky to me.
This maintains existing focus and also keeps the scroll. You know... like the browser should. Only minimally tested in chrome and only supports Y-axis.
// you could make this target a specific input instead of document
document.addEventListener('wheel', event => {
if (!event.target) return;
const isNumberInput = event.target.nodeName === 'INPUT' && event.target.type === 'number';
const isFocused = event.target === document.activeElement;
if (isNumberInput && isFocused) {
// prevent stupid input change
event.preventDefault();
// since we're taking over scrolling, we want to make sure
// nothing else gets the event
event.stopPropagation();
// finally we reapply the scroll
applyScroll(event);
}
}, { passive: false });
// this walks up the tree for event.target to find the first
// scrollable parent. this is probably good enough for most situations.
const applyScroll = event => {
try {
// console.debug('attempting to reapply scroll. searching for scrollable container...');
let scrollContainer = event.target;
while (scrollContainer && scrollContainer !== document.body && !elementIsScrollable(scrollContainer)) {
scrollContainer = scrollContainer.parentElement;
// console.debug('\t-> container was not scrollable. checking parent', scrollContainer);
}
if (scrollContainer) {
// console.debug('scrollContainer container found. applying scroll', scrollContainer, event.deltaY);
scrollContainer.scrollBy({ top: event.deltaY });
}
else {
// console.debug('no scrollContainer found');
}
}
catch (err) {
console.info('failed to reapply scroll', err, event);
}
};
const elementIsScrollable = element => {
const { scrollHeight = 0, offsetHeight = 0 } = element;
const scrollable = style.overflowY === 'auto' || style.overflowY === 'scroll';
return scrollable && scrollHeight > 0 && offsetHeight > 0 && element.scrollHeight > element.offsetHeight;
};
Non-JS solution
I like using type=number since it brings up a nice keyboard on iOS.
The keyboard is nice indeed. But we can get the same behaviour with:
<input inputmode="numeric" pattern="[0-9]*" />
Taken from gov.uk which was linked in the MUI docs. Works nicely for our product.
Grain of salt
Please check browser support for inputmode. Most mobile browsers are supported, and to me inputmode is mostly about the mobile experience. But YMMV.
While trying to solve this for myself, I noticed that it's actually possible to retain the scrolling of the page and focus of the input while disabling number changes by attempting to re-fire the caught event on the parent element of the <input type="number"/> on which it was caught, simply like this:
e.target.parentElement.dispatchEvent(e);
However, this causes an error in browser console, and is probably not guaranteed to work everywhere (I only tested on Firefox), since it is intentionally invalid code.
Another solution which works nicely at least on Firefox and Chromium is to temporarily make the <input> element readOnly, like this:
function handleScroll(e) {
if (e.target.tagName.toLowerCase() === 'input'
&& (e.target.type === 'number')
&& (e.target === document.activeElement)
&& !e.target.readOnly
) {
e.target.readOnly = true;
setTimeout(function(el){ el.readOnly = false; }, 0, e.target);
}
}
document.addEventListener('wheel', function(e){ handleScroll(e); });
One side effect that I've noticed is that it may cause the field to flicker for a split-second if you have different styling for readOnly fields, but for my case at least, this doesn't seem to be an issue.
Similarly, (as explained in James' answer) instead of modifying the readOnly property, you can blur() the field and then focus() it back, but again, depending on styles in use, some flickering might occur.
Alternatively, as mentioned in other comments here, you can just call preventDefault() on the event instead. Assuming that you only handle wheel events on number inputs which are in focus and under the mouse cursor (that's what the three conditions above signify), negative impact on user experience would be close to none.
function fixNumericScrolling() {
$$( "input[type=number]" ).addEvent( "mousewheel", function(e) {
stopAll(e);
} );
}
function stopAll(e) {
if( typeof( e.preventDefault ) != "undefined" ) e.preventDefault();
if( typeof( e.stopImmediatePropagation ) != "undefined" ) e.stopImmediatePropagation();
if( typeof( event ) != "undefined" ) {
if( typeof( event.preventDefault ) != "undefined" ) event.preventDefault();
if( typeof( event.stopImmediatePropagation ) != "undefined" ) event.stopImmediatePropagation();
}
return false;
}
Most answers blur the number element even if the cursor isn't hovering over it; the below does not
document.addEventListener("wheel", function(event) {
if (document.activeElement.type === "number" &&
document.elementFromPoint(event.x, event.y) == document.activeElement) {
document.activeElement.blur();
}
});
https://jsfiddle.net/s06puv3j/1/
I was struggling with the solution. So, This and other posts help me to do this. We need to change some stuff regarding the best answer here. So in order to disable scrolling, we must add the following:
<script>
$(document).ready(function() {
$('input[type=number]').on('wheel',function(e){ $(this).blur(); });
});
</script>
Instead of using "onwheel" we use "wheel" :)
Antd / React + Typescript answer
const myComponent = () => {
const inputRef: React.RefObject<HTMLInputElement> = createRef();
return <Input
ref={inputRef}
type="number"
onWheel={(e) => {
if (inputRef && inputRef.current && inputRef.current.blur) {
inputRef.current.blur();
}
e.preventDefault();
}}
/>
}
Angular solution. One directive to rule them all!
In contrast to other solutions, with this solution the user
does not loose focus on the input
and still able to scroll!
See it on StackBlitz
import { Directive, ElementRef, NgZone, OnDestroy } from '#angular/core';
import { fromEvent, Subscription, takeUntil } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
#Directive({
selector: 'input[type=number]',
})
export class FixNumberInputScrollDirective implements OnDestroy {
private subs = new Subscription();
constructor(elRef: ElementRef<HTMLInputElement>, zone: NgZone) {
const el = elRef.nativeElement;
const focus$ = fromEvent(el, 'focus');
const blur$ = fromEvent(el, 'blur');
// when input is focused, start listening to the scroll of element. On this event blur and
// re-focus on the next tick. This allows for the page scroll to still happen, but the unwanted
// input number change is prevented.
// Stop listening to the scroll when focus is lost
const preventWheel$ = focus$.pipe(
switchMap(() => {
return fromEvent(el, 'wheel', { passive: false }).pipe(
tap(() => {
zone.runOutsideAngular(() => {
el.blur();
setTimeout(() => {
el.focus();
}, 0);
})
}),
takeUntil(blur$)
);
})
);
this.subs.add(preventWheel$.subscribe());
}
ngOnDestroy() {
this.subs.unsubscribe();
}
}
Related
I want to be able to listen to <input type="number" /> step UP (increment) and step down events with jQuery. (currently I can only understand how to listen to change event)
For input type="number", you can use the change event, but will possibly have more cases to handle and create some clutter. I broke it down, I recommend using the "mouseup" event for the increment feature (which will mainly be used from pc) But if the user uses a device instead, I would use the event 'keyup' since the increment feature will not appear and the user will have an on-screen keyboard instead. The change event will listen for both.
For example :
$(".counter").bind('mouseup', function () {
if($(this).val() == undefined || $(this).val() == "")
return; /* Exit dont bother with handling this later, if its not needed leave. */
/* Important Check against integers and not strings. */
/* Comparing against arrays will give unexecpted behavior, size is different then the value. */
var newVal = parseInt($(this).val());
var oldVal = parseInt($(this).data('old-value'));
if (oldVal < newVal) {
alert('incrementing');
}
else{
alert('decrementing');
}
$(this).data('old-value', $(this).val());
});
$(".counter").bind('keyup', function () {
/* Similar logic */
});
I use "bind" instead of "on" or the by method "change" since "on" is a shorthand for bind.
JSFiddle Demo
There is no event for up and down. You can use change event
$(".counter").change(function () {
alert($(this).val());
})
DEMO
You can try something like, You can store previous value and compare with currently value and identify up or down
$(".counter").change(function () {
if ($(this).data('old-value') < $(this).val()) {
alert('Alert up');
} else {
alert('Alert dowm');
}
$(this).data('old-value', $(this).val());
})
DEMO
I'm not sure if there is a listener of the stepUp and StepDown but you can externalize the PLUS and LESS buttons of the input number using:
document.getElementById("myNumber").stepUp(5); //increase the value +5
document.getElementById("myNumber").stepDown(5); // decrease the value -5
So you can finally achieve your objective!
Reference: https://www.w3schools.com/jsref/met_number_stepup.asp
NOTE: Be aware that IE11 and EDGE doesn't implement stepUp / stepDown. On my case I just remove the + and - icon and leave only the input number field. Hope EDGE die soon...
I know this is a relatively old question but, came across it in my search.
In case anyone comes here looking for the answer to this on a custom element triggering the stepUp() or stepDown() like me and not wanting to use jQuery, the below should help:
On the element handling the click to trigger the stepUp()/stepDown(), add this (and change the elements to whatever relation they have to each other):
onclick="this.elementToStepDown.stepDown();this.elementToStepDown.dispatchEvent(new Event('change'))"
Fuller example:
<input class="input--hide_controls" id="number_of_ducks" type="number" value="7" step="1"/>
<div class="increment" onclick="this.parentNode.querySelector('input').stepUp();this.parentNode.querySelector('input').dispatchEvent(new Event('change'))"><i class="im im-plus"></i></div>
Or, extract it out and put it in a JS function that does the same and call that onclick:
let increment = (element) => {
element.stepUp();
element.dispatchEvent(new Event('change'))
}
let decrement = (element) => {
element.stepDown();
element.dispatchEvent(new Event('change'))
}
Then listen out for the 'change' event on the input.
There's no specific event for up and down. Your closest bet is to use the change event. You might also take a look at the oninput event but I am not sure it is supported by all browsers:
$('#myinput').on('input', function() {
});
You should do this:
var $counter = $("your_counter");
var number = $counter.val();
$counter.change(function () {
if($counter.val() > number)
alert('up');
else
alert('down');
number = $counter.val();
});
Here is the demo: http://jsfiddle.net/6bb8S/
html
<input type="number" data-number="0" class="counter" value="0" />
you need to put the same value in the data-number and in the value
jquery
$(document).on('change', '.counter', function() {
//get number of input
var number = $(this).attr("data-number");
//console.log(number);
if($(this).val() > number){
alert('up');
}else{
alert('down');
}
//set number of input for the next step
$(this).attr("data-number", $(this).val());
});
https://jsfiddle.net/maicon_card/46hdm8pj/
works with multiple inputs
I have a VueJS app where a HTML button is on the screen and when it's clicked it's appropriate to have "focus" but I'd like the option to remove the focus after a short delay.
if (props.defocus === "true" || props.defocus === true) {
// de-focus the button after 400ms
setTimeout(() => {
if (el.value !== null) {
el.value.blur();
}
}, 400);
}
Now I did mention that this is a VueJS project but really this is just DOM API and JS/TS. I use setTimeout to wait 400ms and I am able to call the blur() function on the button. However, this does not appear to do anything. Now I guess this could be two things:
I have successfully removed the focus on the button element according to the DOM but somehow VueJS is not picking this up and reacting to it (aka, the focus styling remains)
The call to focus() is somehow silently ignored (there are no errors)
Is there a way to inspect the focus state in developer tools? Is there something I'm missing in getting the behavior I'm looking for?
Just for the visual minded in the group, the component above is where when I click on a button and make it selected the focus state is represented by a purple outline. I want that to go away after a user has clicked it.
Trying to use the pointer to the button didn't work but instead I found that simply querying for the focus element gives me a handler where I can call blur and have it work:
if (props.defocus === "true" || props.defocus === true) {
// de-focus the button after delay
setTimeout(() => {
// QUERY FOR FOCUS ELEMENT
const e2 = document.querySelector(":focus");
if (e2 && (e2 as HTMLButtonElement).blur) {
(e2 as HTMLButtonElement).blur();
}
}, Number(props.defocusDelay));
}
This works without issue and unless someone else has a better solution I will mark this as the correct one.
Make a directive file Defocus.ts;
export default {
name: 'defocus',
priority: 700,
bind(el: any, binding: any) {
el._onClick = () => {
setTimeout(()=> {
el.blur();
}, 400)
}
el.addEventListener('click', el._onClick);
},
unbind(el: any) {
el.removeEventListener('click', el._onClick);
}
};
Then you import and use
import Defocus from "#/shared/directives/Defocus";
#Component ({
directives: {
Defocus
}
})
Then in template:
<button v-defocus>Click Me</button>
This should be pretty simple but for some reason it isn't working, I'm getting the proper console.logs at the right time, but the focus isn't going to the correct place, please refer to my jsfiddle
https://jsfiddle.net/bqt0np9d/
function checkTabPress(e) {
"use strict";
// pick passed event of global event object
e = e || event;
if (e.keyCode === 9) {
if (e.shiftKey) {
console.log('back tab pressed');
firstItem.onblur=function(){
console.log('last a focus left');
lastItem.focus();
};
e.preventDefault();
}
console.log('tab pressed');
lastItem.onblur=function(){
console.log('last a focus left');
firstItem.focus();
};
e.preventDefault();
}
}
modal.addEventListener('keyup', checkTabPress);
I had to lock focus within a modal that we had used within a React component.
I added eventListner for KEY DOWN and collected Tab and Shift+Tab
class Modal extends Component {
componentDidMount() {
window.addEventListener("keyup", this.handleKeyUp, false);
window.addEventListener("keydown", this.handleKeyDown, false);
}
componentWillUnmount() {
window.removeEventListener("keyup", this.handleKeyUp, false);
window.removeEventListener("keydown", this.handleKeyDown, false);
}
handleKeyDown = (e) => {
//Fetch node list from which required elements could be grabbed as needed.
const modal = document.getElementById("modal_parent");
const tags = [...modal.querySelectorAll('select, input, textarea, button, a, li')].filter(e1 => window.getComputedStyle(e1).getPropertyValue('display') === 'block');
const focusable = modal.querySelectorAll('button, [href], input, select, textarea, li, a,[tabindex]:not([tabindex="-1"])');
const firstFocusable = focusable[0];
const lastFocusable = focusable[focusable.length - 1];
if (e.ctrlKey || e.altKey) {
return;
}
const keys = {
9: () => { //9 = TAB
if (e.shiftKey && e.target === firstFocusable) {
lastFocusable.focus();
}
if (e.target === lastFocusable) {
firstFocusable.focus();
}
}
};
if (keys[e.keyCode]) {
keys[e.keyCode]();
}
}
}
One of the problems is that you are using keyup instead of keydown. The keyup will only fire after the tab has already fired. However, making that change to your code results in the keyboard being trapped on one of the links. The code is flawed.
Here is some code that does what you want (using jQuery)
http://dylanb.github.io/javascripts/periodic-1.1.js
// Add keyboard handling for TAB circling
$modal.on('keydown', function (e) {
var cancel = false;
if (e.ctrlKey || e.metaKey || e.altKey) {
return;
}
switch(e.which) {
case 27: // ESC
$modal.hide();
lastfocus.focus();
cancel = true;
break;
case 9: // TAB
if (e.shiftKey) {
if (e.target === links[0]) {
links[links.length - 1].focus();
cancel = true;
}
} else {
if (e.target === links[links.length - 1]) {
links[0].focus();
cancel = true;
}
}
break;
}
if (cancel) {
e.preventDefault();
}
});
You can see a working version of this dialog here
http://dylanb.github.io/periodic-aria11-attributes.html
Click the text in one of the colored boxes to see the dialog pop up.
The e.preventDefault() has no effect on the keyup event (as the default browser action has already been fired)
Despite this, your example works. But only if there are links before and after the modal
If you change your HTML code with the following, adding one link before and one link after the modal; you will see that your focus is trapped in the modal:
other link
<div id="modal">
Link One
Link Two
</div>
other link
That's because there is no default browser action in such case, and then no action to prevent.
Trapping focus within a modal is very hard to do it on your own. If you're able to install third-party dependencies in your project, you can use the focus-trap package.
You can easily trap focus to any component with vanilla Javascript;
import { createFocusTrap } from 'focus-trap'
const modal = document.getElementById('modal')
const focusTrap = createFocusTrap('#modal', {
onActivate: function () {
modal.className = 'trap is-visible'
},
onDeactivate: function () {
modal.className = 'trap'
},
})
document.getElementById('show').addEventListener('click', function () {
focusTrap.activate()
})
document.getElementById('hide').addEventListener('click', function () {
focusTrap.deactivate()
})
or even React;
import React from 'react'
import ReactDOM from 'react-dom'
// Use the wrapper package of `focus-trap` to use with React.
import FocusTrap from 'focus-trap-react'
const Demo = () => {
const [showModal, setShowModal] = React.useState(false)
return (
<div>
<button onClick={() => setShowModal(true)}>show modal</button>
<FocusTrap active={showModal}>
<div id="modal">
Modal with with some{' '}
focusable elements.
<button onClick={() => setShowModal(false)}>
hide modal
</button>
</div>
</FocusTrap>
</div>
)
}
ReactDOM.render(<Demo />, document.getElementById('demo'))
I did a small write-up about the package here, which explains how to use it with either vanilla Javascript or React.
I thought I had solved trapping the focus on a modal by using tab, shift+tab, and arrow keys detection on keyup and keydown, focus, focusin, focusout on the first and last focusable elements inside the modal and a focus event for the window to set the focus back on the first focusable element on the form in case the focus "escaped" the modal or for situations like jumping from the address bar to the document using tab, but something weird happened. I had activated "Caret Browsing" in one of my browsers accidently, and that's when I realized all methods to trap focus failed miserably. I personally went on a rabbit whole to solve this for a modal. I tried focusin, focusout on the modal, matching focus-within pseudo classes, {capture: true} on the focus event from the modal and window, nothing worked.
This is how I solved it.
I recreated the modal to have a different structure. For the sake of simplicity, I am omitting a lot of things, like the aria attributes, classes, how to get all focusable elements, etc.
<component-name>
#shadow-root (closed)
<div class="wrapper">
<div class="backdrop"></div>
<div class="window>
<div tabindex="0" class="trap-focus-top"> </div>
<div class="content">
<div class="controls"><!-- Close button, whatever --></div>
<header><slot name="header"></slot></header>
<div class="body"><slot></slot></div>
<footer><slot name="footer"></slot></footer>
</div>
<div tabindex="0" class="trap-focus-bottom"> </div>
</div>
</div>
</component-name>
Search the contents div for focusable elements, to save the first and last one. If you find only one then that one will be first and last. If you find zero, then set the div for the body (.body) tabindex to "0" so that you have at least one element to set the focus on.
Before and after the content div we have two focusable divs, trap-focus-top and trap-focus-bottom, the first one when getting focus will jump the focus to the last focusable element detected on step one, and the second one will jump the focus to the first focusable element detected on step one. No need to capture any key events, just focus event on these elements. If you notice the non-breaking space on trap-focus elements, this is for mimicking content, because I noticed that the arrow keys went through these elements without firing any events when empty. When I realized this I added some content and everything worked, so I added a non-breaking space and styled the elements so that they do not occupy any space.
Capture all focus events from the window with the use capture flag set to true, so that every focus event whose target was different to the component (focus events inside the shadow-root wont't be captured with the actual target but the component itself) will result in the focus being set on the modal elements again.
Now there's another problem, let's say there's zero focusable elements on your modal outside of any controls, like a button to close the modal, then we set tabindex to 0 on the modal's body, your focus should go from the close button to the modal's body and vice versa, now, the caret browsing won't work on the content because the div.body will have the focus, not the actual content. This means I have to create another function that places the cursor at the beginning of the content whenever the body receives the focus.
startCursor = () => {
/* componentbody is a placeholder for the element with the actual content */
let text = componentbody.childNodes[0];
if (text) {
let range = new Range();
let selection = document.getSelection();
range.setStart(text, 0);
range.setEnd(text, 0);
selection.removeAllRanges();
selection.addRange(range);
componentbody.scrollTop = 0;/* In case the body has a scrollbar */
}
}
For anyone out there, this is what worked for me.
I want to run a function when a user edits the content of a div with contenteditable attribute. What's the equivalent of an onchange event?
I'm using jQuery so any solutions that uses jQuery is preferred. Thanks!
2022 update
As pointed out in the comments, this doesn't answer the question asked, which wanted the equivalent of the change event rather than the input event. However, I'll leave it here as is.
Original answer
I'd suggest attaching listeners to key events fired by the editable element, though you need to be aware that keydown and keypress events are fired before the content itself is changed. This won't cover every possible means of changing the content: the user can also use cut, copy and paste from the Edit or context browser menus, so you may want to handle the cut copy and paste events too. Also, the user can drop text or other content, so there are more events there (mouseup, for example). You may want to poll the element's contents as a fallback.
UPDATE 29 October 2014
The HTML5 input event is the answer in the long term. At the time of writing, it is supported for contenteditable elements in current Mozilla (from Firefox 14) and WebKit/Blink browsers, but not IE.
Demo:
document.getElementById("editor").addEventListener("input", function() {
console.log("input event fired");
}, false);
<div contenteditable="true" id="editor">Please type something in here</div>
Demo: http://jsfiddle.net/ch6yn/2691/
Here is a more efficient version which uses on for all contenteditables. It's based off the top answers here.
$('body').on('focus', '[contenteditable]', function() {
const $this = $(this);
$this.data('before', $this.html());
}).on('blur keyup paste input', '[contenteditable]', function() {
const $this = $(this);
if ($this.data('before') !== $this.html()) {
$this.data('before', $this.html());
$this.trigger('change');
}
});
The project is here: https://github.com/balupton/html5edit
Consider using MutationObserver. These observers are designed to react to changes in the DOM, and as a performant replacement to Mutation Events.
Pros:
Fires when any change occurs, which is difficult to achieve by listening to key events as suggested by other answers. For example, all of these work well: drag & drop, italicizing, copy/cut/paste through context menu.
Designed with performance in mind.
Simple, straightforward code. It's a lot easier to understand and debug code that listens to one event rather than code that listens to 10 events.
Google has an excellent mutation summary library which makes using MutationObservers very easy.
Cons:
Requires a very recent version of Firefox (14.0+), Chrome (18+), or IE (11+).
New API to understand
Not a lot of information available yet on best practices or case studies
Learn more:
I wrote a little snippet to compare using MutationObserers to handling a variety of events. I used balupton's code since his answer has the most upvotes.
Mozilla has an excellent page on the API
Take a look at the MutationSummary library
non jQuery quick and dirty answer:
function setChangeListener (div, listener) {
div.addEventListener("blur", listener);
div.addEventListener("keyup", listener);
div.addEventListener("paste", listener);
div.addEventListener("copy", listener);
div.addEventListener("cut", listener);
div.addEventListener("delete", listener);
div.addEventListener("mouseup", listener);
}
var div = document.querySelector("someDiv");
setChangeListener(div, function(event){
console.log(event);
});
I have modified lawwantsin 's answer like so and this works for me. I use the keyup event instead of keypress which works great.
$('#editor').on('focus', function() {
before = $(this).html();
}).on('blur keyup paste', function() {
if (before != $(this).html()) { $(this).trigger('change'); }
});
$('#editor').on('change', function() {alert('changed')});
Two options:
1) For modern (evergreen) browsers:
The "input" event would act as an alternative "change" event.
https://developer.mozilla.org/en-US/docs/Web/Events/input
document.querySelector('div').addEventListener('input', (e) => {
// Do something with the "change"-like event
});
or
<div oninput="someFunc(event)"></div>
or (with jQuery)
$('div').on('click', function(e) {
// Do something with the "change"-like event
});
2) To account for IE11 and modern (evergreen) browsers:
This watches for element changes and their contents inside the div.
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
var div = document.querySelector('div');
var divMO = new window.MutationObserver(function(e) {
// Do something on change
});
divMO.observe(div, { childList: true, subtree: true, characterData: true });
const p = document.querySelector('p')
const result = document.querySelector('div')
const observer = new MutationObserver((mutationRecords) => {
result.textContent = mutationRecords[0].target.data
// result.textContent = p.textContent
})
observer.observe(p, {
characterData: true,
subtree: true,
})
<p contenteditable>abc</p>
<div />
Here's what worked for me:
var clicked = {}
$("[contenteditable='true']").each(function(){
var id = $(this).attr("id");
$(this).bind('focus', function() {
// store the original value of element first time it gets focus
if(!(id in clicked)){
clicked[id] = $(this).html()
}
});
});
// then once the user clicks on save
$("#save").click(function(){
for(var id in clicked){
var original = clicked[id];
var current = $("#"+id).html();
// check if value changed
if(original != current) save(id,current);
}
});
This thread was very helpful while I was investigating the subject.
I've modified some of the code available here into a jQuery plugin so it is in a re-usable form, primarily to satisfy my needs but others may appreciate a simpler interface to jumpstart using contenteditable tags.
https://gist.github.com/3410122
Update:
Due to its increasing popularity the plugin has been adopted by Makesites.org
Development will continue from here:
https://github.com/makesites/jquery-contenteditable
Non JQuery answer...
function makeEditable(elem){
elem.setAttribute('contenteditable', 'true');
elem.addEventListener('blur', function (evt) {
elem.removeAttribute('contenteditable');
elem.removeEventListener('blur', evt.target);
});
elem.focus();
}
To use it, call on (say) a header element with id="myHeader"
makeEditable(document.getElementById('myHeader'))
That element will now be editable by the user until it loses focus.
In Angular 2+
<div contentEditable (input)="type($event)">
Value
</div>
#Component({
...
})
export class ContentEditableComponent {
...
type(event) {
console.log(event.data) // <-- The pressed key
console.log(event.path[0].innerHTML) // <-- The content of the div
}
}
To avoid timers and "save" buttons, you may use blur event wich fires when the element loses focus. but to be sure that the element was actually changed (not just focused and defocused), its content should be compared against its last version. or use keydown event to set some "dirty" flag on this element.
Here is the solution I ended up using and works fabulously. I use $(this).text() instead because I am just using a one line div that is content editable. But you may also use .html() this way you dont have to worry about the scope of a global/non-global variable and the before is actually attached to the editor div.
$('body').delegate('#editor', 'focus', function(){
$(this).data('before', $(this).html());
});
$('#client_tasks').delegate('.task_text', 'blur', function(){
if($(this).data('before') != $(this).html()){
/* do your stuff here - like ajax save */
alert('I promise, I have changed!');
}
});
You need to use input event type
Demo
HTML
<div id="editor" contenteditable="true" >Some text here</div>
JS
const input = document.getElementById('editor');
input.addEventListener('input', updateValue);
function updateValue(e) {
console.log(e.target);
}
know more
The onchange event doesn't fires when an element with the contentEditable attribute is changed, a suggested approach could be to add a button, to "save" the edition.
Check this plugin which handles the issue in that way:
Creating a quick and dirty jQuery contentEditable Plugin
Using DOMCharacterDataModified under MutationEvents will lead to the same. The timeout is setup to prevent sending incorrect values (e.g. in Chrome I had some issues with space key)
var timeoutID;
$('[contenteditable]').bind('DOMCharacterDataModified', function() {
clearTimeout(timeoutID);
$that = $(this);
timeoutID = setTimeout(function() {
$that.trigger('change')
}, 50)
});
$('[contentEditable]').bind('change', function() {
console.log($(this).text());
})
JSFIDDLE example
I built a jQuery plugin to do this.
(function ($) {
$.fn.wysiwygEvt = function () {
return this.each(function () {
var $this = $(this);
var htmlold = $this.html();
$this.bind('blur keyup paste copy cut mouseup', function () {
var htmlnew = $this.html();
if (htmlold !== htmlnew) {
$this.trigger('change')
}
})
})
}
})(jQuery);
You can simply call $('.wysiwyg').wysiwygEvt();
You can also remove / add events if you wish
A simple answer in JQuery, I just created this code and thought it will be helpful for others too
var cont;
$("div [contenteditable=true]").focus(function() {
cont=$(this).html();
});
$("div [contenteditable=true]").blur(function() {
if ($(this).html()!=cont) {
//Here you can write the code to run when the content change
}
});
For me, I want to check the input is valid or not.
If valid, then update, Otherwise show an error message and keep the value as same as before.
Skill: When you edit done, usually, it will trigger the blur event.
Example
<span contenteditable="true">try input somethings.</span>
<script>
const elem = document.querySelector(`span`)
let oldValue = elem.innerText
elem.onkeydown = (keyboardEvent) => {
if (keyboardEvent.key === "Enter") {
elem.blur() // set focusout
}
}
elem.onblur = (e) => {
const curValue = elem.innerText
if (curValue === oldValue) {
return
}
if (curValue.length <= 50) { // 👈 Input your conditions.
// 👇 fail
elem.innerText = oldValue
// (Optional) Add error message
elem.insertAdjacentHTML("beforeend", `<span style="margin-left:5px;color:red">error length=${curValue.length}. Must greater than 50. undo to the previous value.</span>`)
const errMsg = elem.querySelector(`span`)
setTimeout(() => errMsg.remove(), 3500) // wait 3.5 second, and then remove it.
return
}
// 👇 OK, update
oldValue = curValue
}
</script>
Check this idea out.
http://pastie.org/1096892
I think it's close. HTML 5 really needs to add the change event to the spec. The only problem is that the callback function evaluates if (before == $(this).html()) before the content is actually updated in $(this).html(). setTimeout don't work, and it's sad. Let me know what you think.
Based on #balupton's answer:
$(document).on('focus', '[contenteditable]', e => {
const self = $(e.target)
self.data('before', self.html())
})
$(document).on('blur', '[contenteditable]', e => {
const self = $(e.target)
if (self.data('before') !== self.html()) {
self.trigger('change')
}
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
I'm using jquery ui autocomplete and want to decipher between focus events triggered by keyboard interaction and mouse interaction. How would I go about this?
$('input').autocomplete({
source: function(request, response) {
...
},
focus: function(event, ui) {
// If focus triggered by keyboard interaction
alert('do something');
// If focus event triggered by mouse interaction
alert('do something else');
}
});
Thanks
The only way I can think of doing this is to have a handler listen in on the keypress and click events, and toggle a boolean flag on/off. Then on the focus handler of your input, you can just check what the value of your flag is, and go from there.
Probably something like
var isClick;
$(document).bind('click', function() { isClick = true; })
.bind('keypress', function() { isClick = false; })
;
var focusHandler = function () {
if (isClick) {
// clicky!
} else {
// tabby!
}
}
$('input').focus(function() {
// we set a small timeout to let the click / keypress event to trigger
// and update our boolean
setTimeout(focusHandler,100);
});
Whipped up a small working prototype on jsFiddle (don't you just love this site?). Check it out if you want.
Of course, this is all running off a focus event on an <input>, but the focus handler on the autocomplete works in the same way.
The setTimeout will introduce a bit of lag, but at 100ms, it might be negligible, based on your needs.
You should actually be able to determine this from the event-Object that is passed into the focus-event. Depending on your code structure this might be different, but there is usually a property called originalEvent in there, which might be nested to some depth. Examine the event-object more closely to determine the correct syntax. Then test on mousenter or keydown via regular expression. Something like this:
focus: function(event, ui){
if(/^key/.test(event.originalEvent.originalEvent.type)){
//code for keydown
}else{
//code for mouseenter and any other event
}
}
The easiest and most elegant way I've found of achieving this is to use the "What Input?" library. It's tiny (~2K minified), and gives you access to the event type both in scripts:
if (whatInput.ask() === 'mouse') {
// do something
}
...and also (via a single data attribute that it adds to the document body) styles:
[data-whatinput="mouse"] :focus,
[data-whatinput="touch"] :focus {
// focus styles for mouse and touch only
}
I particularly like the fact that where you just want a different visual behaviour for mouse / keyboard it makes it possible to do that in the stylesheet (where it really belongs) rather than via some hacky bit of event-checking Javascript (though of course if you do need to do something that's not just purely visual, the former approach lets you handle it in Javascript instead).
The first thing that comes to mind is that you can find the position of the mouse and check to see if its within the position of the element
Use this to store the position of the element:
var input = $('#your_autocompleted_element_id'),
offset = input.offset(),
input_x = offset.top,
input_y = offset.left,
input_w = input.outerWidth(),
input_h = input.outerHeight();
Then use this to find absolute position of the mouse within the window:
var cur_mx, cur_my;
$(document).mousemove(function(e){
cur_mx = e.pageX;
cur_my = e.pageY;
});
Then in your autcomplete setup:
focus: function(event, ui) {
// mouse is doing the focus when...
// mouse x is greater than input x and less than input x + input width
// and y is greater than input y and less than input y + input height
if (cur_mx >= input_x && cur_mx <= input_x + input_w && cur_my >= input_y && cur_my <= input_y + input_h) {
// do your silly mouse focus witchcraft here
} else {
// keyboard time!
}
}
This can be handled using mousedown event, see my example below.
this.focusFrom = 'keyboard' =>
onFocus = () => {
if (this.focusFrom === 'keyboard') {
// do something when focus from keyboard
}
}
handleMouseDown = () => {
this.focusFrom = 'mouse';
}
handleOnClick = () => {
this.focusFrom = 'keyboard';
}