From a836149fe36d23f4d9363dd70907b31867d7c19d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 16 Jan 2024 10:26:32 +0100 Subject: [PATCH 01/19] fix: company name --- docs/docs/Fields/CustomField.md | 2 +- docs/docs/intro.mdx | 2 +- docs/docs/translation.mdx | 2 +- docs/docusaurus.config.js | 12 ++++++------ docs/src/pages/pricing.jsx | 2 +- visual-editor/package.json | 14 +++++++------- .../src/components/Sidebar/SidebarFooter.tsx | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/docs/Fields/CustomField.md b/docs/docs/Fields/CustomField.md index c2a62912..32f1f23f 100644 --- a/docs/docs/Fields/CustomField.md +++ b/docs/docs/Fields/CustomField.md @@ -76,4 +76,4 @@ export const Number = defineField({ }) ``` -You can check how [it is used internally](https://github.com/boxraiser/visual-editor/tree/main/visual-editor/src/fields) +You can check how [it is used internally](https://github.com/ciklik/visual-editor/tree/main/visual-editor/src/fields) diff --git a/docs/docs/intro.mdx b/docs/docs/intro.mdx index f945cc5f..d9028230 100644 --- a/docs/docs/intro.mdx +++ b/docs/docs/intro.mdx @@ -24,7 +24,7 @@ let editor = new VisualEditor() Then register your page components using the `registerComponent` method that expect 2 parameters - A unique identifier for the block (string) -- An option object (see [EditorComponentDefinition](https://github.com/boxraiser/visual-editor/blob/main/visual-editor/src/types.ts#L38) for more information) +- An option object (see [EditorComponentDefinition](https://github.com/ciklik/visual-editor/blob/main/visual-editor/src/types.ts#L38) for more information) ```js import { HTMLText, Repeater, Text, Row, Select } from '@boxraiser/visual-editor' diff --git a/docs/docs/translation.mdx b/docs/docs/translation.mdx index 67b4e203..8108c487 100644 --- a/docs/docs/translation.mdx +++ b/docs/docs/translation.mdx @@ -14,4 +14,4 @@ const editor = new VisualEditor({ }) ``` -You can see all the available keys in the [source code](https://github.com/boxraiser/visual-editor/blob/main/visual-editor/src/langs/en.ts). \ No newline at end of file +You can see all the available keys in the [source code](https://github.com/ciklik/visual-editor/blob/main/visual-editor/src/langs/en.ts). diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index e940141d..ca587c64 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -8,12 +8,12 @@ const darkCodeTheme = themes.dracula; const config = { title: "Visual Editor", tagline: "Edit your page block by block", - url: "https://boxraiser.github.io", + url: "https://ciklik.github.io", baseUrl: "/visual-editor/", onBrokenLinks: "warn", onBrokenMarkdownLinks: "warn", favicon: "img/favicon.ico", - organizationName: "BoxRaiser", // Usually your GitHub org/user name. + organizationName: "Ciklik", // Usually your GitHub org/user name. projectName: "visual-editor", // Usually your repo name. presets: [ [ @@ -23,7 +23,7 @@ const config = { docs: { sidebarPath: require.resolve("./sidebars.js"), // Please change this to your repo. - editUrl: "https://github.com/boxraiser/visual-editor/tree/main/docs/", + editUrl: "https://github.com/ciklik/visual-editor/tree/main/docs/", }, blog: false, theme: { @@ -50,7 +50,7 @@ const config = { label: "Pricing", }, { - href: "https://github.com/boxraiser/visual-editor", + href: "https://github.com/ciklik/visual-editor", label: "GitHub", position: "right", }, @@ -73,12 +73,12 @@ const config = { items: [ { label: "Github", - href: "https://github.com/boxraiser/visual-editor", + href: "https://github.com/ciklik/visual-editor", }, ], }, ], - copyright: `Copyright © ${new Date().getFullYear()} Boxraiser. Built with Docusaurus.`, + copyright: `Copyright © ${new Date().getFullYear()} Boxraiser. Built with Docusaurus.`, }, prism: { theme: lightCodeTheme, diff --git a/docs/src/pages/pricing.jsx b/docs/src/pages/pricing.jsx index cfd5ac15..5eed6405 100644 --- a/docs/src/pages/pricing.jsx +++ b/docs/src/pages/pricing.jsx @@ -31,7 +31,7 @@ export default function Pricing() {
diff --git a/visual-editor/package.json b/visual-editor/package.json index d57e639c..6922dac7 100644 --- a/visual-editor/package.json +++ b/visual-editor/package.json @@ -1,12 +1,12 @@ { - "name": "@boxraiser/visual-editor", - "version": "0.1.4", + "name": "@ciklik/visual-editor", + "version": "0.2.0", "type": "module", "main": "VisualEditor.js", "types": "./VisualEditor.d.ts", "repository": { "type": "git", - "url": "https://github.com/boxraiser/visual-editor" + "url": "https://github.com/ciklik/visual-editor" }, "scripts": { "dev": "concurrently \"vite\" \"php -S 0.0.0.0:8000 -ddisplay_errors=1 -t server/\"", @@ -64,8 +64,6 @@ "react-flip-toolkit": "^7.1.0", "react-popper": "^2.3.0", "react-use": "^17.4.0", - "vite": "^7.0.6", - "vitest": "^3.2.4", "zustand": "^4.4.2" }, "devDependencies": { @@ -76,7 +74,7 @@ "@types/react-dom": "^18.2.10", "concurrently": "^7.6.0", "cypress": "^15.6.0", - "esbuild": "^0.19.4", + "esbuild": "^0.27.3", "esbuild-node-externals": "^1.9.0", "prettier": "^3.0.3", "react": "^18.2.0", @@ -84,7 +82,9 @@ "rollup-plugin-analyzer": "^4.0.0", "start-server-and-test": "^2.0.1", "tsc-alias": "^1.8.8", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "vite": "^7.3.1", + "vitest": "^3.2.4" }, "peerDependencies": { "react": "^18.0.0", diff --git a/visual-editor/src/components/Sidebar/SidebarFooter.tsx b/visual-editor/src/components/Sidebar/SidebarFooter.tsx index 8743b614..70a0119a 100644 --- a/visual-editor/src/components/Sidebar/SidebarFooter.tsx +++ b/visual-editor/src/components/Sidebar/SidebarFooter.tsx @@ -8,9 +8,9 @@ export function SidebarFooter() { {t('poweredBy')}
-
+ From 8600c4d44da9ef59d1ec7c8ea92b6a43c0b74c56 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 1 Feb 2024 21:17:39 +0100 Subject: [PATCH 02/19] feat: components & templates events --- docs/docs/changelog.mdx | 7 + docs/docs/events.mdx | 13 + visual-editor/index.html | 18 +- visual-editor/src/VisualEditor.tsx | 85 +++--- .../components/Preview/PreviewAddFloating.tsx | 12 +- .../src/components/Preview/PreviewItem.tsx | 58 +--- .../components/Preview/PreviewItemWrapper.tsx | 94 +++++++ .../src/components/Preview/PreviewItems.tsx | 6 +- .../components/Preview/PreviewPostMessage.tsx | 93 ++++--- .../src/components/Sidebar/Sidebar.tsx | 14 +- .../src/components/Sidebar/SidebarFooter.tsx | 5 +- .../components/Sidebar/SidebarTemplates.tsx | 1 + visual-editor/src/constants.ts | 9 + visual-editor/src/elements/PreviewWrapper.tsx | 53 ++-- visual-editor/src/store.tsx | 261 ++++++++++-------- visual-editor/vite.config.ts | 2 +- 16 files changed, 443 insertions(+), 288 deletions(-) create mode 100644 docs/docs/events.mdx create mode 100644 visual-editor/src/components/Preview/PreviewItemWrapper.tsx diff --git a/docs/docs/changelog.mdx b/docs/docs/changelog.mdx index 11f7b06d..09421c8f 100644 --- a/docs/docs/changelog.mdx +++ b/docs/docs/changelog.mdx @@ -4,6 +4,13 @@ sidebar_position: 10 # Changelog +## 0.2 - 2024/02/01 + +- Feat: Add a new `templates` event (cancelable) +- Feat: Add a new `components` event (cancelable) +- Chore: Remove `detail` in the change event +- Fix: key missing on templates listing + ## 0.1.0 - 2023/03/16 - Feat: Add a new `postMessagePreview` option for VisualEditor diff --git a/docs/docs/events.mdx b/docs/docs/events.mdx new file mode 100644 index 00000000..cbb11c03 --- /dev/null +++ b/docs/docs/events.mdx @@ -0,0 +1,13 @@ +--- +sidebar_position: 7 +--- + +# Events + +If you need to create custom behaviour you can listen for some events on the editor + +| **Event** | **Description** | **Methods** | +|-----------|-----------------|-------------| +| `change` | Fires when the value of the editor is changed by the user | `{}` | +| `templates` | Fires when the user click on the template button | - **preventDefault** | +| `components` | Fires when the user click on the add component button | - **preventDefault** : `() => void`
- **detail.add** : `(name: string, data?: object) => void` | diff --git a/visual-editor/index.html b/visual-editor/index.html index d1aab6a3..b4f0ae48 100644 --- a/visual-editor/index.html +++ b/visual-editor/index.html @@ -128,25 +128,25 @@

} ])) - // Set the first editor value - - // Toggle the editor document.querySelectorAll('.button').forEach((button) => { button.addEventListener('click', toggleEditor) const editor = document.getElementById(button.dataset.target) - editor.addEventListener('change', (e) => - console.log(`change #${button.dataset.target} : `, e) - ) + const eventNames = ['change', 'templates', 'components'] + for (const eventName of eventNames) { + editor.addEventListener(eventName, (e) => { + console.log(`Event:${eventName}#${button.dataset.target}`, e); + }) + } editor.addEventListener('close', (e) => { editor.setAttribute('hidden', 'hidden') }) }) - // Test how the field handle being updated + // Test how the field handle being synced document.querySelector('#editor1').addEventListener('change', (e) => { - localStorage.setItem('editor1value', e.detail) - e.target.setAttribute('value', e.detail); + localStorage.setItem('editor1value', e.target.value) + e.target.setAttribute('value', e.target.value); }) diff --git a/visual-editor/src/VisualEditor.tsx b/visual-editor/src/VisualEditor.tsx index 88526f1e..53ad2fce 100644 --- a/visual-editor/src/VisualEditor.tsx +++ b/visual-editor/src/VisualEditor.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef } from 'react' -import { createRoot } from 'react-dom/client' +import { createRoot, type Root } from 'react-dom/client' import type { EditorComponentData, EditorComponentDefinition, @@ -18,6 +18,7 @@ import { BaseStyles } from 'src/components/BaseStyles' import { Translations as EN } from 'src/langs/en' import { useStopPropagation } from 'src/hooks/useStopPropagation' import { InsertPosition } from 'src/enum' +import { Events } from 'src/constants' const components: EditorComponentDefinitions = {} const templates: EditorComponentTemplate[] = [] @@ -33,36 +34,38 @@ export class VisualEditor { static i18n: Translation = EN static postMessagePreview: boolean = false - constructor (options: { lang?: Translation, postMessagePreview?: boolean } = {}) { + constructor( + options: { lang?: Translation; postMessagePreview?: boolean } = {} + ) { VisualEditor.i18n = options.lang ?? EN VisualEditor.postMessagePreview = options.postMessagePreview ?? false } - registerComponent (name: string, definition: EditorComponentDefinition) { + registerComponent(name: string, definition: EditorComponentDefinition) { components[name] = { label: 'title', ...definition } } - registerTemplate (template: EditorComponentTemplate) { + registerTemplate(template: EditorComponentTemplate) { templates.push(template) } - defineElement (elementName: string = 'visual-editor') { + defineElement(elementName: string = 'visual-editor') { // We only declare the class in this function to avoid any problem with SSR class VisualEditorElement extends HTMLElement { static changeEventName = 'change' - private _mounted: boolean = false private _data: EditorComponentData[] | null = null private _value = '' + private _root: Root | null = null - static get observedAttributes () { + static get observedAttributes() { return ['hidden', 'value'] } - get value (): string { + get value(): string { return this._value } - set value (v: string) { + set value(v: string) { if (v === this._value) { return } @@ -71,36 +74,36 @@ export class VisualEditor { this.render() } - connectedCallback () { + connectedCallback() { this._value = this.getAttribute('value') || '[]' this.render() - this._mounted = true } - attributeChangedCallback ( + attributeChangedCallback( name: string, oldValue?: string, - newValue?: string, + newValue?: string ) { - if (!this._mounted) { + if (!this._root) { return false } // Si la valeur change, on réinitialise la version traduite du JSON - if (name === 'value') { - // Saute le nouveau rendu si la valeur n'est pas nouvelle - if (newValue === this._value) { - return - } - this._value = newValue! + if (name === 'value' && newValue) { + this.value = newValue + return } this.render() } - disconnectedCallback () { - this._mounted = false + disconnectedCallback() { + if (!this._root) { + return + } + this._root.unmount() + this._root = null } - private parseValue (value: string): EditorComponentData[] { + private parseValue(value: string): EditorComponentData[] { if (this._data === null) { try { const json = JSON.parse(value) @@ -109,19 +112,23 @@ export class VisualEditor { }) } catch (e) { console.error('Impossible de parser les données', value, e) - alert('Impossible de parser les données de l\'éditeur visuel') + alert("Impossible de parser les données de l'éditeur visuel") this._data = [] } } return this._data! } - private render () { + private render() { const data = this.parseValue(this._value) const hiddenCategories = this.getAttribute('hidden-categories')?.split(';') ?? [] - createRoot(this).render( + if (!this._root) { + this._root = createRoot(this) + } + + this._root.render( - , + ) } } @@ -171,15 +174,15 @@ type VisualEditorProps = { onChange: (v: string) => void } -export function VisualEditorComponent ({ - value, - previewUrl, - name, - element, - iconsUrl, - visible: visibleProps, - onChange, - }: VisualEditorProps) { +export function VisualEditorComponent({ + value, + previewUrl, + name, + element, + iconsUrl, + visible: visibleProps, + onChange, +}: VisualEditorProps) { const skipNextChange = useRef(true) // Skip emitting a change event on the next update (usefull for external changes) const updateData = useUpdateData() const data = useData() diff --git a/visual-editor/src/components/Preview/PreviewAddFloating.tsx b/visual-editor/src/components/Preview/PreviewAddFloating.tsx index ff5074ac..335f2e0e 100644 --- a/visual-editor/src/components/Preview/PreviewAddFloating.tsx +++ b/visual-editor/src/components/Preview/PreviewAddFloating.tsx @@ -3,11 +3,14 @@ import styled from '@emotion/styled' import { UnstyledButton } from 'src/components/ui' type PreviewAddFloatingProps = { - onClick?: (e: SyntheticEvent) => void, + onClick?: (e: SyntheticEvent) => void style?: CSSProperties } -export function PreviewAddFloating({ onClick, style }: PreviewAddFloatingProps) { +export function PreviewAddFloating({ + onClick, + style, +}: PreviewAddFloatingProps) { return ( +

