Полная переработка плагина

—Добавлена возможность закреплять подсказку с помощью ЛКМ
—Изменена работа с Markdown
—Адаптивный дизайн
—Работа с API CodeMirror 6
—Работа с предварительным просмотром
—Изменена обработка переноса строк
This commit is contained in:
chetsCMD 2026-02-05 14:21:02 +03:00 committed by GitHub
parent e7dee1720c
commit 355822aa96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

236
main.js
View file

@ -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, '&quot;')}" 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;