/*! * 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-2021, Martin Wendt (https://wwWendt.de) * * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.38.2 * @date 2022-06-30T18:24:06Z */ (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: "2.38.2", // 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").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().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").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