src/jquery.fancytree.gridnav.js
/*!
* jquery.fancytree.gridnav.js
*
* Support keyboard navigation for trees with embedded input controls.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
(function (factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define([
"jquery",
"./jquery.fancytree",
"./jquery.fancytree.table",
], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
require("./jquery.fancytree.table"); // core + table
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function ($) {
"use strict";
/*******************************************************************************
* Private functions and variables
*/
// Allow these navigation keys even when input controls are focused
var KC = $.ui.keyCode,
// which keys are *not* handled by embedded control, but passed to tree
// navigation handler:
NAV_KEYS = {
text: [KC.UP, KC.DOWN],
checkbox: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
link: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
radiobutton: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
"select-one": [KC.LEFT, KC.RIGHT],
"select-multiple": [KC.LEFT, KC.RIGHT],
};
/* Calculate TD column index (considering colspans).*/
function getColIdx($tr, $td) {
var colspan,
td = $td.get(0),
idx = 0;
$tr.children().each(function () {
if (this === td) {
return false;
}
colspan = $(this).prop("colspan");
idx += colspan ? colspan : 1;
});
return idx;
}
/* Find TD at given column index (considering colspans).*/
function findTdAtColIdx($tr, colIdx) {
var colspan,
res = null,
idx = 0;
$tr.children().each(function () {
if (idx >= colIdx) {
res = $(this);
return false;
}
colspan = $(this).prop("colspan");
idx += colspan ? colspan : 1;
});
return res;
}
/* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */
function findNeighbourTd($target, keyCode) {
var $tr,
colIdx,
$td = $target.closest("td"),
$tdNext = null;
switch (keyCode) {
case KC.LEFT:
$tdNext = $td.prev();
break;
case KC.RIGHT:
$tdNext = $td.next();
break;
case KC.UP:
case KC.DOWN:
$tr = $td.parent();
colIdx = getColIdx($tr, $td);
while (true) {
$tr = keyCode === KC.UP ? $tr.prev() : $tr.next();
if (!$tr.length) {
break;
}
// Skip hidden rows
if ($tr.is(":hidden")) {
continue;
}
// Find adjacent cell in the same column
$tdNext = findTdAtColIdx($tr, colIdx);
// Skip cells that don't conatain a focusable element
if ($tdNext && $tdNext.find(":input,a").length) {
break;
}
}
break;
}
return $tdNext;
}
/*******************************************************************************
* Extension code
*/
$.ui.fancytree.registerExtension({
name: "gridnav",
version: "@VERSION",
// Default options for this extension.
options: {
autofocusInput: false, // Focus first embedded input if node gets activated
handleCursorKeys: true, // Allow UP/DOWN in inputs to move to prev/next node
},
treeInit: function (ctx) {
// gridnav requires the table extension to be loaded before itself
this._requireExtension("table", true, true);
this._superApply(arguments);
this.$container.addClass("fancytree-ext-gridnav");
// Activate node if embedded input gets focus (due to a click)
this.$container.on("focusin", function (event) {
var ctx2,
node = $.ui.fancytree.getNode(event.target);
if (node && !node.isActive()) {
// Call node.setActive(), but also pass the event
ctx2 = ctx.tree._makeHookContext(node, event);
ctx.tree._callHook("nodeSetActive", ctx2, true);
}
});
},
nodeSetActive: function (ctx, flag, callOpts) {
var $outer,
opts = ctx.options.gridnav,
node = ctx.node,
event = ctx.originalEvent || {},
triggeredByInput = $(event.target).is(":input");
flag = flag !== false;
this._superApply(arguments);
if (flag) {
if (ctx.options.titlesTabbable) {
if (!triggeredByInput) {
$(node.span)
.find("span.fancytree-title")
.trigger("focus");
node.setFocus();
}
// If one node is tabbable, the container no longer needs to be
ctx.tree.$container.attr("tabindex", "-1");
// ctx.tree.$container.removeAttr("tabindex");
} else if (opts.autofocusInput && !triggeredByInput) {
// Set focus to input sub input (if node was clicked, but not
// when TAB was pressed )
$outer = $(node.tr || node.span);
$outer.find(":input:enabled").first().trigger("focus");
}
}
},
nodeKeydown: function (ctx) {
var inputType,
handleKeys,
$td,
opts = ctx.options.gridnav,
event = ctx.originalEvent,
$target = $(event.target);
if ($target.is(":input:enabled")) {
inputType = $target.prop("type");
} else if ($target.is("a")) {
inputType = "link";
}
// ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType);
if (inputType && opts.handleCursorKeys) {
handleKeys = NAV_KEYS[inputType];
if (handleKeys && $.inArray(event.which, handleKeys) >= 0) {
$td = findNeighbourTd($target, event.which);
if ($td && $td.length) {
// ctx.node.debug("ignore keydown in input", event.which, handleKeys);
$td.find(":input:enabled,a").trigger("focus");
// Prevent Fancytree default navigation
return false;
}
}
return true;
}
// ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType);
return this._superApply(arguments);
},
});
// Value returned by `require('jquery.fancytree..')`
return $.ui.fancytree;
}); // End of closure