UNPKG

@silexlabs/silex

Version:

Free and easy website builder for everyone.

370 lines (359 loc) 13.4 kB
/* * Silex website builder, free/libre no-code tool for makers. * Copyright (c) 2023 lexoyo and Silex Labs foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { Editor } from 'grapesjs' /** * Core GrapesJS commands for blocks, components, styles and classes. * Registered as AI capabilities via grapesjs-ai-capabilities. */ function findComponentById(editor: Editor, id: string) { const all: any[] = [] const collect = (c) => { all.push(c); c.components().forEach(collect) } collect(editor.getWrapper()) return all.find(c => c.getId() === id) } export default (editor: Editor) => { // Blocks editor.Commands.add('blocks:list', () => { return editor.BlockManager.getAll().map(b => ({ id: b.getId(), label: b.getLabel(), category: b.getCategoryLabel(), })) }) editor.Commands.add('blocks:add', (_ed, _sender, options: any = {}) => { const { blockId } = options if (!blockId) throw new Error('Required: blockId. Use blocks:list to see available blocks.') const block = editor.BlockManager.get(blockId) if (!block) throw new Error(`Block "${blockId}" not found. Use blocks:list to see available blocks.`) const selected = editor.getSelected() || editor.getWrapper() return selected.append(block.getContent())?.[0]?.toHTML() }) // Components editor.Commands.add('components:list', () => { const walk = (comp, depth = 0) => { const result: any[] = [{ id: comp.getId(), tagName: comp.get('tagName'), type: comp.get('type'), name: comp.getName(), depth, }] comp.components().forEach(c => result.push(...walk(c, depth + 1))) return result } return walk(editor.getWrapper()) }) editor.Commands.add('components:select', (_ed, _sender, options: any = {}) => { const { id } = options if (!id) throw new Error('Required: id. Use components:list to see all component ids.') const found = findComponentById(editor, id) if (!found) throw new Error(`Component "${id}" not found. Use components:list to see all component ids.`) editor.select(found) }) editor.Commands.add('components:remove', (_ed, _sender, options: any = {}) => { const comp = options.id ? findComponentById(editor, options.id) : editor.getSelected() if (!comp) throw new Error(options.id ? `Component "${options.id}" not found. Use components:list to see all component ids.` : 'No component selected. Use components:select first, or pass {id}.') if (comp === editor.getWrapper()) throw new Error('Cannot remove the body component.') comp.remove() }) editor.Commands.add('components:move', (_ed, _sender, options: any = {}) => { const { id, targetId, position } = options if (!id) throw new Error('Required: id — the component to move. Use components:list to see all ids.') if (!targetId) throw new Error('Required: targetId — the parent to move into. Use components:list to see all ids.') const comp = findComponentById(editor, id) if (!comp) throw new Error(`Component "${id}" not found. Use components:list to see all component ids.`) const target = findComponentById(editor, targetId) if (!target) throw new Error(`Target "${targetId}" not found. Use components:list to see all component ids.`) const idx = typeof position === 'number' ? position : undefined target.append(comp.clone(), { at: idx }) comp.remove() }) editor.Commands.add('components:update', (_ed, _sender, options: any = {}) => { const selected = editor.getSelected() if (!selected) throw new Error('No component selected. Use components:select first.') const { content, tagName, attributes } = options if (content !== undefined) selected.components(content) if (tagName) selected.set('tagName', tagName) if (attributes && typeof attributes === 'object') { Object.entries(attributes).forEach(([k, v]) => selected.addAttributes({ [k]: v })) } if (content === undefined && !tagName && !attributes) { throw new Error('Required: at least one of {content, tagName, attributes}. Example: {content: "<b>Hello</b>"} or {tagName: "section"} or {attributes: {title: "My div"}}') } }) // Styles (operate on the active CSS rule, not inline styles) editor.Commands.add('styles:get', () => { const rule = (editor as any).StyleManager?.getSelected?.() if (!rule) throw new Error('No selector active. Use selector:edit-style first.') return { selector: rule.selectorsToString?.() ?? '', style: rule.getStyle(), mediaText: rule.get('mediaText') ?? '', } }) editor.Commands.add('styles:set', (_ed, _sender, options: any = {}) => { const rule = (editor as any).StyleManager?.getSelected?.() if (!rule) throw new Error('No selector active. Use selector:edit-style first.') const { property, value, ...rest } = options const existing = rule.getStyle?.() ?? {} if (property && value !== undefined) { rule.setStyle({ ...existing, [property]: value }) } else if (Object.keys(rest).length) { rule.setStyle({ ...existing, ...rest }) } else { throw new Error('Required: {property, value} or CSS key-value pairs. Example: {property: "color", value: "red"} or {"color": "red", "font-size": "16px"}') } }) editor.Commands.add('styles:remove', (_ed, _sender, options: any = {}) => { const rule = (editor as any).StyleManager?.getSelected?.() if (!rule) throw new Error('No selector active. Use selector:edit-style first.') const { property } = options if (!property) throw new Error('Required: property (CSS property name, e.g. "color", "font-size", "margin")') const style = rule.getStyle() delete style[property] rule.setStyle(style) }) // CSS Classes editor.Commands.add('classes:list', () => { const selected = editor.getSelected() if (!selected) throw new Error('No component selected. Use components:select first.') return selected.getClasses() }) editor.Commands.add('classes:add', (_ed, _sender, options: any = {}) => { const selected = editor.getSelected() if (!selected) throw new Error('No component selected. Use components:select first.') const { name } = options if (!name) throw new Error('Required: name (CSS class name, e.g. "my-card", "container"). Use classes:list to see existing classes.') selected.addClass(name) }) editor.Commands.add('classes:remove', (_ed, _sender, options: any = {}) => { const selected = editor.getSelected() if (!selected) throw new Error('No component selected. Use components:select first.') const { name } = options if (!name) throw new Error('Required: name (CSS class name). Use classes:list to see classes on the selected component.') selected.removeClass(name) }) // Devices editor.Commands.add('device:list', () => { return editor.Devices.getDevices().map((d: any) => ({ id: d.id, name: d.get('name'), width: d.get('width'), widthMedia: d.get('widthMedia'), })) }) editor.Commands.add('device:set', (_ed, _sender, options: any = {}) => { const { name } = options if (!name) throw new Error('Required: name (device name or id, e.g. "Desktop", "Tablet", "Mobile"). Use device:list to see available devices.') const dev = editor.Devices.get(name) || editor.Devices.getDevices().find((d: any) => d.get('name') === name) if (!dev) throw new Error(`Device "${name}" not found. Use device:list to see available devices.`) editor.Devices.select(dev) }) // History editor.Commands.add('history:undo', () => { editor.UndoManager.undo() }) editor.Commands.add('history:redo', () => { editor.UndoManager.redo() }) // Register AI capabilities editor.on('ai-capabilities:ready', (addCapability) => { addCapability({ id: 'blocks:list', command: 'blocks:list', description: 'List available blocks', readOnly: true, tags: ['blocks'], }) addCapability({ id: 'blocks:add', command: 'blocks:add', description: 'Insert a block into selected component', inputSchema: { type: 'object', required: ['blockId'], properties: { blockId: { type: 'string' }, }, }, tags: ['blocks'], }) addCapability({ id: 'components:list', command: 'components:list', description: 'List all components in the page', readOnly: true, tags: ['components'], }) addCapability({ id: 'components:select', command: 'components:select', description: 'Select a component by id', inputSchema: { type: 'object', required: ['id'], properties: { id: { type: 'string' }, }, }, tags: ['components'], }) addCapability({ id: 'components:remove', command: 'components:remove', description: 'Remove a component', destructive: true, inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Component id. Omit to remove selected.' }, }, }, tags: ['components'], }) addCapability({ id: 'components:move', command: 'components:move', description: 'Move a component into another parent', inputSchema: { type: 'object', required: ['id', 'targetId'], properties: { id: { type: 'string' }, targetId: { type: 'string' }, position: { type: 'number', description: 'Index in target children' }, }, }, tags: ['components'], }) addCapability({ id: 'components:update', command: 'components:update', description: 'Update content, tagName or attributes of selected component', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'HTML content' }, tagName: { type: 'string' }, attributes: { type: 'object' }, }, }, tags: ['components'], }) addCapability({ id: 'styles:get', command: 'styles:get', description: 'Get CSS styles from the active selector (set via selector:edit-style)', readOnly: true, tags: ['styles'], }) addCapability({ id: 'styles:set', command: 'styles:set', description: 'Set CSS style on the active selector (set via selector:edit-style)', inputSchema: { type: 'object', properties: { property: { type: 'string' }, value: { type: 'string' }, }, }, tags: ['styles'], }) addCapability({ id: 'styles:remove', command: 'styles:remove', description: 'Remove a CSS property from the active selector (set via selector:edit-style)', destructive: true, inputSchema: { type: 'object', required: ['property'], properties: { property: { type: 'string' }, }, }, tags: ['styles'], }) addCapability({ id: 'classes:list', command: 'classes:list', description: 'List CSS classes on selected component', readOnly: true, tags: ['classes'], }) addCapability({ id: 'classes:add', command: 'classes:add', description: 'Add CSS class to selected component', inputSchema: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, }, }, tags: ['classes'], }) addCapability({ id: 'classes:remove', command: 'classes:remove', description: 'Remove CSS class from selected component', destructive: true, inputSchema: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, }, }, tags: ['classes'], }) addCapability({ id: 'device:list', command: 'device:list', description: 'List available responsive breakpoints/devices', readOnly: true, tags: ['device'], }) addCapability({ id: 'device:set', command: 'device:set', description: 'Switch responsive breakpoint by device name', inputSchema: { type: 'object', required: ['name'], properties: { name: { type: 'string', description: 'Device name or id (e.g. "Desktop", "Tablet", "Mobile")' }, }, }, tags: ['device'], }) addCapability({ id: 'history:undo', command: 'history:undo', description: 'Undo the last change', tags: ['history'], }) addCapability({ id: 'history:redo', command: 'history:redo', description: 'Redo the last undone change', tags: ['history'], }) }) }