obsidian#PopoverSuggest TypeScript Examples
The following examples show how to use
obsidian#PopoverSuggest.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: query.ts From obsidian-map-view with GNU General Public License v3.0 | 5 votes |
export class QuerySuggest extends PopoverSuggest<Suggestion> {
suggestionsDiv: HTMLDivElement;
app: App;
sourceElement: TextComponent;
selection: Suggestion = null;
lastSuggestions: Suggestion[];
// Event handers that were registered, in the format of [name, lambda]
eventHandlers: [string, any][] = [];
constructor(app: App, sourceElement: TextComponent, scope?: Scope) {
super(app, scope);
this.app = app;
this.sourceElement = sourceElement;
}
open() {
this.suggestionsDiv = this.app.workspace.containerEl.createDiv({
cls: 'suggestion-container mod-search-suggestion',
});
this.suggestionsDiv.style.position = 'fixed';
this.suggestionsDiv.style.top =
this.sourceElement.inputEl.getClientRects()[0].bottom + 'px';
this.suggestionsDiv.style.left =
this.sourceElement.inputEl.getClientRects()[0].left + 'px';
const keyUp = async () => {
// We do this in keyup because we want the selection to update first
this.doSuggestIfNeeded();
};
const mouseUp = async () => {
// We do this in keyup because we want the selection to update first
this.doSuggestIfNeeded();
};
const keyDown = async (ev: KeyboardEvent) => {
if (ev.key == 'Enter' && this.selection) {
this.selectSuggestion(this.selection, ev);
this.doSuggestIfNeeded();
} else if (ev.key == 'ArrowDown' || ev.key == 'ArrowUp') {
if (this.lastSuggestions.length == 0) return;
let index = this.lastSuggestions.findIndex(
(value) => value == this.selection
);
const direction = ev.key == 'ArrowDown' ? 1 : -1;
do {
index += direction;
if (index >= this.lastSuggestions.length) index = 0;
if (index < 0) index = this.lastSuggestions.length - 1;
} while (this.lastSuggestions[index].group);
this.updateSelection(this.lastSuggestions[index]);
ev.preventDefault();
}
};
this.eventHandlers.push(
['keyup', keyUp],
['mouseup', mouseUp],
['keydown', keyDown]
);
this.sourceElement.inputEl.addEventListener('keyup', keyUp);
this.sourceElement.inputEl.addEventListener('mouseup', mouseUp);
this.sourceElement.inputEl.addEventListener('keydown', keyDown);
this.doSuggestIfNeeded();
}
doSuggestIfNeeded() {
const suggestions = this.createSuggestions();
suggestions.splice(consts.MAX_QUERY_SUGGESTIONS);
if (!this.compareSuggestions(suggestions, this.lastSuggestions)) {
this.clear();
this.lastSuggestions = suggestions;
this.renderSuggestions(suggestions, this.suggestionsDiv);
}
}
compareSuggestions(suggestions1: Suggestion[], suggestions2: Suggestion[]) {
if (!suggestions1 && !suggestions2) return true;
if (!suggestions1 || !suggestions2) return false;
if (suggestions1.length != suggestions2.length) return false;
for (const [i, s1] of suggestions1.entries()) {
const s2 = suggestions2[i];
if (
s1.text != s2.text ||
s1.textToInsert != s2.textToInsert ||
s1.append != s2.append ||
s1.insertAt != s2.insertAt ||
s1.insertSkip != s2.insertSkip
)
return false;
}
return true;
}
clear() {
while (this.suggestionsDiv.firstChild)
this.suggestionsDiv.removeChild(this.suggestionsDiv.firstChild);
this.selection = null;
this.lastSuggestions = [];
}
createSuggestions(): Suggestion[] {
const cursorPos = this.sourceElement.inputEl.selectionStart;
const input = this.sourceElement.getValue();
const tagMatch = getTagUnderCursor(input, cursorPos);
// Doesn't include a closing parenthesis
const pathMatch = matchByPosition(
input,
/path:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
cursorPos
);
const linkedToMatch = matchByPosition(
input,
/linkedto:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
cursorPos
);
const linkedFromMatch = matchByPosition(
input,
/linkedfrom:((\"([\w\s\/\-\\\.]*)\")|([\w\/\-\\\.]*))/g,
cursorPos
);
if (tagMatch) {
const tagQuery = tagMatch[1] ?? '';
// Return a tag name with the pound (#) sign removed if any
const noPound = (tagName: string) => {
return tagName.startsWith('#') ? tagName.substring(1) : tagName;
};
// Find all tags that include the query, with the pound sign removed, case insensitive
const allTagNames = utils
.getAllTagNames(this.app)
.filter((value) =>
value
.toLowerCase()
.includes(noPound(tagQuery).toLowerCase())
);
let toReturn: Suggestion[] = [{ text: 'TAGS', group: true }];
for (const tagName of allTagNames) {
toReturn.push({
text: tagName,
textToInsert: `tag:${tagName} `,
insertAt: tagMatch.index,
insertSkip: tagMatch[0].length,
});
}
return toReturn;
} else if (pathMatch)
return this.createPathSuggestions(pathMatch, 'path');
else if (linkedToMatch)
return this.createPathSuggestions(linkedToMatch, 'linkedto');
else if (linkedFromMatch)
return this.createPathSuggestions(linkedFromMatch, 'linkedfrom');
else {
return [
{ text: 'SEARCH OPERATORS', group: true },
{ text: 'tag:', append: '#' },
{ text: 'path:', textToInsert: 'path:""', cursorOffset: -1 },
{
text: 'linkedto:',
textToInsert: 'linkedto:""',
cursorOffset: -1,
},
{
text: 'linkedfrom:',
textToInsert: 'linkedfrom:""',
cursorOffset: -1,
},
{ text: 'LOGICAL OPERATORS', group: true },
{ text: 'AND', append: ' ' },
{ text: 'OR', append: ' ' },
{ text: 'NOT', append: ' ' },
];
}
}
createPathSuggestions(
pathMatch: RegExpMatchArray,
operator: string
): Suggestion[] {
const pathQuery = pathMatch[3] ?? pathMatch[4];
const allPathNames = this.getAllPathNames(pathQuery);
let toReturn: Suggestion[] = [{ text: 'PATHS', group: true }];
for (const pathName of allPathNames) {
toReturn.push({
text: pathName,
textToInsert: `${operator}:"${pathName}" `,
insertAt: pathMatch.index,
insertSkip: pathMatch[0].length,
});
}
return toReturn;
}
renderSuggestions(suggestions: Suggestion[], el: HTMLElement) {
for (const suggestion of suggestions) {
const element = el.createDiv({
cls: 'suggestion-item search-suggest-item',
});
if (suggestion.group) element.addClass('mod-group');
suggestion.element = element;
if (this.selection == suggestion) {
element.addClass('is-selected');
this.selection = suggestion;
}
element.addEventListener('mousedown', (event) => {
this.selectSuggestion(suggestion, event);
});
element.addEventListener('mouseover', () => {
this.updateSelection(suggestion);
});
this.renderSuggestion(suggestion, element);
}
}
close() {
this.suggestionsDiv.remove();
this.clear();
for (const [eventName, handler] of this.eventHandlers)
this.sourceElement.inputEl.removeEventListener(eventName, handler);
}
updateSelection(newSelection: Suggestion) {
if (this.selection && this.selection.element) {
this.selection.element.removeClass('is-selected');
}
if (!newSelection.group) {
newSelection.element?.addClass('is-selected');
this.selection = newSelection;
}
}
renderSuggestion(value: Suggestion, el: HTMLElement) {
el.setText(value.text);
}
selectSuggestion(
suggestion: Suggestion,
event: MouseEvent | KeyboardEvent
) {
// We don't use it, but need it here to inherit from QuerySuggest
if (!suggestion.group) {
const insertAt =
suggestion.insertAt != null
? suggestion.insertAt
: this.sourceElement.inputEl.selectionStart;
const insertSkip = suggestion.insertSkip ?? 0;
let addedText = suggestion.textToInsert ?? suggestion.text;
addedText += suggestion.append ?? '';
const currentText = this.sourceElement.getValue();
const newText =
currentText.substring(0, insertAt) +
addedText +
currentText.substring(insertAt + insertSkip);
this.sourceElement.setValue(newText);
this.sourceElement.inputEl.selectionEnd =
this.sourceElement.inputEl.selectionStart =
insertAt +
addedText.length +
(suggestion?.cursorOffset ?? 0);
// Don't allow a click to steal the focus from the text box
event.preventDefault();
// This causes the text area to scroll to the new cursor position
this.sourceElement.inputEl.blur();
this.sourceElement.inputEl.focus();
// Refresh the suggestion box
this.doSuggestIfNeeded();
// Make the UI react to the change
this.sourceElement.onChanged();
}
}
getAllPathNames(search: string): string[] {
const allFiles = this.app.vault.getFiles();
let toReturn: string[] = [];
for (const file of allFiles) {
if (
!search ||
(search &&
file.path.toLowerCase().includes(search.toLowerCase()))
)
toReturn.push(file.path);
}
return toReturn;
}
}