mirror of
https://github.com/cubexy/obsidian-pdf-printer.git
synced 2026-07-22 05:48:03 +00:00
improved folder structure, add various settings, improve logging & code performance & file size
This commit is contained in:
parent
ea1694a798
commit
3259a79552
6 changed files with 358 additions and 71 deletions
155
main.ts
155
main.ts
|
|
@ -1,4 +1,5 @@
|
|||
import { NodeCanvasFactory } from "canvasFactory";
|
||||
import { NodeCanvasFactory } from "utils/canvas/canvasFactory";
|
||||
import { FolderSuggest } from "utils/suggest/folderSuggest";
|
||||
import {
|
||||
App,
|
||||
Editor,
|
||||
|
|
@ -11,35 +12,32 @@ import {
|
|||
loadPdfJs,
|
||||
} from "obsidian";
|
||||
|
||||
interface PDFPrinterSettings {
|
||||
mySetting: string;
|
||||
interface PdfPrinterSettings {
|
||||
imageFolder: string;
|
||||
imageQuality: number;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: PDFPrinterSettings = {
|
||||
mySetting: "default",
|
||||
const DEFAULT_SETTINGS: PdfPrinterSettings = {
|
||||
imageFolder: "",
|
||||
imageQuality: 0.5,
|
||||
};
|
||||
|
||||
type PDFDocumentBuffer = {
|
||||
type PdfDocumentBuffer = {
|
||||
fileName: string;
|
||||
pages: PDFPage[];
|
||||
pages: PdfPage[];
|
||||
};
|
||||
|
||||
type PDFPage = {
|
||||
type PdfPage = {
|
||||
pageNumber: number;
|
||||
buffer: ArrayBuffer;
|
||||
};
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: PDFPrinterSettings;
|
||||
export default class PdfPrinterPlugin extends Plugin {
|
||||
settings: PdfPrinterSettings;
|
||||
|
||||
async onload() {
|
||||
console.log("loading plugin");
|
||||
await this.loadSettings();
|
||||
|
||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText("Status Bar Text");
|
||||
|
||||
this.addCommand({
|
||||
id: "convert-pdf-to-images",
|
||||
name: "Convert PDF to images",
|
||||
|
|
@ -49,27 +47,35 @@ export default class MyPlugin extends Plugin {
|
|||
if (!pdfFile) {
|
||||
return;
|
||||
}
|
||||
const fileBufferArray = await this.parsePDF(pdfFile);
|
||||
const imagePathList = await this.convertPDFBufferToImages({
|
||||
fileName: pdfFile.name,
|
||||
new Notice(`Printing document '${pdfFile.name}'...`);
|
||||
const fileBufferArray = await this.parsePdf(pdfFile);
|
||||
const imagePathList = await this.convertPdfBufferToImages({
|
||||
fileName: pdfFile.basename,
|
||||
pages: fileBufferArray,
|
||||
});
|
||||
|
||||
if (imagePathList.length === 0) {
|
||||
new Notice(
|
||||
"No images could be generated from the document."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.replaceSelection(
|
||||
imagePathList
|
||||
.map((imagePath) => `![[${imagePath}]]`)
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
new Notice(
|
||||
`Document '${pdfFile.name}' was printed successfully.`
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
||||
this.addSettingTab(new PdfPrinterSettingsTab(this.app, this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log("unloading pdf printer plugin");
|
||||
}
|
||||
onunload() {}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
|
|
@ -97,16 +103,6 @@ export default class MyPlugin extends Plugin {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the path of a file and returns the TFile object if it exists in the vault.
|
||||
* If the file is not found, it returns null.
|
||||
* @param path path to the file (either just the name or the full path)
|
||||
* @returns File object if it exists, null otherwise
|
||||
*/
|
||||
private fetchVaultFile(path: string): TFile | null {
|
||||
return this.app.metadataCache.getFirstLinkpathDest(path, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* attempts to parse a string as a file path
|
||||
* @param input the string to parse
|
||||
|
|
@ -125,7 +121,7 @@ export default class MyPlugin extends Plugin {
|
|||
);
|
||||
return null;
|
||||
}
|
||||
const file = this.fetchVaultFile(filePath);
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(filePath, "");
|
||||
if (!file) {
|
||||
new Notice(`File not found: ${filePath}.`);
|
||||
return null;
|
||||
|
|
@ -145,14 +141,14 @@ export default class MyPlugin extends Plugin {
|
|||
* @throws Error if the canvas or context cannot be created.
|
||||
* @returns An array of strings representing the paths of the saved PNG images.
|
||||
*/
|
||||
async parsePDF(file: TFile): Promise<PDFPage[]> {
|
||||
async parsePdf(file: TFile): Promise<PdfPage[]> {
|
||||
const pdfjs = await loadPdfJs();
|
||||
const arrayBuffer = await this.app.vault.adapter.readBinary(file.path);
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
const document = await pdfjs.getDocument(buffer).promise;
|
||||
|
||||
const pages: number = document.numPages;
|
||||
let pdfBuffer: PDFPage[] = [];
|
||||
let pdfBuffer: PdfPage[] = [];
|
||||
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
const page = await document.getPage(i);
|
||||
|
|
@ -164,23 +160,32 @@ export default class MyPlugin extends Plugin {
|
|||
false
|
||||
);
|
||||
if (!canvas || !context) {
|
||||
console.error("could not generate canvas or context");
|
||||
console.error(
|
||||
"pdf-printer: could not generate canvas or context"
|
||||
);
|
||||
return [];
|
||||
}
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
const blob = await new Promise<Blob | null>((resolve) => {
|
||||
/** @ts-ignore because toBlob exists, but is not found */
|
||||
canvas.toBlob(resolve, "image/png");
|
||||
canvas.toBlob(
|
||||
resolve,
|
||||
"image/webp",
|
||||
this.settings.imageQuality
|
||||
);
|
||||
});
|
||||
|
||||
if (blob) {
|
||||
pdfBuffer.push({
|
||||
pageNumber: i,
|
||||
buffer: await blob.arrayBuffer(), // Store base64 string (binary data)
|
||||
buffer: await blob.arrayBuffer(),
|
||||
});
|
||||
} else {
|
||||
console.error(`could not generate blob for page ${i}`);
|
||||
console.error(
|
||||
`pdf-printer: could not generate blob for page ${i}`
|
||||
);
|
||||
}
|
||||
canvasFactory.destroy({ canvas, context });
|
||||
}
|
||||
|
||||
return pdfBuffer;
|
||||
|
|
@ -192,42 +197,74 @@ export default class MyPlugin extends Plugin {
|
|||
* @param pageNumber The page number of the PDF.
|
||||
* @returns The path of the saved PNG file.
|
||||
*/
|
||||
private async convertPDFBufferToImages(
|
||||
pdfBuffer: PDFDocumentBuffer
|
||||
private async convertPdfBufferToImages(
|
||||
pdfBuffer: PdfDocumentBuffer
|
||||
): Promise<string[]> {
|
||||
let uuid = crypto.randomUUID();
|
||||
while (
|
||||
await this.app.vault.adapter.exists(
|
||||
`${this.settings.imageFolder}/${uuid}`
|
||||
)
|
||||
) {
|
||||
// if folder already exists, generate a new uuid
|
||||
uuid = crypto.randomUUID();
|
||||
}
|
||||
await this.app.vault.createFolder(
|
||||
`${this.settings.imageFolder}/${uuid}`
|
||||
);
|
||||
const writeTasks = pdfBuffer.pages.map(async (page) => {
|
||||
const name = `${pdfBuffer.fileName}-${page.pageNumber}.png`;
|
||||
await this.app.vault.createBinary(name, page.buffer);
|
||||
return name;
|
||||
const fileName = `${pdfBuffer.fileName}-${page.pageNumber}.webp`;
|
||||
const fullPath = `${this.settings.imageFolder}/${uuid}/${fileName}`;
|
||||
await this.app.vault.createBinary(fullPath, page.buffer);
|
||||
return fileName;
|
||||
});
|
||||
return Promise.all(writeTasks);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
class PdfPrinterSettingsTab extends PluginSettingTab {
|
||||
plugin: PdfPrinterPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
constructor(app: App, plugin: PdfPrinterPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Setting #1")
|
||||
.setDesc("It's a secret")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("Enter your secret")
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.setName("Image quality")
|
||||
.setDesc(
|
||||
"Quality of the printed images. Higher values result in better quality but larger file sizes."
|
||||
)
|
||||
.addSlider((cb) => {
|
||||
cb.setLimits(0, 1, 0.01)
|
||||
.setValue(this.plugin.settings.imageQuality)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
this.plugin.settings.imageQuality = value;
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName("Printed image folder path")
|
||||
.setDesc("Place printed images from PDFs in this folder.")
|
||||
.addSearch((cb) => {
|
||||
new FolderSuggest(this.app, cb.inputEl);
|
||||
cb.setPlaceholder("Example: folder1/folder2")
|
||||
.setValue(this.plugin.settings.imageFolder)
|
||||
.onChange(async (new_folder) => {
|
||||
new_folder = new_folder.trim();
|
||||
new_folder = new_folder.replace(/\/$/, "");
|
||||
|
||||
this.plugin.settings.imageFolder = new_folder;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
/** @ts-ignore */
|
||||
cb.containerEl.addClass("templater_search");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
package-lock.json
generated
32
package-lock.json
generated
|
|
@ -1,13 +1,17 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"name": "obsidian-pdf-printer",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"name": "obsidian-pdf-printer",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"canvas": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
|
|
@ -184,6 +188,15 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"dev": true,
|
||||
|
|
@ -688,9 +701,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -1594,11 +1607,6 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf2img-electron": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/pdf2img-electron/-/pdf2img-electron-1.2.3.tgz",
|
||||
"integrity": "sha512-KXgIjhrRH3ItOIYwF1zBMx8C1xcJ3tCF9RKChm/8vq7SGZH1nZL6jR0E/sz0qMvneXs65fK6jK3HU3UikKD0SQ=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -20,5 +20,9 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"canvas": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
utils/suggest/folderSuggest.ts
Normal file
37
utils/suggest/folderSuggest.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||
|
||||
import { App, TAbstractFile, TFolder } from "obsidian";
|
||||
import { TextInputSuggest } from "utils/suggest/suggest";
|
||||
|
||||
export class FolderSuggest extends TextInputSuggest<TFolder> {
|
||||
constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) {
|
||||
super(app, inputEl);
|
||||
}
|
||||
|
||||
getSuggestions(inputStr: string): TFolder[] {
|
||||
const abstractFiles = this.app.vault.getAllLoadedFiles();
|
||||
const folders: TFolder[] = [];
|
||||
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||
|
||||
abstractFiles.forEach((folder: TAbstractFile) => {
|
||||
if (
|
||||
folder instanceof TFolder &&
|
||||
folder.path.toLowerCase().contains(lowerCaseInputStr)
|
||||
) {
|
||||
folders.push(folder);
|
||||
}
|
||||
});
|
||||
|
||||
return folders.slice(0, 1000);
|
||||
}
|
||||
|
||||
renderSuggestion(file: TFolder, el: HTMLElement): void {
|
||||
el.setText(file.path);
|
||||
}
|
||||
|
||||
selectSuggestion(file: TFolder): void {
|
||||
this.inputEl.value = file.path;
|
||||
this.inputEl.trigger("input");
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
201
utils/suggest/suggest.ts
Normal file
201
utils/suggest/suggest.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||
|
||||
import { App, ISuggestOwner, Scope } from "obsidian";
|
||||
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
|
||||
|
||||
const wrapAround = (value: number, size: number): number => {
|
||||
return ((value % size) + size) % size;
|
||||
};
|
||||
|
||||
class Suggest<T> {
|
||||
private owner: ISuggestOwner<T>;
|
||||
private values: T[];
|
||||
private suggestions: HTMLDivElement[];
|
||||
private selectedItem: number;
|
||||
private containerEl: HTMLElement;
|
||||
|
||||
constructor(
|
||||
owner: ISuggestOwner<T>,
|
||||
containerEl: HTMLElement,
|
||||
scope: Scope
|
||||
) {
|
||||
this.owner = owner;
|
||||
this.containerEl = containerEl;
|
||||
|
||||
containerEl.on(
|
||||
"click",
|
||||
".suggestion-item",
|
||||
this.onSuggestionClick.bind(this)
|
||||
);
|
||||
containerEl.on(
|
||||
"mousemove",
|
||||
".suggestion-item",
|
||||
this.onSuggestionMouseover.bind(this)
|
||||
);
|
||||
|
||||
scope.register([], "ArrowUp", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.setSelectedItem(this.selectedItem - 1, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
scope.register([], "ArrowDown", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.setSelectedItem(this.selectedItem + 1, true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
scope.register([], "Enter", (event) => {
|
||||
if (!event.isComposing) {
|
||||
this.useSelectedItem(event);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
|
||||
event.preventDefault();
|
||||
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
this.useSelectedItem(event);
|
||||
}
|
||||
|
||||
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
|
||||
const item = this.suggestions.indexOf(el);
|
||||
this.setSelectedItem(item, false);
|
||||
}
|
||||
|
||||
setSuggestions(values: T[]) {
|
||||
this.containerEl.empty();
|
||||
const suggestionEls: HTMLDivElement[] = [];
|
||||
|
||||
values.forEach((value) => {
|
||||
const suggestionEl = this.containerEl.createDiv("suggestion-item");
|
||||
this.owner.renderSuggestion(value, suggestionEl);
|
||||
suggestionEls.push(suggestionEl);
|
||||
});
|
||||
|
||||
this.values = values;
|
||||
this.suggestions = suggestionEls;
|
||||
this.setSelectedItem(0, false);
|
||||
}
|
||||
|
||||
useSelectedItem(event: MouseEvent | KeyboardEvent) {
|
||||
const currentValue = this.values[this.selectedItem];
|
||||
if (currentValue) {
|
||||
this.owner.selectSuggestion(currentValue, event);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
|
||||
const normalizedIndex = wrapAround(
|
||||
selectedIndex,
|
||||
this.suggestions.length
|
||||
);
|
||||
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
|
||||
const selectedSuggestion = this.suggestions[normalizedIndex];
|
||||
|
||||
prevSelectedSuggestion?.removeClass("is-selected");
|
||||
selectedSuggestion?.addClass("is-selected");
|
||||
|
||||
this.selectedItem = normalizedIndex;
|
||||
|
||||
if (scrollIntoView) {
|
||||
selectedSuggestion.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
|
||||
protected app: App;
|
||||
protected inputEl: HTMLInputElement | HTMLTextAreaElement;
|
||||
|
||||
private popper: PopperInstance;
|
||||
private scope: Scope;
|
||||
private suggestEl: HTMLElement;
|
||||
private suggest: Suggest<T>;
|
||||
|
||||
constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) {
|
||||
this.app = app;
|
||||
this.inputEl = inputEl;
|
||||
this.scope = new Scope();
|
||||
|
||||
this.suggestEl = createDiv("suggestion-container");
|
||||
const suggestion = this.suggestEl.createDiv("suggestion");
|
||||
this.suggest = new Suggest(this, suggestion, this.scope);
|
||||
|
||||
this.scope.register([], "Escape", this.close.bind(this));
|
||||
|
||||
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
|
||||
this.inputEl.addEventListener("focus", this.onInputChanged.bind(this));
|
||||
this.inputEl.addEventListener("blur", this.close.bind(this));
|
||||
this.suggestEl.on(
|
||||
"mousedown",
|
||||
".suggestion-container",
|
||||
(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onInputChanged(): void {
|
||||
const inputStr = this.inputEl.value;
|
||||
const suggestions = this.getSuggestions(inputStr);
|
||||
|
||||
if (!suggestions) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
this.suggest.setSuggestions(suggestions);
|
||||
this.open(this.app.workspace.containerEl, this.inputEl);
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
open(container: HTMLElement, inputEl: HTMLElement): void {
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
container.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
{
|
||||
name: "sameWidth",
|
||||
enabled: true,
|
||||
fn: ({ state, instance }) => {
|
||||
// Note: positioning needs to be calculated twice -
|
||||
// first pass - positioning it according to the width of the popper
|
||||
// second pass - position it with the width bound to the reference element
|
||||
// we need to early exit to avoid an infinite loop
|
||||
const targetWidth = `${state.rects.reference.width}px`;
|
||||
if (state.styles.popper.width === targetWidth) {
|
||||
return;
|
||||
}
|
||||
state.styles.popper.width = targetWidth;
|
||||
instance.update();
|
||||
},
|
||||
phase: "beforeWrite",
|
||||
requires: ["computeStyles"],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.app.keymap.popScope(this.scope);
|
||||
|
||||
this.suggest.setSuggestions([]);
|
||||
if (this.popper) this.popper.destroy();
|
||||
this.suggestEl.detach();
|
||||
}
|
||||
|
||||
abstract getSuggestions(inputStr: string): T[];
|
||||
abstract renderSuggestion(item: T, el: HTMLElement): void;
|
||||
abstract selectSuggestion(item: T): void;
|
||||
}
|
||||
Loading…
Reference in a new issue