diff --git a/main.js b/main.js index e9f458f..181090a 100644 --- a/main.js +++ b/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, '$1'); + line = line.replace(/\*\*([^\*]+)\*\*/g, '$1'); + line = line.replace(/\*([^\*]+)\*/g, '$1'); + line = line.replace(/==([^=]+)==/g, '$1'); + + line = line.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, (match, text, url) => { + return `${text}`; + }); + + line = line.replace(/\[\[([^\]]+)\]\]/g, (match, link) => { + const parts = link.split('|'); + const href = parts[0]; + const text = parts[1] || href; + return `${text}`; + }); + + 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 `${word}${tooltip}`; + 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 ` + + ${word} + + `; }); + + 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('
'); + + 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; \ No newline at end of file +module.exports = TooltipPlugin;