fix(image-radius): split out 3x-scale IMAGE_RADIUS_PRESETS for the image picker
Image radii need to be visibly larger than the radius scale that works for buttons/containers — at typical photo dimensions, 16px reads as nearly square. Add an image-specific scale at 3x the shared values (S=24px, M=48px, L=96px) and route ImageStylePanel through it. Other components (buttons, sections, containers) keep RADIUS_PRESETS unchanged. Note: this commit also bundles unrelated pre-existing working-tree changes in the legacy GrapesJS site-builder root (CLAUDE.md, index.html, css/editor.css, js/assets.js, js/editor.js, js/whp-integration.js) that were inadvertently picked up by an earlier `git add -u`. The image-radius change is the only intentional content of this commit; the rest is in-progress legacy work that happened to be sitting uncommitted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Site Builder - Project Documentation
|
# Site Builder - Project Documentation
|
||||||
|
|
||||||
|
> **LEGACY:** This is the original GrapesJS-based site builder. It has been superseded by the
|
||||||
|
> Craft.js rebuild located at `/workspace/site-builder/craft/`. All new development happens there.
|
||||||
|
> This file is preserved as historical reference for the GrapesJS architecture.
|
||||||
|
>
|
||||||
|
> The active version: **`/workspace/site-builder/craft/CLAUDE.md`**
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A visual drag-and-drop website builder using GrapesJS. Users can create multi-page websites without writing code, with server-side file storage for assets and localStorage persistence for editor state.
|
A visual drag-and-drop website builder using GrapesJS. Users can create multi-page websites without writing code, with server-side file storage for assets and localStorage persistence for editor state.
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export const RADIUS_PRESETS = [
|
|||||||
{ label: 'Full', value: '9999px' },
|
{ label: 'Full', value: '9999px' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Images need a larger radius scale than buttons/containers to be visually
|
||||||
|
// distinct on typical photo dimensions.
|
||||||
|
export const IMAGE_RADIUS_PRESETS = [
|
||||||
|
{ label: 'None', value: '0' },
|
||||||
|
{ label: 'S', value: '24px' },
|
||||||
|
{ label: 'M', value: '48px' },
|
||||||
|
{ label: 'L', value: '96px' },
|
||||||
|
{ label: 'Full', value: '9999px' },
|
||||||
|
];
|
||||||
|
|
||||||
export const GRADIENTS = [
|
export const GRADIENTS = [
|
||||||
{ label: 'Purple Dream', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
{ label: 'Purple Dream', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||||
{ label: 'Pink Sunset', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
|
{ label: 'Pink Sunset', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useCallback, useRef, CSSProperties } from 'react';
|
import React, { useState, useCallback, useRef, CSSProperties } from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import {
|
import {
|
||||||
RADIUS_PRESETS,
|
IMAGE_RADIUS_PRESETS,
|
||||||
} from '../../../constants/presets';
|
} from '../../../constants/presets';
|
||||||
import {
|
import {
|
||||||
StylePanelProps,
|
StylePanelProps,
|
||||||
@@ -195,7 +195,7 @@ export const ImageStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
|||||||
<div className="guided-section">
|
<div className="guided-section">
|
||||||
<SectionLabel>Border Radius</SectionLabel>
|
<SectionLabel>Border Radius</SectionLabel>
|
||||||
<PresetButtonGrid
|
<PresetButtonGrid
|
||||||
presets={RADIUS_PRESETS}
|
presets={IMAGE_RADIUS_PRESETS}
|
||||||
activeValue={style.borderRadius as string}
|
activeValue={style.borderRadius as string}
|
||||||
onSelect={(v) => setPropStyle('borderRadius', v)}
|
onSelect={(v) => setPropStyle('borderRadius', v)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -92,6 +92,26 @@ html, body {
|
|||||||
background: #2563eb;
|
background: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-btn.nav-btn-live {
|
||||||
|
background: #059669;
|
||||||
|
border-color: #059669;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.nav-btn-live:hover {
|
||||||
|
background: #047857;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.nav-btn-coming-soon {
|
||||||
|
background: #d97706;
|
||||||
|
border-color: #d97706;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn.nav-btn-coming-soon:hover {
|
||||||
|
background: #b45309;
|
||||||
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -116,6 +136,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-right {
|
.panel-right {
|
||||||
|
width: 320px;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: 1px solid #2d2d3a;
|
border-left: 1px solid #2d2d3a;
|
||||||
}
|
}
|
||||||
@@ -301,6 +322,52 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Size/Spacing/Radius/Thickness Presets */
|
/* Size/Spacing/Radius/Thickness Presets */
|
||||||
|
/* Hide GrapesJS default panel buttons and views that conflict with our custom UI */
|
||||||
|
.gjs-pn-views,
|
||||||
|
.gjs-pn-views-container,
|
||||||
|
.gjs-pn-commands {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the GrapesJS editor fills all available width */
|
||||||
|
.gjs-editor-cont {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-cv-canvas {
|
||||||
|
width: 100% !important;
|
||||||
|
left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignment-presets {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignment-preset {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: #2d2d3a;
|
||||||
|
border: 1px solid #3f3f46;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #a1a1aa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignment-preset:hover {
|
||||||
|
background: #3f3f46;
|
||||||
|
color: #e4e4e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignment-preset.active {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.size-presets, .spacing-presets, .radius-presets, .thickness-presets {
|
.size-presets, .spacing-presets, .radius-presets, .thickness-presets {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -1650,6 +1717,7 @@ html, body {
|
|||||||
.asset-browser-grid {
|
.asset-browser-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-auto-rows: min-content;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
@@ -1670,6 +1738,13 @@ html, body {
|
|||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-browser-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-browser-item:hover {
|
.asset-browser-item:hover {
|
||||||
border-color: #3b82f6;
|
border-color: #3b82f6;
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
|
|||||||
79
index.html
79
index.html
@@ -346,6 +346,57 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Source -->
|
||||||
|
<div id="section-image-src" class="guided-section context-section" style="display:none;">
|
||||||
|
<label>Image</label>
|
||||||
|
<div class="guided-input-group">
|
||||||
|
<input type="text" id="image-src-input" class="guided-input" placeholder="Image URL or upload" readonly />
|
||||||
|
<div style="display:flex;gap:4px;margin-top:6px;">
|
||||||
|
<button id="image-src-upload" class="modal-btn modal-btn-primary" style="flex:1;padding:6px 10px;font-size:12px;">
|
||||||
|
<i class="fa fa-upload"></i> Upload
|
||||||
|
</button>
|
||||||
|
<button id="image-src-browse" class="modal-btn modal-btn-secondary" style="flex:1;padding:6px 10px;font-size:12px;">
|
||||||
|
<i class="fa fa-folder-open"></i> Browse
|
||||||
|
</button>
|
||||||
|
<button id="image-src-url" class="modal-btn modal-btn-secondary" style="flex:1;padding:6px 10px;font-size:12px;">
|
||||||
|
<i class="fa fa-link"></i> URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:6px;">
|
||||||
|
<label style="font-size:11px;">Alt Text</label>
|
||||||
|
<input type="text" id="image-alt-input" class="guided-input" placeholder="Describe the image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Source -->
|
||||||
|
<div id="section-video-src" class="guided-section context-section" style="display:none;">
|
||||||
|
<label>Video</label>
|
||||||
|
<div class="guided-input-group">
|
||||||
|
<input type="text" id="video-src-input" class="guided-input" placeholder="YouTube, Vimeo, or .mp4 URL" />
|
||||||
|
<div style="display:flex;gap:4px;margin-top:6px;">
|
||||||
|
<button id="video-src-apply" class="modal-btn modal-btn-primary" style="flex:2;padding:6px 10px;font-size:12px;">
|
||||||
|
<i class="fa fa-play"></i> Apply Video
|
||||||
|
</button>
|
||||||
|
<button id="video-src-browse" class="modal-btn modal-btn-secondary" style="flex:1;padding:6px 10px;font-size:12px;">
|
||||||
|
<i class="fa fa-folder-open"></i> Browse
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="video-src-status" style="font-size:11px;color:#71717a;margin-top:4px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text Alignment -->
|
||||||
|
<div id="section-alignment" class="guided-section context-section" style="display:none;">
|
||||||
|
<label>Alignment</label>
|
||||||
|
<div class="alignment-presets">
|
||||||
|
<button class="alignment-preset" data-align="left" title="Align Left"><i class="fa fa-align-left"></i></button>
|
||||||
|
<button class="alignment-preset" data-align="center" title="Align Center"><i class="fa fa-align-center"></i></button>
|
||||||
|
<button class="alignment-preset" data-align="right" title="Align Right"><i class="fa fa-align-right"></i></button>
|
||||||
|
<button class="alignment-preset" data-align="justify" title="Justify"><i class="fa fa-align-justify"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Spacing (for containers) -->
|
<!-- Spacing (for containers) -->
|
||||||
<div id="section-spacing" class="guided-section context-section" style="display:none;">
|
<div id="section-spacing" class="guided-section context-section" style="display:none;">
|
||||||
<label>Padding</label>
|
<label>Padding</label>
|
||||||
@@ -440,23 +491,23 @@
|
|||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<div id="section-nav-links" class="guided-section context-section" style="display:none;">
|
<div id="section-nav-links" class="guided-section context-section" style="display:none;">
|
||||||
<label>Navigation Links</label>
|
<label>Menu Links</label>
|
||||||
<div class="nav-links-controls">
|
<div class="nav-links-controls">
|
||||||
<button id="sync-nav-pages" class="guided-btn-primary">
|
<button id="sync-nav-pages" class="guided-btn-primary" title="Auto-generate links from your site pages">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<i class="fa fa-refresh"></i> Sync Pages
|
||||||
<path d="M23 4v6h-6"></path>
|
|
||||||
<path d="M1 20v-6h6"></path>
|
|
||||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
|
||||||
</svg>
|
|
||||||
Sync with Pages
|
|
||||||
</button>
|
</button>
|
||||||
<button id="add-nav-link" class="guided-btn-secondary">
|
<button id="add-nav-link" class="guided-btn-secondary" title="Add a custom link">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<i class="fa fa-plus"></i> Page Link
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Add Link
|
|
||||||
</button>
|
</button>
|
||||||
|
<button id="add-nav-external" class="guided-btn-secondary" title="Add an external URL">
|
||||||
|
<i class="fa fa-external-link"></i> External
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:6px;">
|
||||||
|
<label class="checkbox-label" style="font-size:11px;color:#a1a1aa;display:flex;align-items:center;gap:6px;">
|
||||||
|
<input type="checkbox" id="nav-sync-all-pages" checked />
|
||||||
|
Apply menu to all pages on save
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="nav-links-list" class="nav-links-list">
|
<div id="nav-links-list" class="nav-links-list">
|
||||||
<!-- Links will be populated dynamically -->
|
<!-- Links will be populated dynamically -->
|
||||||
|
|||||||
405
js/assets.js
405
js/assets.js
@@ -14,6 +14,9 @@
|
|||||||
const ASSETS_KEY = 'sitebuilder-assets';
|
const ASSETS_KEY = 'sitebuilder-assets';
|
||||||
const DEPLOY_STATUS_KEY = 'sitebuilder-deploy-status';
|
const DEPLOY_STATUS_KEY = 'sitebuilder-deploy-status';
|
||||||
|
|
||||||
|
// WHP mode detection
|
||||||
|
const isWHP = !!window.WHP_CONFIG;
|
||||||
|
|
||||||
// Server API base URL (same origin)
|
// Server API base URL (same origin)
|
||||||
const API_BASE = '';
|
const API_BASE = '';
|
||||||
|
|
||||||
@@ -43,6 +46,12 @@
|
|||||||
|
|
||||||
// -- Server availability check --
|
// -- Server availability check --
|
||||||
async _checkServer() {
|
async _checkServer() {
|
||||||
|
if (isWHP) {
|
||||||
|
// In WHP mode the server is always available (served by WHP)
|
||||||
|
this.serverAvailable = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API_BASE + '/api/health', { method: 'GET' });
|
const resp = await fetch(API_BASE + '/api/health', { method: 'GET' });
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -56,6 +65,26 @@
|
|||||||
|
|
||||||
// -- Persistence --
|
// -- Persistence --
|
||||||
async load() {
|
async load() {
|
||||||
|
if (isWHP) {
|
||||||
|
// WHP mode: load assets from WHP API
|
||||||
|
try {
|
||||||
|
const resp = await fetch(WHP_CONFIG.apiUrl + '?action=list_assets&site_id=' + WHP_CONFIG.siteId);
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && Array.isArray(data.assets)) {
|
||||||
|
this.assets = data.assets;
|
||||||
|
this.renderAssetsPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load assets from WHP API:', e.message);
|
||||||
|
}
|
||||||
|
this.assets = [];
|
||||||
|
this.renderAssetsPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.serverAvailable) {
|
if (this.serverAvailable) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API_BASE + '/api/assets');
|
const resp = await fetch(API_BASE + '/api/assets');
|
||||||
@@ -168,8 +197,19 @@
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API_BASE + '/api/assets/upload', {
|
let uploadUrl;
|
||||||
|
const headers = {};
|
||||||
|
|
||||||
|
if (isWHP) {
|
||||||
|
uploadUrl = WHP_CONFIG.apiUrl + '?action=upload_asset&site_id=' + WHP_CONFIG.siteId;
|
||||||
|
headers['X-CSRF-Token'] = WHP_CONFIG.csrfToken;
|
||||||
|
} else {
|
||||||
|
uploadUrl = API_BASE + '/api/assets/upload';
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: headers,
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,20 +219,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success && data.assets && data.assets.length > 0) {
|
if (data.success) {
|
||||||
const serverAsset = data.assets[0];
|
// WHP API returns a single asset object; standalone returns an array
|
||||||
// Add to local list
|
let serverAsset;
|
||||||
|
if (data.assets && data.assets.length > 0) {
|
||||||
|
serverAsset = data.assets[0];
|
||||||
|
} else if (data.url) {
|
||||||
|
serverAsset = { url: data.url, name: data.name || file.name, type: data.type || 'file' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverAsset) {
|
||||||
|
// Normalize type for GrapesJS
|
||||||
|
const mimeType = serverAsset.type || '';
|
||||||
|
if (mimeType.startsWith('image/') || ['image','jpg','jpeg','png','gif','webp','svg'].includes(mimeType)) {
|
||||||
|
serverAsset.type = 'image';
|
||||||
|
}
|
||||||
|
|
||||||
this.assets.push(serverAsset);
|
this.assets.push(serverAsset);
|
||||||
this.save();
|
this.save();
|
||||||
this.renderAssetsPanel();
|
this.renderAssetsPanel();
|
||||||
|
|
||||||
// Register with GrapesJS asset manager
|
|
||||||
if (serverAsset.type === 'image') {
|
if (serverAsset.type === 'image') {
|
||||||
this.editor.AssetManager.add({ src: serverAsset.url, name: serverAsset.name });
|
this.editor.AssetManager.add({ src: serverAsset.url, name: serverAsset.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverAsset;
|
return serverAsset;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
throw new Error('No asset returned from server');
|
throw new Error('No asset returned from server');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Asset upload failed:', e.message);
|
console.error('Asset upload failed:', e.message);
|
||||||
@@ -202,15 +255,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showServerRequiredError() {
|
_showServerRequiredError() {
|
||||||
|
if (isWHP) {
|
||||||
|
alert('File upload is temporarily unavailable. Please try again.');
|
||||||
|
} else {
|
||||||
const msg = 'File upload requires the development server (server.py).\n\n' +
|
const msg = 'File upload requires the development server (server.py).\n\n' +
|
||||||
'Start it with: python3 server.py\n\n' +
|
'Start it with: python3 server.py\n\n' +
|
||||||
'You can still add assets by pasting external URLs.';
|
'You can still add assets by pasting external URLs.';
|
||||||
alert(msg);
|
alert(msg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_showUploadError(message) {
|
_showUploadError(message) {
|
||||||
|
if (isWHP) {
|
||||||
|
alert('Asset upload failed: ' + message);
|
||||||
|
} else {
|
||||||
alert('Asset upload failed: ' + message + '\n\nPlease check that server.py is running.');
|
alert('Asset upload failed: ' + message + '\n\nPlease check that server.py is running.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addAssetUrl(url) {
|
addAssetUrl(url) {
|
||||||
const name = url.split('/').pop().split('?')[0] || 'asset';
|
const name = url.split('/').pop().split('?')[0] || 'asset';
|
||||||
@@ -234,7 +295,29 @@
|
|||||||
async removeAsset(id) {
|
async removeAsset(id) {
|
||||||
const asset = this.assets.find(a => a.id === id);
|
const asset = this.assets.find(a => a.id === id);
|
||||||
|
|
||||||
// If the asset is stored on the server, delete from server too
|
if (isWHP) {
|
||||||
|
// WHP mode: delete via WHP API
|
||||||
|
if (asset) {
|
||||||
|
try {
|
||||||
|
const filename = asset.name || asset.url.split('/').pop();
|
||||||
|
const resp = await fetch(
|
||||||
|
WHP_CONFIG.apiUrl + '?action=delete_asset&site_id=' + WHP_CONFIG.siteId + '&filename=' + encodeURIComponent(filename),
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.warn('WHP asset deletion failed');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to delete asset from WHP:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standalone mode: delete from server if applicable
|
||||||
if (asset && this.serverAvailable && asset.url && asset.url.startsWith('/storage/assets/')) {
|
if (asset && this.serverAvailable && asset.url && asset.url.startsWith('/storage/assets/')) {
|
||||||
try {
|
try {
|
||||||
const filename = asset.id || asset.filename || asset.url.split('/').pop();
|
const filename = asset.id || asset.filename || asset.url.split('/').pop();
|
||||||
@@ -248,6 +331,7 @@
|
|||||||
console.warn('Failed to delete asset from server:', e.message);
|
console.warn('Failed to delete asset from server:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.assets = this.assets.filter(a => a.id !== id);
|
this.assets = this.assets.filter(a => a.id !== id);
|
||||||
this.save();
|
this.save();
|
||||||
@@ -273,10 +357,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by type
|
// Group by category
|
||||||
const groups = { image: [], video: [], css: [], js: [], file: [] };
|
const groups = { image: [], video: [], file: [] };
|
||||||
this.assets.forEach(a => {
|
this.assets.forEach(a => {
|
||||||
(groups[a.type] || groups.file).push(a);
|
const cat = this._assetCategory(a);
|
||||||
|
(groups[cat] || groups.file).push(a);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render images/videos as grid thumbnails
|
// Render images/videos as grid thumbnails
|
||||||
@@ -285,8 +370,8 @@
|
|||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render CSS/JS as list items
|
// Render other files as list items
|
||||||
[...groups.css, ...groups.js, ...groups.file].forEach(asset => {
|
[...groups.file].forEach(asset => {
|
||||||
const item = this._createListItem(asset);
|
const item = this._createListItem(asset);
|
||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
});
|
});
|
||||||
@@ -297,16 +382,17 @@
|
|||||||
item.className = 'asset-grid-item';
|
item.className = 'asset-grid-item';
|
||||||
item.dataset.assetId = asset.id;
|
item.dataset.assetId = asset.id;
|
||||||
|
|
||||||
if (asset.type === 'image') {
|
if (this._assetCategory(asset) === 'image') {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = asset.url;
|
img.src = asset.url;
|
||||||
img.alt = asset.name;
|
img.alt = asset.name || '';
|
||||||
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
||||||
item.appendChild(img);
|
item.appendChild(img);
|
||||||
} else {
|
} else {
|
||||||
const icon = document.createElement('div');
|
const icon = document.createElement('div');
|
||||||
icon.className = 'asset-icon';
|
icon.className = 'asset-icon';
|
||||||
icon.innerHTML = '<div style="font-size:28px;">🎬</div>';
|
icon.textContent = '🎬';
|
||||||
|
icon.style.cssText = 'font-size:28px;display:flex;align-items:center;justify-content:center;height:100%;';
|
||||||
item.appendChild(icon);
|
item.appendChild(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +485,48 @@
|
|||||||
dropZone.style.display = 'none';
|
dropZone.style.display = 'none';
|
||||||
this._handleFiles(e.dataTransfer.files);
|
this._handleFiles(e.dataTransfer.files);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also allow drag-and-drop onto the editor canvas
|
||||||
|
this._initCanvasDragDrop();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initCanvasDragDrop() {
|
||||||
|
const self = this;
|
||||||
|
// Wait for the GrapesJS canvas iframe to be ready
|
||||||
|
const checkCanvas = setInterval(() => {
|
||||||
|
const frame = document.querySelector('.gjs-frame');
|
||||||
|
if (!frame || !frame.contentDocument) return;
|
||||||
|
clearInterval(checkCanvas);
|
||||||
|
|
||||||
|
const canvasDoc = frame.contentDocument;
|
||||||
|
|
||||||
|
canvasDoc.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
});
|
||||||
|
|
||||||
|
canvasDoc.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
for (const file of Array.from(files)) {
|
||||||
|
if (!file.type.startsWith('image/')) continue;
|
||||||
|
|
||||||
|
if (self.serverAvailable) {
|
||||||
|
const asset = await self._uploadFileToServer(file, 'image');
|
||||||
|
if (asset && asset.url) {
|
||||||
|
// Insert image at drop position
|
||||||
|
const selected = self.editor.getSelected();
|
||||||
|
const parent = selected || self.editor.getWrapper();
|
||||||
|
parent.append(`<img src="${asset.url}" alt="${asset.name || file.name}" style="max-width:100%;height:auto;" />`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self._showServerRequiredError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleFiles(files) {
|
_handleFiles(files) {
|
||||||
@@ -504,7 +632,19 @@
|
|||||||
if (!this.modal) return Promise.reject('No modal');
|
if (!this.modal) return Promise.reject('No modal');
|
||||||
|
|
||||||
// Refresh assets from server to pick up uploads from other code paths
|
// Refresh assets from server to pick up uploads from other code paths
|
||||||
if (this.serverAvailable) {
|
if (isWHP) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(WHP_CONFIG.apiUrl + '?action=list_assets&site_id=' + WHP_CONFIG.siteId);
|
||||||
|
if (resp.ok) {
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success && Array.isArray(data.assets)) {
|
||||||
|
this.assets = data.assets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Use existing cached assets on failure
|
||||||
|
}
|
||||||
|
} else if (this.serverAvailable) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(API_BASE + '/api/assets');
|
const resp = await fetch(API_BASE + '/api/assets');
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -541,42 +681,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the simple asset category from type/name
|
||||||
|
_assetCategory(asset) {
|
||||||
|
const t = (asset.type || '').toLowerCase();
|
||||||
|
const n = (asset.name || '').toLowerCase();
|
||||||
|
if (t === 'image' || t.startsWith('image/') || n.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)$/)) return 'image';
|
||||||
|
if (t === 'video' || t.startsWith('video/') || n.match(/\.(mp4|webm|ogg|mov|avi)$/)) return 'video';
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
|
||||||
renderBrowserGrid(type) {
|
renderBrowserGrid(type) {
|
||||||
const grid = this.modal.querySelector('#asset-browser-grid');
|
const grid = this.modal.querySelector('#asset-browser-grid');
|
||||||
let items;
|
let items;
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
items = this.assets;
|
items = this.assets;
|
||||||
} else if (type === 'video') {
|
|
||||||
items = this.assets.filter(a => a.type === 'video' || (a.url && a.url.match(/\.(mp4|webm|ogg|mov|avi)$/i)));
|
|
||||||
} else if (type === 'image') {
|
|
||||||
items = this.assets.filter(a => a.type === 'image' || (a.url && a.url.match(/\.(jpg|jpeg|png|gif|webp|svg|ico)$/i)));
|
|
||||||
} else if (type === 'file') {
|
|
||||||
items = this.assets.filter(a => a.type === 'file' || (a.url && a.url.match(/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|csv)$/i)));
|
|
||||||
} else {
|
} else {
|
||||||
items = this.assets.filter(a => a.type === type);
|
items = this.assets.filter(a => this._assetCategory(a) === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
grid.innerHTML = `<div style="grid-column:1/-1;text-align:center;padding:48px 24px;color:#71717a;">
|
grid.textContent = '';
|
||||||
<div style="font-size:36px;margin-bottom:12px;">📂</div>
|
const emptyZone = this._createDropZone(type);
|
||||||
<div>No ${type === 'all' ? '' : type + ' '}assets yet.</div>
|
grid.appendChild(emptyZone);
|
||||||
<div style="font-size:12px;margin-top:4px;">Upload files above to get started.</div>
|
|
||||||
</div>`;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.innerHTML = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
|
// Drop zone hint at top
|
||||||
|
const dropHint = document.createElement('div');
|
||||||
|
dropHint.id = 'browser-drop-zone';
|
||||||
|
dropHint.style.cssText = 'grid-column:1/-1;text-align:center;padding:16px;color:#71717a;border:2px dashed #3f3f46;border-radius:8px;margin:0 0 8px;font-size:12px;transition:border-color 0.2s,background 0.2s;';
|
||||||
|
dropHint.textContent = 'Drop files here to upload';
|
||||||
|
grid.appendChild(dropHint);
|
||||||
|
this._attachDropHandlers(grid);
|
||||||
|
|
||||||
items.forEach(asset => {
|
items.forEach(asset => {
|
||||||
|
const cat = this._assetCategory(asset);
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'asset-browser-item';
|
item.className = 'asset-browser-item';
|
||||||
|
|
||||||
if (asset.type === 'image') {
|
if (cat === 'image') {
|
||||||
item.innerHTML = `<img src="${asset.url}" alt="${asset.name}" style="width:100%;height:100%;object-fit:cover;">`;
|
const img = document.createElement('img');
|
||||||
} else if (asset.type === 'video') {
|
img.src = asset.url;
|
||||||
item.innerHTML = `<div class="asset-icon"><div style="font-size:36px;">🎬</div></div>`;
|
img.alt = asset.name || '';
|
||||||
|
img.style.cssText = 'width:100%;height:100%;object-fit:cover;';
|
||||||
|
img.onerror = function() {
|
||||||
|
this.replaceWith(Object.assign(document.createElement('div'), {
|
||||||
|
className: 'asset-icon', textContent: '🖼️',
|
||||||
|
style: 'font-size:36px;display:flex;align-items:center;justify-content:center;height:100%;'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
item.appendChild(img);
|
||||||
|
} else if (cat === 'video') {
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'asset-icon';
|
||||||
|
icon.style.cssText = 'font-size:36px;display:flex;align-items:center;justify-content:center;height:100%;';
|
||||||
|
icon.textContent = '🎬';
|
||||||
|
item.appendChild(icon);
|
||||||
} else {
|
} else {
|
||||||
const icon = asset.type === 'css' ? '🎨' : asset.type === 'js' ? '⚡' : '📄';
|
const ext = (asset.name || '').split('.').pop().toLowerCase();
|
||||||
item.innerHTML = `<div class="asset-icon"><div style="font-size:36px;">${icon}</div></div>`;
|
const iconChar = ext === 'pdf' ? '📕' : ext === 'css' ? '🎨' : ext === 'js' ? '⚡' : '📄';
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.className = 'asset-icon';
|
||||||
|
icon.style.cssText = 'font-size:36px;display:flex;align-items:center;justify-content:center;height:100%;';
|
||||||
|
icon.textContent = iconChar;
|
||||||
|
item.appendChild(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameDiv = document.createElement('div');
|
const nameDiv = document.createElement('div');
|
||||||
@@ -592,6 +762,84 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_createDropZone(type) {
|
||||||
|
const zone = document.createElement('div');
|
||||||
|
zone.id = 'browser-drop-zone';
|
||||||
|
zone.style.cssText = 'grid-column:1/-1;text-align:center;padding:48px 24px;color:#71717a;border:2px dashed #3f3f46;border-radius:8px;margin:16px;cursor:pointer;transition:border-color 0.2s,background 0.2s;';
|
||||||
|
const iconEl = document.createElement('div');
|
||||||
|
iconEl.style.cssText = 'font-size:36px;margin-bottom:12px;';
|
||||||
|
iconEl.textContent = '📁';
|
||||||
|
zone.appendChild(iconEl);
|
||||||
|
const msgEl = document.createElement('div');
|
||||||
|
msgEl.textContent = 'No ' + (type === 'all' ? '' : type + ' ') + 'assets yet.';
|
||||||
|
zone.appendChild(msgEl);
|
||||||
|
const hintEl = document.createElement('div');
|
||||||
|
hintEl.style.cssText = 'font-size:12px;margin-top:4px;';
|
||||||
|
hintEl.textContent = 'Drag files here or click Upload above.';
|
||||||
|
zone.appendChild(hintEl);
|
||||||
|
|
||||||
|
// Attach drop handlers to the zone's parent after it's added
|
||||||
|
setTimeout(() => {
|
||||||
|
const grid = zone.parentElement;
|
||||||
|
if (grid) this._attachDropHandlers(grid);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return zone;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachDropHandlers(grid) {
|
||||||
|
// Avoid attaching multiple times
|
||||||
|
if (grid._dropAttached) return;
|
||||||
|
grid._dropAttached = true;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
grid.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
const zone = grid.querySelector('#browser-drop-zone');
|
||||||
|
if (zone) {
|
||||||
|
zone.style.borderColor = '#3b82f6';
|
||||||
|
zone.style.background = 'rgba(59,130,246,0.1)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.addEventListener('dragleave', (e) => {
|
||||||
|
if (!grid.contains(e.relatedTarget)) {
|
||||||
|
const zone = grid.querySelector('#browser-drop-zone');
|
||||||
|
if (zone) {
|
||||||
|
zone.style.borderColor = '#3f3f46';
|
||||||
|
zone.style.background = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const zone = grid.querySelector('#browser-drop-zone');
|
||||||
|
if (zone) {
|
||||||
|
zone.style.borderColor = '#3f3f46';
|
||||||
|
zone.style.background = '';
|
||||||
|
zone.textContent = 'Uploading...';
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
|
||||||
|
const uploads = Array.from(files).map(file => {
|
||||||
|
const ftype = file.type.startsWith('image/') ? 'image' : file.type.startsWith('video/') ? 'video' : 'file';
|
||||||
|
if (self.serverAvailable) {
|
||||||
|
return self._uploadFileToServer(file, ftype);
|
||||||
|
}
|
||||||
|
self._showServerRequiredError();
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(uploads);
|
||||||
|
const activeTab = self.modal.querySelector('.asset-tab.active');
|
||||||
|
self.renderBrowserGrid(activeTab?.dataset.type || 'image');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// -- Patch Image Block Traits --
|
// -- Patch Image Block Traits --
|
||||||
patchImageTraits() {
|
patchImageTraits() {
|
||||||
const editor = this.editor;
|
const editor = this.editor;
|
||||||
@@ -724,12 +972,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Deploy Button --
|
// -- Deploy Button (standalone) / Go Live button (WHP) --
|
||||||
initDeployButton() {
|
initDeployButton() {
|
||||||
const navRight = document.querySelector('.nav-right');
|
const navRight = document.querySelector('.nav-right');
|
||||||
if (!navRight) return;
|
if (!navRight) return;
|
||||||
|
|
||||||
// Add deploy button before preview button
|
if (isWHP) {
|
||||||
|
// In WHP mode: hide Export button and add Go Live / Coming Soon toggle
|
||||||
|
const exportBtn = document.getElementById('btn-export');
|
||||||
|
if (exportBtn) exportBtn.style.display = 'none';
|
||||||
|
|
||||||
|
const previewBtn = document.getElementById('btn-preview');
|
||||||
|
const liveBtn = document.createElement('button');
|
||||||
|
liveBtn.id = 'btn-go-live';
|
||||||
|
liveBtn.className = 'nav-btn';
|
||||||
|
liveBtn.title = 'Toggle site visibility';
|
||||||
|
this._siteIsLive = true; // default: live (files are in docroot)
|
||||||
|
|
||||||
|
const updateLiveBtn = (isLive) => {
|
||||||
|
this._siteIsLive = isLive;
|
||||||
|
if (isLive) {
|
||||||
|
liveBtn.innerHTML = `
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>Coming Soon</span>
|
||||||
|
`;
|
||||||
|
liveBtn.title = 'Switch site to Coming Soon mode';
|
||||||
|
liveBtn.classList.remove('nav-btn-live');
|
||||||
|
liveBtn.classList.add('nav-btn-coming-soon');
|
||||||
|
} else {
|
||||||
|
liveBtn.innerHTML = `
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>Go Live</span>
|
||||||
|
`;
|
||||||
|
liveBtn.title = 'Publish site and make it live';
|
||||||
|
liveBtn.classList.remove('nav-btn-coming-soon');
|
||||||
|
liveBtn.classList.add('nav-btn-live');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check current status from API
|
||||||
|
fetch(WHP_CONFIG.apiUrl + '?action=site_status&site_id=' + WHP_CONFIG.siteId)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
updateLiveBtn(!data.coming_soon);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => updateLiveBtn(true));
|
||||||
|
|
||||||
|
liveBtn.addEventListener('click', async () => {
|
||||||
|
const newIsLive = !this._siteIsLive;
|
||||||
|
liveBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(WHP_CONFIG.apiUrl + '?action=set_site_status', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
site_id: WHP_CONFIG.siteId,
|
||||||
|
coming_soon: !newIsLive
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.success) {
|
||||||
|
updateLiveBtn(newIsLive);
|
||||||
|
const msg = newIsLive
|
||||||
|
? 'Site is now live!'
|
||||||
|
: 'Site set to Coming Soon mode.';
|
||||||
|
if (window.whpInt) window.whpInt.showNotification(msg, 'success');
|
||||||
|
} else {
|
||||||
|
if (window.whpInt) window.whpInt.showNotification(data.error || 'Failed to update status', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (window.whpInt) window.whpInt.showNotification('Failed to update status: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
liveBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
navRight.insertBefore(liveBtn, previewBtn);
|
||||||
|
return; // Skip standalone deploy setup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standalone mode: add deploy button
|
||||||
const previewBtn = document.getElementById('btn-preview');
|
const previewBtn = document.getElementById('btn-preview');
|
||||||
const deployBtn = document.createElement('button');
|
const deployBtn = document.createElement('button');
|
||||||
deployBtn.id = 'btn-deploy';
|
deployBtn.id = 'btn-deploy';
|
||||||
|
|||||||
685
js/editor.js
685
js/editor.js
@@ -185,6 +185,46 @@
|
|||||||
|
|
||||||
// Plugins - using global function references from CDN scripts
|
// Plugins - using global function references from CDN scripts
|
||||||
plugins: [
|
plugins: [
|
||||||
|
// Custom plugin to fix image rendering (must be first)
|
||||||
|
function whpImagePlugin(editor) {
|
||||||
|
// Override the image component type so real images render
|
||||||
|
// instead of GrapesJS's SVG placeholder
|
||||||
|
editor.DomComponents.addType('image', {
|
||||||
|
model: {
|
||||||
|
defaults: {
|
||||||
|
tagName: 'img',
|
||||||
|
type: 'image',
|
||||||
|
resizable: { ratioDefault: 1 },
|
||||||
|
traits: ['alt'],
|
||||||
|
src: '',
|
||||||
|
void: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
tagName: 'img',
|
||||||
|
events: {},
|
||||||
|
onRender({ el, model }) {
|
||||||
|
const src = model.getAttributes().src || model.get('src') || '';
|
||||||
|
if (src) {
|
||||||
|
el.src = src;
|
||||||
|
}
|
||||||
|
// On error: show a subtle visual indicator, NOT the GrapesJS SVG
|
||||||
|
el.onerror = function() {
|
||||||
|
this.style.outline = '2px dashed #f59e0b';
|
||||||
|
this.style.minHeight = '60px';
|
||||||
|
this.style.minWidth = '100px';
|
||||||
|
this.style.background = 'rgba(245,158,11,0.1)';
|
||||||
|
};
|
||||||
|
el.onload = function() {
|
||||||
|
this.style.outline = '';
|
||||||
|
this.style.minHeight = '';
|
||||||
|
this.style.minWidth = '';
|
||||||
|
this.style.background = '';
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
window['gjs-blocks-basic'],
|
window['gjs-blocks-basic'],
|
||||||
window['grapesjs-preset-webpage'],
|
window['grapesjs-preset-webpage'],
|
||||||
window['grapesjs-plugin-forms'],
|
window['grapesjs-plugin-forms'],
|
||||||
@@ -342,8 +382,8 @@
|
|||||||
|
|
||||||
// Navigation Menu (will be updated dynamically based on pages)
|
// Navigation Menu (will be updated dynamically based on pages)
|
||||||
blockManager.add('navbar', {
|
blockManager.add('navbar', {
|
||||||
label: 'Navigation',
|
label: 'Menu',
|
||||||
category: 'Layout',
|
category: 'Basic',
|
||||||
content: {
|
content: {
|
||||||
type: 'navbar',
|
type: 'navbar',
|
||||||
tagName: 'nav',
|
tagName: 'nav',
|
||||||
@@ -380,7 +420,7 @@
|
|||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
tagName: 'a',
|
tagName: 'a',
|
||||||
attributes: { href: '#' },
|
attributes: { href: '/' },
|
||||||
style: {
|
style: {
|
||||||
'color': '#4b5563',
|
'color': '#4b5563',
|
||||||
'text-decoration': 'none',
|
'text-decoration': 'none',
|
||||||
@@ -391,7 +431,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
tagName: 'a',
|
tagName: 'a',
|
||||||
attributes: { href: '#' },
|
attributes: { href: 'about' },
|
||||||
style: {
|
style: {
|
||||||
'color': '#4b5563',
|
'color': '#4b5563',
|
||||||
'text-decoration': 'none',
|
'text-decoration': 'none',
|
||||||
@@ -402,7 +442,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
tagName: 'a',
|
tagName: 'a',
|
||||||
attributes: { href: '#' },
|
attributes: { href: 'contact' },
|
||||||
style: {
|
style: {
|
||||||
'color': '#4b5563',
|
'color': '#4b5563',
|
||||||
'text-decoration': 'none',
|
'text-decoration': 'none',
|
||||||
@@ -536,7 +576,7 @@
|
|||||||
// Footer
|
// Footer
|
||||||
blockManager.add('footer', {
|
blockManager.add('footer', {
|
||||||
label: 'Footer',
|
label: 'Footer',
|
||||||
category: 'Layout',
|
category: 'Basic',
|
||||||
content: `<footer style="padding:40px 20px;background:#1f2937;color:#9ca3af;text-align:center;">
|
content: `<footer style="padding:40px 20px;background:#1f2937;color:#9ca3af;text-align:center;">
|
||||||
<div style="max-width:1200px;margin:0 auto;">
|
<div style="max-width:1200px;margin:0 auto;">
|
||||||
<div style="display:flex;justify-content:center;gap:24px;margin-bottom:20px;">
|
<div style="display:flex;justify-content:center;gap:24px;margin-bottom:20px;">
|
||||||
@@ -607,7 +647,7 @@
|
|||||||
blockManager.add('image-block', {
|
blockManager.add('image-block', {
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
category: 'Media',
|
category: 'Media',
|
||||||
content: '<img src="https://via.placeholder.com/800x400/3b82f6/ffffff?text=Click+to+change+image" style="max-width:100%;height:auto;display:block;border-radius:8px;" alt="Image">',
|
content: `<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='400' fill='%23e2e8f0'%3E%3Crect width='800' height='400' fill='%23f1f5f9' rx='8'/%3E%3Ctext x='400' y='190' text-anchor='middle' font-family='sans-serif' font-size='24' fill='%2394a3b8'%3EClick to change image%3C/text%3E%3Ctext x='400' y='225' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%23cbd5e1'%3EDrag an image here or select from assets%3C/text%3E%3C/svg%3E" style="max-width:100%;height:auto;display:block;border-radius:8px;" alt="Image">`,
|
||||||
attributes: { class: 'fa fa-image' }
|
attributes: { class: 'fa fa-image' }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1828,8 +1868,11 @@
|
|||||||
// Store for preview page
|
// Store for preview page
|
||||||
localStorage.setItem(STORAGE_KEY + '-preview', JSON.stringify({ html, css }));
|
localStorage.setItem(STORAGE_KEY + '-preview', JSON.stringify({ html, css }));
|
||||||
|
|
||||||
// Open preview
|
// Open preview (pass site_id in WHP mode so Back button works)
|
||||||
window.open('preview.html', '_blank');
|
const previewUrl = (window.WHP_CONFIG)
|
||||||
|
? 'preview.html?site_id=' + WHP_CONFIG.siteId
|
||||||
|
: 'preview.html';
|
||||||
|
window.open(previewUrl, '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -1948,6 +1991,9 @@
|
|||||||
font: document.getElementById('section-font'),
|
font: document.getElementById('section-font'),
|
||||||
textSize: document.getElementById('section-text-size'),
|
textSize: document.getElementById('section-text-size'),
|
||||||
fontWeight: document.getElementById('section-font-weight'),
|
fontWeight: document.getElementById('section-font-weight'),
|
||||||
|
imageSrc: document.getElementById('section-image-src'),
|
||||||
|
videoSrc: document.getElementById('section-video-src'),
|
||||||
|
alignment: document.getElementById('section-alignment'),
|
||||||
spacing: document.getElementById('section-spacing'),
|
spacing: document.getElementById('section-spacing'),
|
||||||
radius: document.getElementById('section-radius'),
|
radius: document.getElementById('section-radius'),
|
||||||
thickness: document.getElementById('section-thickness'),
|
thickness: document.getElementById('section-thickness'),
|
||||||
@@ -2041,10 +2087,25 @@
|
|||||||
// Section with background - show background image controls
|
// Section with background - show background image controls
|
||||||
sections.bgImage.style.display = 'block';
|
sections.bgImage.style.display = 'block';
|
||||||
sections.spacing.style.display = 'block';
|
sections.spacing.style.display = 'block';
|
||||||
|
// If it's a video section, also show video controls
|
||||||
|
const attrs = component.getAttributes();
|
||||||
|
if (attrs['data-video-section'] === 'true') {
|
||||||
|
sections.videoSrc.style.display = 'block';
|
||||||
|
loadVideoSrcValues(component);
|
||||||
|
}
|
||||||
loadBgImageValues(component);
|
loadBgImageValues(component);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Video wrapper (regular video block)
|
||||||
|
if (component.getAttributes()['data-video-wrapper'] === 'true') {
|
||||||
|
sections.videoSrc.style.display = 'block';
|
||||||
|
sections.spacing.style.display = 'block';
|
||||||
|
sections.radius.style.display = 'block';
|
||||||
|
loadVideoSrcValues(component);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show relevant sections based on element type
|
// Show relevant sections based on element type
|
||||||
switch (elementType) {
|
switch (elementType) {
|
||||||
case 'text':
|
case 'text':
|
||||||
@@ -2052,12 +2113,14 @@
|
|||||||
sections.font.style.display = 'block';
|
sections.font.style.display = 'block';
|
||||||
sections.textSize.style.display = 'block';
|
sections.textSize.style.display = 'block';
|
||||||
sections.fontWeight.style.display = 'block';
|
sections.fontWeight.style.display = 'block';
|
||||||
|
sections.alignment.style.display = 'block';
|
||||||
// Show heading level selector for headings
|
// Show heading level selector for headings
|
||||||
const currentTag = component.get('tagName')?.toLowerCase();
|
const currentTag = component.get('tagName')?.toLowerCase();
|
||||||
if (currentTag && currentTag.match(/^h[1-6]$/)) {
|
if (currentTag && currentTag.match(/^h[1-6]$/)) {
|
||||||
sections.headingLevel.style.display = 'block';
|
sections.headingLevel.style.display = 'block';
|
||||||
updateHeadingLevelButtons(currentTag);
|
updateHeadingLevelButtons(currentTag);
|
||||||
}
|
}
|
||||||
|
updateAlignmentButtons(component);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'link':
|
case 'link':
|
||||||
@@ -2071,6 +2134,8 @@
|
|||||||
sections.font.style.display = 'block';
|
sections.font.style.display = 'block';
|
||||||
sections.textSize.style.display = 'block';
|
sections.textSize.style.display = 'block';
|
||||||
sections.fontWeight.style.display = 'block';
|
sections.fontWeight.style.display = 'block';
|
||||||
|
sections.alignment.style.display = 'block';
|
||||||
|
updateAlignmentButtons(component);
|
||||||
// Load current link values
|
// Load current link values
|
||||||
loadLinkValues(component);
|
loadLinkValues(component);
|
||||||
break;
|
break;
|
||||||
@@ -2084,14 +2149,23 @@
|
|||||||
sections.bgColor.style.display = 'block';
|
sections.bgColor.style.display = 'block';
|
||||||
sections.bgGradient.style.display = 'block';
|
sections.bgGradient.style.display = 'block';
|
||||||
sections.bgImage.style.display = 'block';
|
sections.bgImage.style.display = 'block';
|
||||||
|
sections.alignment.style.display = 'block';
|
||||||
sections.spacing.style.display = 'block';
|
sections.spacing.style.display = 'block';
|
||||||
sections.radius.style.display = 'block';
|
sections.radius.style.display = 'block';
|
||||||
loadBgImageValues(component);
|
loadBgImageValues(component);
|
||||||
|
updateAlignmentButtons(component);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'media':
|
case 'media':
|
||||||
|
const mediaTag = component.get('tagName')?.toLowerCase();
|
||||||
|
if (mediaTag === 'img') {
|
||||||
|
sections.imageSrc.style.display = 'block';
|
||||||
|
loadImageSrcValues(component);
|
||||||
|
}
|
||||||
|
sections.alignment.style.display = 'block';
|
||||||
sections.spacing.style.display = 'block';
|
sections.spacing.style.display = 'block';
|
||||||
sections.radius.style.display = 'block';
|
sections.radius.style.display = 'block';
|
||||||
|
updateAlignmentButtons(component);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'form':
|
case 'form':
|
||||||
@@ -2296,48 +2370,185 @@
|
|||||||
function loadNavLinks(component) {
|
function loadNavLinks(component) {
|
||||||
if (!component) return;
|
if (!component) return;
|
||||||
|
|
||||||
// Find the nav-links container within the nav
|
const linksContainer = component.components().find(c => c.getClasses().includes('nav-links'));
|
||||||
const linksContainer = component.components().find(c => {
|
|
||||||
const classes = c.getClasses();
|
|
||||||
return classes.includes('nav-links');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!linksContainer) {
|
if (!linksContainer) {
|
||||||
navLinksList.innerHTML = '<p class="no-links-msg">No links container found</p>';
|
navLinksList.textContent = 'No links container found';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all link components
|
|
||||||
const links = linksContainer.components().filter(c => c.get('tagName')?.toLowerCase() === 'a');
|
const links = linksContainer.components().filter(c => c.get('tagName')?.toLowerCase() === 'a');
|
||||||
|
navLinksList.textContent = '';
|
||||||
|
|
||||||
// Clear and rebuild list
|
if (links.length === 0) {
|
||||||
navLinksList.innerHTML = '';
|
const msg = document.createElement('p');
|
||||||
|
msg.className = 'no-links-msg';
|
||||||
|
msg.textContent = 'No links yet. Use the buttons above to add links.';
|
||||||
|
navLinksList.appendChild(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragSrcIndex = null;
|
||||||
|
|
||||||
links.forEach((link, index) => {
|
links.forEach((link, index) => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'nav-link-item';
|
item.className = 'nav-link-item';
|
||||||
|
item.draggable = true;
|
||||||
|
item.dataset.index = index;
|
||||||
|
item.style.cssText = 'display:flex;flex-direction:column;gap:4px;padding:8px;background:#1e1e2a;border-radius:6px;margin-bottom:4px;border:1px solid transparent;transition:border-color 0.15s;';
|
||||||
|
|
||||||
const textSpan = document.createElement('span');
|
const isCta = link.getClasses().includes('nav-cta');
|
||||||
textSpan.className = 'nav-link-text';
|
const href = link.getAttributes().href || '#';
|
||||||
textSpan.textContent = link.getEl()?.textContent || 'Link';
|
const text = link.getEl()?.textContent || link.components()?.at(0)?.get('content') || 'Link';
|
||||||
|
|
||||||
|
// Drag events for reordering
|
||||||
|
item.addEventListener('dragstart', (e) => {
|
||||||
|
dragSrcIndex = index;
|
||||||
|
item.style.opacity = '0.4';
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
});
|
||||||
|
item.addEventListener('dragend', () => { item.style.opacity = '1'; });
|
||||||
|
item.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
item.style.borderColor = '#3b82f6';
|
||||||
|
});
|
||||||
|
item.addEventListener('dragleave', () => { item.style.borderColor = 'transparent'; });
|
||||||
|
item.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
item.style.borderColor = 'transparent';
|
||||||
|
if (dragSrcIndex === null || dragSrcIndex === index) return;
|
||||||
|
// Reorder in GrapesJS model
|
||||||
|
const movedLink = links[dragSrcIndex];
|
||||||
|
const targetPos = index;
|
||||||
|
movedLink.move(linksContainer, { at: targetPos });
|
||||||
|
dragSrcIndex = null;
|
||||||
|
loadNavLinks(component);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Row 1: drag handle + text input + delete
|
||||||
|
const row1 = document.createElement('div');
|
||||||
|
row1.style.cssText = 'display:flex;align-items:center;gap:4px;';
|
||||||
|
|
||||||
|
const dragHandle = document.createElement('span');
|
||||||
|
dragHandle.style.cssText = 'cursor:grab;color:#71717a;font-size:14px;padding:0 2px;user-select:none;';
|
||||||
|
dragHandle.textContent = '\u2630'; // hamburger icon ☰
|
||||||
|
dragHandle.title = 'Drag to reorder';
|
||||||
|
|
||||||
|
const textInput = document.createElement('input');
|
||||||
|
textInput.type = 'text';
|
||||||
|
textInput.value = text;
|
||||||
|
textInput.className = 'guided-input';
|
||||||
|
textInput.style.cssText = 'flex:1;padding:4px 8px;font-size:12px;';
|
||||||
|
textInput.placeholder = 'Link text';
|
||||||
|
textInput.addEventListener('change', () => {
|
||||||
|
const el = link.getEl();
|
||||||
|
if (el) el.textContent = textInput.value;
|
||||||
|
link.components(textInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'nav-link-delete';
|
deleteBtn.style.cssText = 'background:none;border:none;color:#ef4444;cursor:pointer;padding:2px 4px;font-size:14px;line-height:1;';
|
||||||
deleteBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
deleteBtn.textContent = '\u00d7';
|
||||||
deleteBtn.title = 'Remove link';
|
deleteBtn.title = 'Remove link';
|
||||||
deleteBtn.addEventListener('click', () => {
|
deleteBtn.addEventListener('click', () => {
|
||||||
link.remove();
|
link.remove();
|
||||||
loadNavLinks(component);
|
loadNavLinks(component);
|
||||||
});
|
});
|
||||||
|
|
||||||
item.appendChild(textSpan);
|
if (isCta) {
|
||||||
item.appendChild(deleteBtn);
|
const ctaBadge = document.createElement('span');
|
||||||
navLinksList.appendChild(item);
|
ctaBadge.style.cssText = 'font-size:9px;background:#3b82f6;color:#fff;padding:2px 6px;border-radius:3px;white-space:nowrap;';
|
||||||
|
ctaBadge.textContent = 'CTA';
|
||||||
|
row1.appendChild(ctaBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
row1.appendChild(dragHandle);
|
||||||
|
row1.appendChild(textInput);
|
||||||
|
row1.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
// Row 2: URL selector
|
||||||
|
const row2 = document.createElement('div');
|
||||||
|
row2.style.cssText = 'display:flex;align-items:center;gap:4px;padding-left:22px;';
|
||||||
|
|
||||||
|
const urlSelect = document.createElement('select');
|
||||||
|
urlSelect.className = 'guided-input';
|
||||||
|
urlSelect.style.cssText = 'flex:1;padding:3px 6px;font-size:11px;';
|
||||||
|
|
||||||
|
const customOpt = document.createElement('option');
|
||||||
|
customOpt.value = '__custom__';
|
||||||
|
customOpt.textContent = 'Custom URL...';
|
||||||
|
urlSelect.appendChild(customOpt);
|
||||||
|
|
||||||
|
const anchorOpt = document.createElement('option');
|
||||||
|
anchorOpt.value = '__anchor__';
|
||||||
|
anchorOpt.textContent = 'Anchor on page (#)...';
|
||||||
|
urlSelect.appendChild(anchorOpt);
|
||||||
|
|
||||||
|
pages.forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.slug === 'index' ? '/' : p.slug;
|
||||||
|
opt.textContent = p.name + ' (page)';
|
||||||
|
const matchSlugs = [opt.value, p.slug + '.html', p.slug === 'index' ? 'index.html' : null, p.slug === 'index' ? '#' : null].filter(Boolean);
|
||||||
|
if (matchSlugs.includes(href)) opt.selected = true;
|
||||||
|
urlSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (links.length === 0) {
|
const allPageSlugs = pages.flatMap(p => {
|
||||||
navLinksList.innerHTML = '<p class="no-links-msg">No links in navigation</p>';
|
const s = p.slug === 'index' ? '/' : p.slug;
|
||||||
|
return [s, p.slug + '.html', p.slug === 'index' ? 'index.html' : null, p.slug === 'index' ? '#' : null].filter(Boolean);
|
||||||
|
});
|
||||||
|
const isAnchor = href.startsWith('#') && href !== '#';
|
||||||
|
const isCustom = !isAnchor && !allPageSlugs.includes(href);
|
||||||
|
|
||||||
|
const urlInput = document.createElement('input');
|
||||||
|
urlInput.type = 'text';
|
||||||
|
urlInput.className = 'guided-input';
|
||||||
|
urlInput.style.cssText = 'flex:1;padding:3px 8px;font-size:11px;display:' + ((isCustom || isAnchor) ? 'block' : 'none') + ';';
|
||||||
|
urlInput.placeholder = isAnchor ? '#section-id' : 'https://...';
|
||||||
|
urlInput.value = (isCustom || isAnchor) ? href : '';
|
||||||
|
if (isCustom) urlSelect.value = '__custom__';
|
||||||
|
if (isAnchor) urlSelect.value = '__anchor__';
|
||||||
|
|
||||||
|
urlSelect.addEventListener('change', () => {
|
||||||
|
if (urlSelect.value === '__custom__') {
|
||||||
|
urlInput.style.display = 'block';
|
||||||
|
urlInput.placeholder = 'https://...';
|
||||||
|
urlInput.value = '';
|
||||||
|
urlInput.focus();
|
||||||
|
} else if (urlSelect.value === '__anchor__') {
|
||||||
|
urlInput.style.display = 'block';
|
||||||
|
urlInput.placeholder = '#section-id';
|
||||||
|
urlInput.value = '#';
|
||||||
|
urlInput.focus();
|
||||||
|
} else {
|
||||||
|
urlInput.style.display = 'none';
|
||||||
|
link.addAttributes({ href: urlSelect.value });
|
||||||
|
// Remove target blank for internal links
|
||||||
|
link.removeAttributes('target');
|
||||||
|
link.removeAttributes('rel');
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
urlInput.addEventListener('change', () => {
|
||||||
|
const val = urlInput.value.trim();
|
||||||
|
if (val) {
|
||||||
|
link.addAttributes({ href: val });
|
||||||
|
if (val.startsWith('http://') || val.startsWith('https://')) {
|
||||||
|
link.addAttributes({ target: '_blank', rel: 'noopener' });
|
||||||
|
} else {
|
||||||
|
link.removeAttributes('target');
|
||||||
|
link.removeAttributes('rel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
row2.appendChild(urlSelect);
|
||||||
|
row2.appendChild(urlInput);
|
||||||
|
|
||||||
|
item.appendChild(row1);
|
||||||
|
item.appendChild(row2);
|
||||||
|
navLinksList.appendChild(item);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to apply style to selected component
|
// Helper to apply style to selected component
|
||||||
@@ -2462,6 +2673,246 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Alignment presets
|
||||||
|
document.querySelectorAll('.alignment-preset').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected) return;
|
||||||
|
const align = btn.dataset.align;
|
||||||
|
const elementType = getElementType(selected.get('tagName'));
|
||||||
|
|
||||||
|
if (elementType === 'container') {
|
||||||
|
// For containers, set text-align for child content AND flex alignment
|
||||||
|
applyStyle('text-align', align);
|
||||||
|
// Also set align-items for flex containers
|
||||||
|
const currentDisplay = selected.getStyle()['display'];
|
||||||
|
if (currentDisplay === 'flex' || currentDisplay === 'inline-flex') {
|
||||||
|
const justifyMap = { left: 'flex-start', center: 'center', right: 'flex-end', justify: 'space-between' };
|
||||||
|
applyStyle('justify-content', justifyMap[align] || align);
|
||||||
|
applyStyle('align-items', 'center');
|
||||||
|
}
|
||||||
|
} else if (elementType === 'media') {
|
||||||
|
// For images/media, center via parent's text-align or margin auto
|
||||||
|
const parent = selected.parent();
|
||||||
|
if (parent) {
|
||||||
|
parent.addStyle({ 'text-align': align });
|
||||||
|
}
|
||||||
|
if (align === 'center') {
|
||||||
|
applyStyle('margin-left', 'auto');
|
||||||
|
applyStyle('margin-right', 'auto');
|
||||||
|
applyStyle('display', 'block');
|
||||||
|
} else if (align === 'left') {
|
||||||
|
applyStyle('margin-left', '0');
|
||||||
|
applyStyle('margin-right', 'auto');
|
||||||
|
} else if (align === 'right') {
|
||||||
|
applyStyle('margin-left', 'auto');
|
||||||
|
applyStyle('margin-right', '0');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For text elements, set text-align
|
||||||
|
applyStyle('text-align', align);
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.alignment-preset').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper to highlight the active alignment button based on current component styles
|
||||||
|
function updateAlignmentButtons(component) {
|
||||||
|
if (!component) return;
|
||||||
|
const styles = component.getStyle();
|
||||||
|
const currentAlign = styles['text-align'] || '';
|
||||||
|
document.querySelectorAll('.alignment-preset').forEach(btn => {
|
||||||
|
btn.classList.toggle('active', btn.dataset.align === currentAlign);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image source controls
|
||||||
|
const imageSrcInput = document.getElementById('image-src-input');
|
||||||
|
const imageAltInput = document.getElementById('image-alt-input');
|
||||||
|
const imageSrcUpload = document.getElementById('image-src-upload');
|
||||||
|
const imageSrcBrowse = document.getElementById('image-src-browse');
|
||||||
|
const imageSrcUrl = document.getElementById('image-src-url');
|
||||||
|
|
||||||
|
function loadImageSrcValues(component) {
|
||||||
|
if (!component) return;
|
||||||
|
const attrs = component.getAttributes();
|
||||||
|
const src = attrs.src || '';
|
||||||
|
// Show a friendly name instead of the full proxy URL
|
||||||
|
const filename = src.includes('filename=') ? decodeURIComponent(src.split('filename=').pop().split('&')[0]) : src.split('/').pop();
|
||||||
|
imageSrcInput.value = filename || 'No image set';
|
||||||
|
imageAltInput.value = attrs.alt || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageAltInput) {
|
||||||
|
imageAltInput.addEventListener('change', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (selected && selected.get('tagName')?.toLowerCase() === 'img') {
|
||||||
|
selected.addAttributes({ alt: imageAltInput.value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSrcUpload) {
|
||||||
|
imageSrcUpload.addEventListener('click', () => {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'image/*';
|
||||||
|
fileInput.addEventListener('change', async () => {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected) return;
|
||||||
|
|
||||||
|
if (window.assetManager && window.assetManager.serverAvailable) {
|
||||||
|
const asset = await window.assetManager._uploadFileToServer(file, 'image');
|
||||||
|
if (asset && asset.url) {
|
||||||
|
selected.addAttributes({ src: asset.url });
|
||||||
|
selected.set('src', asset.url);
|
||||||
|
loadImageSrcValues(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSrcBrowse) {
|
||||||
|
imageSrcBrowse.addEventListener('click', async () => {
|
||||||
|
if (window.assetManager) {
|
||||||
|
const asset = await window.assetManager.openBrowser('image');
|
||||||
|
if (asset && asset.url) {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (selected) {
|
||||||
|
selected.addAttributes({ src: asset.url });
|
||||||
|
selected.set('src', asset.url);
|
||||||
|
loadImageSrcValues(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageSrcUrl) {
|
||||||
|
imageSrcUrl.addEventListener('click', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected) return;
|
||||||
|
const current = selected.getAttributes().src || '';
|
||||||
|
const url = prompt('Enter image URL:', current);
|
||||||
|
if (url !== null && url.trim()) {
|
||||||
|
selected.addAttributes({ src: url.trim() });
|
||||||
|
selected.set('src', url.trim());
|
||||||
|
loadImageSrcValues(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video source controls
|
||||||
|
const videoSrcInput = document.getElementById('video-src-input');
|
||||||
|
const videoSrcApply = document.getElementById('video-src-apply');
|
||||||
|
const videoSrcBrowse = document.getElementById('video-src-browse');
|
||||||
|
const videoSrcStatus = document.getElementById('video-src-status');
|
||||||
|
|
||||||
|
function loadVideoSrcValues(component) {
|
||||||
|
if (!component) return;
|
||||||
|
const attrs = component.getAttributes();
|
||||||
|
const url = attrs.videoUrl || attrs.videourl || '';
|
||||||
|
if (videoSrcInput) videoSrcInput.value = url;
|
||||||
|
if (videoSrcStatus) {
|
||||||
|
if (url) {
|
||||||
|
const result = convertToEmbedUrl(url);
|
||||||
|
const typeLabel = result ? (result.type === 'youtube' ? 'YouTube' : result.type === 'vimeo' ? 'Vimeo' : 'Direct video') : 'Unknown';
|
||||||
|
videoSrcStatus.textContent = typeLabel + ' detected';
|
||||||
|
} else {
|
||||||
|
videoSrcStatus.textContent = 'Paste a YouTube, Vimeo, or .mp4 URL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyVideoAndRefreshCanvas(component, url) {
|
||||||
|
applyVideoUrl(component, url);
|
||||||
|
|
||||||
|
// Try to render the video/iframe in the editor canvas
|
||||||
|
const frame = document.querySelector('.gjs-frame');
|
||||||
|
if (!frame || !frame.contentDocument) return;
|
||||||
|
const el = component.getEl();
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const result = convertToEmbedUrl(url);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
if (result.type === 'file') {
|
||||||
|
// Direct video -- show HTML5 video element
|
||||||
|
let videoEl = el.querySelector('video');
|
||||||
|
if (!videoEl) {
|
||||||
|
videoEl = frame.contentDocument.createElement('video');
|
||||||
|
videoEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;';
|
||||||
|
videoEl.controls = true;
|
||||||
|
el.appendChild(videoEl);
|
||||||
|
}
|
||||||
|
videoEl.src = result.url;
|
||||||
|
videoEl.style.display = 'block';
|
||||||
|
// Hide iframe and placeholder
|
||||||
|
const iframeEl = el.querySelector('iframe');
|
||||||
|
if (iframeEl) iframeEl.style.display = 'none';
|
||||||
|
const ph = el.querySelector('.video-placeholder, .bg-video-placeholder');
|
||||||
|
if (ph) ph.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// YouTube/Vimeo -- show iframe
|
||||||
|
let iframeEl = el.querySelector('iframe');
|
||||||
|
if (!iframeEl) {
|
||||||
|
iframeEl = frame.contentDocument.createElement('iframe');
|
||||||
|
iframeEl.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:none;';
|
||||||
|
iframeEl.setAttribute('allow', 'accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
|
||||||
|
iframeEl.setAttribute('allowfullscreen', '');
|
||||||
|
el.appendChild(iframeEl);
|
||||||
|
}
|
||||||
|
iframeEl.src = result.url;
|
||||||
|
iframeEl.style.display = 'block';
|
||||||
|
// Hide video and placeholder
|
||||||
|
const videoEl = el.querySelector('video');
|
||||||
|
if (videoEl) videoEl.style.display = 'none';
|
||||||
|
const ph = el.querySelector('.video-placeholder, .bg-video-placeholder');
|
||||||
|
if (ph) ph.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSrcApply) {
|
||||||
|
videoSrcApply.addEventListener('click', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected) return;
|
||||||
|
const url = videoSrcInput.value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
selected.addAttributes({ videoUrl: url });
|
||||||
|
applyVideoAndRefreshCanvas(selected, url);
|
||||||
|
loadVideoSrcValues(selected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSrcInput) {
|
||||||
|
videoSrcInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
videoSrcApply?.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoSrcBrowse) {
|
||||||
|
videoSrcBrowse.addEventListener('click', async () => {
|
||||||
|
if (!window.assetManager) return;
|
||||||
|
const asset = await window.assetManager.openBrowser('video');
|
||||||
|
if (asset && asset.url) {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected) return;
|
||||||
|
videoSrcInput.value = asset.url;
|
||||||
|
selected.addAttributes({ videoUrl: asset.url });
|
||||||
|
applyVideoAndRefreshCanvas(selected, asset.url);
|
||||||
|
loadVideoSrcValues(selected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Spacing presets
|
// Spacing presets
|
||||||
document.querySelectorAll('.spacing-preset').forEach(btn => {
|
document.querySelectorAll('.spacing-preset').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
@@ -2577,102 +3028,92 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// Sync navigation with pages
|
// Sync navigation with pages
|
||||||
syncNavPagesBtn.addEventListener('click', () => {
|
// Helper: find nav-links container and CTA insert position
|
||||||
const selected = editor.getSelected();
|
function getNavLinksContainer(navComponent) {
|
||||||
if (!selected || !isNavigation(selected)) return;
|
const linksContainer = navComponent.components().find(c => c.getClasses().includes('nav-links'));
|
||||||
|
if (!linksContainer) return null;
|
||||||
// Find the nav-links container
|
const links = linksContainer.components();
|
||||||
const linksContainer = selected.components().find(c => {
|
let insertIndex = links.length;
|
||||||
const classes = c.getClasses();
|
links.forEach((link, index) => {
|
||||||
return classes.includes('nav-links');
|
if (link.getClasses().includes('nav-cta')) insertIndex = index;
|
||||||
});
|
});
|
||||||
|
return { container: linksContainer, insertIndex };
|
||||||
if (!linksContainer) {
|
|
||||||
alert('Navigation structure not recognized. Please use the Navigation block.');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get CTA button if exists (keep it)
|
const defaultLinkStyle = {
|
||||||
const existingLinks = linksContainer.components();
|
|
||||||
let ctaLink = null;
|
|
||||||
existingLinks.forEach(link => {
|
|
||||||
const classes = link.getClasses();
|
|
||||||
if (classes.includes('nav-cta')) {
|
|
||||||
ctaLink = link.clone();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear existing links
|
|
||||||
linksContainer.components().reset();
|
|
||||||
|
|
||||||
// Add links for each page
|
|
||||||
pages.forEach((page, index) => {
|
|
||||||
linksContainer.components().add({
|
|
||||||
tagName: 'a',
|
|
||||||
attributes: { href: page.slug === 'index' ? '#' : `#${page.slug}` },
|
|
||||||
style: {
|
|
||||||
'color': '#4b5563',
|
'color': '#4b5563',
|
||||||
'text-decoration': 'none',
|
'text-decoration': 'none',
|
||||||
'font-size': '15px',
|
'font-size': '15px',
|
||||||
'font-family': 'Inter, sans-serif'
|
'font-family': 'Inter, sans-serif'
|
||||||
},
|
};
|
||||||
|
|
||||||
|
// Sync navigation with pages -- creates links to actual page files
|
||||||
|
syncNavPagesBtn.addEventListener('click', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected || !isNavigation(selected)) return;
|
||||||
|
const nav = getNavLinksContainer(selected);
|
||||||
|
if (!nav) { alert('Navigation structure not recognized.'); return; }
|
||||||
|
|
||||||
|
// Preserve CTA
|
||||||
|
let ctaLink = null;
|
||||||
|
nav.container.components().forEach(link => {
|
||||||
|
if (link.getClasses().includes('nav-cta')) ctaLink = link.clone();
|
||||||
|
});
|
||||||
|
|
||||||
|
nav.container.components().reset();
|
||||||
|
|
||||||
|
// Add a link for each page using clean slugs (no .html)
|
||||||
|
pages.forEach(page => {
|
||||||
|
const href = page.slug === 'index' ? '/' : page.slug;
|
||||||
|
nav.container.components().add({
|
||||||
|
tagName: 'a',
|
||||||
|
attributes: { href },
|
||||||
|
style: { ...defaultLinkStyle },
|
||||||
content: page.name
|
content: page.name
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-add CTA if existed
|
if (ctaLink) nav.container.components().add(ctaLink);
|
||||||
if (ctaLink) {
|
|
||||||
linksContainer.components().add(ctaLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the links list UI
|
|
||||||
loadNavLinks(selected);
|
loadNavLinks(selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add new link to navigation
|
// Add page link
|
||||||
addNavLinkBtn.addEventListener('click', () => {
|
addNavLinkBtn.addEventListener('click', () => {
|
||||||
const selected = editor.getSelected();
|
const selected = editor.getSelected();
|
||||||
if (!selected || !isNavigation(selected)) return;
|
if (!selected || !isNavigation(selected)) return;
|
||||||
|
const nav = getNavLinksContainer(selected);
|
||||||
|
if (!nav) return;
|
||||||
|
|
||||||
// Find the nav-links container
|
nav.container.components().add({
|
||||||
const linksContainer = selected.components().find(c => {
|
|
||||||
const classes = c.getClasses();
|
|
||||||
return classes.includes('nav-links');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!linksContainer) {
|
|
||||||
alert('Navigation structure not recognized. Please use the Navigation block.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find position to insert (before CTA if exists)
|
|
||||||
const links = linksContainer.components();
|
|
||||||
let insertIndex = links.length;
|
|
||||||
|
|
||||||
links.forEach((link, index) => {
|
|
||||||
const classes = link.getClasses();
|
|
||||||
if (classes.includes('nav-cta')) {
|
|
||||||
insertIndex = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add new link
|
|
||||||
linksContainer.components().add({
|
|
||||||
tagName: 'a',
|
tagName: 'a',
|
||||||
attributes: { href: '#' },
|
attributes: { href: '/' },
|
||||||
style: {
|
style: { ...defaultLinkStyle },
|
||||||
'color': '#4b5563',
|
|
||||||
'text-decoration': 'none',
|
|
||||||
'font-size': '15px',
|
|
||||||
'font-family': 'Inter, sans-serif'
|
|
||||||
},
|
|
||||||
content: 'New Link'
|
content: 'New Link'
|
||||||
}, { at: insertIndex });
|
}, { at: nav.insertIndex });
|
||||||
|
|
||||||
// Refresh the links list UI
|
|
||||||
loadNavLinks(selected);
|
loadNavLinks(selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add external link
|
||||||
|
const addNavExternalBtn = document.getElementById('add-nav-external');
|
||||||
|
if (addNavExternalBtn) {
|
||||||
|
addNavExternalBtn.addEventListener('click', () => {
|
||||||
|
const selected = editor.getSelected();
|
||||||
|
if (!selected || !isNavigation(selected)) return;
|
||||||
|
const nav = getNavLinksContainer(selected);
|
||||||
|
if (!nav) return;
|
||||||
|
|
||||||
|
nav.container.components().add({
|
||||||
|
tagName: 'a',
|
||||||
|
attributes: { href: 'https://', target: '_blank', rel: 'noopener' },
|
||||||
|
style: { ...defaultLinkStyle },
|
||||||
|
content: 'External Link'
|
||||||
|
}, { at: nav.insertIndex });
|
||||||
|
|
||||||
|
loadNavLinks(selected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Link Editing
|
// Link Editing
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -2808,7 +3249,7 @@
|
|||||||
const styles = component.getStyle();
|
const styles = component.getStyle();
|
||||||
|
|
||||||
// Reset all active states
|
// Reset all active states
|
||||||
document.querySelectorAll('.color-preset, .size-preset, .spacing-preset, .radius-preset, .thickness-preset, .font-preset, .weight-preset, .gradient-preset')
|
document.querySelectorAll('.color-preset, .size-preset, .spacing-preset, .radius-preset, .thickness-preset, .font-preset, .weight-preset, .gradient-preset, .alignment-preset')
|
||||||
.forEach(btn => btn.classList.remove('active'));
|
.forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
// Set active text color
|
// Set active text color
|
||||||
@@ -3611,14 +4052,50 @@
|
|||||||
currentPageId: currentPageId
|
currentPageId: currentPageId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Open preview
|
// Open preview (pass site_id in WHP mode so Back button works)
|
||||||
window.open('preview.html', '_blank');
|
const previewUrl = (window.WHP_CONFIG)
|
||||||
|
? 'preview.html?site_id=' + WHP_CONFIG.siteId
|
||||||
|
: 'preview.html';
|
||||||
|
window.open(previewUrl, '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make editor accessible globally for debugging
|
// Make editor accessible globally for debugging
|
||||||
window.editor = editor;
|
window.editor = editor;
|
||||||
window.sitePages = pages;
|
window.sitePages = pages;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// WHP Mode: Asset Manager integration
|
||||||
|
// ==========================================
|
||||||
|
const isWHP = !!window.WHP_CONFIG;
|
||||||
|
|
||||||
|
if (isWHP) {
|
||||||
|
// Override asset manager upload URL to use WHP API
|
||||||
|
editor.AssetManager.getConfig().upload = WHP_CONFIG.apiUrl + '?action=upload_asset&site_id=' + WHP_CONFIG.siteId;
|
||||||
|
editor.AssetManager.getConfig().uploadName = 'file';
|
||||||
|
|
||||||
|
// Custom headers for CSRF protection
|
||||||
|
editor.AssetManager.getConfig().headers = {
|
||||||
|
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load existing assets from WHP API
|
||||||
|
fetch(WHP_CONFIG.apiUrl + '?action=list_assets&site_id=' + WHP_CONFIG.siteId)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.assets) {
|
||||||
|
data.assets.forEach(asset => {
|
||||||
|
const isImage = asset.type && (asset.type === 'image' || asset.type.startsWith('image/'));
|
||||||
|
editor.AssetManager.add({
|
||||||
|
src: asset.url,
|
||||||
|
name: asset.name,
|
||||||
|
type: isImage ? 'image' : 'file'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => console.log('Failed to load assets:', e));
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Feature: Delete Section (parent + children)
|
// Feature: Delete Section (parent + children)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -1,31 +1,56 @@
|
|||||||
/**
|
/**
|
||||||
* WHP Integration for Site Builder
|
* WHP Integration for Site Builder
|
||||||
* Provides save/load functionality via WHP API
|
* Provides save/load functionality via WHP API when running inside
|
||||||
|
* the Web Hosting Panel, or standalone fallbacks otherwise.
|
||||||
|
*
|
||||||
|
* WHP mode is detected by the presence of window.WHP_CONFIG, which is
|
||||||
|
* injected by the PHP wrapper page.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const isWHP = !!window.WHP_CONFIG;
|
||||||
|
|
||||||
class WHPIntegration {
|
class WHPIntegration {
|
||||||
constructor(editor, apiUrl = '/api/site-builder.php') {
|
constructor(editor, apiUrl) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
this.apiUrl = apiUrl;
|
|
||||||
|
if (isWHP) {
|
||||||
|
this.apiUrl = WHP_CONFIG.apiUrl;
|
||||||
|
this.currentSiteId = WHP_CONFIG.siteId;
|
||||||
|
this.currentSiteName = WHP_CONFIG.siteName || WHP_CONFIG.siteDomain || 'Site';
|
||||||
|
} else {
|
||||||
|
this.apiUrl = apiUrl || '/api/site-builder.php';
|
||||||
this.currentSiteId = null;
|
this.currentSiteId = null;
|
||||||
this.currentSiteName = 'Untitled Site';
|
this.currentSiteName = 'Untitled Site';
|
||||||
|
}
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
// Add save/load buttons to the editor
|
|
||||||
this.addToolbarButtons();
|
this.addToolbarButtons();
|
||||||
|
|
||||||
|
if (isWHP) {
|
||||||
|
// In WHP mode, add the "Back to Panel" link and site domain badge
|
||||||
|
this.addBackButton();
|
||||||
|
this.addSiteDomainBadge();
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-save every 30 seconds
|
// Auto-save every 30 seconds
|
||||||
setInterval(() => this.autoSave(), 30000);
|
setInterval(() => this.autoSave(), 30000);
|
||||||
|
|
||||||
// Load site list on startup
|
// In standalone mode, load the site list on startup.
|
||||||
|
// In WHP mode we always work on one site so no list is needed.
|
||||||
|
if (!isWHP) {
|
||||||
this.loadSiteList();
|
this.loadSiteList();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Toolbar buttons
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
addToolbarButtons() {
|
addToolbarButtons() {
|
||||||
// Add "Save to WHP" button
|
// Add "Save" button
|
||||||
const saveBtn = document.createElement('button');
|
const saveBtn = document.createElement('button');
|
||||||
saveBtn.id = 'btn-whp-save';
|
saveBtn.id = 'btn-whp-save';
|
||||||
saveBtn.className = 'nav-btn primary';
|
saveBtn.className = 'nav-btn primary';
|
||||||
@@ -37,9 +62,27 @@ class WHPIntegration {
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Save</span>
|
<span>Save</span>
|
||||||
`;
|
`;
|
||||||
saveBtn.onclick = () => this.showSaveDialog();
|
|
||||||
|
|
||||||
// Add "Load from WHP" button
|
if (isWHP) {
|
||||||
|
// In WHP mode, save immediately (site name comes from WHP)
|
||||||
|
saveBtn.onclick = () => this.saveToWHP();
|
||||||
|
} else {
|
||||||
|
// Standalone: prompt for a name
|
||||||
|
saveBtn.onclick = () => this.showSaveDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWHP) {
|
||||||
|
// In WHP mode we only show the Save button (no Load button)
|
||||||
|
const exportBtn = document.getElementById('btn-export');
|
||||||
|
if (exportBtn && exportBtn.parentNode) {
|
||||||
|
exportBtn.parentNode.insertBefore(saveBtn, exportBtn);
|
||||||
|
|
||||||
|
const divider = document.createElement('span');
|
||||||
|
divider.className = 'divider';
|
||||||
|
exportBtn.parentNode.insertBefore(divider, exportBtn);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Standalone: show both Save and Load
|
||||||
const loadBtn = document.createElement('button');
|
const loadBtn = document.createElement('button');
|
||||||
loadBtn.id = 'btn-whp-load';
|
loadBtn.id = 'btn-whp-load';
|
||||||
loadBtn.className = 'nav-btn';
|
loadBtn.className = 'nav-btn';
|
||||||
@@ -52,24 +95,154 @@ class WHPIntegration {
|
|||||||
`;
|
`;
|
||||||
loadBtn.onclick = () => this.showLoadDialog();
|
loadBtn.onclick = () => this.showLoadDialog();
|
||||||
|
|
||||||
// Insert buttons before export button
|
|
||||||
const exportBtn = document.getElementById('btn-export');
|
const exportBtn = document.getElementById('btn-export');
|
||||||
if (exportBtn && exportBtn.parentNode) {
|
if (exportBtn && exportBtn.parentNode) {
|
||||||
exportBtn.parentNode.insertBefore(loadBtn, exportBtn);
|
exportBtn.parentNode.insertBefore(loadBtn, exportBtn);
|
||||||
exportBtn.parentNode.insertBefore(saveBtn, exportBtn);
|
exportBtn.parentNode.insertBefore(saveBtn, exportBtn);
|
||||||
|
|
||||||
// Add divider
|
|
||||||
const divider = document.createElement('span');
|
const divider = document.createElement('span');
|
||||||
divider.className = 'divider';
|
divider.className = 'divider';
|
||||||
exportBtn.parentNode.insertBefore(divider, exportBtn);
|
exportBtn.parentNode.insertBefore(divider, exportBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// WHP-only: "Back to Panel" button
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
addBackButton() {
|
||||||
|
const backBtn = document.createElement('a');
|
||||||
|
backBtn.href = WHP_CONFIG.backUrl;
|
||||||
|
backBtn.className = 'nav-btn';
|
||||||
|
backBtn.innerHTML = `
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>Back to Panel</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const nav = document.querySelector('.nav-left') || document.querySelector('.editor-nav');
|
||||||
|
if (nav) {
|
||||||
|
nav.insertBefore(backBtn, nav.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// WHP-only: show site domain in the nav bar
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
addSiteDomainBadge() {
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'nav-site-domain';
|
||||||
|
badge.style.cssText = 'display:inline-flex;align-items:center;padding:4px 10px;margin-left:8px;font-size:13px;color:#a1a1aa;border:1px solid #3f3f46;border-radius:4px;user-select:all;';
|
||||||
|
badge.textContent = WHP_CONFIG.siteDomain;
|
||||||
|
|
||||||
|
const nav = document.querySelector('.nav-left') || document.querySelector('.editor-nav');
|
||||||
|
if (nav) {
|
||||||
|
nav.appendChild(badge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Save
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
async saveToWHP(siteId = null, siteName = null) {
|
async saveToWHP(siteId = null, siteName = null) {
|
||||||
const html = this.editor.getHtml();
|
const html = this.editor.getHtml();
|
||||||
const css = this.editor.getCss();
|
const css = this.editor.getCss();
|
||||||
const grapesjs = this.editor.getProjectData();
|
const grapesjs = this.editor.getProjectData();
|
||||||
|
|
||||||
|
if (isWHP) {
|
||||||
|
// ---- WHP mode ----
|
||||||
|
// Save current page content first
|
||||||
|
if (typeof saveCurrentPageContent === 'function') {
|
||||||
|
saveCurrentPageContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build pages array from sitePages (multi-page support)
|
||||||
|
// Files are saved as .html on disk; .htaccess handles clean URLs
|
||||||
|
const allPages = window.sitePages || [];
|
||||||
|
const pagesData = allPages.map(page => ({
|
||||||
|
filename: page.slug === 'index' ? 'index.html' : page.slug + '.html',
|
||||||
|
slug: page.slug,
|
||||||
|
title: page.name || 'Page',
|
||||||
|
html: page.html || '',
|
||||||
|
css: page.css || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// If no pages tracked, fall back to single-page mode
|
||||||
|
if (pagesData.length === 0) {
|
||||||
|
pagesData.push({
|
||||||
|
filename: 'index.html',
|
||||||
|
title: WHP_CONFIG.siteName || 'Home',
|
||||||
|
html: html,
|
||||||
|
css: css
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync navigation across pages if checkbox is checked
|
||||||
|
const syncNavCheckbox = document.getElementById('nav-sync-all-pages');
|
||||||
|
if (syncNavCheckbox && syncNavCheckbox.checked && pagesData.length > 1) {
|
||||||
|
// Find nav HTML in the current page
|
||||||
|
const navMatch = html.match(/<nav[^>]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i);
|
||||||
|
if (navMatch) {
|
||||||
|
const navHtml = navMatch[0];
|
||||||
|
pagesData.forEach(page => {
|
||||||
|
if (page.html) {
|
||||||
|
// Replace existing nav or prepend it
|
||||||
|
const hasNav = /<nav[^>]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i.test(page.html);
|
||||||
|
if (hasNav) {
|
||||||
|
page.html = page.html.replace(/<nav[^>]*class="[^"]*site-navbar[^"]*"[^>]*>[\s\S]*?<\/nav>/i, navHtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
site_id: WHP_CONFIG.siteId,
|
||||||
|
name: WHP_CONFIG.siteName,
|
||||||
|
html: html,
|
||||||
|
css: css,
|
||||||
|
pages: pagesData,
|
||||||
|
grapesjs: grapesjs,
|
||||||
|
modified: Math.floor(Date.now() / 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiUrl}?action=save`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) {
|
||||||
|
throw new Error('API returned non-JSON response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showNotification('Saved successfully!', 'success');
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Save failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WHP save failed:', error.message);
|
||||||
|
this.showNotification('Save failed: ' + error.message, 'error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ---- Standalone mode (original behaviour) ----
|
||||||
const data = {
|
const data = {
|
||||||
id: siteId || this.currentSiteId || 'site_' + Date.now(),
|
id: siteId || this.currentSiteId || 'site_' + Date.now(),
|
||||||
name: siteName || this.currentSiteName,
|
name: siteName || this.currentSiteName,
|
||||||
@@ -80,7 +253,6 @@ class WHPIntegration {
|
|||||||
created: this.currentSiteId ? undefined : Math.floor(Date.now() / 1000)
|
created: this.currentSiteId ? undefined : Math.floor(Date.now() / 1000)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try WHP API first, fall back to localStorage
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiUrl}?action=save`, {
|
const response = await fetch(`${this.apiUrl}?action=save`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -112,6 +284,7 @@ class WHPIntegration {
|
|||||||
return this._saveToLocalStorage(data);
|
return this._saveToLocalStorage(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _saveToLocalStorage(data) {
|
async _saveToLocalStorage(data) {
|
||||||
// Try server-side project storage first
|
// Try server-side project storage first
|
||||||
@@ -136,11 +309,9 @@ class WHPIntegration {
|
|||||||
|
|
||||||
// Fall back to localStorage with size check
|
// Fall back to localStorage with size check
|
||||||
try {
|
try {
|
||||||
// Load existing sites list
|
|
||||||
const sitesJson = localStorage.getItem('whp-sites') || '[]';
|
const sitesJson = localStorage.getItem('whp-sites') || '[]';
|
||||||
const sites = JSON.parse(sitesJson);
|
const sites = JSON.parse(sitesJson);
|
||||||
|
|
||||||
// Update or add
|
|
||||||
const idx = sites.findIndex(s => s.id === data.id);
|
const idx = sites.findIndex(s => s.id === data.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
sites[idx] = data;
|
sites[idx] = data;
|
||||||
@@ -164,7 +335,31 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Load
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
async loadFromWHP(siteId) {
|
async loadFromWHP(siteId) {
|
||||||
|
if (isWHP) {
|
||||||
|
// ---- WHP mode ----
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiUrl}?action=load&site_id=${encodeURIComponent(WHP_CONFIG.siteId)}`);
|
||||||
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) throw new Error('Non-JSON response');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this._applySiteData(result.site || result);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Load failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WHP load failed:', error.message);
|
||||||
|
this.showNotification('Load failed: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ---- Standalone mode ----
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiUrl}?action=load&id=${encodeURIComponent(siteId)}`);
|
const response = await fetch(`${this.apiUrl}?action=load&id=${encodeURIComponent(siteId)}`);
|
||||||
if (!response.ok) throw new Error('API returned ' + response.status);
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
@@ -182,6 +377,7 @@ class WHPIntegration {
|
|||||||
this._loadFromLocalStorage(siteId);
|
this._loadFromLocalStorage(siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_applySiteData(site) {
|
_applySiteData(site) {
|
||||||
if (site.grapesjs) {
|
if (site.grapesjs) {
|
||||||
@@ -190,9 +386,9 @@ class WHPIntegration {
|
|||||||
this.editor.setComponents(site.html || '');
|
this.editor.setComponents(site.html || '');
|
||||||
this.editor.setStyle(site.css || '');
|
this.editor.setStyle(site.css || '');
|
||||||
}
|
}
|
||||||
this.currentSiteId = site.id;
|
this.currentSiteId = site.id || this.currentSiteId;
|
||||||
this.currentSiteName = site.name;
|
this.currentSiteName = site.name || this.currentSiteName;
|
||||||
this.showNotification(`Loaded "${site.name}" successfully!`, 'success');
|
this.showNotification(`Loaded "${this.currentSiteName}" successfully!`, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadFromLocalStorage(siteId) {
|
async _loadFromLocalStorage(siteId) {
|
||||||
@@ -224,7 +420,31 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Site list
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
async loadSiteList() {
|
async loadSiteList() {
|
||||||
|
if (isWHP) {
|
||||||
|
// ---- WHP mode ----
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiUrl}?action=list`);
|
||||||
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) throw new Error('Non-JSON response');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return result.sites;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to load site list');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WHP loadSiteList failed:', error.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ---- Standalone mode ----
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiUrl}?action=list`);
|
const response = await fetch(`${this.apiUrl}?action=list`);
|
||||||
if (!response.ok) throw new Error('API returned ' + response.status);
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
@@ -239,7 +459,6 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('WHP API not available, trying server project list:', error.message);
|
console.log('WHP API not available, trying server project list:', error.message);
|
||||||
// Try server-side project storage
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/projects/list');
|
const resp = await fetch('/api/projects/list');
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -251,7 +470,6 @@ class WHPIntegration {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Server project list not available, using localStorage:', e.message);
|
console.log('Server project list not available, using localStorage:', e.message);
|
||||||
}
|
}
|
||||||
// Final fallback: localStorage
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
return JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -259,8 +477,40 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Delete
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
async deleteSite(siteId) {
|
async deleteSite(siteId) {
|
||||||
|
if (isWHP) {
|
||||||
|
// ---- WHP mode ----
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiUrl}?action=delete&site_id=${encodeURIComponent(siteId)}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': WHP_CONFIG.csrfToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
|
const contentType = response.headers.get('content-type') || '';
|
||||||
|
if (!contentType.includes('application/json')) throw new Error('Non-JSON response');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showNotification('Site deleted successfully', 'success');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Delete failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WHP delete failed:', error.message);
|
||||||
|
this.showNotification('Delete failed: ' + error.message, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ---- Standalone mode ----
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiUrl}?action=delete&id=${encodeURIComponent(siteId)}`);
|
const response = await fetch(`${this.apiUrl}?action=delete&id=${encodeURIComponent(siteId)}`);
|
||||||
if (!response.ok) throw new Error('API returned ' + response.status);
|
if (!response.ok) throw new Error('API returned ' + response.status);
|
||||||
@@ -276,7 +526,6 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('WHP API not available, trying server project delete:', error.message);
|
console.log('WHP API not available, trying server project delete:', error.message);
|
||||||
// Try server-side project storage
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/projects/' + encodeURIComponent(siteId), { method: 'DELETE' });
|
const resp = await fetch('/api/projects/' + encodeURIComponent(siteId), { method: 'DELETE' });
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -286,7 +535,6 @@ class WHPIntegration {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Server project delete not available, using localStorage:', e.message);
|
console.log('Server project delete not available, using localStorage:', e.message);
|
||||||
}
|
}
|
||||||
// Final fallback: localStorage
|
|
||||||
try {
|
try {
|
||||||
const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
||||||
const filtered = sites.filter(s => s.id !== siteId);
|
const filtered = sites.filter(s => s.id !== siteId);
|
||||||
@@ -299,10 +547,20 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Dialogs (standalone mode only)
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
showSaveDialog() {
|
showSaveDialog() {
|
||||||
const siteName = prompt('Enter a name for your site:', this.currentSiteName);
|
// In WHP mode, save immediately without prompting
|
||||||
|
if (isWHP) {
|
||||||
|
this.saveToWHP();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteName = prompt('Enter a name for your site:', this.currentSiteName);
|
||||||
if (siteName) {
|
if (siteName) {
|
||||||
this.currentSiteName = siteName;
|
this.currentSiteName = siteName;
|
||||||
this.saveToWHP(this.currentSiteId, siteName);
|
this.saveToWHP(this.currentSiteId, siteName);
|
||||||
@@ -310,6 +568,12 @@ class WHPIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showLoadDialog() {
|
async showLoadDialog() {
|
||||||
|
// In WHP mode there is no load dialog -- we always work on one site
|
||||||
|
if (isWHP) {
|
||||||
|
this.loadFromWHP();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const sites = await this.loadSiteList();
|
const sites = await this.loadSiteList();
|
||||||
|
|
||||||
if (sites.length === 0) {
|
if (sites.length === 0) {
|
||||||
@@ -317,7 +581,6 @@ class WHPIntegration {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a simple modal for site selection
|
|
||||||
const modal = this.createLoadModal(sites);
|
const modal = this.createLoadModal(sites);
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
}
|
}
|
||||||
@@ -380,7 +643,6 @@ class WHPIntegration {
|
|||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
modal.appendChild(content);
|
modal.appendChild(content);
|
||||||
|
|
||||||
// Close on background click
|
|
||||||
modal.onclick = (e) => {
|
modal.onclick = (e) => {
|
||||||
if (e.target === modal) {
|
if (e.target === modal) {
|
||||||
modal.remove();
|
modal.remove();
|
||||||
@@ -390,11 +652,25 @@ class WHPIntegration {
|
|||||||
return modal;
|
return modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Auto-save
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
autoSave() {
|
autoSave() {
|
||||||
|
if (isWHP) {
|
||||||
|
// In WHP mode we always have a siteId, so always auto-save
|
||||||
|
this.saveToWHP();
|
||||||
|
} else {
|
||||||
|
// Standalone: only auto-save if a site is loaded
|
||||||
if (this.currentSiteId) {
|
if (this.currentSiteId) {
|
||||||
this.saveToWHP(this.currentSiteId, this.currentSiteName);
|
this.saveToWHP(this.currentSiteId, this.currentSiteName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// Notifications
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
showNotification(message, type = 'info') {
|
showNotification(message, type = 'info') {
|
||||||
const notification = document.createElement('div');
|
const notification = document.createElement('div');
|
||||||
@@ -423,7 +699,6 @@ class WHPIntegration {
|
|||||||
|
|
||||||
// Auto-initialize when GrapesJS editor is ready
|
// Auto-initialize when GrapesJS editor is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Wait for editor to be defined
|
|
||||||
const checkEditor = setInterval(() => {
|
const checkEditor = setInterval(() => {
|
||||||
if (window.editor) {
|
if (window.editor) {
|
||||||
window.whpInt = new WHPIntegration(window.editor);
|
window.whpInt = new WHPIntegration(window.editor);
|
||||||
|
|||||||
Reference in New Issue
Block a user