@@ -96,10 +99,13 @@

value="[]" > - + diff --git a/visual-editor/src/VisualEditor.tsx b/visual-editor/src/VisualEditor.tsx index 49f3dcf4..74dfabe3 100644 --- a/visual-editor/src/VisualEditor.tsx +++ b/visual-editor/src/VisualEditor.tsx @@ -9,10 +9,9 @@ import type { Translation, } from 'src/types' import { Layout } from 'src/components/Layout' -import { StoreProvider, usePartialStore } from 'src/store' +import { Store, StoreProvider, usePartialStore } from 'src/store' import { indexify, stringifyFields } from 'src/functions/object' import { useClipboardPaste } from 'src/hooks/useClipboardPaste' -import { useUpdateEffect } from 'src/hooks/useUpdateEffect' import { fillDefaults } from 'src/functions/fields' import { useStateDelayed } from 'src/hooks/useStateDelayed' import { BaseStyles } from 'src/components/BaseStyles' @@ -39,12 +38,13 @@ export class VisualEditor { static i18n: Translation = EN static postMessagePreview: boolean = false static devices: Device[] + constructor( options: { lang?: Translation postMessagePreview?: boolean devices?: Device[] - } = {} + } = {}, ) { VisualEditor.i18n = options.lang ?? EN VisualEditor.devices = options.devices ?? defaultDevices @@ -63,36 +63,48 @@ export class VisualEditor { // We only declare the class in this function to avoid any problem with SSR class VisualEditorElement extends HTMLElement { static changeEventName = 'change' - private _data: EditorComponentData[] | null = null - private _value = '' + // React root private _root: Root | null = null + // Access zustand store used by the VisualEditor + private _store: Store | null = null static get observedAttributes() { return ['hidden', 'value'] } get value(): string { - return this._value + return stringifyFields(this.valueAsArray) + } + + get valueAsArray(): EditorComponentData[] { + return this._store?.getState().data ?? [] } - set value(v: string) { - if (v === this._value) { + set value(v: string | EditorComponentData[] | ((v: EditorComponentData[]) => EditorComponentData[])) { + if (!this._store) { + console.error('Cannot set value for an unconnected visual editor') + return; + } + const state = this._store.getState() + if (typeof v === 'string') { + state.setDataFromOutside(this.parseValue(v)) return } - this._value = v - this._data = null - this.render() + if (typeof v === 'function') { + state.setDataFromOutside(v(state.data)) + return + } + state.setDataFromOutside(indexify(v)) } connectedCallback() { - this._value = this.getAttribute('value') || '[]' this.render() } attributeChangedCallback( name: string, oldValue?: string, - newValue?: string + newValue?: string, ) { if (!this._root) { return false @@ -110,27 +122,28 @@ export class VisualEditor { return } this._root.unmount() + this._store = null this._root = null } - private parseValue(value: string): EditorComponentData[] { - if (this._data === null) { - try { - const json = JSON.parse(value) - this._data = indexify(json).map((value: EditorComponentData) => { - return fillDefaults(value, components[value._name]?.fields ?? []) - }) - } catch (e) { - console.error('Impossible de parser les données', value, e) - alert("Impossible de parser les données de l'éditeur visuel") - this._data = [] - } + private parseValue(value?: string): EditorComponentData[] { + if (!value) { + return [] + } + try { + const json = JSON.parse(value) + return indexify(json).map((value: EditorComponentData) => { + return fillDefaults(value, components[value._name]?.fields ?? []) + }) + } catch (e) { + console.error('Impossible de parser les données', value, e) + alert('Impossible de parser les données de l\'éditeur visuel') + return [] } - return this._data! } private render() { - const data = this.parseValue(this._value) + const data = this.parseValue(this.getAttribute('value')?.toString()) const hiddenCategories = this.getAttribute('hidden-categories')?.split(';') ?? [] @@ -150,23 +163,16 @@ export class VisualEditor { (this.getAttribute('insertPosition') ?? InsertPosition.Start) as InsertPosition } + onStore={(store) => this._store = store} > { - if (this._value === value) { - return - } - this._value = value - this.dispatchEvent(new CustomEvent(Events.Change)) - }} /> - + , ) } } @@ -176,54 +182,33 @@ export class VisualEditor { } type VisualEditorProps = { - value: EditorComponentData[] previewUrl: string name: string iconsUrl: string visible: boolean element: Element - onChange: (v: string) => void } export function VisualEditorComponent({ - value, - previewUrl, - name, - element, - iconsUrl, - visible: visibleProps, - onChange, -}: VisualEditorProps) { - const skipNextChange = useRef(true) // Skip emitting a change event on the next update (usefull for external changes) - const { data, updateData } = usePartialStore('updateData', 'data') + previewUrl, + name, + element, + iconsUrl, + visible: visibleProps, + }: VisualEditorProps) { const visible = useStateDelayed(visibleProps) const handleClose = () => { element.dispatchEvent(new Event('close')) } - const doNothing = () => null // React wants handler :( - // JSON nettoyé - const cleanedData = useMemo(() => stringifyFields(data), [data]) - // Synchronise l'état du composant avec la prop value - useUpdateEffect(() => { - skipNextChange.current = true - updateData(value) - }, [value]) useClipboardPaste(visible) - useEffect(() => { - if (skipNextChange.current) { - skipNextChange.current = false - } else { - onChange(cleanedData) - } - }, [cleanedData]) // We want to avoid bubbling change & close event const div = useRef(null) useStopPropagation(div, ['change', 'close']) if (!visible) { return ( -