feat: add stats block widget management and update block_3_button component content

This commit is contained in:
2026-07-11 21:33:27 +07:00
parent 3e5432ba65
commit 324f3a505b
19 changed files with 410 additions and 139 deletions
@@ -14,6 +14,7 @@
--color-success: #28a745;
--color-warning: #ffc107;
--color-danger: #dc3545;
--color-old-brick: #881C1C;
/* Neutrals */
--color-background: #ffffff;
@@ -57,4 +58,12 @@
/* Allows clicks to pass through to the video controls */
z-index: 1;
/* Ensures it sits above the video */
}
/* Custom Color Utilities */
.text-old-brick {
color: var(--color-old-brick) !important;
}
.bg-old-brick {
background-color: var(--color-old-brick) !important;
}
@@ -1,5 +1,6 @@
/**
* Custom Editor.js Block Tool for embedding predefined HTML snippets.
* Uses iframe for WYSIWYG preview - snippets look exactly like on the live page.
*/
class HtmlSnippetTool {
static get toolbox() {
@@ -21,12 +22,10 @@ class HtmlSnippetTool {
this.wrapper = document.createElement('div');
this.wrapper.classList.add('ce-snippet-wrapper');
this.wrapper.style.border = '1px solid #ddd';
this.wrapper.style.padding = '15px';
this.wrapper.style.borderRadius = '5px';
this.wrapper.style.background = '#fafafa';
this.wrapper.style.width = '100%';
this.wrapper.style.boxSizing = 'border-box';
this.wrapper.style.overflowX = 'auto'; // allow scrolling instead of stretching editor
if (this.data.id) {
this._showPreview(this.data.id);
@@ -43,11 +42,13 @@ class HtmlSnippetTool {
const title = document.createElement('h4');
title.style.marginTop = '0';
title.style.marginBottom = '10px';
title.style.padding = '15px 15px 0 15px';
title.innerText = 'Insert Predefined HTML Snippet';
const inputContainer = document.createElement('div');
inputContainer.style.display = 'flex';
inputContainer.style.gap = '10px';
inputContainer.style.padding = '0 15px 15px 15px';
const input = document.createElement('input');
input.classList.add('ce-input');
@@ -73,8 +74,48 @@ class HtmlSnippetTool {
}
_showPreview(snippetId) {
this.wrapper.innerHTML = '<div style="color: #666;">Loading preview...</div>';
this.wrapper.innerHTML = '';
// Header bar with snippet name and Edit ID button
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.padding = '8px 15px';
header.style.borderBottom = '1px solid #ddd';
header.style.background = '#f0f0f0';
header.style.borderRadius = '5px 5px 0 0';
const title = document.createElement('strong');
title.innerText = 'Snippet: ' + snippetId;
title.style.fontSize = '13px';
title.style.color = '#555';
const editBtn = document.createElement('button');
editBtn.innerText = 'Edit ID';
editBtn.style.fontSize = '12px';
editBtn.style.cursor = 'pointer';
editBtn.style.padding = '2px 8px';
editBtn.style.border = '1px solid #ccc';
editBtn.style.borderRadius = '3px';
editBtn.style.background = '#fff';
editBtn.addEventListener('click', () => {
this._showInput();
});
header.appendChild(title);
header.appendChild(editBtn);
this.wrapper.appendChild(header);
// Loading indicator
const loadingDiv = document.createElement('div');
loadingDiv.style.padding = '20px';
loadingDiv.style.color = '#666';
loadingDiv.style.textAlign = 'center';
loadingDiv.innerText = 'Loading preview...';
this.wrapper.appendChild(loadingDiv);
// Fetch the snippet HTML
fetch('/api/manage/snippets/' + encodeURIComponent(snippetId))
.then(response => {
if (!response.ok) throw new Error('Snippet not found');
@@ -83,47 +124,88 @@ class HtmlSnippetTool {
.then(html => {
this.data.id = snippetId;
this.wrapper.innerHTML = '';
const header = document.createElement('div');
header.style.display = 'flex';
header.style.justifyContent = 'space-between';
header.style.alignItems = 'center';
header.style.marginBottom = '10px';
header.style.borderBottom = '1px solid #ddd';
header.style.paddingBottom = '5px';
const title = document.createElement('strong');
title.innerText = 'Snippet: ' + snippetId;
const editBtn = document.createElement('button');
editBtn.innerText = 'Edit ID';
editBtn.style.fontSize = '12px';
editBtn.style.cursor = 'pointer';
editBtn.addEventListener('click', () => {
this._showInput();
});
header.appendChild(title);
header.appendChild(editBtn);
const previewArea = document.createElement('div');
previewArea.innerHTML = html;
// Prevent interactions inside preview from submitting forms or acting up
previewArea.style.pointerEvents = 'none';
this.wrapper.appendChild(header);
this.wrapper.appendChild(previewArea);
// Remove loading indicator
loadingDiv.remove();
// Create an iframe for isolated WYSIWYG rendering
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.border = 'none';
iframe.style.display = 'block';
iframe.style.borderRadius = '0 0 5px 5px';
iframe.style.minHeight = '80px';
// Scrolling is disabled; height auto-adjusts
iframe.scrolling = 'no';
this.wrapper.appendChild(iframe);
// Write the full HTML document into the iframe, including theme CSS
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400..700;1,400..700&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/umass.css">
<link rel="stylesheet" href="/css/custom.css">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background: #fff;
}
/* Prevent any internal links from being clickable */
a { pointer-events: none; }
/* Ensure images/videos scale properly */
img, video, iframe { max-width: 100%; height: auto; }
</style>
</head>
<body>
${html}
</body>
</html>
`);
iframeDoc.close();
// Auto-resize iframe to fit content
const resizeIframe = () => {
try {
const body = iframeDoc.body;
const docEl = iframeDoc.documentElement;
const height = Math.max(
body.scrollHeight, body.offsetHeight,
docEl.clientHeight, docEl.scrollHeight, docEl.offsetHeight
);
iframe.style.height = height + 'px';
} catch (e) {
iframe.style.height = '300px';
}
};
// Resize after load and after images load
iframe.addEventListener('load', resizeIframe);
setTimeout(resizeIframe, 500);
setTimeout(resizeIframe, 1500);
setTimeout(resizeIframe, 3000);
})
.catch(error => {
this.wrapper.innerHTML = '<div style="color: red;">Error: ' + error.message + '</div>';
loadingDiv.remove();
const errorDiv = document.createElement('div');
errorDiv.style.padding = '15px';
errorDiv.innerHTML = '<div style="color: red;">Error: ' + error.message + '</div>';
const backBtn = document.createElement('button');
backBtn.innerText = 'Try Again';
backBtn.style.marginTop = '10px';
backBtn.style.cursor = 'pointer';
backBtn.addEventListener('click', () => this._showInput());
this.wrapper.appendChild(backBtn);
errorDiv.appendChild(backBtn);
this.wrapper.appendChild(errorDiv);
});
}