mirror of
https://github.com/chetscmd/Hover-Tooltips-Obsidian.git
synced 2026-07-22 06:43:03 +00:00
Полная переработка плагина
—Добавлена возможность закреплять подсказку с помощью ЛКМ —Изменена работа с Markdown —Адаптивный дизайн —Работа с API CodeMirror 6 —Работа с предварительным просмотром —Изменена обработка переноса строк
This commit is contained in:
parent
e7dee1720c
commit
355822aa96
1 changed files with 232 additions and 4 deletions
236
main.js
236
main.js
|
|
@ -1,15 +1,169 @@
|
|||
const { Plugin, Modal } = require('obsidian');
|
||||
const { EditorView, Decoration, ViewPlugin, WidgetType } = require('@codemirror/view');
|
||||
const { RangeSetBuilder } = require('@codemirror/state');
|
||||
|
||||
class TooltipWidget extends WidgetType {
|
||||
constructor(plugin, word, tooltip) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.word = word;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'tooltip-word';
|
||||
span.textContent = this.word;
|
||||
span.setAttribute('data-tooltip', this.tooltip);
|
||||
|
||||
span.addEventListener('mouseenter', (e) => {
|
||||
this.plugin.showTooltip(span, this.tooltip);
|
||||
});
|
||||
|
||||
span.addEventListener('mouseleave', (e) => {
|
||||
this.plugin.hideTooltip();
|
||||
});
|
||||
|
||||
return span;
|
||||
}
|
||||
|
||||
eq(other) {
|
||||
return other.word === this.word && other.tooltip === this.tooltip;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipViewPlugin = (plugin) => {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
constructor(view) {
|
||||
this.view = view;
|
||||
this.decorations = this.buildDecorations(view, plugin);
|
||||
}
|
||||
|
||||
update(update) {
|
||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||
this.decorations = this.buildDecorations(update.view, 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();
|
||||
|
||||
let match;
|
||||
while ((match = 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),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{ decorations: (v) => v.decorations }
|
||||
);
|
||||
};
|
||||
|
||||
class TooltipPlugin extends Plugin {
|
||||
processMarkdownLine(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];
|
||||
const text = parts[1] || href;
|
||||
return `<a href="${href}" class="internal-link">${text}</a>`;
|
||||
});
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
onload() {
|
||||
console.log("Tooltip Plugin loaded");
|
||||
|
||||
this.tooltipElement = null;
|
||||
this.pinnedTooltip = null;
|
||||
|
||||
this.registerEditorExtension(tooltipViewPlugin(this));
|
||||
|
||||
this.registerMarkdownPostProcessor((el) => {
|
||||
const regex = /\{\/\{([^\/]+)\/([^}]+)\}\/\}/g;
|
||||
const regex = /\{\/\{([^\/\n]+)\/([^}\n]+)\}\/\}/g;
|
||||
el.innerHTML = el.innerHTML.replace(regex, (_, word, tooltip) => {
|
||||
tooltip = tooltip.trim().replace(/\}$/, "");
|
||||
return `<span class="tooltip-word">${word}<span class="tooltip-text">${tooltip}</span></span>`;
|
||||
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(' / ');
|
||||
|
||||
return `
|
||||
<span class="tooltip-word" data-tooltip="${formattedTooltip.replace(/"/g, '"')}" onclick="event.stopPropagation();">
|
||||
${word}
|
||||
</span>
|
||||
`;
|
||||
});
|
||||
|
||||
el.querySelectorAll('.tooltip-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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, 'click', (e) => {
|
||||
if (this.pinnedTooltip && !this.pinnedTooltip.contains(e.target) &&
|
||||
(!this.tooltipElement || !this.tooltipElement.contains(e.target))) {
|
||||
this.pinnedTooltip = null;
|
||||
this.hideTooltip();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
|
|
@ -19,12 +173,83 @@ class TooltipPlugin extends Plugin {
|
|||
const selection = editor.getSelection();
|
||||
const word = selection || "word";
|
||||
const description = await this.promptForDescription(word);
|
||||
const formatted = `{/{${word}/${description}}/}`;
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showTooltip(element, content) {
|
||||
if (!this.tooltipElement) {
|
||||
this.tooltipElement = document.createElement('div');
|
||||
this.tooltipElement.className = 'tooltip-text';
|
||||
this.tooltipElement.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.body.appendChild(this.tooltipElement);
|
||||
}
|
||||
|
||||
const lines = content.split(' / ').map(line => line.trim());
|
||||
|
||||
const processedLines = lines.map(line => {
|
||||
const processedLine = this.processMarkdownLine(line);
|
||||
return processedLine;
|
||||
});
|
||||
|
||||
const tooltipHTML = processedLines.join('<br>');
|
||||
|
||||
this.tooltipElement.innerHTML = tooltipHTML;
|
||||
|
||||
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 => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const linkText = link.getAttribute('href');
|
||||
app.workspace.openLinkText(linkText, '', false);
|
||||
});
|
||||
});
|
||||
|
||||
const externalLinks = this.tooltipElement.querySelectorAll('a.external-link');
|
||||
externalLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hideTooltip() {
|
||||
if (this.tooltipElement) {
|
||||
this.tooltipElement.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
async promptForDescription(word) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new TooltipPromptModal(this.app, word, resolve);
|
||||
|
|
@ -34,6 +259,9 @@ class TooltipPlugin extends Plugin {
|
|||
|
||||
onunload() {
|
||||
console.log("Tooltip Plugin unloaded");
|
||||
if (this.tooltipElement) {
|
||||
this.tooltipElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,4 +340,4 @@ class TooltipPromptModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = TooltipPlugin;
|
||||
module.exports = TooltipPlugin;
|
||||
|
|
|
|||
Loading…
Reference in a new issue