refactor: markdown editor

This commit is contained in:
Jannat Patel
2025-12-08 12:33:38 +05:30
parent 93ffcdb8f9
commit 6e93f952ab
2 changed files with 82 additions and 114 deletions

View File

@@ -6,8 +6,8 @@ export class Markdown {
this.api = api
this.data = data || {}
this.config = config || {}
this.text = data.text || ''
this.readOnly = readOnly
this.text = data.text || ''
this.placeholder = __("Type '/' for commands or select text to format")
}
@@ -30,65 +30,28 @@ export class Markdown {
const div = document.createElement('div')
app.mount(div)
return {
title: '',
icon: div.innerHTML,
}
}
onPaste(event) {
const data = {
text: event.detail.data.innerHTML,
}
this.data = data
window.requestAnimationFrame(() => {
if (!this.wrapper) {
return
}
this.wrapper.innerHTML = this.data.text || ''
})
return { title: '', icon: div.innerHTML }
}
static get pasteConfig() {
return {
tags: ['P'],
}
return { tags: ['P'] }
}
render() {
this.wrapper = document.createElement('div')
this.wrapper.classList.add('cdx-block', 'ce-paragraph')
this.wrapper.contentEditable = !this.readOnly
this.wrapper.dataset.placeholder = this.placeholder
this.wrapper.innerHTML = this.text
if (!this.readOnly) {
this.wrapper.contentEditable = true
this.wrapper.innerHTML = this.text
this.wrapper.addEventListener('focus', () =>
this._togglePlaceholder()
)
this.wrapper.addEventListener('blur', () =>
this._togglePlaceholder()
)
this.wrapper.addEventListener('input', (event) => {
this._togglePlaceholder()
let value = event.target.textContent
if (event.keyCode === 32 && value.startsWith('#')) {
this.convertToHeader(event, value)
} else if (event.keyCode == 189) {
this.convertBlock('list', {
style: 'unordered',
})
} else if (/^[a-zA-Z]/.test(event.key)) {
this.convertBlock('paragraph', {
text: value,
})
} else if (event.keyCode === 13 || event.keyCode === 190) {
this.parseContent(event)
}
})
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
}
return this.wrapper
@@ -99,10 +62,9 @@ export class Markdown {
'.cdx-block.ce-paragraph[data-placeholder]'
)
blocks.forEach((block) => {
if (block !== this.wrapper) {
delete block.dataset.placeholder
}
if (block !== this.wrapper) delete block.dataset.placeholder
})
if (this.wrapper.innerHTML.trim() === '') {
this.wrapper.dataset.placeholder = this.placeholder
} else {
@@ -110,102 +72,107 @@ export class Markdown {
}
}
convertToHeader(event, value) {
event.preventDefault()
if (['#', '##', '###', '####', '#####', '######'].includes(value)) {
let level = value.length
event.target.textContent = ''
this.convertBlock('header', {
level: level,
})
}
}
_onKeyDown(event) {
const text = this.wrapper.textContent
parseContent(event) {
event.preventDefault()
let previousLine = this.wrapper.textContent
if (event.keyCode === 190) {
previousLine = previousLine + '.'
}
if (previousLine && this.hasImage(previousLine)) {
if (event.key === ' ' && /^#{1,6}$/.test(text)) {
event.preventDefault()
const level = text.length
this.wrapper.textContent = ''
this.convertBlock('image')
} else if (previousLine && this.hasLink(previousLine)) {
const { text, url } = this.extractLink(previousLine)
const anchorTag = `<a href="${url}" target="_blank">${text}</a>`
this.convertBlock('paragraph', {
text: previousLine.replace(/\[.+?\]\(.+?\)/, anchorTag),
})
} else if (previousLine && previousLine.startsWith('- ')) {
this.convertBlock('list', {
this._convertBlock('header', { level })
} else if (event.key === ' ' && text === '-') {
event.preventDefault()
this.wrapper.textContent = ''
this._convertBlock('list', {
style: 'unordered',
items: [
{
content: previousLine.replace('- ', ''),
},
],
items: [{ content: '' }],
})
} else if (previousLine && previousLine.startsWith('1.')) {
this.convertBlock('list', {
style: 'ordered',
items: [
{
content: previousLine.replace('1.', ''),
},
],
})
} else if (previousLine && this.canBeEmbed(previousLine)) {
} else if (event.key === ' ' && /^1\.$/.test(text)) {
event.preventDefault()
this.wrapper.textContent = ''
this.convertBlock('embed', {
source: previousLine,
this._convertBlock('list', {
style: 'ordered',
items: [{ content: '' }],
})
} else {
this.convertBlock('paragraph', {
text: previousLine,
} else if (this._isEmbed(text) && event.key === 'Enter') {
event.preventDefault()
this.wrapper.textContent = ''
this._convertBlock('embed', { source: text })
} else if (event.key === 'Enter') {
setTimeout(() => this._checkMarkdownAfterEnter(), 0)
}
}
_checkMarkdownAfterEnter() {
const text = this.wrapper.textContent.trim()
if (this._isImage(text)) {
this._convertBlock('image', {
file: { url: this._extractImage(text).url },
})
}
}
async convertBlock(type, data, index = null) {
async _convertBlock(type, data) {
const currentIndex = this.api.blocks.getCurrentBlockIndex()
const currentBlock = this.api.blocks.getBlockByIndex(currentIndex)
if (!currentBlock) return
await this.api.blocks.convert(currentBlock.id, type, data)
this.api.caret.focus(true)
setTimeout(() => {
const newIndex = this.api.blocks.getCurrentBlockIndex()
const newBlock = this.api.blocks.getBlockByIndex(newIndex)
if (newBlock && newBlock.holder) {
const holder = newBlock.holder.querySelector(
'[contenteditable="true"]'
)
if (holder) {
holder.focus()
// Place caret at end
const range = document.createRange()
range.selectNodeContents(holder)
range.collapse(false)
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
} else {
this.api.caret.focus(true)
}
} else {
this.api.caret.focus(true)
}
}, 0)
}
save(blockContent) {
return {
text: blockContent.innerHTML,
}
return { text: blockContent.innerHTML }
}
hasImage(line) {
return /!\[.+?\]\(.+?\)/.test(line)
_isImage(text) {
return /!\[.+?\]\(.+?\)/.test(text)
}
extractImage(line) {
const match = line.match(/!\[(.+?)\]\((.+?)\)/)
if (match) {
return { alt: match[1], url: match[2] }
}
_extractImage(text) {
const match = text.match(/!\[(.+?)\]\((.+?)\)/)
if (match) return { alt: match[1], url: match[2] }
return { alt: '', url: '' }
}
hasLink(line) {
return /\[.+?\]\(.+?\)/.test(line)
_isLink(text) {
return /\[.+?\]\(.+?\)/.test(text)
}
extractLink(line) {
const match = line.match(/\[(.+?)\]\((.+?)\)/)
if (match) {
return { text: match[1], url: match[2] }
}
_extractLink(text) {
const match = text.match(/\[(.+?)\]\((.+?)\)/)
if (match) return { text: match[1], url: match[2] }
return { text: '', url: '' }
}
canBeEmbed(line) {
return /^https?:\/\/.+/.test(line.trim())
_isEmbed(text) {
return /^https?:\/\/.+/.test(text.trim())
}
}