refactor link tooltip

pull/601/head
Pete Matsyburka 4 weeks ago
parent 262118e047
commit 8c5298ed2e

@ -36,25 +36,28 @@ function loadTiptap () {
} }
class LinkTooltip { class LinkTooltip {
constructor (tooltip, input, saveButton, removeButton, editor) { constructor (container, editor) {
this.tooltip = tooltip this.container = container
this.input = input
this.saveButton = saveButton
this.removeButton = removeButton
this.editor = editor this.editor = editor
const template = document.createElement('template')
template.innerHTML = container.dataset.linkTooltipHtml
this.tooltip = template.content.firstElementChild
this.input = this.tooltip.querySelector('input')
this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')
container.style.position = 'relative'
container.appendChild(this.tooltip)
} }
isVisible () { isVisible () {
return !this.tooltip.classList.contains('hidden') return !this.tooltip.classList.contains('hidden')
} }
updatePosition () {
const rect = this.editor.view.coordsAtPos(this.pos)
this.tooltip.style.left = `${rect.left}px`
this.tooltip.style.top = `${rect.bottom + 6}px`
}
normalizeUrl (url) { normalizeUrl (url) {
if (!url) return url if (!url) return url
if (/^{/i.test(url)) return url if (/^{/i.test(url)) return url
@ -64,14 +67,19 @@ class LinkTooltip {
return `https://${url}` return `https://${url}`
} }
show (url, pos) { show (url, pos, { focus = false } = {}) {
this.input.value = url || '' this.input.value = url || ''
this.removeButton.classList.toggle('hidden', !url) this.removeButton.classList.toggle('hidden', !url)
this.pos = pos
this.tooltip.classList.remove('hidden') this.tooltip.classList.remove('hidden')
this.updatePosition() const coords = this.editor.view.coordsAtPos(pos)
const containerRect = this.container.getBoundingClientRect()
this.tooltip.style.left = `${coords.left - containerRect.left}px`
this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`
if (focus) this.input.focus()
this.saveHandler = () => { this.saveHandler = () => {
const inputUrl = this.input.value.trim() const inputUrl = this.input.value.trim()
@ -102,17 +110,9 @@ class LinkTooltip {
this.saveButton.addEventListener('click', this.saveHandler, { once: true }) this.saveButton.addEventListener('click', this.saveHandler, { once: true })
this.removeButton.addEventListener('click', this.removeHandler, { once: true }) this.removeButton.addEventListener('click', this.removeHandler, { once: true })
this.input.addEventListener('keydown', this.keyHandler) this.input.addEventListener('keydown', this.keyHandler)
this.scrollHandler = () => this.updatePosition()
window.addEventListener('scroll', this.scrollHandler, true)
} }
hide () { hide () {
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler, true)
this.scrollHandler = null
}
if (this.saveHandler) { if (this.saveHandler) {
this.saveButton.removeEventListener('click', this.saveHandler) this.saveButton.removeEventListener('click', this.saveHandler)
this.saveHandler = null this.saveHandler = null
@ -128,7 +128,7 @@ class LinkTooltip {
this.keyHandler = null this.keyHandler = null
} }
this.tooltip?.classList.add('hidden') this.tooltip.classList.add('hidden')
this.currentMark = null this.currentMark = null
} }
} }
@ -140,13 +140,7 @@ export default actionable(targetable(class extends HTMLElement {
'boldButton', 'boldButton',
'italicButton', 'italicButton',
'underlineButton', 'underlineButton',
'linkButton', 'linkButton'
'undoButton',
'redoButton',
'linkTooltipElement',
'linkInput',
'linkSaveButton',
'linkRemoveButton'
] ]
async connectedCallback () { async connectedCallback () {
@ -255,22 +249,14 @@ export default actionable(targetable(class extends HTMLElement {
}, },
onBlur: () => { onBlur: () => {
setTimeout(() => { setTimeout(() => {
if (!this.linkTooltipElement.contains(document.activeElement)) { if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
this.linkTooltip.hide() this.linkTooltip.hide()
} }
}, 0) }, 0)
} }
}) })
this.linkTooltip = new LinkTooltip( this.linkTooltip = new LinkTooltip(this, this.editor)
this.linkTooltipElement,
this.linkInput,
this.linkSaveButton,
this.linkRemoveButton,
this.editor
)
this.setupToolbar()
} }
adjustShortcutsForPlatform () { adjustShortcutsForPlatform () {
@ -285,46 +271,53 @@ export default actionable(targetable(class extends HTMLElement {
} }
} }
setupToolbar () { bold (e) {
this.boldButton?.addEventListener('click', (e) => { e.preventDefault()
e.preventDefault()
this.editor.chain().focus().toggleBold().run()
this.updateToolbarState()
})
this.italicButton?.addEventListener('click', (e) => { this.editor.chain().focus().toggleBold().run()
e.preventDefault() this.updateToolbarState()
this.editor.chain().focus().toggleItalic().run() }
this.updateToolbarState()
})
this.underlineButton?.addEventListener('click', (e) => { italic (e) {
e.preventDefault() e.preventDefault()
this.editor.chain().focus().toggleUnderline().run()
this.updateToolbarState()
})
this.linkButton?.addEventListener('click', (e) => { this.editor.chain().focus().toggleItalic().run()
e.preventDefault() this.updateToolbarState()
this.toggleLink() }
})
this.undoButton?.addEventListener('click', (e) => { underline (e) {
e.preventDefault() e.preventDefault()
this.editor.chain().focus().undo().run()
})
this.redoButton?.addEventListener('click', (e) => { this.editor.chain().focus().toggleUnderline().run()
e.preventDefault() this.updateToolbarState()
this.editor.chain().focus().redo().run() }
})
linkSelection (e) {
e.preventDefault()
this.toggleLink()
this.updateToolbarState()
}
undo (e) {
e.preventDefault()
this.editor.chain().focus().undo().run()
this.updateToolbarState()
}
redo (e) {
e.preventDefault()
this.editor.chain().focus().redo().run()
this.updateToolbarState()
} }
updateToolbarState () { updateToolbarState () {
this.boldButton?.classList.toggle('bg-base-200', this.editor.isActive('bold')) this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
this.italicButton?.classList.toggle('bg-base-200', this.editor.isActive('italic')) this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
this.underlineButton?.classList.toggle('bg-base-200', this.editor.isActive('underline')) this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
this.linkButton?.classList.toggle('bg-base-200', this.editor.isActive('link')) this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
} }
handleLinkTooltip (editor) { handleLinkTooltip (editor) {
@ -337,6 +330,8 @@ export default actionable(targetable(class extends HTMLElement {
return return
} }
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
let linkStart = from let linkStart = from
const start = editor.state.doc.resolve(from).start() const start = editor.state.doc.resolve(from).start()
@ -348,12 +343,8 @@ export default actionable(targetable(class extends HTMLElement {
} }
} }
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
this.linkTooltip.hide() this.linkTooltip.hide()
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart) this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
this.linkTooltip.currentMark = mark this.linkTooltip.currentMark = mark
} }
@ -366,7 +357,7 @@ export default actionable(targetable(class extends HTMLElement {
const { from } = this.editor.state.selection const { from } = this.editor.state.selection
this.linkTooltip.hide() this.linkTooltip.hide()
this.linkTooltip.show(this.editor.getAttributes('link').href, from) this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
} }
} }
@ -389,7 +380,6 @@ export default actionable(targetable(class extends HTMLElement {
if (this.editor) { if (this.editor) {
this.editor.destroy() this.editor.destroy()
this.editor = null
} }
} }
})) }))

@ -1,37 +1,48 @@
<markdown-editor> <% link_tooltip_html = capture do %>
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
<% end %>
<markdown-editor data-link-tooltip-html="<%= link_tooltip_html.squish %>">
<div class="border border-base-content/20 rounded-2xl bg-white"> <div class="border border-base-content/20 rounded-2xl bg-white">
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;"> <div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="tooltip tooltip-top" data-tip="<%= t('bold') %> (Ctrl+B)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
<button type="button" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('bold', class: 'w-4 h-4') %> <%= svg_icon('bold', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top" data-tip="<%= t('italic') %> (Ctrl+I)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
<button type="button" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('italic', class: 'w-4 h-4') %> <%= svg_icon('italic', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top" data-tip="<%= t('underline') %> (Ctrl+U)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
<button type="button" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('underline', class: 'w-4 h-4') %> <%= svg_icon('underline', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top" data-tip="<%= t('link') %> (Ctrl+K)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
<button type="button" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('link', class: 'w-4 h-4') %> <%= svg_icon('link', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
</div> </div>
<div class="mx-2 h-5 border-l border-base-content/20"></div> <div class="mx-2 h-5 border-l border-base-content/20"></div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="tooltip tooltip-top" data-tip="<%= t('undo') %> (Ctrl+Z)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
<button type="button" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %> <%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
<div class="tooltip tooltip-top" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)"> <div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
<button type="button" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300"> <button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %> <%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
</button> </button>
</div> </div>
@ -55,14 +66,5 @@
</div> </div>
<div data-target="markdown-editor.editorElement"></div> <div data-target="markdown-editor.editorElement"></div>
</div> </div>
<div data-target="markdown-editor.linkTooltipElement" class="hidden fixed flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50">
<input data-target="markdown-editor.linkInput" type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
<button type="button" data-target="markdown-editor.linkSaveButton" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
</button>
<button type="button" data-target="markdown-editor.linkRemoveButton" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
</button>
</div>
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %> <%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
</markdown-editor> </markdown-editor>

Loading…
Cancel
Save