Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use standard dropdowns for autocomplete #9453

Merged
merged 7 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module.exports = [
findFormParent: "readonly",
fireEvent: "readonly",
Form: "readonly",
FormChecker: "readonly",
getElementOverflowParams: "readonly",
hoverNotification: "readonly",
iota: "writeable",
Expand Down
2 changes: 2 additions & 0 deletions war/src/main/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Tooltips from "@/components/tooltips";
import StopButtonLink from "@/components/stop-button-link";
import ConfirmationLink from "@/components/confirmation-link";
import Dialogs from "@/components/dialogs";
import Autocomplete from "@/components/autocomplete";

Dropdowns.init();
Notifications.init();
Expand All @@ -13,3 +14,4 @@ Tooltips.init();
StopButtonLink.init();
ConfirmationLink.init();
Dialogs.init();
Autocomplete.init();
120 changes: 120 additions & 0 deletions war/src/main/js/components/autocomplete/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import behaviorShim from "@/util/behavior-shim";
import Utils from "@/components/dropdowns/utils";

function init() {
function addValue(value, item, delimiter) {
const prev = value.includes(delimiter)
? value.substring(0, value.lastIndexOf(delimiter) + 1) + " "
: "";
return prev + item + delimiter + " ";
}

function validate(e) {
if (e.targetUrl) {
var method = e.getAttribute("checkMethod") || "post";
try {
FormChecker.delayedCheck(e.targetUrl(), method, e.targetElement);
} catch (x) {
console.warn(x);
return;
}
}
}

function convertSuggestionToItem(suggestion, e) {
const delimiter = e.getAttribute("autoCompleteDelimChar");
const confirm = () => {
e.value = delimiter
? addValue(e.value, suggestion.name, delimiter)
: suggestion.name;
validate(e);
e.focus();
};
return {
label: suggestion.name,
onClick: confirm,
onKeyPress: (evt) => {
if (evt.key == "Tab") {
confirm();
e.dropdown.hide();
evt.preventDefault();
}
},
};
}

function createAndShowDropdown(e, div, suggestions) {
const items = suggestions
.splice(0, 10)
.map((s) => convertSuggestionToItem(s, e));
if (!e.dropdown) {
Utils.generateDropdown(
div,
(instance) => {
e.dropdown = instance;
},
true,
);
}
e.dropdown.setContent(Utils.generateDropdownItems(items, true));
e.dropdown.show();
}

function updateSuggestions(e, div) {
const text = e.value.trim();
const delimiter = e.getAttribute("autoCompleteDelimChar");
const word = delimiter ? text.split(delimiter).reverse()[0].trim() : text;
if (!word) {
if (e.dropdown) {
e.dropdown.hide();
}
return;
}
const url =
e.getAttribute("autoCompleteUrl") + "?value=" + encodeURIComponent(word);
fetch(url)
.then((rsp) => (rsp.ok ? rsp.json() : {}))
.then((response) =>
createAndShowDropdown(e, div, response.suggestions || []),
);
}

function debounce(callback) {
callback.running = false;
return () => {
if (!callback.running) {
callback.running = true;
setTimeout(() => {
callback();
callback.running = false;
}, 300);
}
};
}

behaviorShim.specify(
"INPUT.auto-complete",
"input-auto-complete",
0,
function (e) {
// form field with auto-completion support
// insert the auto-completion container
var div = document.createElement("DIV");
e.parentNode.insertBefore(div, e.nextElementSibling);
e.style.position = "relative";
// otherwise menu won't hide on tab with nothing selected
// needs delay as without that it blocks click selection of an item
e.addEventListener("focusout", () =>
setTimeout(() => e.dropdown && e.dropdown.hide(), 200),
);
e.addEventListener(
"input",
debounce(() => {
updateSuggestions(e, div);
}),
);
},
);
}

export default { init };
4 changes: 3 additions & 1 deletion war/src/main/js/components/dropdowns/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ function menuItem(options) {
if (options.onClick) {
item.addEventListener("click", (event) => options.onClick(event));
}

if (options.onKeyPress) {
item.onkeypress = options.onKeyPress;
}
return item;
}

Expand Down
20 changes: 16 additions & 4 deletions war/src/main/js/components/dropdowns/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected";
* @param element - the element to generate the dropdown for
* @param callback - called to retrieve the list of dropdown items
*/
function generateDropdown(element, callback) {
function generateDropdown(element, callback, immediate) {
tippy(
element,
Object.assign({}, Templates.dropdown(), {
onCreate(instance) {
instance.reference.addEventListener("mouseenter", () => {
const onload = () => {
if (instance.loaded) {
return;
}
Expand All @@ -26,7 +26,12 @@ function generateDropdown(element, callback) {
});

callback(instance);
});
};
if (immediate) {
onload();
} else {
instance.reference.addEventListener("mouseenter", onload);
}
},
}),
);
Expand Down Expand Up @@ -86,7 +91,10 @@ function generateDropdownItems(items, compact) {
menuItems,
() => menuItems.querySelectorAll(".jenkins-dropdown__item"),
SELECTED_ITEM_CLASS,
(selectedItem, key) => {
(selectedItem, key, evt) => {
if (!selectedItem) {
return;
}
switch (key) {
case "ArrowLeft": {
const root = selectedItem.closest("[data-tippy-root]");
Expand All @@ -110,6 +118,10 @@ function generateDropdownItems(items, compact) {
.classList.add(SELECTED_ITEM_CLASS);
break;
}
default:
if (selectedItem.onkeypress) {
selectedItem.onkeypress(evt);
}
}
},
(container) => {
Expand Down
2 changes: 1 addition & 1 deletion war/src/main/js/util/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function makeKeyboardNavigable(
selectedItem.click();
}
} else {
additionalBehaviours(selectedItem, e.key);
additionalBehaviours(selectedItem, e.key, e);
}
}
});
Expand Down
43 changes: 0 additions & 43 deletions war/src/main/webapp/scripts/hudson-behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -1310,49 +1310,6 @@ function rowvgStartEachRow(recursive, f) {
},
);

Behaviour.specify(
"INPUT.auto-complete",
"input-auto-complete",
++p,
function (e) {
// form field with auto-completion support
// insert the auto-completion container
var div = document.createElement("DIV");
e.parentNode.insertBefore(div, e.nextElementSibling);
e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break

var ds = new YAHOO.util.XHRDataSource(e.getAttribute("autoCompleteUrl"));
ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
ds.responseSchema = {
resultsList: "suggestions",
fields: ["name"],
};

// Instantiate the AutoComplete
var ac = new YAHOO.widget.AutoComplete(e, div, ds);
ac.generateRequest = function (query) {
return "?value=" + query;
};
ac.autoHighlight = false;
ac.prehighlightClassName = "yui-ac-prehighlight";
ac.animSpeed = 0;
ac.formatResult = ac.formatEscapedResult;
ac.useShadow = true;
ac.autoSnapContainer = true;
ac.delimChar = e.getAttribute("autoCompleteDelimChar");
ac.doBeforeExpandContainer = function (textbox, container) {
// adjust the width every time we show it
container.style.width = textbox.clientWidth + "px";
var Dom = YAHOO.util.Dom;
Dom.setXY(container, [
Dom.getX(textbox),
Dom.getY(textbox) + textbox.offsetHeight,
]);
return true;
};
},
);

Behaviour.specify(
"A.jenkins-help-button",
"a-jenkins-help-button",
Expand Down
Loading