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