summaryrefslogtreecommitdiffstats
path: root/assets/admin/lib/component-tool.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/admin/lib/component-tool.js')
-rw-r--r--assets/admin/lib/component-tool.js133
1 files changed, 133 insertions, 0 deletions
diff --git a/assets/admin/lib/component-tool.js b/assets/admin/lib/component-tool.js
new file mode 100644
index 0000000..3217667
--- /dev/null
+++ b/assets/admin/lib/component-tool.js
@@ -0,0 +1,133 @@
+/**
+ * EditorJS block tool for embedding web components with props.
+ *
+ * Saved data format:
+ * {
+ * "name": "site-greeting",
+ * "props": { "name": "visitor", "theme": "dark" }
+ * }
+ *
+ * Renders in HTML as: <site-greeting name="visitor" theme="dark"></site-greeting>
+ */
+export default class ComponentTool {
+ static get toolbox() {
+ return {
+ title: 'Component',
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" stroke-linecap="round" d="M7 8l-4 4 4 4M17 8l4 4-4 4M14 4l-4 16"/></svg>',
+ };
+ }
+
+ constructor({ data }) {
+ this.data = {
+ name: data.name || '',
+ props: data.props || {},
+ };
+ this.wrapper = null;
+ }
+
+ render() {
+ this.wrapper = document.createElement('div');
+ this.wrapper.classList.add('component-tool');
+ this._renderUI();
+ return this.wrapper;
+ }
+
+ _renderUI() {
+ this.wrapper.innerHTML = '';
+
+ // Component name input
+ 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; });
+
+ // Props section
+ 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 = '';
+ const entries = Object.entries(this.data.props);
+ entries.forEach(([key, value], i) => {
+ 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;';
+
+ // Rebuild props object on change
+ const updateProps = () => {
+ const newProps = {};
+ container.querySelectorAll('div').forEach((r) => {
+ const inputs = r.querySelectorAll('input');
+ if (inputs[0] && inputs[0].value) {
+ newProps[inputs[0].value] = inputs[1] ? inputs[1].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);
+ });
+ }
+
+ save() {
+ // Clean out empty-key props
+ const cleaned = {};
+ for (const [k, v] of Object.entries(this.data.props)) {
+ if (k.trim()) cleaned[k.trim()] = v;
+ }
+ return {
+ name: this.data.name.trim(),
+ props: cleaned,
+ };
+ }
+
+ validate(savedData) {
+ return savedData.name.trim() !== '';
+ }
+}