diff --git a/ui/dev/package-lock.json b/ui/dev/package-lock.json index 270b42a..f6bd1b1 100644 --- a/ui/dev/package-lock.json +++ b/ui/dev/package-lock.json @@ -26,7 +26,10 @@ "grapesjs-tui-image-editor": "^1.0.2", "grapesjs-typed": "^2.0.1", "html2canvas": "^1.4.1", - "quasar": "^2.18.2" + "postcss": "^8.1.0", + "quasar": "^2.18.2", + "vue": "^3.2.29", + "vue-router": "^4.0.12" }, "engines": { "node": ">= 8.9.0", @@ -3605,7 +3608,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.0", "@vue/shared": "3.5.18", @@ -3619,7 +3621,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-core": "3.5.18", "@vue/shared": "3.5.18" @@ -3630,7 +3631,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.0", "@vue/compiler-core": "3.5.18", @@ -3648,7 +3648,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.18", "@vue/shared": "3.5.18" @@ -3658,15 +3657,13 @@ "version": "6.6.4", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@vue/reactivity": { "version": "3.5.18", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", "license": "MIT", - "peer": true, "dependencies": { "@vue/shared": "3.5.18" } @@ -3676,7 +3673,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.18", "@vue/shared": "3.5.18" @@ -3687,7 +3683,6 @@ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/reactivity": "3.5.18", "@vue/runtime-core": "3.5.18", @@ -3700,7 +3695,6 @@ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-ssr": "3.5.18", "@vue/shared": "3.5.18" @@ -3713,8 +3707,7 @@ "version": "3.5.18", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -5686,8 +5679,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debounce": { "version": "1.2.1", @@ -8447,7 +8439,6 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -12564,7 +12555,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.18", "@vue/compiler-sfc": "3.5.18", @@ -12651,7 +12641,6 @@ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^6.6.4" }, diff --git a/ui/dev/package.json b/ui/dev/package.json index 96e6b44..3d87ea8 100644 --- a/ui/dev/package.json +++ b/ui/dev/package.json @@ -46,6 +46,9 @@ "grapesjs-tui-image-editor": "^1.0.2", "grapesjs-typed": "^2.0.1", "html2canvas": "^1.4.1", - "quasar": "^2.18.2" + "postcss": "^8.1.0", + "quasar": "^2.18.2", + "vue": "^3.2.29", + "vue-router": "^4.0.12" } } diff --git a/ui/dev/quasar.config.js b/ui/dev/quasar.config.js index 720ae27..d9100a5 100644 --- a/ui/dev/quasar.config.js +++ b/ui/dev/quasar.config.js @@ -66,20 +66,21 @@ export default function (ctx) { headers: { 'Access-Control-Allow-Origin': '*' }, - proxy: { - // Using the proxy instance - '/api/': { + proxy: [ + { + context: ['/api/'], target: 'http://localhost:3000/', secure: false, changeOrigin: true, pathRewrite: { '^/api/': '/' } // Remove /api/ from the request path }, - '/files/': { + { + context: ['/files/'], target: 'http://localhost:3001/', secure: false, changeOrigin: true } - } + ] }, ssr: { diff --git a/ui/src/components/Component.js b/ui/src/components/Component.js index e1d0ad8..953ab16 100644 --- a/ui/src/components/Component.js +++ b/ui/src/components/Component.js @@ -2,19 +2,21 @@ import { styleManager, deviceManager } from './config' import { h, ref, onMounted, onBeforeUnmount } from 'vue' import LayerTitle from './LayerTitle' import { Dialog } from 'quasar' -import grapesjs from 'grapesjs' -import webpage from 'grapesjs-preset-webpage' -import blocksBasic from 'grapesjs-blocks-basic' -import countdown from 'grapesjs-component-countdown' -import pluginExport from 'grapesjs-plugin-export' -import tabs from 'grapesjs-tabs' -import customCode from 'grapesjs-custom-code' -import touch from 'grapesjs-touch' -import imageEditor from 'grapesjs-tui-image-editor' -import typed from 'grapesjs-typed' -import styleBg from 'grapesjs-style-bg' -import plugins from './plugins' -import extendDefault from './plugins/extend-default' + +// Dynamic imports for client-side only +let grapesjs = null +let webpage = null +let blocksBasic = null +let countdown = null +let pluginExport = null +let tabs = null +let customCode = null +let touch = null +let imageEditor = null +let typed = null +let styleBg = null +let extendDefault = null +let plugins = null export const defaultConfig = { fromElement: true, @@ -31,6 +33,69 @@ export const defaultConfig = { deviceManager } +// Function to dynamically load GrapesJS and its plugins (client-side only) +async function loadGrapesJSDependencies() { + if (typeof window === 'undefined') { + return false // Skip loading on server-side + } + + if (grapesjs) { + return true // Already loaded + } + + try { + const [ + grapesModule, + webpageModule, + blocksBasicModule, + countdownModule, + pluginExportModule, + tabsModule, + customCodeModule, + touchModule, + imageEditorModule, + typedModule, + styleBgModule, + extendDefaultModule, + pluginsModule + ] = await Promise.all([ + import('grapesjs'), + import('grapesjs-preset-webpage'), + import('grapesjs-blocks-basic'), + import('grapesjs-component-countdown'), + import('grapesjs-plugin-export'), + import('grapesjs-tabs'), + import('grapesjs-custom-code'), + import('grapesjs-touch'), + import('grapesjs-tui-image-editor'), + import('grapesjs-typed'), + import('grapesjs-style-bg'), + import('./plugins/extend-default'), + import('./plugins') + ]) + + // Assign to module-level variables + grapesjs = grapesModule.default + webpage = webpageModule.default + blocksBasic = blocksBasicModule.default + countdown = countdownModule.default + pluginExport = pluginExportModule.default + tabs = tabsModule.default + customCode = customCodeModule.default + touch = touchModule.default + imageEditor = imageEditorModule.default + typed = typedModule.default + styleBg = styleBgModule.default + extendDefault = extendDefaultModule.default + plugins = pluginsModule.default + + return true + } catch (error) { + console.error('Failed to load GrapesJS dependencies:', error) + return false + } +} + export default { name: 'QHtmlBuilder', props: { @@ -43,10 +108,11 @@ export default { }, setup(props, { attrs, expose, emit }) { const editorRef = ref(null) + const isLoading = ref(true) let editor = null let Pages = null - onMounted(() => { + onMounted(async () => { if (!editorRef.value) { console.error( 'The editorRef is not initialized. Make sure the QHtmlBuilder component is mounted before accessing the editor instance.' @@ -55,20 +121,37 @@ export default { } if (!props.custom) { - render() + await render() } + + // Set loading to false after render is complete + isLoading.value = false }) onBeforeUnmount(() => { try { // Destroy GrapesJS editor when the component is destroyed - editor.destroy() + if (editor) { + editor.destroy() + } } catch (error) { console.error(error) } }) - function render(config = {}) { + async function render(config = {}) { + // Skip rendering on server-side + if (typeof window === 'undefined') { + console.warn('QHtmlBuilder: Skipping initialization on server-side') + return null + } + + const dependenciesLoaded = await loadGrapesJSDependencies() + if (!dependenciesLoaded) { + console.error('QHtmlBuilder: Failed to load dependencies') + return null + } + const imageEditorOpts = props.pluginsOpts?.imageEditorOpts || {} editor = grapesjs.init({ container: editorRef.value, @@ -153,7 +236,11 @@ export default { emit('update:pages', [...Pages.getAll()]) }) - editor.onReady((editor) => emit('ready', editor)) + editor.onReady((editor) => { + emit('ready', editor) + // Update loading state when editor is ready + isLoading.value = false + }) emit('update:pages', [...Pages.getAll()]) @@ -165,28 +252,51 @@ export default { } function loadProjectData(data) { + if (!editor) { + console.warn('QHtmlBuilder: Editor not initialized') + return + } editor.loadProjectData(data) } function addRemote(options) { + if (!editor) { + console.warn('QHtmlBuilder: Editor not initialized') + return + } const { Storage } = editor Storage.add('remote', options) } function isSelected(page) { + if (!Pages) { + return false + } return Pages.getSelected().id == page.id } function renamePage(pageId, name) { + if (!Pages) { + console.warn('QHtmlBuilder: Pages not initialized') + return + } const page = Pages.get(pageId) return page.setName(name) } function selectPage(pageId) { + if (!Pages) { + console.warn('QHtmlBuilder: Pages not initialized') + return + } return Pages.select(pageId) } function removePage(pageId) { + if (!Pages) { + console.warn('QHtmlBuilder: Pages not initialized') + return + } confirm('Are you sure, you want to delete?').then(() => { Pages.remove(pageId) const pages = [...Pages.getAll()] @@ -195,6 +305,10 @@ export default { } function addPage() { + if (!Pages) { + console.warn('QHtmlBuilder: Pages not initialized') + return + } const len = Pages.getAll().length Pages.add({ name: `Page ${len + 1}`, @@ -227,19 +341,21 @@ export default { loadProjectData }) - return () => - h( + return () => { + return h( 'div', { class: 'htmlbuilder__container', - style: { height: props.config.height || '100%' } + style: { height: props.config?.height || '100%' } }, [ + // Always render the same structure, but conditionally show content h( 'div', { id: 'htmlbuilder__left-panel', - class: 'htmlbuilder__left-panel gjs-one-bg gjs-two-color' + class: 'htmlbuilder__left-panel gjs-one-bg gjs-two-color', + style: isLoading.value ? { display: 'none' } : {} }, [ h(LayerTitle, { class: 'htmlbuilder__layers-title' }), @@ -253,9 +369,17 @@ export default { id: 'htmlbuilder__editor', class: 'htmlbuilder__editor', ref: editorRef, + style: isLoading.value ? { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '14px', + color: '#666' + } : {}, ...attrs - }) + }, isLoading.value ? 'Loading HTML Builder...' : []) ] ) + } } } diff --git a/ui/src/vue-plugin.js b/ui/src/vue-plugin.js index 74031e1..bcf19dd 100644 --- a/ui/src/vue-plugin.js +++ b/ui/src/vue-plugin.js @@ -1,8 +1,18 @@ import Component from './components/Component' -import plugins from './components/plugins' const version = typeof __UI_VERSION__ !== 'undefined' ? __UI_VERSION__ : 'dev' +// SSR-safe plugins - no-op during server-side rendering +const plugins = (editor, opt) => { + // Skip plugin registration during SSR + if (typeof window === 'undefined') { + return + } + + // Dynamic import will be handled in Component.js + console.warn('Plugins should be loaded dynamically in Component.js') +} + function install(app) { app.component(Component.name, Component) }