Can a user exit a contenteditable with a key? - javascript

I currently have a content editable that allows users to change text within a span. I want to make it so that the content editable container closes after the user presses the Enter key.
container.addEventListener('click', (event) => {
if (event.target.classList.contains('targetSpan')){
document.removeEventListener('keydown', keyDownEvents);
document.addEventListener('keydown', (event) =>{
if (event.key === 'Enter'){
document.addEventListener("keydown", keyDownEvents);
//function to also close contenteditable container
}
});
}
});
I currently have an event listener that is removed upon click on the span that has the contenteditable attribute. I want to add back the event listener when the user clicks the enter key. However, I also want the contenteditable container to close

This might work for you.
var container = document.getElementById("container");
container.addEventListener('click', (event) => {
if (event.target.classList.contains('targetSpan')){
container.setAttribute('contenteditable',true);
container.addEventListener('keydown', checkKey);
}
});
container.addEventListener('blur',(event) => {
container.removeEventListener('keydown',checkKey)
});
function checkKey(e) {
if(e.key === 'Enter') {
container.blur();
}
}
<span class="targetSpan" contenteditable="true" id="container">I'm a dummy text, edit me</span>
EDIT
If you don't need the click event, and .targetSpan class checking, you can shorten your code, by doing something like this.
var container = document.getElementById("container");
container.addEventListener('keydown', checkKey);
function checkKey(e) {
if(e.key === 'Enter') {
container.blur();
}
}
In this case, there's no need to check for the click event and attach the keydown inside of it. contenteditable=true let's you type inside your element by design. The keydown event listener was left the way it was, in case you need to remove it at some point.

Related

Jquery .append() appends on every event

I have very minimal javascript and jquery knowledge, so the problem is, when click with mouse "li" tab and press ENTER jquery attach .append() on every mouse click event.
$(document).on("click", function(event){
let $ul_elem = $(event.target).parent().parent();
const $li = '<li><input type="text" name="geras"></li>'
if($(event.target).parent().prop("localName")=="li"){
console.log($(this.event))
$(window).on("keydown", function(event){
if(event.keyCode == 13) {
event.preventDefault();
$($ul_elem).append($li);
$($ul_elem).children().last().children().focus();
return false
}
})
}
else{
$(window).unbind("keydown");
}
})
in result if i click like 5 times with my mouse on the same li tab, it will create 5 new const $li elements and i dont want that.
You should avoid binding event handlers in context of another event handler. Your code currently rebinds the keydown handler on each click (when the if statements succeeds.) This is why the code appends several elements on each Enter. I guess you want to append li elements to ul elements conditionally. If yes this is one way of doing it:
$(document).on("click", "ul", function() {
$(this).toggleClass('active');
});
$(window).on("keydown", 'ul.active', function(event) {
if (event.keyCode == 13) {
const $ul_elem = $(this);
const li = '<li><input type="text" name="geras"></li>'
event.preventDefault();
$ul_elem.append(li);
$ul_elem.children().last().children().focus();
return false
}
})
This makes the keydown handler work conditionally by using event delegation. In case you want to listen to the keydown event of input elements you can just code $(document).on("keydown", 'input[type=text]', function(event) { ... and there is no need to use click handler. I'm still not sure what you are trying to achieve though.
what I want to do is after i put text in my default li > input field and press ENTER i want to create new li in ul
This should do it:
$(document).on("keyup", 'li input[type=text]', function(event) {
if (event.keyCode == 13) {
const $ul_elem = $(this).closest('ul');
const li = '<li><input type="text" name="geras"></li>'
event.preventDefault();
$ul_elem.append(li);
$ul_elem.children().last().children().focus();
}
})

this.id on an Element Event Listener becomes an array of all id's of elements clicked

I have an event listener on all textboxes. When a textbox is clicked, I'd like to open a keyboard. On Enter of the keyboard I'd then like to use the id of the textbox which called it to do some logic. However the id (txtbxId in code) just becomes the first textbox I click, then the second textbox I click in an array.
E.g, the alert becomes 'textbox1' - after second textbox click alert is 'textbox1' 'textbox2'
I've tried to force the variable id to '', to delete it etc. to no avail,
Code snippet here:
$('.textbox').click(function() {
var txtbxId = this.id;
$("#Keyboard").show();
$(document).on('keydown', function(e) {
if (e.which === 13) {
alert(txtbxId);
}
});
});
});
The issue is because you're nesting events. Therefore as well as duplicating the keydown event when a click event happens, you're supplying each individual id to those events.
To fix this, use a single event handler for all the .textbox elements, and read their own id from the reference to the element which raised the event which is available through the this keyword:
$('.textbox').click(function() {
$("#Keyboard").show();
});
$(document).on('keydown', '.textbox', function(e) {
if (e.which === 13) {
alert(this.id);
}
});
The problem is that your on('keydown') function the first time you click a textbox never gets unassigned, so for every time you click a .textbox, you're making a NEW keydown callback, but not removing your old ones.
I would recommend making an object outside of your onClick callback which manages .keydown callbacks, so that you only have one at any time.
Something like this:
window.keydownmanager = {
init: () => {
$(document).on('keydown', function (e) {
window.keydownmanager.callback(e);
});
},
callback: () => {},
setCallback: (cb) => {
window.keydownmanager.callback = cb;
}
}
And inside your onClick callback, do this:
var txtbxId = this.id;
$("#Keyboard").show();
window.keydownmanager.setCallback(function(e) {
if (e.which === 13) {
alert(txtbxId);
}
})

