Notes/src/public/app/services/note_autocomplete.js

340 lines
10 KiB
JavaScript
Raw Normal View History

import server from "./server.js";
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js";
2019-05-22 20:53:59 +02:00
import utils from './utils.js';
2020-09-21 00:07:46 +02:00
import noteCreateService from './note_create.js';
2021-04-16 23:01:56 +02:00
import froca from "./froca.js";
// this key needs to have this value, so it's hit by the tooltip
2020-05-16 22:11:09 +02:00
const SELECTED_NOTE_PATH_KEY = "data-note-path";
2018-11-14 00:05:09 +01:00
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link";
2020-09-21 22:08:54 +02:00
async function autocompleteSourceForCKEditor(queryText) {
return await new Promise((res, rej) => {
autocompleteSource(queryText, rows => {
res(rows.map(row => {
return {
action: row.action,
noteTitle: row.noteTitle,
id: `@${row.notePathTitle}`,
2020-09-21 22:08:54 +02:00
name: row.notePathTitle,
link: `#${row.notePath}`,
2020-09-21 22:08:54 +02:00
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
}
}));
}, {
allowCreatingNotes: true
2020-09-21 22:08:54 +02:00
});
});
}
async function autocompleteSource(term, cb, options = {}) {
2021-05-22 12:35:41 +02:00
const activeNoteId = appContext.tabManager.getActiveContextNoteId();
2020-09-21 00:07:46 +02:00
2024-11-23 20:51:51 +08:00
let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${options.fastSearch}`);
if (term.trim().length >= 1 && options.allowCreatingNotes) {
2020-09-21 00:07:46 +02:00
results = [
{
2020-09-21 22:08:54 +02:00
action: 'create-note',
2020-09-21 00:07:46 +02:00
noteTitle: term,
2020-09-21 22:08:54 +02:00
parentNoteId: activeNoteId || 'root',
highlightedNotePathTitle: `Create and link child note "${utils.escapeHtml(term)}"`
2020-09-21 00:07:46 +02:00
}
].concat(results);
}
if (term.trim().length >= 1 && options.allowJumpToSearchNotes) {
2024-11-20 14:22:39 +08:00
results = results.concat([
{
action: 'search-notes',
noteTitle: term,
highlightedNotePathTitle: `Search for "${utils.escapeHtml(term)}" <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
}
]);
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
action: 'external-link',
externalLink: term,
highlightedNotePathTitle: `Insert external link to "${utils.escapeHtml(term)}"`
}
].concat(results);
}
2020-09-21 00:07:46 +02:00
cb(results);
}
function clearText($el) {
2019-05-22 20:53:59 +02:00
if (utils.isMobile()) {
return;
}
2020-05-16 22:11:09 +02:00
$el.setSelectedNotePath("");
2019-11-09 17:39:48 +01:00
$el.autocomplete("val", "").trigger('change');
}
function setText($el, text) {
if (utils.isMobile()) {
return;
}
$el.setSelectedNotePath("");
$el
.autocomplete("val", text.trim())
.autocomplete("open");
}
function showRecentNotes($el) {
2019-05-22 20:53:59 +02:00
if (utils.isMobile()) {
return;
}
2020-05-16 22:11:09 +02:00
$el.setSelectedNotePath("");
$el.autocomplete("val", "");
$el.autocomplete('open');
2019-11-09 17:45:22 +01:00
$el.trigger('focus');
}
2024-11-23 20:51:51 +08:00
function fullTextSearch($el,options){
const searchString = $el.autocomplete('val');
if (searchString.trim().length >= 1) {
const originalFastSearch = options.fastSearch;
2024-11-23 20:51:51 +08:00
clearText($el);
options.fastSearch = false;
$el.autocomplete('val', searchString);
2024-11-23 20:51:51 +08:00
$el.autocomplete('open');
$el.trigger('focus');
options.fastSearch = originalFastSearch;
}
2024-11-23 20:51:51 +08:00
}
function initNoteAutocomplete($el, options) {
2019-05-22 20:53:59 +02:00
if ($el.hasClass("note-autocomplete-input") || utils.isMobile()) {
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
2019-05-22 20:53:59 +02:00
return $el;
}
2019-05-22 20:53:59 +02:00
options = options || {};
options.fastSearch = true; // Perform fast search by default
2019-05-22 20:53:59 +02:00
$el.addClass("note-autocomplete-input");
const $clearTextButton = $("<button>")
.addClass("input-group-text input-clearer-button bx bxs-tag-x")
.prop("title", "Clear text field");
const $showRecentNotesButton = $("<button>")
.addClass("input-group-text show-recent-notes-button bx bx-time")
.prop("title", "Show recent notes");
2024-11-23 20:51:51 +08:00
const $fullTextSearchButton = $("<button>")
.addClass("input-group-text full-text-search-button bx bx-search")
.prop("title", "Full text search (Shift+Enter)");
2024-11-23 20:51:51 +08:00
const $goToSelectedNoteButton = $("<button>")
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
2024-11-23 20:51:51 +08:00
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
2019-05-22 20:53:59 +02:00
if (!options.hideGoToSelectedNoteButton) {
$el.after($goToSelectedNoteButton);
2019-05-22 20:53:59 +02:00
}
2019-11-09 17:39:48 +01:00
$clearTextButton.on('click', () => clearText($el));
2019-05-22 20:53:59 +02:00
2019-11-09 17:39:48 +01:00
$showRecentNotesButton.on('click', e => {
2019-05-22 20:53:59 +02:00
showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediately and not show the results
2019-05-22 20:53:59 +02:00
return false;
});
2024-11-23 20:51:51 +08:00
$fullTextSearchButton.on('click', e => {
fullTextSearch($el, options);
});
let autocompleteOptions = {};
if (options.container) {
autocompleteOptions.dropdownMenuContainer = options.container;
autocompleteOptions.debug = true; // don't close on blur
}
if (options.allowJumpToSearchNotes) {
2024-11-20 14:22:39 +08:00
$el.on('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
// Prevent Ctrl + Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
$el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
}
});
}
2024-11-23 20:51:51 +08:00
$el.on('keydown', async (event) => {
if (event.shiftKey && event.key === 'Enter') {
// Prevent Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
fullTextSearch($el,options)
}
});
2019-05-22 20:53:59 +02:00
$el.autocomplete({
...autocompleteOptions,
appendTo: document.querySelector('body'),
2019-05-22 20:53:59 +02:00
hint: false,
autoselect: true,
2022-06-03 08:07:27 +02:00
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
2023-05-05 23:41:11 +02:00
// re-querying of the autocomplete source which then changes the currently selected suggestion
2022-06-03 08:07:27 +02:00
openOnFocus: false,
2019-05-22 20:53:59 +02:00
minLength: 0,
tabAutocomplete: false
}, [
{
source: (term, cb) => autocompleteSource(term, cb, options),
2020-05-16 22:11:09 +02:00
displayKey: 'notePathTitle',
2019-05-22 20:53:59 +02:00
templates: {
2020-09-21 00:07:46 +02:00
suggestion: suggestion => suggestion.highlightedNotePathTitle
2019-05-22 20:53:59 +02:00
},
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache: false
}
]);
2020-09-21 00:07:46 +02:00
$el.on('autocomplete:selected', async (event, suggestion) => {
if (suggestion.action === 'external-link') {
$el.setSelectedNotePath(null);
$el.setSelectedExternalLink(suggestion.externalLink);
$el.autocomplete("val", suggestion.externalLink);
$el.autocomplete("close");
$el.trigger('autocomplete:externallinkselected', [suggestion]);
return;
}
2020-09-21 22:08:54 +02:00
if (suggestion.action === 'create-note') {
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType();
if (!success) {
return;
}
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
2020-09-21 00:07:46 +02:00
title: suggestion.noteTitle,
activate: false,
type: noteType,
templateNoteId: templateNoteId
2020-09-21 00:07:46 +02:00
});
2023-04-15 00:06:13 +02:00
const hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId;
suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
2020-09-21 00:07:46 +02:00
}
2024-11-20 14:22:39 +08:00
if (suggestion.action === 'search-notes') {
const searchString = suggestion.noteTitle;
appContext.triggerCommand('searchNotes', { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);
$el.autocomplete("val", suggestion.noteTitle);
2020-09-21 00:07:46 +02:00
$el.autocomplete("close");
2020-09-21 22:08:54 +02:00
$el.trigger('autocomplete:noteselected', [suggestion]);
});
2019-05-22 20:53:59 +02:00
$el.on('autocomplete:closed', () => {
if (!$el.val().trim()) {
clearText($el);
}
});
$el.on('autocomplete:opened', () => {
if ($el.attr("readonly")) {
$el.autocomplete('close');
}
});
// clear any event listener added in previous invocation of this function
$el.off('autocomplete:noteselected');
return $el;
}
function init() {
2020-05-16 22:11:09 +02:00
$.fn.getSelectedNotePath = function () {
if (!$(this).val().trim()) {
return "";
} else {
2020-05-16 22:11:09 +02:00
return $(this).attr(SELECTED_NOTE_PATH_KEY);
}
};
2018-11-12 23:34:22 +01:00
2020-08-12 23:39:05 +02:00
$.fn.getSelectedNoteId = function () {
const notePath = $(this).getSelectedNotePath();
if (!notePath) {
return null;
}
2020-08-12 23:39:05 +02:00
const chunks = notePath.split('/');
return chunks.length >= 1 ? chunks[chunks.length - 1] : null;
}
2020-05-16 22:11:09 +02:00
$.fn.setSelectedNotePath = function (notePath) {
notePath = notePath || "";
2020-05-16 22:11:09 +02:00
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
2020-05-16 22:11:09 +02:00
.toggleClass("disabled", !notePath.trim())
.attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed
};
$.fn.getSelectedExternalLink = function () {
if (!$(this).val().trim()) {
return "";
} else {
return $(this).attr(SELECTED_EXTERNAL_LINK_KEY);
}
};
$.fn.setSelectedExternalLink = function (externalLink) {
if (externalLink) {
$(this)
.closest(".input-group")
.find(".go-to-selected-note-button")
.toggleClass("disabled", true);
}
}
2021-01-19 22:10:24 +01:00
$.fn.setNote = async function (noteId) {
2021-04-16 22:57:37 +02:00
const note = noteId ? await froca.getNote(noteId, true) : null;
2021-01-19 22:10:24 +01:00
$(this)
.val(note ? note.title : "")
.setSelectedNotePath(noteId);
}
}
export default {
2020-09-21 22:08:54 +02:00
autocompleteSourceForCKEditor,
initNoteAutocomplete,
showRecentNotes,
setText,
init
2020-05-16 22:11:09 +02:00
}