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>
736 lines
28 KiB
JavaScript
736 lines
28 KiB
JavaScript
/**
|
|
* WHP Integration for Site Builder
|
|
* 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 {
|
|
constructor(editor, apiUrl) {
|
|
this.editor = editor;
|
|
|
|
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.currentSiteName = 'Untitled Site';
|
|
}
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
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
|
|
setInterval(() => this.autoSave(), 30000);
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Toolbar buttons
|
|
// -------------------------------------------------
|
|
|
|
addToolbarButtons() {
|
|
// Add "Save" button
|
|
const saveBtn = document.createElement('button');
|
|
saveBtn.id = 'btn-whp-save';
|
|
saveBtn.className = 'nav-btn primary';
|
|
saveBtn.innerHTML = `
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
|
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
|
<polyline points="7 3 7 8 15 8"></polyline>
|
|
</svg>
|
|
<span>Save</span>
|
|
`;
|
|
|
|
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');
|
|
loadBtn.id = 'btn-whp-load';
|
|
loadBtn.className = 'nav-btn';
|
|
loadBtn.innerHTML = `
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
</svg>
|
|
<span>Load</span>
|
|
`;
|
|
loadBtn.onclick = () => this.showLoadDialog();
|
|
|
|
const exportBtn = document.getElementById('btn-export');
|
|
if (exportBtn && exportBtn.parentNode) {
|
|
exportBtn.parentNode.insertBefore(loadBtn, exportBtn);
|
|
exportBtn.parentNode.insertBefore(saveBtn, exportBtn);
|
|
|
|
const divider = document.createElement('span');
|
|
divider.className = 'divider';
|
|
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) {
|
|
const html = this.editor.getHtml();
|
|
const css = this.editor.getCss();
|
|
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 = {
|
|
id: siteId || this.currentSiteId || 'site_' + Date.now(),
|
|
name: siteName || this.currentSiteName,
|
|
html: html,
|
|
css: css,
|
|
grapesjs: grapesjs,
|
|
modified: Math.floor(Date.now() / 1000),
|
|
created: this.currentSiteId ? undefined : Math.floor(Date.now() / 1000)
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${this.apiUrl}?action=save`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
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.currentSiteId = result.site.id;
|
|
this.currentSiteName = result.site.name;
|
|
this.showNotification(`Saved "${result.site.name}" successfully!`, 'success');
|
|
return result.site;
|
|
} else {
|
|
throw new Error(result.error || 'Save failed');
|
|
}
|
|
} catch (error) {
|
|
console.log('WHP API not available, using localStorage fallback:', error.message);
|
|
return this._saveToLocalStorage(data);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _saveToLocalStorage(data) {
|
|
// Try server-side project storage first
|
|
try {
|
|
const resp = await fetch('/api/projects/save', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (resp.ok) {
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
this.currentSiteId = data.id;
|
|
this.currentSiteName = data.name;
|
|
this.showNotification(`Saved "${data.name}" to server!`, 'success');
|
|
return { id: data.id, name: data.name };
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Server project save not available, trying localStorage:', e.message);
|
|
}
|
|
|
|
// Fall back to localStorage with size check
|
|
try {
|
|
const sitesJson = localStorage.getItem('whp-sites') || '[]';
|
|
const sites = JSON.parse(sitesJson);
|
|
|
|
const idx = sites.findIndex(s => s.id === data.id);
|
|
if (idx >= 0) {
|
|
sites[idx] = data;
|
|
} else {
|
|
sites.push(data);
|
|
}
|
|
|
|
localStorage.setItem('whp-sites', JSON.stringify(sites));
|
|
|
|
this.currentSiteId = data.id;
|
|
this.currentSiteName = data.name;
|
|
this.showNotification(`Saved "${data.name}" to local storage!`, 'success');
|
|
return { id: data.id, name: data.name };
|
|
} catch (err) {
|
|
if (err.name === 'QuotaExceededError' || err.message.includes('quota')) {
|
|
this.showNotification('Storage full! Start server.py for unlimited storage.', 'error');
|
|
} else {
|
|
this.showNotification(`Save failed: ${err.message}`, 'error');
|
|
}
|
|
console.error('localStorage save error:', err);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Load
|
|
// -------------------------------------------------
|
|
|
|
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 {
|
|
const response = await fetch(`${this.apiUrl}?action=load&id=${encodeURIComponent(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);
|
|
} else {
|
|
throw new Error(result.error || 'Load failed');
|
|
}
|
|
} catch (error) {
|
|
console.log('WHP API not available, loading from localStorage:', error.message);
|
|
this._loadFromLocalStorage(siteId);
|
|
}
|
|
}
|
|
}
|
|
|
|
_applySiteData(site) {
|
|
if (site.grapesjs) {
|
|
this.editor.loadProjectData(site.grapesjs);
|
|
} else {
|
|
this.editor.setComponents(site.html || '');
|
|
this.editor.setStyle(site.css || '');
|
|
}
|
|
this.currentSiteId = site.id || this.currentSiteId;
|
|
this.currentSiteName = site.name || this.currentSiteName;
|
|
this.showNotification(`Loaded "${this.currentSiteName}" successfully!`, 'success');
|
|
}
|
|
|
|
async _loadFromLocalStorage(siteId) {
|
|
// Try server-side project storage first
|
|
try {
|
|
const resp = await fetch('/api/projects/' + encodeURIComponent(siteId));
|
|
if (resp.ok) {
|
|
const result = await resp.json();
|
|
if (result.success && result.project) {
|
|
this._applySiteData(result.project);
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Server project load not available, trying localStorage:', e.message);
|
|
}
|
|
|
|
// Fall back to localStorage
|
|
try {
|
|
const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
|
const site = sites.find(s => s.id === siteId);
|
|
if (site) {
|
|
this._applySiteData(site);
|
|
} else {
|
|
this.showNotification('Site not found in local storage', 'error');
|
|
}
|
|
} catch (err) {
|
|
this.showNotification(`Load failed: ${err.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Site list
|
|
// -------------------------------------------------
|
|
|
|
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 {
|
|
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.log('WHP API not available, trying server project list:', error.message);
|
|
try {
|
|
const resp = await fetch('/api/projects/list');
|
|
if (resp.ok) {
|
|
const result = await resp.json();
|
|
if (result.success && Array.isArray(result.projects)) {
|
|
return result.projects;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log('Server project list not available, using localStorage:', e.message);
|
|
}
|
|
try {
|
|
return JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Delete
|
|
// -------------------------------------------------
|
|
|
|
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 {
|
|
const response = await fetch(`${this.apiUrl}?action=delete&id=${encodeURIComponent(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.showNotification('Site deleted successfully', 'success');
|
|
return true;
|
|
} else {
|
|
throw new Error(result.error || 'Delete failed');
|
|
}
|
|
} catch (error) {
|
|
console.log('WHP API not available, trying server project delete:', error.message);
|
|
try {
|
|
const resp = await fetch('/api/projects/' + encodeURIComponent(siteId), { method: 'DELETE' });
|
|
if (resp.ok) {
|
|
this.showNotification('Site deleted from server', 'success');
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.log('Server project delete not available, using localStorage:', e.message);
|
|
}
|
|
try {
|
|
const sites = JSON.parse(localStorage.getItem('whp-sites') || '[]');
|
|
const filtered = sites.filter(s => s.id !== siteId);
|
|
localStorage.setItem('whp-sites', JSON.stringify(filtered));
|
|
this.showNotification('Site deleted from local storage', 'success');
|
|
return true;
|
|
} catch (err) {
|
|
this.showNotification(`Delete failed: ${err.message}`, 'error');
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Dialogs (standalone mode only)
|
|
// -------------------------------------------------
|
|
|
|
showSaveDialog() {
|
|
// 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) {
|
|
this.currentSiteName = siteName;
|
|
this.saveToWHP(this.currentSiteId, siteName);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
if (sites.length === 0) {
|
|
alert('No saved sites found.');
|
|
return;
|
|
}
|
|
|
|
const modal = this.createLoadModal(sites);
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
createLoadModal(sites) {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'whp-modal';
|
|
modal.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
`;
|
|
|
|
const content = document.createElement('div');
|
|
content.style.cssText = `
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
max-width: 600px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
|
`;
|
|
|
|
let html = '<h2 style="margin-top: 0;">Load Site</h2>';
|
|
html += '<div style="display: grid; gap: 12px;">';
|
|
|
|
sites.forEach(site => {
|
|
const date = new Date(site.modified * 1000).toLocaleString();
|
|
html += `
|
|
<div style="border: 1px solid #ddd; padding: 12px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
|
|
<div>
|
|
<strong>${site.name}</strong><br>
|
|
<small style="color: #666;">Modified: ${date}</small>
|
|
</div>
|
|
<div>
|
|
<button onclick="window.whpInt.loadFromWHP('${site.id}'); this.closest('.whp-modal').remove();"
|
|
style="padding: 6px 12px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px;">
|
|
Load
|
|
</button>
|
|
<button onclick="if(confirm('Delete this site?')) { window.whpInt.deleteSite('${site.id}').then(() => this.closest('.whp-modal').remove()); }"
|
|
style="padding: 6px 12px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += '</div>';
|
|
html += '<button onclick="this.closest(\'.whp-modal\').remove()" style="margin-top: 16px; padding: 8px 16px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>';
|
|
|
|
content.innerHTML = html;
|
|
modal.appendChild(content);
|
|
|
|
modal.onclick = (e) => {
|
|
if (e.target === modal) {
|
|
modal.remove();
|
|
}
|
|
};
|
|
|
|
return modal;
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Auto-save
|
|
// -------------------------------------------------
|
|
|
|
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) {
|
|
this.saveToWHP(this.currentSiteId, this.currentSiteName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------
|
|
// Notifications
|
|
// -------------------------------------------------
|
|
|
|
showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 20px;
|
|
padding: 16px 24px;
|
|
background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : '#0066cc'};
|
|
color: white;
|
|
border-radius: 4px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
|
z-index: 10001;
|
|
animation: slideIn 0.3s ease-out;
|
|
`;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOut 0.3s ease-out';
|
|
setTimeout(() => notification.remove(), 300);
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
// Auto-initialize when GrapesJS editor is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const checkEditor = setInterval(() => {
|
|
if (window.editor) {
|
|
window.whpInt = new WHPIntegration(window.editor);
|
|
clearInterval(checkEditor);
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
// Add animation styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|