/** @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 } }