Focus event firing after a focus inside of a function

I'm using a hidden input to keybind my app with it but without triggering events when i write on other input-fields
-clicks on element {
-hide element
-creates an input text-field(to edit the element)
-focus the input
- on blur or submit changes the element and remove the input
}
but if you add this new event :
- click anywhere in the container {
-focus the hidden app input (so it can use keybinding)
}
when user clicks on the element it ends firing the blur event without letting the user edit it first because its activating the second block event.
so it's either skipping the focus part of the first block
or the focus of the second block is activating after the focus on the first one
I'm maybe using the wrong approach to solving it
but I don't know why it's behaving that way.
actual code:
$("#hiddenInput").focus()
var elem = $("#nameClip");
function evenConditional(id) {
if ($(id).val() !== "") {
elem.text($(id).val())
storedObj.name = $(id).val();
}
$(id).parent().remove();
elem.show();
}
$("#name").on("click", function() {
elem.hide();
elem.after(
$("<form/>").append(
$("<input/>").addClass("rename")
)
);
$(".rename").focus();
});
$(".rename").blur(function() {
evenConditional(this);
});
$(".rename").closest("form").on("submit", function(e) {
e.preventDefault();
evenConditional(this);
});
/// regaining focus on click
$(".container").on("click", function(e) {
$("#hiddenInput").focus()
});
css:
#hiddenInput {
position:absolute;
top: -2000;
}
Since the #name element is in the .container element, when you click on it, the click event bubbles up to the container, causing the click-event handler for the container to get executed.
One way to fix this would be to stop the click event from bubbling:
$("#name").on("click", function(e) {
e.stopPropagation();
There can be side effects from doing that though. Particularly, there may be other event handlers that will not get executed because of that. Such as handlers that close opened menus.
The other option would be to place conditional logic in the click handler for the container so it does not execute if the click originated with the name element.
$(".container").on("click", function(e) {
var nameElement = $("#name")[0];
if ((e.target != nameElement) and !$.contains(nameElement , e.target)) {
$("#hiddenInput").focus();
}
});

Vanilla javascript Trap Focus in modal (accessibility tabbing )

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.

Gracefully bubble up with a clicktarget

I am working with this plugin that runs off of the data attribute. Basically when you click anywhere on the body it will determine if the click target has this specific data-vzpop. The problem is lets say I have a div and inside the div is an a href. It only acknowledges the a href as the click target and not the div (which makes sense).
What I want to try and do in some cases is put the data attribute on the containing div that way anything within the div works on click.
Here is a sample of the issue with jsfiddle it requires viewing the console so you can actually see which element is registered as being clicked.
<div data-vzpop>
Click Me
</div>
$('body').on('click', function(evt){
var clickTarget = evt.target;
if ($(clickTarget).attr('data-vzpop') !== undefined){
evt.preventDefault();
console.log('called correctly')
} else {
console.log('not called correctly')
}
console.log(clickTarget)
});
fiddle
You would use Event delegation:
$('body').on('click', '[data-vzpop]', function(evt) {
This will only trigger when the evt.target has a data attribute of data-vzpop, no matter the value.
If you want items inside the [data-vzpop] to trigger it as well, you would use your original click event but check that the $(clickTarget).closest('[data-vzpop]').length > 0 to determine if it's a nested target.
$('body').on('click', function(evt){
var clickTarget = evt.target;
if ($(clickTarget).attr('data-vzpop') != null ||
$(clickTarget).closest('[data-vzpop]').length > 0){
evt.preventDefault();
console.log('called correctly')
} else {
console.log('not called correctly')
}
console.log(clickTarget)
});

Categories

Resources