adjust editor

pull/601/head
Pete Matsyburka 4 weeks ago
parent 39cc82ce0c
commit e9c8e4d325

@ -155,3 +155,7 @@ button[disabled] .enabled, button.btn-disabled .enabled {
.font-courier {
font-family: "Courier New", Consolas, "Liberation Mono", monospace, ui-monospace, SFMono-Regular, Menlo, Monaco;
}
markdown-editor [contenteditable] p {
margin-bottom: 18px;
}

@ -1,4 +1,5 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'
function loadTiptap () {
return Promise.all([
@ -35,78 +36,63 @@ function loadTiptap () {
}
class LinkTooltip {
constructor (tooltip, input, saveButton, removeButton, normalizeUrlFn) {
constructor (tooltip, input, saveButton, removeButton, editor) {
this.tooltip = tooltip
this.input = input
this.saveButton = saveButton
this.removeButton = removeButton
this.normalizeUrl = normalizeUrlFn
this.targetElement = null
this.clickOutsideTimeout = null
this.closeOnClickOutside = null
this.scrollHandler = null
this.saveHandler = null
this.removeHandler = null
this.keyHandler = null
this.editor = editor
}
updatePosition () {
const rect = this.targetElement.getBoundingClientRect()
this.tooltip.style.left = `${rect.left}px`
this.tooltip.style.top = `${rect.bottom + 5}px`
isVisible () {
return !this.tooltip.classList.contains('hidden')
}
setupClickOutside () {
this.closeOnClickOutside = (e) => {
if (!this.tooltip.contains(e.target)) {
this.hide()
}
}
updatePosition () {
const rect = this.editor.view.coordsAtPos(this.pos)
this.clickOutsideTimeout = setTimeout(() => {
if (this.closeOnClickOutside) {
document.addEventListener('click', this.closeOnClickOutside)
}
}, 100)
this.tooltip.style.left = `${rect.left}px`
this.tooltip.style.top = `${rect.bottom + 6}px`
}
setupScrollTracking () {
this.scrollHandler = () => this.updatePosition()
window.addEventListener('scroll', this.scrollHandler, true)
}
normalizeUrl (url) {
if (!url) return url
if (/^{/i.test(url)) return url
if (/^https?:\/\//i.test(url)) return url
if (/^mailto:/i.test(url)) return url
show ({ url, targetElement, onSave, onRemove }) {
this.hide()
return `https://${url}`
}
show (url, pos) {
this.input.value = url || ''
this.removeButton.classList.toggle('hidden', !url)
this.targetElement = targetElement
this.updatePosition()
this.pos = pos
this.tooltip.classList.remove('hidden')
this.input.focus()
this.input.select()
const save = () => {
const inputUrl = this.input.value.trim()
this.updatePosition()
this.hide()
this.saveHandler = () => {
const inputUrl = this.input.value.trim()
if (inputUrl) {
onSave(this.normalizeUrl(inputUrl))
this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
}
this.hide()
}
this.saveHandler = () => save()
this.removeHandler = () => {
if (onRemove) onRemove()
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
this.hide()
}
this.keyHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault()
save()
this.saveHandler()
} else if (e.key === 'Escape') {
e.preventDefault()
this.hide()
@ -117,26 +103,16 @@ class LinkTooltip {
this.removeButton.addEventListener('click', this.removeHandler, { once: true })
this.input.addEventListener('keydown', this.keyHandler)
this.setupScrollTracking()
this.setupClickOutside()
this.scrollHandler = () => this.updatePosition()
window.addEventListener('scroll', this.scrollHandler, true)
}
hide () {
if (this.clickOutsideTimeout) {
clearTimeout(this.clickOutsideTimeout)
this.clickOutsideTimeout = null
}
if (this.scrollHandler) {
window.removeEventListener('scroll', this.scrollHandler, true)
this.scrollHandler = null
}
if (this.closeOnClickOutside) {
document.removeEventListener('click', this.closeOnClickOutside)
this.closeOnClickOutside = null
}
if (this.saveHandler) {
this.saveButton.removeEventListener('click', this.saveHandler)
this.saveHandler = null
@ -153,16 +129,14 @@ class LinkTooltip {
}
this.tooltip?.classList.add('hidden')
this.targetElement = null
this.currentMark = null
}
}
export default targetable(class extends HTMLElement {
export default actionable(targetable(class extends HTMLElement {
static [target.static] = [
'textarea',
'editorElement',
'variableButton',
'variableDropdown',
'boldButton',
'italicButton',
'underlineButton',
@ -181,14 +155,6 @@ export default targetable(class extends HTMLElement {
this.textarea.style.display = 'none'
this.adjustShortcutsForPlatform()
this.linkTooltip = new LinkTooltip(
this.linkTooltipElement,
this.linkInput,
this.linkSaveButton,
this.linkRemoveButton,
(url) => this.normalizeUrl(url)
)
const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()
const buildDecorations = (doc) => {
@ -245,12 +211,20 @@ export default targetable(class extends HTMLElement {
HardBreak,
UndoRedo,
Link.extend({
inclusive: false
inclusive: true,
addKeyboardShortcuts: () => ({
'Mod-k': () => {
this.toggleLink()
return true
}
})
}).configure({
openOnClick: false,
HTMLAttributes: {
class: 'link',
style: 'color: #2563eb; text-decoration: underline; cursor: pointer;'
'data-turbo': 'false',
style: 'color: #2563eb; text-decoration: underline; cursor: text;'
}
}),
Underline,
@ -260,58 +234,36 @@ export default targetable(class extends HTMLElement {
contentType: 'markdown',
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none p-3 outline-none focus:outline-none min-h-[120px]'
},
handleClick: (view, pos, event) => {
const clickedPos = view.posAtCoords({ left: event.clientX, top: event.clientY })
if (!clickedPos) return false
const linkMark = view.state.doc.resolve(clickedPos.pos).marks().find(m => m.type.name === 'link')
if (linkMark) {
event.preventDefault()
this.editor.chain().setTextSelection(clickedPos.pos).extendMarkRange('link').run()
this.toggleLink()
return true
}
return false
style: 'min-height: 220px',
class: 'p-3 outline-none focus:outline-none'
}
},
onUpdate: ({ editor }) => {
this.textarea.value = editor.getMarkdown()
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
},
onSelectionUpdate: () => {
onSelectionUpdate: ({ editor }) => {
this.updateToolbarState()
this.handleLinkTooltip(editor)
},
onBlur: () => {
setTimeout(() => {
if (!this.linkTooltipElement.contains(document.activeElement)) {
this.linkTooltip.hide()
}
}, 0)
}
})
this.setupToolbar()
if (this.variableButton) {
this.variableButton.addEventListener('click', () => {
this.variableDropdown.classList.toggle('hidden')
})
this.variableDropdown.addEventListener('click', (e) => {
const variable = e.target.closest('[data-variable]')?.dataset.variable
if (variable) {
this.insertVariable(variable)
this.variableDropdown.classList.add('hidden')
}
})
this.linkTooltip = new LinkTooltip(
this.linkTooltipElement,
this.linkInput,
this.linkSaveButton,
this.linkRemoveButton,
this.editor
)
document.addEventListener('click', (e) => {
if (!this.variableButton.contains(e.target) && !this.variableDropdown.contains(e.target)) {
this.variableDropdown.classList.add('hidden')
}
})
}
this.setupToolbar()
}
adjustShortcutsForPlatform () {
@ -368,36 +320,61 @@ export default targetable(class extends HTMLElement {
this.linkButton?.classList.toggle('bg-base-200', this.editor.isActive('link'))
}
normalizeUrl (url) {
if (!url) return url
if (/^https?:\/\//i.test(url)) return url
if (/^mailto:/i.test(url)) return url
handleLinkTooltip (editor) {
const { from } = editor.state.selection
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
return `https://${url}`
if (!mark) {
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
return
}
let linkStart = from
const start = editor.state.doc.resolve(from).start()
for (let i = from - 1; i >= start; i--) {
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
linkStart = i
} else {
break
}
}
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
this.linkTooltip.hide()
this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
this.linkTooltip.currentMark = mark
}
toggleLink () {
const { from } = this.editor.state.selection
if (this.editor.isActive('link')) {
this.linkTooltip.hide()
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
this.updateToolbarState()
} else {
const { from } = this.editor.state.selection
const rect = this.editor.view.coordsAtPos(from)
const fakeElement = { getBoundingClientRect: () => rect }
this.linkTooltip.hide()
this.linkTooltip.show(this.editor.getAttributes('link').href, from)
}
}
const previousUrl = this.editor.getAttributes('link').href
insertVariable (e) {
const variable = e.target.closest('[data-variable]')?.dataset.variable
this.linkTooltip.show({
url: previousUrl,
targetElement: fakeElement,
onSave: (url) => {
this.editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
},
onRemove: previousUrl
? () => { this.editor.chain().focus().extendMarkRange('link').unsetLink().run() }
: null
})
}
if (variable) {
const { from, to } = this.editor.state.selection
insertVariable (variable) {
this.editor.chain().focus().insertContent(`{${variable}}`).run()
if (variable.includes('link') && from !== to) {
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
} else {
this.editor.chain().focus().insertContent(`{${variable}}`).run()
}
}
}
disconnectedCallback () {
@ -408,4 +385,4 @@ export default targetable(class extends HTMLElement {
this.editor = null
}
}
})
}))

@ -37,15 +37,15 @@
</div>
</div>
<% if local_assigns[:variables]&.any? %>
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'code' => t('variables.code'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="ml-auto relative">
<button type="button" data-target="markdown-editor.variableButton" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
<div class="dropdown dropdown-end ml-auto">
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
<%= t('add_variable') %>
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
</button>
<div data-target="markdown-editor.variableDropdown" class="hidden absolute right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
</label>
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
<% local_assigns[:variables]&.each do |variable| %>
<button type="button" data-variable="<%= variable %>" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
<%= variable_labels.fetch(variable, "{#{variable}}") %>
</button>
<% end %>
@ -55,7 +55,7 @@
</div>
<div data-target="markdown-editor.editorElement"></div>
</div>
<div data-target="markdown-editor.linkTooltipElement" class="hidden fixed flex bg-white border border-base-300 rounded-xl shadow-lg p-1 gap-1 items-center z-50">
<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 w-80 text-sm outline-none" 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') %>

@ -1014,7 +1014,7 @@ en: &en
account_name: Account name
submitter_link: Submitter link
template_name: Template name
submission_submitters: Submission submitters
submission_submitters: Submitters list
submission_link: Submission link
documents_link: Documents link
time:
@ -2016,7 +2016,7 @@ es: &es
account_name: Nombre de la cuenta
submitter_link: Enlace del firmante
template_name: Nombre de la plantilla
submission_submitters: Firmantes del envío
submission_submitters: Lista de firmantes
submission_link: Enlace del envío
documents_link: Enlace de los documentos
time:
@ -3019,7 +3019,7 @@ it: &it
account_name: Nome dell'account
submitter_link: Link del firmatario
template_name: Nome del modello
submission_submitters: Firmatari dell'invio
submission_submitters: Lista dei firmatari
submission_link: Link dell'invio
documents_link: Link dei documenti
time:
@ -4018,7 +4018,7 @@ fr: &fr
account_name: Nom du compte
submitter_link: Lien du signataire
template_name: Nom du modèle
submission_submitters: Signataires de la soumission
submission_submitters: Liste des signataires
submission_link: Lien de la soumission
documents_link: Lien des documents
time:
@ -5020,7 +5020,7 @@ pt: &pt
account_name: Nome da conta
submitter_link: Link do signatário
template_name: Nome do modelo
submission_submitters: Signatários da submissão
submission_submitters: Lista de signatários
submission_link: Link da submissão
documents_link: Link dos documentos
time:
@ -6022,7 +6022,7 @@ de: &de
account_name: Kontoname
submitter_link: Link des Unterzeichners
template_name: Vorlagenname
submission_submitters: Unterzeichner der Einreichung
submission_submitters: Liste der Unterzeichner
submission_link: Link der Einreichung
documents_link: Link der Dokumente
time:
@ -7409,7 +7409,7 @@ nl: &nl
account_name: Accountnaam
submitter_link: Link van de ondertekenaar
template_name: Sjabloonnaam
submission_submitters: Ondertekenaars van de inzending
submission_submitters: Lijst van ondertekenaars
submission_link: Link van de inzending
documents_link: Link van de documenten
time:

@ -28,7 +28,7 @@ module MarkdownToHtml
link_parts.map.with_index do |part, index|
if part.match?(%r{\A(?:https?://|www\.)}) &&
!link_parts[index - 1]&.match?(/\]\(\s*\z/)
!(index > 0 && link_parts[index - 1]&.match?(/\]\(\s*\z/))
trail = part.match(/([.,;:!?]+)\z/)[1] if part.match?(/[.,;:!?]+\z/)
clean = trail ? part.chomp(trail) : part
url = clean.start_with?('www.') ? "http://#{clean}" : clean
@ -68,7 +68,6 @@ module MarkdownToHtml
tag = lambda do |t|
desc = TAGS[t[1] || '']
return t unless desc
return desc[0] unless desc[1]
is_end = context.last == t
is_end ? context.pop : context.push(t)
@ -95,15 +94,16 @@ module MarkdownToHtml
elsif m[1]
chunk = '<a>'
elsif m[5]
chunk = if m[5] == '***'
if context.include?('*') && context.include?('**')
tag.call('*') + tag.call('**')
else
tag.call('**') + tag.call('*')
end
else
tag.call(m[5])
end
chunk =
if m[5] == '***'
if context.include?('*') && context.include?('**')
tag.call('*') + tag.call('**')
else
tag.call('**') + tag.call('*')
end
else
tag.call(m[5])
end
end
out += prev.to_s + chunk

Loading…
Cancel
Save