Notes/apps/client/src/widgets/quick_search.ts

198 lines
6.7 KiB
TypeScript
Raw Normal View History

2021-01-31 22:45:45 +01:00
import BasicWidget from "./basic_widget.js";
import server from "../services/server.js";
import linkService from "../services/link.js";
2021-04-16 23:01:56 +02:00
import froca from "../services/froca.js";
2021-01-31 22:45:45 +01:00
import utils from "../services/utils.js";
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js";
import shortcutService from "../services/shortcuts.js";
2024-09-13 22:08:52 +03:00
import { t } from "../services/i18n.js";
import { Dropdown, Tooltip } from "bootstrap";
2021-01-31 22:45:45 +01:00
const TPL = /*html*/`
2021-05-17 21:46:18 +02:00
<div class="quick-search input-group input-group-sm">
2021-01-31 22:45:45 +01:00
<style>
.quick-search {
2021-06-13 22:55:31 +02:00
padding: 10px 10px 10px 0px;
height: 50px;
2021-05-22 22:55:24 +02:00
}
2021-05-22 22:55:24 +02:00
.quick-search button, .quick-search input {
border: 0;
font-size: 100% !important;
}
2021-01-31 22:45:45 +01:00
.quick-search .dropdown-menu {
max-height: 600px;
max-width: 400px;
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
2021-02-01 23:51:04 +01:00
box-shadow: -30px 50px 93px -50px black;
2021-01-31 22:45:45 +01:00
}
</style>
2021-05-22 22:55:24 +02:00
<div class="input-group-prepend">
<button class="btn btn-outline-secondary search-button" type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
2021-01-31 22:45:45 +01:00
<span class="bx bx-search"></span>
</button>
<div class="dropdown-menu tn-dropdown-list"></div>
2021-01-31 22:45:45 +01:00
</div>
2024-09-13 22:08:52 +03:00
<input type="text" class="form-control form-control-sm search-string" placeholder="${t("quick-search.placeholder")}">
2021-01-31 22:45:45 +01:00
</div>`;
2021-02-01 23:51:04 +01:00
const MAX_DISPLAYED_NOTES = 15;
2021-01-31 22:45:45 +01:00
2025-03-16 18:31:31 +02:00
// TODO: Deduplicate with server.
interface QuickSearchResponse {
searchResultNoteIds: string[];
error: string;
}
2021-01-31 22:45:45 +01:00
export default class QuickSearchWidget extends BasicWidget {
2025-03-16 18:31:31 +02:00
private dropdown!: bootstrap.Dropdown;
private $searchString!: JQuery<HTMLElement>;
private $dropdownMenu!: JQuery<HTMLElement>;
2021-01-31 22:45:45 +01:00
doRender() {
this.$widget = $(TPL);
this.$searchString = this.$widget.find(".search-string");
this.$dropdownMenu = this.$widget.find(".dropdown-menu");
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
reference: this.$searchString[0],
popperConfig: {
strategy: "fixed",
placement: "bottom"
}
});
2021-01-31 22:45:45 +01:00
2025-01-09 18:07:02 +02:00
this.$widget.find(".input-group-prepend").on("shown.bs.dropdown", () => this.search());
2021-01-31 22:45:45 +01:00
if (utils.isMobile()) {
2025-01-09 18:07:02 +02:00
this.$searchString.keydown((e) => {
if (e.which === 13) {
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown
} else {
this.dropdown.show();
}
e.preventDefault();
e.stopPropagation();
}
2025-01-09 18:07:02 +02:00
});
}
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut(this.$searchString, "return", () => {
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown
} else {
this.dropdown.show();
}
2021-01-31 22:45:45 +01:00
this.$searchString.focus();
});
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut(this.$searchString, "down", () => {
this.$dropdownMenu.find(".dropdown-item:not(.disabled):first").focus();
2021-02-01 23:51:04 +01:00
});
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut(this.$searchString, "esc", () => {
this.dropdown.hide();
2021-02-05 23:19:43 +01:00
});
2021-01-31 22:45:45 +01:00
return this.$widget;
}
async search() {
2025-03-16 18:31:31 +02:00
const searchString = String(this.$searchString.val())?.trim();
2021-02-01 23:51:04 +01:00
if (!searchString) {
this.dropdown.hide();
2021-02-01 23:51:04 +01:00
return;
}
2021-01-31 22:45:45 +01:00
this.$dropdownMenu.empty();
2024-09-13 22:08:52 +03:00
this.$dropdownMenu.append(`<span class="dropdown-item disabled"><span class="bx bx-loader bx-spin"></span>${t("quick-search.searching")}</span>`);
2021-01-31 22:45:45 +01:00
2025-03-16 18:31:31 +02:00
const { searchResultNoteIds, error } = await server.get<QuickSearchResponse>(`quick-search/${encodeURIComponent(searchString)}`);
2021-01-31 22:45:45 +01:00
2022-12-17 11:58:30 +01:00
if (error) {
2025-03-16 18:31:31 +02:00
let tooltip = new Tooltip(this.$searchString[0], {
2025-01-09 18:07:02 +02:00
trigger: "manual",
title: `Search error: ${error}`,
2025-01-09 18:07:02 +02:00
placement: "right"
2022-12-17 11:58:30 +01:00
});
tooltip.show();
2022-12-17 11:58:30 +01:00
setTimeout(() => tooltip.dispose(), 4000);
2022-12-17 11:58:30 +01:00
}
const displayedNoteIds = searchResultNoteIds.slice(0, Math.min(MAX_DISPLAYED_NOTES, searchResultNoteIds.length));
2021-01-31 22:45:45 +01:00
this.$dropdownMenu.empty();
if (displayedNoteIds.length === 0) {
2024-09-13 22:08:52 +03:00
this.$dropdownMenu.append(`<span class="dropdown-item disabled">${t("quick-search.no-results")}</span>`);
2021-01-31 22:45:45 +01:00
}
2021-04-16 22:57:37 +02:00
for (const note of await froca.getNotes(displayedNoteIds)) {
2024-12-07 01:58:50 +02:00
const $link = await linkService.createLink(note.noteId, { showNotePath: true, showNoteIcon: true });
2025-01-09 18:07:02 +02:00
$link.addClass("dropdown-item");
2021-02-01 23:51:04 +01:00
$link.attr("tabIndex", "0");
2025-01-09 18:07:02 +02:00
$link.on("click", (e) => {
this.dropdown.hide();
2025-01-09 18:07:02 +02:00
if (!e.target || e.target.nodeName !== "A") {
// click on the link is handled by link handling, but we want the whole item clickable
2025-03-18 18:44:48 +01:00
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
}
});
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut($link, "return", () => {
this.dropdown.hide();
2025-03-18 18:44:48 +01:00
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
2021-02-01 23:51:04 +01:00
});
2021-01-31 22:45:45 +01:00
this.$dropdownMenu.append($link);
}
2022-12-17 11:58:30 +01:00
if (searchResultNoteIds.length > MAX_DISPLAYED_NOTES) {
2025-01-09 18:07:02 +02:00
const numRemainingResults = searchResultNoteIds.length - MAX_DISPLAYED_NOTES;
2024-09-13 22:08:52 +03:00
this.$dropdownMenu.append(`<span class="dropdown-item disabled">${t("quick-search.more-results", { number: numRemainingResults })}</span>`);
2021-01-31 22:45:45 +01:00
}
const $showInFullButton = $('<a class="dropdown-item" tabindex="0">').text(t("quick-search.show-in-full-search"));
2021-02-01 23:51:04 +01:00
2024-12-07 01:58:50 +02:00
this.$dropdownMenu.append($(`<div class="dropdown-divider">`));
2021-02-01 23:51:04 +01:00
this.$dropdownMenu.append($showInFullButton);
2025-01-09 18:07:02 +02:00
$showInFullButton.on("click", () => this.showInFullSearch());
2021-02-01 23:51:04 +01:00
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut($showInFullButton, "return", () => this.showInFullSearch());
2021-02-01 23:51:04 +01:00
2025-01-09 18:07:02 +02:00
shortcutService.bindElShortcut(this.$dropdownMenu.find(".dropdown-item:first"), "up", () => this.$searchString.focus());
2021-02-01 23:51:04 +01:00
this.dropdown.update();
2021-01-31 22:45:45 +01:00
}
2021-02-01 23:51:04 +01:00
async showInFullSearch() {
this.dropdown.hide();
2025-01-09 18:07:02 +02:00
await appContext.triggerCommand("searchNotes", {
2025-03-16 18:31:31 +02:00
searchString: String(this.$searchString.val())
});
}
2021-02-01 23:51:04 +01:00
quickSearchEvent() {
this.$searchString.focus();
}
2021-01-31 22:45:45 +01:00
}