mirror of
https://github.com/chetscmd/Hover-Tooltips-Obsidian.git
synced 2026-07-22 06:43:03 +00:00
2.2.0 — global dictionaries
This commit is contained in:
parent
0a6d9e5d5f
commit
37a35a81ea
1 changed files with 558 additions and 32 deletions
590
main.js
590
main.js
|
|
@ -1,7 +1,145 @@
|
|||
const { Plugin, Modal } = require('obsidian');
|
||||
const { Plugin, Modal, PluginSettingTab, Setting, Notice, TFolder, TFile } = require('obsidian');
|
||||
const { EditorView, Decoration, ViewPlugin, WidgetType } = require('@codemirror/view');
|
||||
const { RangeSetBuilder } = require('@codemirror/state');
|
||||
|
||||
const SYNTAX_REGEX = /\{\/\{([^\/\n]+)\/([^}\n]+)\}\/\}/g;
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
dictionaries: [],
|
||||
};
|
||||
|
||||
function escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
function pathMatchesScope(dictionary, filePath) {
|
||||
if (!filePath) return false;
|
||||
if (!dictionary.path) return false;
|
||||
|
||||
if (dictionary.type === 'note') {
|
||||
const normalized = dictionary.path.endsWith('.md') ? dictionary.path : `${dictionary.path}.md`;
|
||||
return filePath === dictionary.path || filePath === normalized;
|
||||
}
|
||||
|
||||
if (dictionary.type === 'folder') {
|
||||
const folder = dictionary.path.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
if (folder === '' || folder === '/') return true;
|
||||
return filePath === folder || filePath.startsWith(folder + '/');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getAllFolderPaths(app) {
|
||||
const folders = [];
|
||||
|
||||
const recurse = (folder) => {
|
||||
if (folder.path && folder.path !== '/') folders.push(folder.path);
|
||||
folder.children.forEach(child => {
|
||||
if (child instanceof TFolder) recurse(child);
|
||||
});
|
||||
};
|
||||
|
||||
recurse(app.vault.getRoot());
|
||||
folders.unshift('/');
|
||||
return folders;
|
||||
}
|
||||
|
||||
class PathSuggest {
|
||||
constructor(inputEl, items, onSelect) {
|
||||
this.inputEl = inputEl;
|
||||
this.items = items;
|
||||
this.onSelect = onSelect;
|
||||
this.containerEl = null;
|
||||
|
||||
this.handleInput = () => this.render(this.getMatches());
|
||||
this.handleFocus = () => this.render(this.getMatches());
|
||||
this.handleBlur = () => {
|
||||
window.setTimeout(() => this.close(), 150);
|
||||
};
|
||||
this.handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') this.close();
|
||||
};
|
||||
|
||||
this.inputEl.addEventListener('input', this.handleInput);
|
||||
this.inputEl.addEventListener('focus', this.handleFocus);
|
||||
this.inputEl.addEventListener('blur', this.handleBlur);
|
||||
this.inputEl.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
getMatches() {
|
||||
const query = this.inputEl.value.toLowerCase();
|
||||
return this.items.filter(item => item.toLowerCase().includes(query)).slice(0, 50);
|
||||
}
|
||||
|
||||
render(matches) {
|
||||
this.close();
|
||||
if (matches.length === 0) return;
|
||||
|
||||
const rect = this.inputEl.getBoundingClientRect();
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tooltip-path-suggest';
|
||||
container.style.position = 'fixed';
|
||||
container.style.zIndex = '2147483647';
|
||||
container.style.left = `${rect.left}px`;
|
||||
container.style.top = `${rect.bottom + 2}px`;
|
||||
container.style.width = `${rect.width}px`;
|
||||
container.style.maxHeight = '220px';
|
||||
container.style.overflowY = 'auto';
|
||||
container.style.backgroundColor = 'var(--background-primary)';
|
||||
container.style.border = '1px solid var(--background-modifier-border)';
|
||||
container.style.borderRadius = '6px';
|
||||
container.style.boxShadow = '0 6px 18px rgba(0, 0, 0, 0.25)';
|
||||
|
||||
matches.forEach(item => {
|
||||
const itemEl = document.createElement('div');
|
||||
itemEl.textContent = item;
|
||||
itemEl.style.padding = '6px 10px';
|
||||
itemEl.style.cursor = 'pointer';
|
||||
itemEl.style.fontSize = '0.9em';
|
||||
|
||||
itemEl.addEventListener('mouseenter', () => {
|
||||
itemEl.style.backgroundColor = 'var(--background-modifier-hover)';
|
||||
});
|
||||
itemEl.addEventListener('mouseleave', () => {
|
||||
itemEl.style.backgroundColor = '';
|
||||
});
|
||||
|
||||
itemEl.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
this.inputEl.value = item;
|
||||
this.inputEl.dispatchEvent(new Event('input'));
|
||||
if (this.onSelect) this.onSelect(item);
|
||||
this.close();
|
||||
});
|
||||
|
||||
container.appendChild(itemEl);
|
||||
});
|
||||
|
||||
document.body.appendChild(container);
|
||||
this.containerEl = container;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.containerEl) {
|
||||
this.containerEl.remove();
|
||||
this.containerEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.close();
|
||||
this.inputEl.removeEventListener('input', this.handleInput);
|
||||
this.inputEl.removeEventListener('focus', this.handleFocus);
|
||||
this.inputEl.removeEventListener('blur', this.handleBlur);
|
||||
this.inputEl.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
}
|
||||
|
||||
class TooltipWidget extends WidgetType {
|
||||
constructor(plugin, word, tooltip) {
|
||||
super();
|
||||
|
|
@ -52,32 +190,88 @@ const tooltipViewPlugin = (plugin) => {
|
|||
|
||||
buildDecorations(view, plugin) {
|
||||
const builder = new RangeSetBuilder();
|
||||
const regex = /\{\/\{([^\/\n]+)\/([^}\n]+)\}\/\}/g;
|
||||
const cursorPos = view.state.selection.main.head;
|
||||
const text = view.state.doc.toString();
|
||||
|
||||
const ranges = [];
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
SYNTAX_REGEX.lastIndex = 0;
|
||||
while ((match = SYNTAX_REGEX.exec(text)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
|
||||
if (cursorPos < start || cursorPos > end) {
|
||||
const word = match[1].trim();
|
||||
let tooltip = match[2].trim().replace(/\}$/, "");
|
||||
|
||||
|
||||
const lines = tooltip.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
||||
tooltip = lines.join(' / ');
|
||||
|
||||
builder.add(start, end, Decoration.replace({
|
||||
widget: new TooltipWidget(plugin, word, tooltip),
|
||||
}));
|
||||
ranges.push({
|
||||
start,
|
||||
end,
|
||||
deco: Decoration.replace({ widget: new TooltipWidget(plugin, word, tooltip) }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const filePath = plugin.getCurrentFilePath(view);
|
||||
const dictMap = plugin.getDictionaryWordsForPath(filePath);
|
||||
|
||||
if (dictMap.size > 0) {
|
||||
const dictRegex = plugin.buildDictionaryRegex(dictMap);
|
||||
if (dictRegex) {
|
||||
dictRegex.lastIndex = 0;
|
||||
let dMatch;
|
||||
while ((dMatch = dictRegex.exec(text)) !== null) {
|
||||
const start = dMatch.index;
|
||||
const end = start + dMatch[0].length;
|
||||
|
||||
const overlaps = ranges.some(r => start < r.end && end > r.start);
|
||||
if (overlaps) continue;
|
||||
if (cursorPos >= start && cursorPos <= end) continue;
|
||||
|
||||
const matchedWord = dMatch[0];
|
||||
const tooltip = dictMap.get(matchedWord.toLowerCase());
|
||||
if (!tooltip) continue;
|
||||
|
||||
ranges.push({
|
||||
start,
|
||||
end,
|
||||
deco: Decoration.mark({
|
||||
class: 'tooltip-word tooltip-dict-word',
|
||||
attributes: { 'data-tooltip': tooltip },
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ranges.sort((a, b) => a.start - b.start);
|
||||
ranges.forEach(r => builder.add(r.start, r.end, r.deco));
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations }
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
eventHandlers: {
|
||||
mouseover(e, view) {
|
||||
const target = e.target;
|
||||
if (target && target.classList && target.classList.contains('tooltip-dict-word')) {
|
||||
const tooltip = target.getAttribute('data-tooltip');
|
||||
if (tooltip) plugin.showTooltip(target, tooltip);
|
||||
}
|
||||
},
|
||||
mouseout(e, view) {
|
||||
const target = e.target;
|
||||
if (target && target.classList && target.classList.contains('tooltip-dict-word')) {
|
||||
plugin.hideTooltip();
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -146,16 +340,16 @@ class TooltipPlugin extends Plugin {
|
|||
|
||||
processMarkdownLine(line) {
|
||||
line = this.escapeHtml(line);
|
||||
|
||||
|
||||
line = line.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
line = line.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>');
|
||||
line = line.replace(/\*([^\*]+)\*/g, '<em>$1</em>');
|
||||
line = line.replace(/==([^=]+)==/g, '<mark>$1</mark>');
|
||||
|
||||
|
||||
line = line.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, text, url) => {
|
||||
return `<a href="${url}" class="external-link" target="_blank" rel="noopener">${text}</a>`;
|
||||
});
|
||||
|
||||
|
||||
line = line.replace(/\[\[([^\]]+)\]\]/g, (match, link) => {
|
||||
const parts = link.split('|');
|
||||
const href = parts[0];
|
||||
|
|
@ -172,25 +366,157 @@ class TooltipPlugin extends Plugin {
|
|||
return div.innerHTML;
|
||||
}
|
||||
|
||||
onload() {
|
||||
async loadSettings() {
|
||||
const data = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
if (!Array.isArray(this.settings.dictionaries)) {
|
||||
this.settings.dictionaries = [];
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
getCurrentFilePath(view) {
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
return activeFile ? activeFile.path : null;
|
||||
}
|
||||
|
||||
getDictionaryWordsForPath(filePath) {
|
||||
const map = new Map();
|
||||
if (!filePath || !this.settings) return map;
|
||||
|
||||
for (const dictionary of this.settings.dictionaries) {
|
||||
if (!pathMatchesScope(dictionary, filePath)) continue;
|
||||
for (const entry of dictionary.words || []) {
|
||||
if (!entry.word) continue;
|
||||
map.set(entry.word.trim().toLowerCase(), entry.tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
buildDictionaryRegex(dictMap) {
|
||||
const words = Array.from(dictMap.keys()).filter(w => w.length > 0);
|
||||
if (words.length === 0) return null;
|
||||
|
||||
words.sort((a, b) => b.length - a.length);
|
||||
const pattern = words.map(escapeRegex).join('|');
|
||||
return new RegExp(`(?<![\\p{L}\\p{N}_])(${pattern})(?![\\p{L}\\p{N}_])`, 'giu');
|
||||
}
|
||||
|
||||
decorateDictionaryWordsInElement(el, filePath) {
|
||||
const dictMap = this.getDictionaryWordsForPath(filePath);
|
||||
if (dictMap.size === 0) return;
|
||||
|
||||
const regex = this.buildDictionaryRegex(dictMap);
|
||||
if (!regex) return;
|
||||
|
||||
const skipTags = new Set(['CODE', 'PRE', 'A', 'SCRIPT', 'STYLE']);
|
||||
|
||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: (node) => {
|
||||
let parent = node.parentElement;
|
||||
while (parent && parent !== el) {
|
||||
if (skipTags.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||
if (parent.classList && parent.classList.contains('tooltip-word')) return NodeFilter.FILTER_REJECT;
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
});
|
||||
|
||||
const textNodes = [];
|
||||
let current;
|
||||
while ((current = walker.nextNode())) {
|
||||
textNodes.push(current);
|
||||
}
|
||||
|
||||
textNodes.forEach(textNode => {
|
||||
const text = textNode.nodeValue;
|
||||
regex.lastIndex = 0;
|
||||
if (!regex.test(text)) return;
|
||||
regex.lastIndex = 0;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
|
||||
if (start > lastIndex) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(lastIndex, start)));
|
||||
}
|
||||
|
||||
const matchedWord = match[0];
|
||||
const tooltip = dictMap.get(matchedWord.toLowerCase());
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'tooltip-word tooltip-dict-word';
|
||||
span.textContent = matchedWord;
|
||||
span.setAttribute('data-tooltip', tooltip || '');
|
||||
|
||||
fragment.appendChild(span);
|
||||
lastIndex = end;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
|
||||
}
|
||||
|
||||
textNode.parentNode.replaceChild(fragment, textNode);
|
||||
});
|
||||
|
||||
el.querySelectorAll('.tooltip-dict-word').forEach(wordElement => {
|
||||
wordElement.addEventListener('mouseenter', (e) => {
|
||||
if (this.pinnedTooltip) return;
|
||||
this.showTooltip(e.target, e.target.getAttribute('data-tooltip'));
|
||||
});
|
||||
|
||||
wordElement.addEventListener('mouseleave', (e) => {
|
||||
if (this.pinnedTooltip) return;
|
||||
this.hideTooltip();
|
||||
});
|
||||
|
||||
wordElement.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const tooltipContent = e.target.getAttribute('data-tooltip');
|
||||
|
||||
if (this.pinnedTooltip === e.target) {
|
||||
this.pinnedTooltip = null;
|
||||
this.hideTooltip();
|
||||
} else {
|
||||
this.pinnedTooltip = e.target;
|
||||
this.showTooltip(e.target, tooltipContent);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async onload() {
|
||||
console.log("Tooltip Plugin loaded");
|
||||
|
||||
this.tooltipElement = null;
|
||||
this.pinnedTooltip = null;
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new TooltipSettingTab(this.app, this));
|
||||
|
||||
this.registerEditorExtension(tooltipViewPlugin(this));
|
||||
|
||||
this.registerMarkdownPostProcessor((el) => {
|
||||
const regex = /\{\/\{([^\/\n]+)\/([^}\n]+)\}\/\}/g;
|
||||
el.innerHTML = el.innerHTML.replace(regex, (_, word, tooltip) => {
|
||||
this.registerMarkdownPostProcessor((el, ctx) => {
|
||||
el.innerHTML = el.innerHTML.replace(SYNTAX_REGEX, (_, word, tooltip) => {
|
||||
tooltip = tooltip.trim().replace(/\}$/, "");
|
||||
const uniqueId = `tooltip-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
|
||||
const lines = tooltip.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
||||
const formattedTooltip = lines.join(' / ');
|
||||
|
||||
|
||||
const escapedWord = this.escapeHtml(word.trim());
|
||||
|
||||
|
||||
return `
|
||||
<span class="tooltip-word" data-tooltip="${formattedTooltip.replace(/"/g, '"')}" onclick="event.stopPropagation();">
|
||||
${escapedWord}
|
||||
|
|
@ -212,7 +538,7 @@ class TooltipPlugin extends Plugin {
|
|||
wordElement.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const tooltipContent = e.target.getAttribute('data-tooltip');
|
||||
|
||||
|
||||
if (this.pinnedTooltip === e.target) {
|
||||
this.pinnedTooltip = null;
|
||||
this.hideTooltip();
|
||||
|
|
@ -222,10 +548,12 @@ class TooltipPlugin extends Plugin {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.decorateDictionaryWordsInElement(el, ctx.sourcePath);
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, 'click', (e) => {
|
||||
if (this.pinnedTooltip && !this.pinnedTooltip.contains(e.target) &&
|
||||
if (this.pinnedTooltip && !this.pinnedTooltip.contains(e.target) &&
|
||||
(!this.tooltipElement || !this.tooltipElement.contains(e.target))) {
|
||||
this.pinnedTooltip = null;
|
||||
this.hideTooltip();
|
||||
|
|
@ -239,11 +567,11 @@ class TooltipPlugin extends Plugin {
|
|||
const selection = editor.getSelection();
|
||||
const word = selection || "word";
|
||||
const description = await this.promptForDescription(word);
|
||||
|
||||
|
||||
const lines = description.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
|
||||
|
||||
const formattedDescription = lines.join(' / ');
|
||||
const formatted = `{/{${word}/${formattedDescription}}/}`;
|
||||
editor.replaceSelection(formatted);
|
||||
|
|
@ -262,7 +590,7 @@ class TooltipPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const lines = content.split(' / ').map(line => line.trim());
|
||||
|
||||
|
||||
const processedLines = lines.map(line => {
|
||||
if (line.match(/<[^>]+>/)) {
|
||||
return line;
|
||||
|
|
@ -270,31 +598,31 @@ class TooltipPlugin extends Plugin {
|
|||
return this.processMarkdownLine(line);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const tooltipHTML = processedLines.join('<br>');
|
||||
const sanitizedHTML = this.sanitizeHtml(tooltipHTML);
|
||||
|
||||
|
||||
this.tooltipElement.innerHTML = sanitizedHTML;
|
||||
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
||||
|
||||
|
||||
let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
|
||||
let top = rect.top - tooltipRect.height - 5;
|
||||
|
||||
|
||||
if (left < 5) left = 5;
|
||||
if (left + tooltipRect.width > window.innerWidth - 5) {
|
||||
left = window.innerWidth - tooltipRect.width - 5;
|
||||
}
|
||||
|
||||
|
||||
if (top < 5) {
|
||||
top = rect.bottom + 5;
|
||||
}
|
||||
|
||||
|
||||
this.tooltipElement.style.left = left + 'px';
|
||||
this.tooltipElement.style.top = top + 'px';
|
||||
this.tooltipElement.classList.add('visible');
|
||||
|
||||
|
||||
const internalLinks = this.tooltipElement.querySelectorAll('a.internal-link');
|
||||
const app = this.app;
|
||||
internalLinks.forEach(link => {
|
||||
|
|
@ -410,4 +738,202 @@ class TooltipPromptModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
class DictionaryWordModal extends Modal {
|
||||
constructor(app, existingEntry, onSubmit) {
|
||||
super(app);
|
||||
this.existingEntry = existingEntry || { word: '', tooltip: '' };
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.existingEntry.word ? "Edit word" : "Add word" });
|
||||
|
||||
let wordValue = this.existingEntry.word;
|
||||
let tooltipValue = (this.existingEntry.tooltip || '').split(' / ').join('\n');
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Word or phrase")
|
||||
.addText(text => {
|
||||
text.setValue(wordValue);
|
||||
text.onChange(v => wordValue = v);
|
||||
text.inputEl.style.width = "100%";
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName("Tooltip")
|
||||
.addTextArea(textarea => {
|
||||
textarea.setValue(tooltipValue);
|
||||
textarea.onChange(v => tooltipValue = v);
|
||||
textarea.inputEl.style.width = "100%";
|
||||
textarea.inputEl.style.minHeight = "100px";
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton(btn => {
|
||||
btn.setButtonText("Save");
|
||||
btn.setCta();
|
||||
btn.onClick(() => {
|
||||
if (!wordValue.trim()) {
|
||||
new Notice("Enter a word or phrase");
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = tooltipValue.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
const formattedTooltip = lines.join(' / ');
|
||||
|
||||
this.onSubmit({ word: wordValue.trim(), tooltip: formattedTooltip });
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class TooltipSettingTab extends PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display() {
|
||||
this.display = this.display.bind(this);
|
||||
this.containerEl.empty();
|
||||
|
||||
const { containerEl } = this;
|
||||
containerEl.createEl("h2", { text: "Persistent Dictionaries" });
|
||||
containerEl.createEl("p", {
|
||||
text: "Dictionaries let you automatically apply tooltips to words in a specific note or across an entire folder of notes, without manually inserting {/{ }/} syntax.",
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Add a new dictionary")
|
||||
.addButton(btn => {
|
||||
btn.setButtonText("+ Dictionary");
|
||||
btn.setCta();
|
||||
btn.onClick(async () => {
|
||||
this.plugin.settings.dictionaries.push({
|
||||
id: generateId(),
|
||||
path: '',
|
||||
type: 'folder',
|
||||
words: [],
|
||||
});
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
this.plugin.settings.dictionaries.forEach((dictionary) => {
|
||||
this.renderDictionary(containerEl, dictionary);
|
||||
});
|
||||
}
|
||||
|
||||
renderDictionary(containerEl, dictionary) {
|
||||
const box = containerEl.createDiv({ cls: "tooltip-dict-box" });
|
||||
box.style.border = "1px solid var(--background-modifier-border)";
|
||||
box.style.borderRadius = "8px";
|
||||
box.style.padding = "12px";
|
||||
box.style.marginTop = "16px";
|
||||
|
||||
new Setting(box)
|
||||
.setName("Scope")
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('folder', 'Folder');
|
||||
dropdown.addOption('note', 'Specific note');
|
||||
dropdown.setValue(dictionary.type);
|
||||
dropdown.onChange(async (value) => {
|
||||
dictionary.type = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
})
|
||||
.addText(text => {
|
||||
text.setPlaceholder(dictionary.type === 'note' ? 'Folder/Note.md' : 'Folder/Subfolder');
|
||||
text.setValue(dictionary.path);
|
||||
text.onChange(async (value) => {
|
||||
dictionary.path = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
|
||||
const items = dictionary.type === 'note'
|
||||
? this.plugin.app.vault.getMarkdownFiles().map(f => f.path)
|
||||
: getAllFolderPaths(this.plugin.app);
|
||||
new PathSuggest(text.inputEl, items, async (value) => {
|
||||
dictionary.path = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
.addExtraButton(btn => {
|
||||
btn.setIcon("trash");
|
||||
btn.setTooltip("Delete dictionary");
|
||||
btn.onClick(async () => {
|
||||
this.plugin.settings.dictionaries = this.plugin.settings.dictionaries.filter(d => d.id !== dictionary.id);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
const wordsContainer = box.createDiv({ cls: "tooltip-dict-words" });
|
||||
wordsContainer.style.marginTop = "8px";
|
||||
|
||||
(dictionary.words || []).forEach((entry, index) => {
|
||||
const row = wordsContainer.createDiv();
|
||||
row.style.display = "flex";
|
||||
row.style.alignItems = "center";
|
||||
row.style.justifyContent = "space-between";
|
||||
row.style.padding = "6px 0";
|
||||
row.style.borderTop = "1px solid var(--background-modifier-border)";
|
||||
|
||||
const label = row.createDiv();
|
||||
label.style.flexGrow = "1";
|
||||
label.createEl("strong", { text: entry.word });
|
||||
label.createEl("div", { text: entry.tooltip, cls: "tooltip-dict-preview" });
|
||||
label.querySelector(".tooltip-dict-preview").style.color = "var(--text-muted)";
|
||||
label.querySelector(".tooltip-dict-preview").style.fontSize = "0.9em";
|
||||
|
||||
const actions = row.createDiv();
|
||||
const editBtn = actions.createEl("button", { text: "✎" });
|
||||
editBtn.style.marginRight = "6px";
|
||||
editBtn.onclick = () => {
|
||||
const modal = new DictionaryWordModal(this.app, entry, async (updated) => {
|
||||
dictionary.words[index] = updated;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const deleteBtn = actions.createEl("button", { text: "✕" });
|
||||
deleteBtn.onclick = async () => {
|
||||
dictionary.words.splice(index, 1);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
};
|
||||
});
|
||||
|
||||
const addWordSetting = new Setting(box)
|
||||
.addButton(btn => {
|
||||
btn.setButtonText("+ Word");
|
||||
btn.onClick(() => {
|
||||
const modal = new DictionaryWordModal(this.app, null, async (entry) => {
|
||||
if (!dictionary.words) dictionary.words = [];
|
||||
dictionary.words.push(entry);
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
addWordSetting.settingEl.style.borderTop = "none";
|
||||
addWordSetting.settingEl.style.marginTop = "8px";
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TooltipPlugin;
|
||||
|
|
|
|||
Loading…
Reference in a new issue