/** @import { BlockTool, BlockToolConstructorOptions, BlockToolData, API, ToolboxConfig } from '@editorjs/editorjs' */
/**
* EditorJS block tool for embedding web components with props.
*
* Saved data format:
* {
* "name": "site-greeting",
* "props": { "name": "visitor", "theme": "dark" }
* }
*
* Renders in HTML as:
*
* @implements {BlockTool}
*/
export default class ComponentTool {
/** @returns {ToolboxConfig} */
static get toolbox() {
return {
title: 'Component',
icon: '',
}
}
static get isReadOnlySupported() {
return true
}
/**
* @param {BlockToolConstructorOptions} options
* @param {BlockToolData} options.data
* @param {API} options.api
* @param {boolean} options.readOnly
*/
constructor({ data, api, readOnly }) {
this.api = api
this.readOnly = readOnly
this.data = {
name: data.name || '',
props: data.props || {},
}
this.wrapper = null
}
/** @returns {HTMLElement} */
render() {
this.wrapper = document.createElement('div')
this.wrapper.classList.add('component-tool')
if (this.readOnly) {
this._renderPreview()
} else {
this._renderEditor()
}
return this.wrapper
}
_renderPreview() {
const tag = document.createElement('code')
tag.style.cssText = 'display:block;padding:6px 8px;background:#f5f5f5;border-radius:4px;font-size:0.85em;color:#555;'
tag.textContent = this._previewText()
this.wrapper.append(tag)
}
_previewText() {
const attrs = Object.entries(this.data.props)
.map(([k, v]) => ` ${k}="${v}"`)
.join('')
return `<${this.data.name || '?'}${attrs}>`
}
_renderEditor() {
this.wrapper.innerHTML = ''
const nameLabel = document.createElement('label')
nameLabel.textContent = 'Component tag name'
nameLabel.style.cssText = 'display:block;font-size:12px;color:#888;margin-bottom:2px;'
const nameInput = document.createElement('input')
nameInput.type = 'text'
nameInput.placeholder = 'e.g. site-greeting'
nameInput.value = this.data.name
nameInput.style.cssText = 'width:100%;padding:6px 8px;border:1px solid #ccc;border-radius:4px;font-family:monospace;margin-bottom:8px;'
nameInput.addEventListener('input', () => { this.data.name = nameInput.value })
const propsLabel = document.createElement('label')
propsLabel.textContent = 'Props'
propsLabel.style.cssText = 'display:block;font-size:12px;color:#888;margin-bottom:2px;'
const propsContainer = document.createElement('div')
propsContainer.classList.add('props-container')
const addBtn = document.createElement('button')
addBtn.type = 'button'
addBtn.textContent = '+ Add prop'
addBtn.style.cssText = 'padding:4px 10px;font-size:12px;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer;margin-top:4px;'
addBtn.addEventListener('click', () => {
this.data.props[''] = ''
this._renderProps(propsContainer)
})
this.wrapper.append(nameLabel, nameInput, propsLabel, propsContainer, addBtn)
this._renderProps(propsContainer)
}
_renderProps(container) {
container.innerHTML = ''
Object.entries(this.data.props).forEach(([key, value]) => {
const row = document.createElement('div')
row.style.cssText = 'display:flex;gap:4px;margin-bottom:4px;'
const kInput = document.createElement('input')
kInput.type = 'text'
kInput.placeholder = 'key'
kInput.value = key
kInput.style.cssText = 'flex:1;padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-family:monospace;'
const vInput = document.createElement('input')
vInput.type = 'text'
vInput.placeholder = 'value'
vInput.value = value
vInput.style.cssText = 'flex:2;padding:4px 6px;border:1px solid #ccc;border-radius:4px;'
const removeBtn = document.createElement('button')
removeBtn.type = 'button'
removeBtn.textContent = '×'
removeBtn.style.cssText = 'padding:4px 8px;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer;'
const updateProps = () => {
const newProps = {}
container.querySelectorAll('div').forEach((r) => {
const [k, v] = r.querySelectorAll('input')
if (k && k.value) newProps[k.value] = v ? v.value : ''
})
this.data.props = newProps
}
kInput.addEventListener('input', updateProps)
vInput.addEventListener('input', updateProps)
removeBtn.addEventListener('click', () => { row.remove(); updateProps() })
row.append(kInput, vInput, removeBtn)
container.append(row)
})
}
/**
* @param {HTMLElement} _blockContent
* @returns {BlockToolData}
*/
save(_blockContent) {
const props = {}
for (const [k, v] of Object.entries(this.data.props)) {
if (k.trim()) props[k.trim()] = v
}
return { name: this.data.name.trim(), props }
}
/** @param {BlockToolData} savedData */
validate(savedData) {
return savedData.name.trim() !== ''
}
destroyed() {
this.wrapper = null
}
}