I am trying to move the focus to the next element in the tab sequence based upon the current element which has focus. Thus far I have not turned up anything in my searches.
function OnFocusOut()
{
var currentElement = $get(currentElementId); // ID set by OnFocusIn
currentElementId = "";
currentElement.nextElementByTabIndex.focus();
}
Of course the nextElementByTabIndex is the key part for this to work. How do I find the next element in the tab sequence? The solution would need to be based using JScript and not something like JQuery.
I've never implemented this, but I've looked into a similar problem, and here's what I would try.
Referencing the jQuery implementation, you must:
Listen for Tab and Shift+Tab
Know which elements are tab-able
Understand how tab order works
1. Listen for Tab and Shift+Tab
Listening for Tab and Shift+Tab are probably well-covered elsewhere on the web, so I'll skip that part.
2. Know which elements are tab-able
Knowing which elements are tab-able is trickier. Basically, an element is tab-able if it is focusable and does not have the attribute tabindex="-1" set. So then we must ask which elements are focusable. The following elements are focusable:
input, select, textarea, button, and object elements that aren't disabled.
a and area elements that have an href or have a numerical value for tabindex set.
any element that has a numerical value for tabindex set.
Furthermore, an element is focusable only if:
None of its ancestors are display: none.
The computed value of visibility is visible. This means that the nearest ancestor to have visibility set must have a value of visible. If no ancestor has visibility set, then the computed value is visible.
More details are in another Stack Overflow answer.
3. Understand how tab order works
The tab order of elements in a document is controlled by the tabindex attribute. If no value is set, the tabindex is effectively 0.
The tabindex order for the document is: 1, 2, 3, …, 0.
Initially, when the body element (or no element) has focus, the first element in the tab order is the lowest non-zero tabindex. If multiple elements have the same tabindex, you then go in document order until you reach the last element with that tabindex. Then you move to the next lowest tabindex and the process continues. Finally, finish with those elements with a zero (or empty) tabindex.
Here's something I build for this purpose:
function focusNextElement() {
//add all elements we want to include in our selection
var focussableElements =
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
if (document.activeElement && document.activeElement.form) {
var focussable = Array.prototype.filter.call(
document.activeElement.form.querySelectorAll(focussableElements),
function (element) {
//check for visibility while always include the current activeElement
return (
element.offsetWidth > 0 ||
element.offsetHeight > 0 ||
element === document.activeElement
);
}
);
var index = focussable.indexOf(document.activeElement);
if (index > -1) {
var nextElement = focussable[index + 1] || focussable[0];
nextElement.focus();
}
}
}
Features:
configurable set of focusable elements
no jQuery needed
works in all modern browsers
fast & lightweight
Without jquery:
First of all, on your tab-able elements, add class="tabable" this will let us select them later.
(Do not forget the "." class selector prefix in the code below)
var lastTabIndex = 10;
function OnFocusOut()
{
var currentElement = $get(currentElementId); // ID set by OnFOcusIn
var curIndex = currentElement.tabIndex; //get current elements tab index
if(curIndex == lastTabIndex) { //if we are on the last tabindex, go back to the beginning
curIndex = 0;
}
var tabbables = document.querySelectorAll(".tabable"); //get all tabable elements
for(var i=0; i<tabbables.length; i++) { //loop through each element
if(tabbables[i].tabIndex == (curIndex+1)) { //check the tabindex to see if it's the element we want
tabbables[i].focus(); //if it's the one we want, focus it and exit the loop
break;
}
}
}
I created a simple jQuery plugin which does just this. It uses the ':tabbable' selector of jQuery UI to find the next 'tabbable' element and selects it.
Example usage:
// Simulate tab key when element is clicked
$('.myElement').bind('click', function(event){
$.tabNext();
return false;
});
The core of the answer lies on finding the next element:
function findNextTabStop(el) {
var universe = document.querySelectorAll('input, button, select, textarea, a[href]');
var list = Array.prototype.filter.call(universe, function(item) {return item.tabIndex >= "0"});
var index = list.indexOf(el);
return list[index + 1] || list[0];
}
Usage:
var nextEl = findNextTabStop(element);
nextEl.focus();
Notice I don't care about prioritizing tabIndex.
It seems that you can check the tabIndex property of an element to determine if it is focusable. An element that is not focusable has a tabindex of "-1".
Then you just need to know the rules for tab stops:
tabIndex="1" has the highest priorty.
tabIndex="2" has the next highest priority.
tabIndex="3" is next, and so on.
tabIndex="0" (or tabbable by default) has the lowest priority.
tabIndex="-1" (or not tabbable by default) does not act as a tab stop.
For two elements that have the same tabIndex, the one that appears first in the DOM has the higher priority.
Here is an example of how to build the list of tab stops, in sequence, using pure Javascript:
function getTabStops(o, a, el) {
// Check if this element is a tab stop
if (el.tabIndex > 0) {
if (o[el.tabIndex]) {
o[el.tabIndex].push(el);
} else {
o[el.tabIndex] = [el];
}
} else if (el.tabIndex === 0) {
// Tab index "0" comes last so we accumulate it seperately
a.push(el);
}
// Check if children are tab stops
for (var i = 0, l = el.children.length; i < l; i++) {
getTabStops(o, a, el.children[i]);
}
}
var o = [],
a = [],
stops = [],
active = document.activeElement;
getTabStops(o, a, document.body);
// Use simple loops for maximum browser support
for (var i = 0, l = o.length; i < l; i++) {
if (o[i]) {
for (var j = 0, m = o[i].length; j < m; j++) {
stops.push(o[i][j]);
}
}
}
for (var i = 0, l = a.length; i < l; i++) {
stops.push(a[i]);
}
We first walk the DOM, collecting up all tab stops in sequence with their index. We then assemble the final list. Notice that we add the items with tabIndex="0" at the very end of the list, after the items with a tabIndex of 1, 2, 3, etc.
For a fully working example, where you can tab around using the "enter" key, check out this fiddle.
Tabbable is a small JS package that gives you a list of all tabbable elements in tab order. So you could find your element within that list, then focus on the next list entry.
The package correctly handles the complicated edge cases mentioned in other answers (e.g., no ancestor can be display: none). And it doesn't depend on jQuery!
As of this writing (version 1.1.1), it has the caveats that it doesn't support IE8, and that browser bugs prevent it from handling contenteditable correctly.
As mentioned in a comment above, I don't think that any browsers expose tab order information. Here a simplified approximation of what the browser does to get the next element in tab order:
var allowedTags = {input: true, textarea: true, button: true};
var walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_ELEMENT,
{
acceptNode: function(node)
{
if (node.localName in allowedTags)
return NodeFilter.FILTER_ACCEPT;
else
NodeFilter.FILTER_SKIP;
}
},
false
);
walker.currentNode = currentElement;
if (!walker.nextNode())
{
// Restart search from the start of the document
walker.currentNode = walker.root;
walker.nextNode();
}
if (walker.currentNode && walker.currentNode != walker.root)
walker.currentNode.focus();
This only considers some tags and ignores tabindex attribute but might be enough depending on what you are trying to achieve.
Here is a more complete version of focusing on the next element. It follows the spec guidelines and sorts the list of elements correctly by using tabindex. Also a reverse variable is defined if you want to get the previous element.
function focusNextElement( reverse, activeElem ) {
/*check if an element is defined or use activeElement*/
activeElem = activeElem instanceof HTMLElement ? activeElem : document.activeElement;
let queryString = [
'a:not([disabled]):not([tabindex="-1"])',
'button:not([disabled]):not([tabindex="-1"])',
'input:not([disabled]):not([tabindex="-1"])',
'select:not([disabled]):not([tabindex="-1"])',
'[tabindex]:not([disabled]):not([tabindex="-1"])'
/* add custom queries here */
].join(','),
queryResult = Array.prototype.filter.call(document.querySelectorAll(queryString), elem => {
/*check for visibility while always include the current activeElement*/
return elem.offsetWidth > 0 || elem.offsetHeight > 0 || elem === activeElem;
}),
indexedList = queryResult.slice().filter(elem => {
/* filter out all indexes not greater than 0 */
return elem.tabIndex == 0 || elem.tabIndex == -1 ? false : true;
}).sort((a, b) => {
/* sort the array by index from smallest to largest */
return a.tabIndex != 0 && b.tabIndex != 0
? (a.tabIndex < b.tabIndex ? -1 : b.tabIndex < a.tabIndex ? 1 : 0)
: a.tabIndex != 0 ? -1 : b.tabIndex != 0 ? 1 : 0;
}),
focusable = [].concat(indexedList, queryResult.filter(elem => {
/* filter out all indexes above 0 */
return elem.tabIndex == 0 || elem.tabIndex == -1 ? true : false;
}));
/* if reverse is true return the previous focusable element
if reverse is false return the next focusable element */
return reverse ? (focusable[focusable.indexOf(activeElem) - 1] || focusable[focusable.length - 1])
: (focusable[focusable.indexOf(activeElem) + 1] || focusable[0]);
}
function focusNextElement(){
var focusable = [].slice.call(document.querySelectorAll("a, button, input, select, textarea, [tabindex], [contenteditable]")).filter(function($e){
if($e.disabled || ($e.getAttribute("tabindex") && parseInt($e.getAttribute("tabindex"))<0)) return false;
return true;
}).sort(function($a, $b){
return (parseFloat($a.getAttribute("tabindex") || 99999) || 99999) - (parseFloat($b.getAttribute("tabindex") || 99999) || 99999);
});
var focusIndex = focusable.indexOf(document.activeElement);
if(focusable[focusIndex+1]) focusable[focusIndex+1].focus();
};
This is my first post on SO, so I don't have enough reputation to comment the accepted answer, but I had to modify the code to the following:
export function focusNextElement () {
//add all elements we want to include in our selection
const focussableElements =
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled])'
if (document.activeElement && document.activeElement.form) {
var focussable = Array.prototype.filter.call(
document.activeElement.form.querySelectorAll(focussableElements),
function (element) {
// if element has tabindex = -1, it is not focussable
if ( element.hasAttribute('tabindex') && element.tabIndex === -1 ){
return false
}
//check for visibility while always include the current activeElement
return (element.offsetWidth > 0 || element.offsetHeight > 0 ||
element === document.activeElement)
});
console.log(focussable)
var index = focussable.indexOf(document.activeElement);
if(index > -1) {
var nextElement = focussable[index + 1] || focussable[0];
console.log(nextElement)
nextElement.focus()
}
}
}
The changing of var to constant is non-critical. The main change is that we get rid of the selector that checks tabindex != "-1". Then later, if the element has the attribute tabindex AND it is set to "-1", we do NOT consider it focussable.
The reason I needed to change this was because when adding tabindex="-1" to an <input>, this element was still considered focussable because it matches the "input[type=text]:not([disabled])" selector. My change is equivalent to "if we are a non-disabled text input, and we have a tabIndex attribute, and the value of that attribute is -1, then we should not be considered focussable.
I believe that when the author of the accepted answer edited their answer to account for the tabIndex attribute, they did not do so correctly. Please let me know if this is not the case
There is the tabindex property that can be set on component. It specifies in which order the input components should be iterated when selecting one and pressing tab. Values above 0 are reserved for custom navigation, 0 is "in natural order" (so would behave differently if set for the first element), -1 means not keyboard focusable:
<!-- navigate with tab key: -->
<input tabindex="1" type="text"/>
<input tabindex="2" type="text"/>
It can also be set for something else than the text input fields but it is not very obvious what it would do there, if anything at all. Even if the navigation works, maybe better to use "natural order" for anything else than the very obvious user input elements.
No, you do not need JQuery or any scripting at all to support this custom path of navigation. You can implement it on the server side without any JavaScript support. From the other side, the property also works fine in React framework but does not require it.
Simplest TAB loop!
// Give the focus onload. (The autofocus attribute does not work on div(s))
document.querySelector('[tabindex]').focus()
document.addEventListener("keydown", (e) => {
e.preventDefault() // Very important here
if (document.activeElement && e.code === "Tab") {
let e = [...document.querySelectorAll('[tabindex]')],
i = e.indexOf(document.activeElement) + 1;
i = i === e.length ? i = 0 : i;
e[i].focus()
}
})
<div tabindex="0">Try to</div>
<div tabindex="1">tap, or long press</div>
<div tabindex="2">the TAB key!</div>
This is a potential enhancement to the great solution that #Kano and #Mx offered. If you want to preserve TabIndex ordering, add this sort in the middle:
// Sort by explicit Tab Index, if any
var sort_by_TabIndex = function (elementA, elementB) {
let a = elementA.tabIndex || 1;
let b = elementB.tabIndex || 1;
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
}
focussable.sort(sort_by_TabIndex);
A word of advice: Don’t try to control where the focus lands during a tab event. Instead try to control which element are and aren’t tabbable by setting the tabIndex of the elements you don’t want to receive focus to -1. E.g.
// `tabContainer` is a container where we want only
// element at a time to be tabbable, e.g. a radio menu.
tabContainer.addEventListener("focusin", () => {
const desired = findDesiredFocusElement();
if (!desired) {
// Just leave the focus be. We have no preference
// at the moment.
return;
}
// Move the focus to the correct element.
desired.focus();
// Remove all undesired elements from the tab order.
for (const undesired of findUndesiredFocusElements()) {
// Make it untabbable.
undesired.tabIndex = -1;
}
});
tabContainer.addEventListener("focusout", (event) => {
for (const element of findRelevantFocusElements()) {
// Give each element back their focus capability.
element.tabIndex = 0;
}
});
Note: This might not be what is best in your situation, e.g. in your case it might be better to control the tab index in some change events or not to reset the tabIndex state on focusout etc.
More info here.
I have a buch of 0-tabIndexes, which I wanted to navigate by keyboard.
Since in that case, only the ORDER of the elements mattered, I did it using document.createTreeWalker
So first you create the filter (you only want [visible] elements, which have an attribute "tabIndex" with a NUMERICAL value.
Then you set the root node, beyond which you don't want to search.
In my case, this.m_tree is a ul-element containing a toggable tree.
If you want the entire document instead, just replace this.m_tree with document.documentElement.
Then you set the current node to the current active element:
ni.currentNode = el; // el = document.activeElement
Then you return ni.nextNode() or ni.previousNode().
Note:
this will NOT return the tabs in the correct order if you have tabIndices != 0 and the element order is NOT the tabIndex order. In case of tabIndex = 0, the tabOrder is always the element order, which is why this works (in that case).
protected createFilter(fn?: (node: Node) => number): NodeFilter
{
// Accept all currently filtered elements.
function acceptNode(node: Node): number
{
return NodeFilter.FILTER_ACCEPT;
}
if (fn == null)
fn = acceptNode;
// Work around Internet Explorer wanting a function instead of an object.
// IE also *requires* this argument where other browsers don't.
const safeFilter: NodeFilter = <NodeFilter><any>fn;
(<any>safeFilter).acceptNode = fn;
return safeFilter;
}
protected createTabbingFilter(): NodeFilter
{
// Accept all currently filtered elements.
function acceptNode(node: Node): number
{
if (!node)
return NodeFilter.FILTER_REJECT;
if (node.nodeType !== Node.ELEMENT_NODE)
return NodeFilter.FILTER_REJECT;
if (window.getComputedStyle(<Element>node).display === "none")
return NodeFilter.FILTER_REJECT;
// "tabIndex": "0"
if (!(<Element>node).hasAttribute("tabIndex"))
return NodeFilter.FILTER_SKIP;
let tabIndex = parseInt((<Element>node).getAttribute("tabIndex"), 10);
if (!tabIndex || isNaN(tabIndex) || !isFinite(tabIndex))
return NodeFilter.FILTER_SKIP;
// if ((<Element>node).tagName !== "LI") return NodeFilter.FILTER_SKIP;
return NodeFilter.FILTER_ACCEPT;
}
return this.createFilter(acceptNode);
}
protected getNextTab(el: HTMLElement): HTMLElement
{
let currentNode: Node;
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createNodeIterator
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
// let ni = document.createNodeIterator(el, NodeFilter.SHOW_ELEMENT);
// let ni = document.createTreeWalker(this.m_tree, NodeFilter.SHOW_ELEMENT);
let ni = document.createTreeWalker(this.m_tree, NodeFilter.SHOW_ELEMENT, this.createTabbingFilter(), false);
ni.currentNode = el;
while (currentNode = ni.nextNode())
{
return <HTMLElement>currentNode;
}
return el;
}
protected getPreviousTab(el: HTMLElement): HTMLElement
{
let currentNode: Node;
let ni = document.createTreeWalker(this.m_tree, NodeFilter.SHOW_ELEMENT, this.createTabbingFilter(), false);
ni.currentNode = el;
while (currentNode = ni.previousNode())
{
return <HTMLElement>currentNode;
}
return el;
}
Note that the while-loop
while (currentNode = ni.nextNode())
{
// Additional checks here
// if(condition) return currentNode;
// else the loop continues;
return <HTMLElement>currentNode; // everything is already filtered down to what we need here
}
is only there if you want if you have additional criterias which you cannot filter in the filter passed to createTreeWalker.
Note this is TypeScript, you need to remove all tokens behind colons (:), and between angle-brackets (<>), e.g. <Element> or :(node: Node) => number to get valid JavaScript.
Here as a service, the transpiled JS:
"use strict";
function createFilter(fn) {
// Accept all currently filtered elements.
function acceptNode(node) {
return NodeFilter.FILTER_ACCEPT;
}
if (fn == null)
fn = acceptNode;
// Work around Internet Explorer wanting a function instead of an object.
// IE also *requires* this argument where other browsers don't.
const safeFilter = fn;
safeFilter.acceptNode = fn;
return safeFilter;
}
function createTabbingFilter() {
// Accept all currently filtered elements.
function acceptNode(node) {
if (!node)
return NodeFilter.FILTER_REJECT;
if (node.nodeType !== Node.ELEMENT_NODE)
return NodeFilter.FILTER_REJECT;
if (window.getComputedStyle(node).display === "none")
return NodeFilter.FILTER_REJECT;
// "tabIndex": "0"
if (!node.hasAttribute("tabIndex"))
return NodeFilter.FILTER_SKIP;
let tabIndex = parseInt(node.getAttribute("tabIndex"), 10);
if (!tabIndex || isNaN(tabIndex) || !isFinite(tabIndex))
return NodeFilter.FILTER_SKIP;
// if ((<Element>node).tagName !== "LI") return NodeFilter.FILTER_SKIP;
return NodeFilter.FILTER_ACCEPT;
}
return createFilter(acceptNode);
}
function getNextTab(el) {
let currentNode;
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createNodeIterator
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
// let ni = document.createNodeIterator(el, NodeFilter.SHOW_ELEMENT);
// let ni = document.createTreeWalker(this.m_tree, NodeFilter.SHOW_ELEMENT);
let ni = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_ELEMENT, createTabbingFilter(), false);
ni.currentNode = el;
while (currentNode = ni.nextNode()) {
return currentNode;
}
return el;
}
function getPreviousTab(el) {
let currentNode;
let ni = document.createTreeWalker(document.documentElement, NodeFilter.SHOW_ELEMENT, createTabbingFilter(), false);
ni.currentNode = el;
while (currentNode = ni.previousNode()) {
return currentNode;
}
return el;
}
function focusNext() {
var query = '[tabindex]';
if (document.activeElement) {
var elements = [...document.querySelectorAll(query)]
var index = elements.indexOf(document.activeElement);
index++
if (index == elements.length) index = 0
elements[index].focus()
}
}
If you want to target other elements, you can modify the query to refine the elements.
Here is my working solution using the tabbable library in my React app to make 'Enter' behave like tab
handleKeyDown={(e) => {
if (e.key === 'Enter') {
const fElts = tabbable(document.documentElement);
const currEltIndex = fElts.findIndex(
(e) => e === document.activeElement,
);
const nextElt = fElts[currEltIndex + (e.shiftKey ? -1 : 1)];
nextElt?.focus();
}
}}
Did you specify your own tabIndex values for each element you want to cycle through?
if so, you can try this:
var lasTabIndex = 10; //Set this to the highest tabIndex you have
function OnFocusOut()
{
var currentElement = $get(currentElementId); // ID set by OnFocusIn
var curIndex = $(currentElement).attr('tabindex'); //get the tab index of the current element
if(curIndex == lastTabIndex) { //if we are on the last tabindex, go back to the beginning
curIndex = 0;
}
$('[tabindex=' + (curIndex + 1) + ']').focus(); //set focus on the element that has a tab index one greater than the current tab index
}
You are using jquery, right?
You just need to change the tab index. The tabindex global attribute indicates that its elements can be focused, and where it participates in sequential keyboard navigation (usually with the Tab key, hence the name).
<p>Click anywhere in this pane, then try tabbing through the elements.</p>
<label>First in tab order:<input type="text"></label>
<div tabindex="0">Tabbable due to tabindex.</div>
<div>Not tabbable: no tabindex.</div>
<label>Third in tab order:<input type="text"></label>
know more
If you use the library "JQuery", you can call this:
Tab:
$.tabNext();
Shift+Tab:
$.tabPrev();
<!DOCTYPE html>
<html>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
<script>
(function($){
'use strict';
/**
* Focusses the next :focusable element. Elements with tabindex=-1 are focusable, but not tabable.
* Does not take into account that the taborder might be different as the :tabbable elements order
* (which happens when using tabindexes which are greater than 0).
*/
$.focusNext = function(){
selectNextTabbableOrFocusable(':focusable');
};
/**
* Focusses the previous :focusable element. Elements with tabindex=-1 are focusable, but not tabable.
* Does not take into account that the taborder might be different as the :tabbable elements order
* (which happens when using tabindexes which are greater than 0).
*/
$.focusPrev = function(){
selectPrevTabbableOrFocusable(':focusable');
};
/**
* Focusses the next :tabable element.
* Does not take into account that the taborder might be different as the :tabbable elements order
* (which happens when using tabindexes which are greater than 0).
*/
$.tabNext = function(){
selectNextTabbableOrFocusable(':tabbable');
};
/**
* Focusses the previous :tabbable element
* Does not take into account that the taborder might be different as the :tabbable elements order
* (which happens when using tabindexes which are greater than 0).
*/
$.tabPrev = function(){
selectPrevTabbableOrFocusable(':tabbable');
};
function tabIndexToInt(tabIndex){
var tabIndexInded = parseInt(tabIndex);
if(isNaN(tabIndexInded)){
return 0;
}else{
return tabIndexInded;
}
}
function getTabIndexList(elements){
var list = [];
for(var i=0; i<elements.length; i++){
list.push(tabIndexToInt(elements.eq(i).attr("tabIndex")));
}
return list;
}
function selectNextTabbableOrFocusable(selector){
var selectables = $(selector);
var current = $(':focus');
// Find same TabIndex of remainder element
var currentIndex = selectables.index(current);
var currentTabIndex = tabIndexToInt(current.attr("tabIndex"));
for(var i=currentIndex+1; i<selectables.length; i++){
if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === currentTabIndex){
selectables.eq(i).focus();
return;
}
}
// Check is last TabIndex
var tabIndexList = getTabIndexList(selectables).sort(function(a, b){return a-b});
if(currentTabIndex === tabIndexList[tabIndexList.length-1]){
currentTabIndex = -1;// Starting from 0
}
// Find next TabIndex of all element
var nextTabIndex = tabIndexList.find(function(element){return currentTabIndex<element;});
for(var i=0; i<selectables.length; i++){
if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === nextTabIndex){
selectables.eq(i).focus();
return;
}
}
}
function selectPrevTabbableOrFocusable(selector){
var selectables = $(selector);
var current = $(':focus');
// Find same TabIndex of remainder element
var currentIndex = selectables.index(current);
var currentTabIndex = tabIndexToInt(current.attr("tabIndex"));
for(var i=currentIndex-1; 0<=i; i--){
if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === currentTabIndex){
selectables.eq(i).focus();
return;
}
}
// Check is last TabIndex
var tabIndexList = getTabIndexList(selectables).sort(function(a, b){return b-a});
if(currentTabIndex <= tabIndexList[tabIndexList.length-1]){
currentTabIndex = tabIndexList[0]+1;// Starting from max
}
// Find prev TabIndex of all element
var prevTabIndex = tabIndexList.find(function(element){return element<currentTabIndex;});
for(var i=selectables.length-1; 0<=i; i--){
if(tabIndexToInt(selectables.eq(i).attr("tabIndex")) === prevTabIndex){
selectables.eq(i).focus();
return;
}
}
}
/**
* :focusable and :tabbable, both taken from jQuery UI Core
*/
$.extend($.expr[ ':' ], {
data: $.expr.createPseudo ?
$.expr.createPseudo(function(dataName){
return function(elem){
return !!$.data(elem, dataName);
};
}) :
// support: jQuery <1.8
function(elem, i, match){
return !!$.data(elem, match[ 3 ]);
},
focusable: function(element){
return focusable(element, !isNaN($.attr(element, 'tabindex')));
},
tabbable: function(element){
var tabIndex = $.attr(element, 'tabindex'),
isTabIndexNaN = isNaN(tabIndex);
return ( isTabIndexNaN || tabIndex >= 0 ) && focusable(element, !isTabIndexNaN);
}
});
/**
* focussable function, taken from jQuery UI Core
* #param element
* #returns {*}
*/
function focusable(element){
var map, mapName, img,
nodeName = element.nodeName.toLowerCase(),
isTabIndexNotNaN = !isNaN($.attr(element, 'tabindex'));
if('area' === nodeName){
map = element.parentNode;
mapName = map.name;
if(!element.href || !mapName || map.nodeName.toLowerCase() !== 'map'){
return false;
}
img = $('img[usemap=#' + mapName + ']')[0];
return !!img && visible(img);
}
return ( /^(input|select|textarea|button|object)$/.test(nodeName) ?
!element.disabled :
'a' === nodeName ?
element.href || isTabIndexNotNaN :
isTabIndexNotNaN) &&
// the element and all of its ancestors must be visible
visible(element);
function visible(element){
return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function(){
return $.css(this, 'visibility') === 'hidden';
}).length;
}
}
})(jQuery);
</script>
<a tabindex="5">5</a><br>
<a tabindex="20">20</a><br>
<a tabindex="3">3</a><br>
<a tabindex="7">7</a><br>
<a tabindex="20">20</a><br>
<a tabindex="0">0</a><br>
<script>
var timer;
function tab(){
window.clearTimeout(timer)
timer = window.setInterval(function(){$.tabNext();}, 1000);
}
function shiftTab(){
window.clearTimeout(timer)
timer = window.setInterval(function(){$.tabPrev();}, 1000);
}
</script>
<button tabindex="-1" onclick="tab()">Tab</button>
<button tabindex="-1" onclick="shiftTab()">Shift+Tab</button>
</body>
</html>
I modify jquery.tabbable PlugIn to complete.
I checked above solutions and found them quite lengthy.
It can be accomplished with just one line of code:
currentElement.nextElementSibling.focus();
or
currentElement.previousElementSibling.focus();
here currentElement may be any i.e. document.activeElement or this if current element is in function's context.
I tracked tab and shift-tab events with keydown event
Here is a snippet that relies on "JQuery":
let cursorDirection = ''
$(document).keydown(function (e) {
let key = e.which || e.keyCode;
if (e.shiftKey) {
//does not matter if user has pressed tab key or not.
//If it matters for you then compare it with 9
cursorDirection = 'prev';
}
else if (key == 9) {
//if tab key is pressed then move next.
cursorDirection = 'next';
}
else {
cursorDirection == '';
}
});
once you have cursor direction then you can use nextElementSibling.focus or previousElementSibling.focus methods
i use this code, which relies on the library "JQuery":
$(document).on('change', 'select', function () {
let next_select = $(this);
// console.log(next_select.toArray())
if (!next_select.parent().parent().next().find('select').length) {
next_select.parent().parent().parent().next().find('input[type="text"]').click()
console.log(next_select.parent().parent().parent().next());
} else if (next_select.parent().parent().next().find('select').prop("disabled")) {
setTimeout(function () {
next_select.parent().parent().next().find('select').select2('open')
}, 1000)
console.log('b');
} else if (next_select.parent().parent().next().find('select').length) {
next_select.parent().parent().next().find('select').select2('open')
console.log('c');
}
});
Related
I have a table with a lot of td's. What would be the most efficient way to select selection 1 to selection 2 and everything in-between?
On selection 1 i give the selected td an id and on selection 2 i give that selection another id in order to later on look for those id's.
Current code:
// small helpers
function _for(e,f) { var i, len=e.length; for(i=0;i<len;i++){ f(e[i]); }}
function _id(e) { return document.getElementById(e); }
// VARs
var main = _id('monthTables'),
td = main.querySelectorAll('td'),
go = false,
stop = false,
i = 0,
s1i = 0,
s2i = 999;
// Loop throught td's
_for(td, function(e){
if( e.id == 'sel1' ) { go = 1; s1i = i; }
if( e.id == 'sel2' ) { stop = 1; s2i = i; }
if( s1i < s2i && go ) {
if(go) { e.classList.add('range'); }
}
if( stop ) { go = 0; }
}) // end loop
Live example (select two dates):
http://cdn.rawgit.com/tommiehansen/lightRange/master/test.html
The code is good in my opinion, maybe you could add few optimizations (like stopping the loop when found the second selected id )
Example for the loop:
function _for(e,f) {
var i, len=e.length;
for(i=0;i<len;i++){ if(f(e[i]) === false) break; }
}
so it will break when the function f returns false
// Loop throught td's
_for(td, function(e){
//...
if( stop ) return false;
}) // end loop
Also i suggest you to use getElementsByTagName instead of querySelectorAll if you want a bit more performance ..but maybe that's an evil micro optimization
An other optimization would be to start looping from the first selected td, maybe storing the index when you select the td and using that value in that code you posted as initial index
I have written some code that allows one element on a page to be expanded to full screen and contracted back to its original size. This code works by saving the states of other elements on the page, altering their properties, and restoring them. This change has to survive a postback, so I'm attempting to use JSON and a hidden input element to preserve the state changes.
The element in question is nested within multiple IFRAMEs. Thus I have to save the document model in which the element resides. However, this causes the JSON conversion to choke. I need a way to resolve this problem that can be easily converted to JSON and back.
Here is the pertinent code:
// e.uniqueID : A unique identifer for the object.
// e.doc: The document model to which the element belongs (so we can find it later).
// e.style: The original style of the element.
function my_preserveElement(gn,elem)
{
if (elem == null) return;
if (elem.style == null) return;
if (elem.id == '') elem.id = PseudoGuid.GetNew();
var e = new Object();
e.uniqueID = elem.id;
e.doc = elem.document;
var cssString;
cssString = elem.style.cssText;
if( typeof(cssString) != 'string' ) { cssString = elem.getAttribute('style'); }
e.style = cssString;
me_aObjectStore[gn][me_aObjectStore[gn].length] = e;
}
function my_restoreElements(gn)
{
for (i = 0; i < me_aObjectStore[gn].length; i++)
{
var e = me_aObjectStore[gn][i];
var elem = e.doc.getElementById(e.uniqueID);
elem.style.cssText = e.style;
elem.setAttribute('style',e.style);
}
me_aObjectStore[gn] = null;
}
Discovered that since the code in question is running in the innermost frame, I need only walk up the frame tree, searching each level for each element by ID. The restore function becomes as follows, and there is no need to keep track of each element's location (just its unique ID).
function my_restoreElements(gn)
{
for (i = 0; i < my_aObjectStore[gn].length; i++)
{
var e = my_aObjectStore[gn][i];
// Find the element in this window or one of its parents.
// Because we need to check the top level frame and w.parent == w.self for that frame,
// we use the number of counts to keep the loop from being endless.
var w = window;
var elem = null;
var count = 0;
do {
elem = w.document.getElementById(e.uniqueID);
w = w.parent;
count++;
} while ((elem == null) && (count < 3))
if (elem != null) {
elem.style.cssText = e.style;
elem.setAttribute('style',e.style);
}
} //for
my_aObjectStore[gn] = null;
}
Note that I explicitly walk up only three levels. That's because in my specific case, the frames are that few levels deep. Other applications that can use this solution may need a deeper depth.
I'm having an issue with a function I've written to "clean" up, see the code below and I'll explain how it works underneath.
clean: function (e) {
var
els = null,
i = 0;
if (e === undefined) {
e = this.cont;
}
els = e.getElementsByTagName('*');
for (i=0;i<els.length;i++) {
if (els[i].className.search('keep') === -1) {
e.removeChild(els[i]);
}
}
return this;
},
The argument e is a dom element, if it isn't supplied this.cont is also a dom element stored earlier in the whole function and e is defaulted to it.
The function loops through all of the child elements and checks it doesn't have the class keep (fairly obvious what this does) and removes any that don't match.
It all seemed to be working but I have an element which has 2 images and 2 inputs none with the class 'keep' but the variable i only gets to 2 and the loop stops (it should reach 4 and remove all four elements)
any help would be greatly appreciated.
/* UPDATE */
Thanks to #pimvb and and #Brett Walker the final code which works great is below.
clean: function (e) {
var
els = null,
i = 0;
if (e === undefined) {
e = this.cont;
}
els = e.getElementsByTagName('*');
i = els.length;
while (i--) {
if (els[i].className.search('keep') === -1) {
els[i].parentNode.removeChild(els[i]);
}
}
return this;
},
The .getElementsByTagName function returns a NodeList which is basically an array but is 'live', which means it's updated automatically if you e.g. remove a child. So when iterating, els.length is changing, resulting in being 2 when you remove 2 children (there are 4 - 2 = 2 left). When having removed 2 children, i == 2 so the loop will end prematurely to what you expect.
To circumvent this and make it a 'static' array, you can convert it into an array like this, which does not update itself:
els = [].slice.call(e.getElementsByTagName('*')); // [].slice.call is a trick to
// convert something like a NodeList
// into a static, real array
As Brett Walker pointed out, you can also iterate backwards, like this:
http://jsfiddle.net/pimvdb/cYKxU/1/
var elements = document.getElementsByTagName("a"),
i = elements.length;
while(i--) { // this will stop as soon as i == 0 because 0 is treated as false
var elem = elements[i]; // current element
if(elem.className == "test") // remove if it should be removed
elem.parentNode.removeChild(elem);
}
This will start at the last element. The .length still gets updated (i.e. becomes less), but this does not matter as you only used it at the beginning, and not during iterating. As a result, you don't suffer from this 'quirk'.
I've got an in page text search using JS, which is here:
$.fn.eoTextSearch = function(pat) {
var out = []
var textNodes = function(n) {
if (!window['Node']) {
window.Node = new Object();
Node.ELEMENT_NODE = 1;
Node.ATTRIBUTE_NODE = 2;
Node.TEXT_NODE = 3;
Node.CDATA_SECTION_NODE = 4;
Node.ENTITY_REFERENCE_NODE = 5;
Node.ENTITY_NODE = 6;
Node.PROCESSING_INSTRUCTION_NODE = 7;
Node.COMMENT_NODE = 8;
Node.DOCUMENT_NODE = 9;
Node.DOCUMENT_TYPE_NODE = 10;
Node.DOCUMENT_FRAGMENT_NODE = 11;
Node.NOTATION_NODE = 12;
}
if (n.nodeType == Node.TEXT_NODE) {
var t = typeof pat == 'string' ?
n.nodeValue.indexOf(pat) != -1 :
pat.test(n.nodeValue);
if (t) {
out.push(n.parentNode)
}
}
else {
$.each(n.childNodes, function(a, b) {
textNodes(b)
})
}
}
this.each(function() {
textNodes(this)
})
return out
};
And I've got the ability to hide columns and rows in a table. When I submit a search and get the highlighted results, there would be in this case, the array length of the text nodes found would be 6, but there would only be 3 highlighted on the page. When you output the array to the console you get this:
So you get the 3 tags which I was expecting, but you see that the array is actually consisting of a [span,undefined,span,undefined,undefined,span]. Thus giving me the length of 6.
<span>
<span>
<span>
[span, undefined, span, undefined, undefined, span]
I don't know why it's not stripping out all of the undefined text nodes when I do the check for them. Here's what I've got for the function.
performTextSearch = function(currentObj){
if($.trim(currentObj.val()).length > 0){
var n = $("body").eoTextSearch($.trim(currentObj.val())),
recordTitle = "matches",
arrayRecheck = new Array(),
genericElemArray = new Array()
if(n.length == 1){
recordTitle = "match"
}
//check to see if we need to do a recount on the array length.
//if it's more than 0, then they're doing a compare and we need to strip out all of the text nodes that don't have a visible parent.
if($(".rows:checked").length > 0){
$.each(n,function(i,currElem){
if($(currElem).length != 0 && typeof currElem != 'undefined'){
if($(currElem).closest("tr").is(":visible") || $(currElem).is(":visible")){
//remove the element from the array
console.log(currElem)
arrayRecheck[i] = currElem
}
}
})
}
if(arrayRecheck.length > 0){
genericElemArray.push(arrayRecheck)
console.log(arrayRecheck)
}
else{
genericElemArray.push(n)
}
genericElemArray = genericElemArray[0]
$("#recordCount").text(genericElemArray.length + " " +recordTitle)
$(".searchResults").show()
for(var i = 0; i < genericElemArray.length; ++i){
void($(genericElemArray[i]).addClass("yellowBkgd").addClass("highLighted"))
}
}
else{
$(".highLighted").css("background","none")
}
}
If you look at the code below "//check to see if we need to do a recount on the array length. ", you'll see where I'm stripping out the text nodes based off of the display and whether or not the object is defined. I'm checking the length instead of undefined because the typeof == undefined wasn't working at all for some reason. Apparently, things are still slipping by though.
Any idea why I'm still getting undefined objects in the array?
My apologies for such a big post!
Thanks in advance
I've modified your eoTextSearch() function to remove dependencies on global variables in exchange for closures:
$.fn.extend({
// helper function
// recurses into a DOM object and calls a custom function for every descendant
eachDescendant: function (callback) {
for (var i=0, j=this.length; i<j; i++) {
callback.call(this[i]);
$.fn.eachDescendant.call(this[i].childNodes, callback);
}
return this;
},
// your text search function, revised
eoTextSearch: function () {
var text = document.createTextNode("test").textContent
? "textContent" : "innerText";
// the "matches" function uses an out param instead of a return value
var matches = function (pat, outArray) {
var isRe = typeof pat.test == "function";
return function() {
if (this.nodeType != 3) return; // ...text nodes only
if (isRe && pat.test(this[text]) || this[text].indexOf(pat) > -1) {
outArray.push(this.parentNode);
}
}
};
// this is the function that will *actually* become eoTextSearch()
return function (stringOrPattern) {
var result = $(); // start with an empty jQuery object
this.eachDescendant( matches(stringOrPattern, result) );
return result;
}
}() // <- instant calling is important here
});
And then you can do something like this:
$("body").eoTextSearch("foo").filter(function () {
return $(this).closest("tr").is(":visible");
});
To remove unwanted elements from the search result. No "recounting the array length" necessary. Or you use each() directly and decide within what to do.
I cannot entirely get my head around your code, but the most likely issue is that you are removing items from the array, but not shrinking the array afterwards. Simply removing items will return you "undefined", and will not collapse the array.
I would suggest that you do one of the following:
Copy the array to a new array, but only copying those items that are not undefined
Only use those array items that are not undefined.
I hope this is something of a help.
Found the answer in another post.
Remove empty elements from an array in Javascript
Ended up using the answer's second option and it worked alright.
In Javascript, I'd like determine whether an element, say an A element, exists inside a given range/textRange. The aim is to determine if the user's current selection contains a link. I am building a rich text editor control.
The range object has a commonAncestorContainer (W3C) or parentElement() (Microsoft) method which returns the closest common anscestor of all elements in the range. However, looking inside this element for A elements won't work, since this common ancestor may also contain elements that aren't in the range, since the range can start or end part way through a parent.
How would you achieve this?
How about selection.containsNode?
https://developer.mozilla.org/en/DOM/Selection/containsNode
something like:
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var result = $('a', range.commonAncestorContainer).filter(function() {
return selection.containsNode(this);
});
console.log(result);
I ended up going with a solution like this:
var findinselection = function(tagname, container) {
var
i, len, el,
rng = getrange(),
comprng,
selparent;
if (rng) {
selparent = rng.commonAncestorContainer || rng.parentElement();
// Look for an element *around* the selected range
for (el = selparent; el !== container; el = el.parentNode) {
if (el.tagName && el.tagName.toLowerCase() === tagname) {
return el;
}
}
// Look for an element *within* the selected range
if (!rng.collapsed && (rng.text === undefined || rng.text) &&
selparent.getElementsByTagName) {
el = selparent.getElementsByTagName(tagname);
comprng = document.createRange ?
document.createRange() : document.body.createTextRange();
for (i = 0, len = el.length; i < len; i++) {
// determine if element el[i] is within the range
if (document.createRange) { // w3c
comprng.selectNodeContents(el[i]);
if (rng.compareBoundaryPoints(Range.END_TO_START, comprng) < 0 &&
rng.compareBoundaryPoints(Range.START_TO_END, comprng) > 0) {
return el[i];
}
}
else { // microsoft
comprng.moveToElementText(el[i]);
if (rng.compareEndPoints("StartToEnd", comprng) < 0 &&
rng.compareEndPoints("EndToStart", comprng) > 0) {
return el[i];
}
}
}
}
}
};
Where getrange() is another function of mine to get the current selection as a range object.
To use, call it like
var link = findselection('a', editor);
Where editor is the contenteditable element, or body in a designmode iframe.
This is a bit of a pain in the bum to do cross-browser. You could use my Rangy library, which is probably overkill for just this task but does make it more straightforward and works in all major browsers. The following code assumes only one Range is selected:
var sel = rangy.getSelection();
if (sel.rangeCount) {
var range = sel.getRangeAt(0);
var links = range.getNodes([1], function(node) {
return node.tagName.toLowerCase() == "a" && range.containsNode(node);
});
}
I'm using this code that works with IE / Chrome / FF: (I'm using it to select rows <tr> in a table)
// yourLink is the DOM element you want to check
var selection = window.getSelection().getRangeAt(0)
var node = document.createRange()
node.selectNode(yourLink)
var s2s = selection.compareBoundaryPoints(Range.START_TO_END, node)
var s2e = selection.compareBoundaryPoints(Range.START_TO_START, node)
var e2s = selection.compareBoundaryPoints(Range.END_TO_START, node)
var e2e = selection.compareBoundaryPoints(Range.END_TO_END, node)
if ((s2s != s2e) || (e2s != e2e) || (s2s!=e2e))
console.log("your node is inside selection")
In the case of the range on the searched element, what I wrote is useful. (But only for that case!)
First I wrote a function that returns the Node found in the range: getNodeFromRange(rangeObject). Using this function it was already easy to write the function that returns the desired Node: findTagInRange(tagName).
function getNodeFromRange(range) {
if(range.endContainer.nodeType==Node.ELEMENT_NODE) {
return range.endContainer;
}
if(range.endContainer.nodeType==Node.TEXT_NODE) {
return range.endContainer.parentNode;
}
else {
// the 'startContainer' it isn't on an Element (<p>, <div>, etc...)
return;
}
}
function findTagInRange(tagName, range) {
var node = getNodeFromRange(range);
if(node && typeof(node.tagName)!='undefiend' && node.tagName.toLowerCase()==tagName.toLowerCase()) {
return $(node);
}
return;
}
And then I can use it as follows:
var link = findTagInRange('A', range);
And I see the determination of the range you've already solved. :)