1/*! 2 * jquery.fancytree.gridnav.js 3 * 4 * Support keyboard navigation for trees with embedded input controls. 5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 6 * 7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8 * 9 * Released under the MIT license 10 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11 * 12 * @version 2.38.3 13 * @date 2023-02-01T20:52:50Z 14 */ 15 16(function (factory) { 17 if (typeof define === "function" && define.amd) { 18 // AMD. Register as an anonymous module. 19 define([ 20 "jquery", 21 "./jquery.fancytree", 22 "./jquery.fancytree.table", 23 ], factory); 24 } else if (typeof module === "object" && module.exports) { 25 // Node/CommonJS 26 require("./jquery.fancytree.table"); // core + table 27 module.exports = factory(require("jquery")); 28 } else { 29 // Browser globals 30 factory(jQuery); 31 } 32})(function ($) { 33 "use strict"; 34 35 /******************************************************************************* 36 * Private functions and variables 37 */ 38 39 // Allow these navigation keys even when input controls are focused 40 41 var KC = $.ui.keyCode, 42 // which keys are *not* handled by embedded control, but passed to tree 43 // navigation handler: 44 NAV_KEYS = { 45 text: [KC.UP, KC.DOWN], 46 checkbox: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 47 link: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 48 radiobutton: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 49 "select-one": [KC.LEFT, KC.RIGHT], 50 "select-multiple": [KC.LEFT, KC.RIGHT], 51 }; 52 53 /* Calculate TD column index (considering colspans).*/ 54 function getColIdx($tr, $td) { 55 var colspan, 56 td = $td.get(0), 57 idx = 0; 58 59 $tr.children().each(function () { 60 if (this === td) { 61 return false; 62 } 63 colspan = $(this).prop("colspan"); 64 idx += colspan ? colspan : 1; 65 }); 66 return idx; 67 } 68 69 /* Find TD at given column index (considering colspans).*/ 70 function findTdAtColIdx($tr, colIdx) { 71 var colspan, 72 res = null, 73 idx = 0; 74 75 $tr.children().each(function () { 76 if (idx >= colIdx) { 77 res = $(this); 78 return false; 79 } 80 colspan = $(this).prop("colspan"); 81 idx += colspan ? colspan : 1; 82 }); 83 return res; 84 } 85 86 /* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */ 87 function findNeighbourTd($target, keyCode) { 88 var $tr, 89 colIdx, 90 $td = $target.closest("td"), 91 $tdNext = null; 92 93 switch (keyCode) { 94 case KC.LEFT: 95 $tdNext = $td.prev(); 96 break; 97 case KC.RIGHT: 98 $tdNext = $td.next(); 99 break; 100 case KC.UP: 101 case KC.DOWN: 102 $tr = $td.parent(); 103 colIdx = getColIdx($tr, $td); 104 while (true) { 105 $tr = keyCode === KC.UP ? $tr.prev() : $tr.next(); 106 if (!$tr.length) { 107 break; 108 } 109 // Skip hidden rows 110 if ($tr.is(":hidden")) { 111 continue; 112 } 113 // Find adjacent cell in the same column 114 $tdNext = findTdAtColIdx($tr, colIdx); 115 // Skip cells that don't conatain a focusable element 116 if ($tdNext && $tdNext.find(":input,a").length) { 117 break; 118 } 119 } 120 break; 121 } 122 return $tdNext; 123 } 124 125 /******************************************************************************* 126 * Extension code 127 */ 128 $.ui.fancytree.registerExtension({ 129 name: "gridnav", 130 version: "2.38.3", 131 // Default options for this extension. 132 options: { 133 autofocusInput: false, // Focus first embedded input if node gets activated 134 handleCursorKeys: true, // Allow UP/DOWN in inputs to move to prev/next node 135 }, 136 137 treeInit: function (ctx) { 138 // gridnav requires the table extension to be loaded before itself 139 this._requireExtension("table", true, true); 140 this._superApply(arguments); 141 142 this.$container.addClass("fancytree-ext-gridnav"); 143 144 // Activate node if embedded input gets focus (due to a click) 145 this.$container.on("focusin", function (event) { 146 var ctx2, 147 node = $.ui.fancytree.getNode(event.target); 148 149 if (node && !node.isActive()) { 150 // Call node.setActive(), but also pass the event 151 ctx2 = ctx.tree._makeHookContext(node, event); 152 ctx.tree._callHook("nodeSetActive", ctx2, true); 153 } 154 }); 155 }, 156 nodeSetActive: function (ctx, flag, callOpts) { 157 var $outer, 158 opts = ctx.options.gridnav, 159 node = ctx.node, 160 event = ctx.originalEvent || {}, 161 triggeredByInput = $(event.target).is(":input"); 162 163 flag = flag !== false; 164 165 this._superApply(arguments); 166 167 if (flag) { 168 if (ctx.options.titlesTabbable) { 169 if (!triggeredByInput) { 170 $(node.span).find("span.fancytree-title").focus(); 171 node.setFocus(); 172 } 173 // If one node is tabbable, the container no longer needs to be 174 ctx.tree.$container.attr("tabindex", "-1"); 175 // ctx.tree.$container.removeAttr("tabindex"); 176 } else if (opts.autofocusInput && !triggeredByInput) { 177 // Set focus to input sub input (if node was clicked, but not 178 // when TAB was pressed ) 179 $outer = $(node.tr || node.span); 180 $outer.find(":input:enabled").first().focus(); 181 } 182 } 183 }, 184 nodeKeydown: function (ctx) { 185 var inputType, 186 handleKeys, 187 $td, 188 opts = ctx.options.gridnav, 189 event = ctx.originalEvent, 190 $target = $(event.target); 191 192 if ($target.is(":input:enabled")) { 193 inputType = $target.prop("type"); 194 } else if ($target.is("a")) { 195 inputType = "link"; 196 } 197 // ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType); 198 199 if (inputType && opts.handleCursorKeys) { 200 handleKeys = NAV_KEYS[inputType]; 201 if (handleKeys && $.inArray(event.which, handleKeys) >= 0) { 202 $td = findNeighbourTd($target, event.which); 203 if ($td && $td.length) { 204 // ctx.node.debug("ignore keydown in input", event.which, handleKeys); 205 $td.find(":input:enabled,a").focus(); 206 // Prevent Fancytree default navigation 207 return false; 208 } 209 } 210 return true; 211 } 212 // ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType); 213 return this._superApply(arguments); 214 }, 215 }); 216 // Value returned by `require('jquery.fancytree..')` 217 return $.ui.fancytree; 218}); // End of closure 219