v1.0.0.1
StocketBase
- (function (global) {
- 'use strict';
-
- const BLOCK_LABELS = {
- paragraph: 'Text',
- heading2: 'Heading 2',
- heading3: 'Heading 3',
- image: 'Image',
- video: 'Video',
- quote: 'Quote',
- list: 'List',
- code: 'Code',
- table: 'Table',
- checklist: 'Checklist',
- callout: 'Highlighted note',
- separator: 'Separator',
- embed: 'Embed',
- contact_form: 'Contact form',
- hero: 'Welcome banner',
- home_featured: 'Featured products',
- home_products: 'Product list',
- };
-
- const SYSTEM_TYPES = ['home_featured', 'home_products'];
-
- const HOME_ONLY_TYPES = ['hero', 'home_featured', 'home_products'];
-
- const SINGLE_USE_TYPES = ['contact_form', 'home_featured', 'home_products'];
-
- const BLOCK_ICONS = {
- paragraph: 'text',
- heading2: 'heading2',
- heading3: 'heading3',
- image: 'image',
- video: 'video',
- quote: 'quote',
- list: 'list',
- code: 'code',
- table: 'table',
- checklist: 'checklist',
- callout: 'callout',
- separator: 'separator',
- embed: 'embed',
- contact_form: 'mail',
- hero: 'layout',
- home_featured: 'star',
- home_products: 'grid',
- };
-
- function iconSvg(name, cls) {
- return '<svg class="' + (cls || 'icon icon-sm') + '" aria-hidden="true"><use href="#icon-' + name + '"></use></svg>';
- }
-
- function uid() {
- return 'b' + Math.random().toString(16).slice(2) + Date.now().toString(16);
- }
-
- const BLOCK_ICONS_KEY = 'stocketbase_dashboard_block_icons';
-
- function blockIconsEnabled() {
- try {
- return global.localStorage.getItem(BLOCK_ICONS_KEY) !== '0';
- } catch (error) {
- return true;
- }
- }
-
- const SPELLCHECK_KEY = 'stocketbase_dashboard_spellcheck';
-
- function spellcheckEnabled() {
- try {
- return global.localStorage.getItem(SPELLCHECK_KEY) !== '0';
- } catch (error) {
- return true;
- }
- }
-
- const PASTE_CLEANUP_KEY = 'stocketbase_dashboard_paste_cleanup';
-
- function pasteCleanupEnabled() {
- try {
- return global.localStorage.getItem(PASTE_CLEANUP_KEY) === '1';
- } catch (error) {
- return false;
- }
- }
-
- let forceCleanPaste = false;
- document.addEventListener('keydown', (event) => {
- if ((event.ctrlKey || event.metaKey) && event.shiftKey && (event.key === 'V' || event.key === 'v')) {
- forceCleanPaste = true;
- }
- });
-
- function attachCleanPaste(el) {
- el.addEventListener('paste', (event) => {
- const shouldClean = pasteCleanupEnabled() || forceCleanPaste;
- forceCleanPaste = false;
- if (!shouldClean) {
- return;
- }
- event.preventDefault();
- const clipboard = event.clipboardData || global.clipboardData;
- const text = clipboard ? clipboard.getData('text/plain') : '';
- document.execCommand('insertText', false, text);
- });
- }
-
- const RECENT_COLORS_KEY = 'stocketbase_editor_recent_colors';
- const PRESET_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#0ea5e9', '#3862f5', '#e85fd0', '#f8fafc', '#8b93b0', '#03040d'];
-
- function loadRecentColors() {
- try {
- const raw = JSON.parse(global.localStorage.getItem(RECENT_COLORS_KEY) || '[]');
- return Array.isArray(raw) ? raw.filter((color) => typeof color === 'string').slice(0, 8) : [];
- } catch (error) {
- return [];
- }
- }
-
- function saveRecentColor(color) {
- try {
- const list = loadRecentColors().filter((existing) => existing.toLowerCase() !== color.toLowerCase());
- list.unshift(color);
- global.localStorage.setItem(RECENT_COLORS_KEY, JSON.stringify(list.slice(0, 8)));
- } catch (error) {
- }
- }
-
- function clamp01(value) {
- return Math.min(1, Math.max(0, value));
- }
-
- function hexToRgb(hex) {
- const clean = hex.replace('#', '');
- const num = parseInt(clean, 16);
- return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 };
- }
-
- function rgbToHex(r, g, b) {
- return '#' + [r, g, b].map((channel) => {
- return Math.round(clamp01(channel / 255) * 255).toString(16).padStart(2, '0');
- }).join('');
- }
-
- function rgbToHsv(r, g, b) {
- r /= 255;
- g /= 255;
- b /= 255;
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- const delta = max - min;
- let h = 0;
- if (delta !== 0) {
- if (max === r) {
- h = ((g - b) / delta) % 6;
- } else if (max === g) {
- h = (b - r) / delta + 2;
- } else {
- h = (r - g) / delta + 4;
- }
- h *= 60;
- if (h < 0) {
- h += 360;
- }
- }
- return { h, s: max === 0 ? 0 : delta / max, v: max };
- }
-
- function hsvToRgb(h, s, v) {
- const c = v * s;
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
- const m = v - c;
- let r = 0;
- let g = 0;
- let b = 0;
- if (h < 60) {
- r = c; g = x; b = 0;
- } else if (h < 120) {
- r = x; g = c; b = 0;
- } else if (h < 180) {
- r = 0; g = c; b = x;
- } else if (h < 240) {
- r = 0; g = x; b = c;
- } else if (h < 300) {
- r = x; g = 0; b = c;
- } else {
- r = c; g = 0; b = x;
- }
- return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
- }
-
- function positionFloating(el, anchorRect, options) {
- const opts = options || {};
- const margin = 8;
- const width = el.offsetWidth;
- const height = el.offsetHeight;
-
- let left = window.scrollX + anchorRect.left;
- const maxLeft = window.scrollX + window.innerWidth - width - margin;
- const minLeft = window.scrollX + margin;
- left = Math.min(left, maxLeft);
- left = Math.max(left, minLeft);
-
- let top;
- if (opts.placement === 'above') {
- top = window.scrollY + anchorRect.top - height - (opts.gap || 0);
- if (anchorRect.top - height - (opts.gap || 0) < margin) {
- top = window.scrollY + anchorRect.bottom + (opts.gap || 0);
- }
- } else {
- top = window.scrollY + anchorRect.bottom + (opts.gap || 0);
- const viewportBottom = window.scrollY + window.innerHeight;
- if (top + height > viewportBottom - margin && anchorRect.top - height - (opts.gap || 0) >= margin) {
- top = window.scrollY + anchorRect.top - height - (opts.gap || 0);
- }
- }
-
- el.style.left = left + 'px';
- el.style.top = top + 'px';
- }
-
- function uploadFile(file, onSuccess, onError) {
- const csrfInput = document.querySelector('input[name="csrf_token"]');
- const formData = new FormData();
- formData.append('file', file);
- if (csrfInput) {
- formData.append('csrf_token', csrfInput.value);
- }
-
- fetch('media-upload.php', { method: 'POST', body: formData })
- .then((response) => response.json())
- .then((data) => {
- if (data.success) {
- onSuccess(data.url);
- } else {
- onError(data.error || 'The upload didn\'t work. Please try again.');
- }
- })
- .catch(() => onError('The upload didn\'t work. Please try again.'));
- }
-
- function defaultDataFor(type) {
- switch (type) {
- case 'paragraph':
- case 'quote':
- return { text: '' };
- case 'heading2':
- case 'heading3':
- return { text: '' };
- case 'image':
- return { src: '', alt: '', caption: '' };
- case 'video':
- return { src: '', caption: '' };
- case 'list':
- return { style: 'unordered', items: [''] };
- case 'code':
- return { language: 'text', code: '' };
- case 'table':
- return { rows: [['', ''], ['', '']] };
- case 'checklist':
- return { items: [{ text: '', checked: false }] };
- case 'callout':
- return { style: 'info', text: '' };
- case 'embed':
- return { url: '' };
- case 'separator':
- return {};
- case 'contact_form':
- return { title: '' };
- case 'hero':
- return { eyebrow: '', title: '', subtitle: '', show_eyebrow: true };
- default:
- return {};
- }
- }
-
- function keepSingleUseBlocks(blocks) {
- const seen = {};
- return blocks.filter((block) => {
- if (!block || SINGLE_USE_TYPES.indexOf(block.type) === -1) {
- return true;
- }
- if (seen[block.type]) {
- return false;
- }
- seen[block.type] = true;
- return true;
- });
- }
-
- class BlockEditor {
- constructor(rootElement) {
- this.root = rootElement;
- this.blocks = [];
- this.allowContact = rootElement.dataset.allowContact === '1';
- this.isHome = rootElement.dataset.home === '1';
- this.hiddenInput = document.getElementById('blocks_json_input');
- this.root.classList.toggle('be-hide-block-icons', !blockIconsEnabled());
- this.draftTimer = null;
-
- const form = this.root.closest('form');
- this.form = form || null;
- const idField = this.form ? this.form.querySelector('[name="product_id"], [name="page_id"], [name="article_id"]') : null;
- this.draftKey = 'stocketbase_draft_' + (idField ? idField.name : 'unknown') + '_' + (idField && idField.value ? idField.value : '0');
-
- this.statsBar = document.createElement('div');
- this.statsBar.className = 'be-stats-bar';
- this.statsText = document.createElement('span');
- this.statsBar.appendChild(this.statsText);
- this.focusToggleBtn = document.createElement('button');
- this.focusToggleBtn.type = 'button';
- this.focusToggleBtn.className = 'be-focus-toggle-btn';
- this.focusToggleBtn.textContent = 'Focus mode';
- this.focusToggleBtn.addEventListener('click', () => this.toggleFocusMode());
- this.statsBar.appendChild(this.focusToggleBtn);
- this.root.insertAdjacentElement('afterend', this.statsBar);
-
- document.addEventListener('keydown', (event) => {
- if (event.key === 'Escape' && document.body.classList.contains('be-focus-mode')) {
- this.toggleFocusMode();
- }
- });
-
- this.loadInitial();
- this.offerDraftRecovery();
- this.isInitialRender = true;
- this.render();
- this.isInitialRender = false;
-
- if (this.form) {
- this.form.addEventListener('submit', () => {
- this.syncHiddenInput();
- try {
- global.localStorage.removeItem(this.draftKey);
- } catch (error) {
- }
- });
- }
-
- this.attachShortcuts();
- }
-
- toggleFocusMode() {
- const active = document.body.classList.toggle('be-focus-mode');
- this.focusToggleBtn.textContent = active ? 'Exit focus mode' : 'Focus mode';
- }
-
- scheduleDraftSave() {
- if (this.isInitialRender) {
- return;
- }
- if (this.draftTimer) {
- clearTimeout(this.draftTimer);
- }
- this.draftTimer = setTimeout(() => {
- try {
- global.localStorage.setItem(this.draftKey, JSON.stringify({ blocks: this.blocks, savedAt: Date.now() }));
- } catch (error) {
- }
- }, 1000);
- }
-
- offerDraftRecovery() {
- let saved = null;
- try {
- saved = JSON.parse(global.localStorage.getItem(this.draftKey) || 'null');
- } catch (error) {
- saved = null;
- }
- if (!saved || !Array.isArray(saved.blocks) || !saved.blocks.length) {
- return;
- }
-
- const banner = document.createElement('div');
- banner.className = 'be-draft-banner';
-
- const label = document.createElement('span');
- label.textContent = 'You have unsaved changes from ' + new Date(saved.savedAt).toLocaleString();
- banner.appendChild(label);
-
- const restoreBtn = document.createElement('button');
- restoreBtn.type = 'button';
- restoreBtn.textContent = 'Restore';
- restoreBtn.addEventListener('click', () => {
- this.blocks = this.normalizeBlocks(saved.blocks);
- this.render();
- banner.remove();
- });
- banner.appendChild(restoreBtn);
-
- const discardBtn = document.createElement('button');
- discardBtn.type = 'button';
- discardBtn.textContent = 'Throw away';
- discardBtn.addEventListener('click', () => {
- try {
- global.localStorage.removeItem(this.draftKey);
- } catch (error) {
- }
- banner.remove();
- });
- banner.appendChild(discardBtn);
-
- this.root.insertAdjacentElement('beforebegin', banner);
- }
-
- getActiveBlockId() {
- const active = document.activeElement;
- const wrapper = active && active.closest ? active.closest('.be-block') : null;
- if (wrapper) {
- return wrapper.dataset.blockId;
- }
- return this.blocks.length ? this.blocks[this.blocks.length - 1].id : null;
- }
-
- getBlocks() {
- return JSON.parse(JSON.stringify(this.blocks));
- }
-
- replaceBlocks(blocks) {
- const next = this.normalizeBlocks(Array.isArray(blocks) ? JSON.parse(JSON.stringify(blocks)) : []);
- if (next.length === 0) {
- next.push({ id: uid(), type: 'paragraph', data: defaultDataFor('paragraph') });
- }
- this.blocks = next;
- this.render();
- }
-
- attachShortcuts() {
- if (!global.StocketBaseShortcuts) {
- return;
- }
-
- document.addEventListener('keydown', (event) => {
- if (!this.root.isConnected) {
- return;
- }
-
- const activeEl = document.activeElement;
- const withinEditor = activeEl && this.root.contains(activeEl);
- const withinForm = this.form && activeEl && this.form.contains(activeEl);
-
- const shortcuts = global.StocketBaseShortcuts.getShortcuts();
- const action = global.StocketBaseShortcuts.findMatchingAction(event, shortcuts);
- if (!action) {
- return;
- }
-
- switch (action) {
- case 'bold':
- case 'italic':
- case 'underline':
- case 'strikethrough':
- if (!withinEditor) {
- return;
- }
- event.preventDefault();
- this.applyFormatShortcut(action);
- break;
-
- case 'add_block': {
- if (!withinEditor) {
- return;
- }
- event.preventDefault();
- const blockId = this.getActiveBlockId();
- const index = this.blocks.findIndex((b) => b.id === blockId);
- this.addBlock(index === -1 ? this.blocks.length - 1 : index, 'paragraph');
- break;
- }
-
- case 'remove_block': {
- if (!withinEditor) {
- return;
- }
- event.preventDefault();
- const blockId = this.getActiveBlockId();
- if (blockId) {
- this.removeBlock(blockId);
- }
- break;
- }
-
- case 'move_block_up': {
- if (!withinEditor) {
- return;
- }
- event.preventDefault();
- const blockId = this.getActiveBlockId();
- if (blockId) {
- this.moveBlock(blockId, -1);
- }
- break;
- }
-
- case 'move_block_down': {
- if (!withinEditor) {
- return;
- }
- event.preventDefault();
- const blockId = this.getActiveBlockId();
- if (blockId) {
- this.moveBlock(blockId, 1);
- }
- break;
- }
-
- case 'save':
- if (!withinForm && !withinEditor) {
- return;
- }
- event.preventDefault();
- if (this.form) {
- this.syncHiddenInput();
- if (this.form.requestSubmit) {
- this.form.requestSubmit();
- } else {
- this.form.submit();
- }
- }
- break;
-
- default:
- break;
- }
- });
- }
-
- applyFormatShortcut(action) {
- const commandMap = {
- bold: 'bold',
- italic: 'italic',
- underline: 'underline',
- strikethrough: 'strikeThrough',
- };
- const command = commandMap[action];
- if (!command) {
- return;
- }
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
- return;
- }
- document.execCommand(command, false, null);
- }
-
- loadInitial() {
- const raw = this.root.dataset.initialBlocks || '{"blocks":[]}';
- try {
- const parsed = JSON.parse(raw);
- this.blocks = Array.isArray(parsed.blocks) ? parsed.blocks : [];
- } catch (error) {
- this.blocks = [];
- }
- this.blocks = this.normalizeBlocks(this.blocks);
-
- if (this.blocks.length === 0) {
- this.blocks.push({ id: uid(), type: 'paragraph', data: defaultDataFor('paragraph') });
- }
- }
-
- syncHiddenInput() {
- if (this.hiddenInput) {
- this.hiddenInput.value = JSON.stringify({ blocks: this.blocks });
- }
- this.updateStats();
- this.scheduleDraftSave();
- }
-
- updateStats() {
- if (!this.statsText) {
- return;
- }
- const stripHtml = (html) => {
- const tmp = document.createElement('div');
- tmp.innerHTML = html || '';
- return tmp.textContent || '';
- };
- const parts = [];
- this.blocks.forEach((block) => {
- const data = block.data || {};
- if (typeof data.text === 'string') {
- parts.push(stripHtml(data.text));
- }
- if (typeof data.caption === 'string') {
- parts.push(data.caption);
- }
- if (Array.isArray(data.items)) {
- data.items.forEach((item) => {
- parts.push(typeof item === 'string' ? item : (item.text || ''));
- });
- }
- if (Array.isArray(data.rows)) {
- data.rows.forEach((row) => {
- row.forEach((cell) => parts.push(cell || ''));
- });
- }
- });
- const text = parts.join(' ').trim();
- const words = text ? text.split(/\s+/).length : 0;
- const chars = text.replace(/\s+/g, '').length;
- const minutes = Math.max(1, Math.round(words / 200));
- this.statsText.textContent = words + ' words, ' + chars + ' characters, about ' + minutes + ' min to read';
- }
-
- normalizeBlocks(blocks) {
- let list = (Array.isArray(blocks) ? blocks : []).filter((block) => block && typeof block === 'object' && typeof block.type === 'string');
- if (!this.isHome) {
- list = list.filter((block) => HOME_ONLY_TYPES.indexOf(block.type) === -1);
- }
- list = keepSingleUseBlocks(list);
- if (this.isHome) {
- SYSTEM_TYPES.forEach((type) => {
- if (!list.some((block) => block.type === type)) {
- list.push({ id: uid(), type: type, data: {} });
- }
- });
- }
- list.forEach((block) => {
- if (!block.data || typeof block.data !== 'object') {
- block.data = defaultDataFor(block.type);
- }
- });
- return list;
- }
-
- isSystemBlock(id) {
- const block = this.blocks.find((b) => b.id === id);
- return !!block && SYSTEM_TYPES.indexOf(block.type) !== -1;
- }
-
- canUseType(type, replacingId) {
- if (SYSTEM_TYPES.indexOf(type) !== -1) {
- return false;
- }
- if (type === 'hero' && !this.isHome) {
- return false;
- }
- if (type === 'contact_form' && !this.allowContact) {
- return false;
- }
- if (SINGLE_USE_TYPES.indexOf(type) === -1) {
- return true;
- }
- return !this.blocks.some((b) => b.type === type && b.id !== replacingId);
- }
-
- addBlock(afterIndex, type) {
- if (!this.canUseType(type, null)) {
- return;
- }
- const block = { id: uid(), type: type, data: defaultDataFor(type) };
- this.blocks.splice(afterIndex + 1, 0, block);
- this.render();
- this.focusBlock(block.id);
- }
-
- transformBlock(id, type) {
- const index = this.blocks.findIndex((b) => b.id === id);
- if (index === -1 || !this.canUseType(type, id) || this.isSystemBlock(id)) {
- return;
- }
- const block = { id: uid(), type: type, data: defaultDataFor(type) };
- this.blocks.splice(index, 1, block);
- this.render();
- this.focusBlock(block.id);
- }
-
- removeBlock(id) {
- if (this.blocks.length <= 1 || this.isSystemBlock(id)) {
- return;
- }
- this.blocks = this.blocks.filter((b) => b.id !== id);
- this.render();
- }
-
- moveBlock(id, direction) {
- const index = this.blocks.findIndex((b) => b.id === id);
- const targetIndex = index + direction;
- if (index === -1 || targetIndex < 0 || targetIndex >= this.blocks.length) {
- return;
- }
- const [block] = this.blocks.splice(index, 1);
- this.blocks.splice(targetIndex, 0, block);
- this.render();
- }
-
- focusBlock(id) {
- requestAnimationFrame(() => {
- const el = this.root.querySelector('[data-block-id="' + id + '"] [data-focus-target]');
- if (el) {
- el.focus();
- }
- });
- }
-
- attachInlineToolbar(el, onChange) {
- let toolbar = null;
- let savedRange = null;
- let colorSpan = null;
- let colorPopover = null;
- let colorPopoverOutsideClick = null;
- let lastAppliedColor = '';
-
- const closeColorPopover = () => {
- if (colorPopoverOutsideClick) {
- document.removeEventListener('mousedown', colorPopoverOutsideClick, true);
- colorPopoverOutsideClick = null;
- }
- if (colorPopover) {
- colorPopover.remove();
- colorPopover = null;
- }
- };
-
- const removeToolbar = () => {
- if (toolbar) {
- toolbar.remove();
- toolbar = null;
- }
- colorSpan = null;
- closeColorPopover();
- };
-
- const selectionInsideEl = () => {
- const selection = window.getSelection();
- if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
- return null;
- }
- const range = selection.getRangeAt(0);
- if (!el.contains(range.commonAncestorContainer)) {
- return null;
- }
- return range;
- };
-
- const wrapSelection = (range, tagName, styleValue) => {
- const wrapper = document.createElement(tagName);
- if (styleValue) {
- wrapper.setAttribute('style', styleValue);
- }
- try {
- range.surroundContents(wrapper);
- } catch (error) {
- wrapper.appendChild(range.extractContents());
- range.insertNode(wrapper);
- }
- const newSelection = window.getSelection();
- newSelection.removeAllRanges();
- const newRange = document.createRange();
- newRange.selectNodeContents(wrapper);
- newSelection.addRange(newRange);
- return wrapper;
- };
-
- const applyFormat = (command) => {
- const range = selectionInsideEl();
- if (!range) {
- return;
- }
- document.execCommand(command, false, null);
- el.focus();
- onChange();
- showToolbar();
- };
-
- const clearFormat = () => {
- const range = selectionInsideEl();
- if (!range) {
- return;
- }
- document.execCommand('removeFormat', false, null);
- document.execCommand('unlink', false, null);
- el.focus();
- onChange();
- colorSpan = null;
- lastAppliedColor = '';
- showToolbar();
- };
-
- const applyColor = (color) => {
- lastAppliedColor = color;
- if (colorSpan && colorSpan.isConnected) {
- colorSpan.style.color = color;
- onChange();
- return;
- }
- if (!savedRange) {
- return;
- }
- colorSpan = wrapSelection(savedRange, 'span', 'color:' + color);
- onChange();
- };
-
- const resetColor = () => {
- if (colorSpan && colorSpan.isConnected) {
- const parent = colorSpan.parentNode;
- while (colorSpan.firstChild) {
- parent.insertBefore(colorSpan.firstChild, colorSpan);
- }
- parent.removeChild(colorSpan);
- colorSpan = null;
- onChange();
- }
- lastAppliedColor = '';
- };
-
- const openColorPopover = (anchorEl) => {
- closeColorPopover();
-
- const popover = document.createElement('div');
- popover.className = 'be-color-popover';
-
- const buildSwatchRow = (label, colors, onPick) => {
- if (!colors.length) {
- return null;
- }
- const row = document.createElement('div');
- row.className = 'be-color-popover-row';
- const rowLabel = document.createElement('span');
- rowLabel.className = 'be-color-popover-label';
- rowLabel.textContent = label;
- row.appendChild(rowLabel);
- const swatches = document.createElement('div');
- swatches.className = 'be-color-popover-swatches';
- colors.forEach((color) => {
- const swatch = document.createElement('button');
- swatch.type = 'button';
- swatch.className = 'be-color-swatch';
- swatch.style.background = color;
- swatch.title = color;
- swatch.addEventListener('mousedown', (event) => event.preventDefault());
- swatch.addEventListener('click', () => onPick(color));
- swatches.appendChild(swatch);
- });
- row.appendChild(swatches);
- return row;
- };
-
- let hue = 0;
- let sat = 0;
- let val = 0;
- let suppressSync = false;
-
- if (lastAppliedColor) {
- const startRgb = hexToRgb(lastAppliedColor);
- const startHsv = rgbToHsv(startRgb.r, startRgb.g, startRgb.b);
- hue = startHsv.h;
- sat = startHsv.s;
- val = startHsv.v;
- }
-
- const pickColor = (color, remember) => {
- applyColor(color);
- if (remember !== false) {
- saveRecentColor(color);
- }
- hexInput.value = color;
- hexPreview.style.background = color;
- const bar = anchorEl.querySelector('.be-color-trigger-bar');
- if (bar) {
- bar.style.background = color;
- }
- if (!suppressSync) {
- const rgb = hexToRgb(color);
- const hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
- hue = hsv.h;
- sat = hsv.s;
- val = hsv.v;
- positionPickerHandles();
- }
- };
-
- const recentRow = buildSwatchRow('Recent', loadRecentColors(), pickColor);
- if (recentRow) {
- popover.appendChild(recentRow);
- }
- const presetsRow = buildSwatchRow('Presets', PRESET_COLORS, pickColor);
- const customTrigger = document.createElement('button');
- customTrigger.type = 'button';
- customTrigger.className = 'be-color-swatch be-color-swatch-custom';
- customTrigger.title = 'Custom color';
- customTrigger.addEventListener('mousedown', (event) => event.preventDefault());
- customTrigger.addEventListener('click', () => {
- customPanel.classList.toggle('open');
- });
- presetsRow.querySelector('.be-color-popover-swatches').appendChild(customTrigger);
- popover.appendChild(presetsRow);
-
- const hexRow = document.createElement('div');
- hexRow.className = 'be-color-popover-hex-row';
-
- const hexPreview = document.createElement('span');
- hexPreview.className = 'be-color-swatch be-color-swatch-preview';
- hexPreview.style.background = lastAppliedColor || 'transparent';
-
- const hexInput = document.createElement('input');
- hexInput.type = 'text';
- hexInput.className = 'be-color-hex-input';
- hexInput.placeholder = '#000000';
- hexInput.value = lastAppliedColor;
- hexInput.maxLength = 7;
- hexInput.spellcheck = false;
-
- const commitHex = () => {
- const value = hexInput.value.trim();
- if (/^#[0-9a-fA-F]{6}$/.test(value)) {
- pickColor(value);
- }
- };
- hexInput.addEventListener('input', () => {
- const value = hexInput.value.trim();
- hexPreview.style.background = /^#[0-9a-fA-F]{6}$/.test(value) ? value : 'transparent';
- });
- hexInput.addEventListener('change', commitHex);
- hexInput.addEventListener('keydown', (event) => {
- if (event.key === 'Enter') {
- event.preventDefault();
- commitHex();
- }
- });
-
- hexRow.appendChild(hexPreview);
- hexRow.appendChild(hexInput);
- popover.appendChild(hexRow);
-
- const customPanel = document.createElement('div');
- customPanel.className = 'be-color-custom-panel';
-
- const svArea = document.createElement('div');
- svArea.className = 'be-color-sv-area';
- const svCursor = document.createElement('div');
- svCursor.className = 'be-color-sv-cursor';
- svArea.appendChild(svCursor);
-
- const hueSlider = document.createElement('div');
- hueSlider.className = 'be-color-hue-slider';
- const hueHandle = document.createElement('div');
- hueHandle.className = 'be-color-hue-handle';
- hueSlider.appendChild(hueHandle);
-
- customPanel.appendChild(svArea);
- customPanel.appendChild(hueSlider);
- popover.appendChild(customPanel);
-
- const positionPickerHandles = () => {
- const rgb = hsvToRgb(hue, sat, val);
- svArea.style.background = 'linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(' + hue + ',100%,50%))';
- svCursor.style.left = (sat * 100) + '%';
- svCursor.style.top = ((1 - val) * 100) + '%';
- svCursor.style.background = rgbToHex(rgb.r, rgb.g, rgb.b);
- hueHandle.style.left = (hue / 360 * 100) + '%';
- hueHandle.style.background = 'hsl(' + hue + ',100%,50%)';
- };
-
- const updateFromPicker = (remember) => {
- suppressSync = true;
- const rgb = hsvToRgb(hue, sat, val);
- const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
- pickColor(hex, remember);
- suppressSync = false;
- positionPickerHandles();
- };
-
- const dragHandler = (area, onMove) => (startEvent) => {
- startEvent.preventDefault();
- area.setPointerCapture(startEvent.pointerId);
- const move = (event) => onMove(event);
- const stop = (event) => {
- onMove(event);
- area.removeEventListener('pointermove', move);
- area.removeEventListener('pointerup', stop);
- saveRecentColor(hexInput.value);
- };
- onMove(startEvent);
- area.addEventListener('pointermove', move);
- area.addEventListener('pointerup', stop);
- };
-
- svArea.addEventListener('pointerdown', dragHandler(svArea, (event) => {
- const rect = svArea.getBoundingClientRect();
- sat = clamp01((event.clientX - rect.left) / rect.width);
- val = 1 - clamp01((event.clientY - rect.top) / rect.height);
- updateFromPicker(false);
- }));
-
- hueSlider.addEventListener('pointerdown', dragHandler(hueSlider, (event) => {
- const rect = hueSlider.getBoundingClientRect();
- hue = clamp01((event.clientX - rect.left) / rect.width) * 360;
- updateFromPicker(false);
- }));
-
- positionPickerHandles();
-
- const resetBtn = document.createElement('button');
- resetBtn.type = 'button';
- resetBtn.className = 'be-color-reset-btn';
- resetBtn.textContent = 'Reset color';
- resetBtn.addEventListener('mousedown', (event) => event.preventDefault());
- resetBtn.addEventListener('click', () => {
- resetColor();
- hexInput.value = '';
- hexPreview.style.background = 'transparent';
- const bar = anchorEl.querySelector('.be-color-trigger-bar');
- if (bar) {
- bar.style.background = 'currentColor';
- }
- closeColorPopover();
- });
- popover.appendChild(resetBtn);
-
- document.body.appendChild(popover);
- const rect = anchorEl.getBoundingClientRect();
- positionFloating(popover, rect, { placement: 'below', gap: 6 });
- colorPopover = popover;
-
- colorPopoverOutsideClick = (event) => {
- if (!popover.contains(event.target) && event.target !== anchorEl) {
- closeColorPopover();
- }
- };
- setTimeout(() => document.addEventListener('mousedown', colorPopoverOutsideClick, true), 0);
- };
-
- const showToolbar = () => {
- const range = selectionInsideEl();
- removeToolbar();
- if (!range) {
- savedRange = null;
- return;
- }
- savedRange = range.cloneRange();
-
- toolbar = document.createElement('div');
- toolbar.className = 'be-inline-toolbar';
-
- const shortcutTitle = (action, fallback) => {
- if (global.StocketBaseShortcuts) {
- const combo = global.StocketBaseShortcuts.getShortcuts()[action];
- if (combo) {
- return fallback + ' (' + combo.combo + ')';
- }
- }
- return fallback;
- };
-
- const boldBtn = document.createElement('button');
- boldBtn.type = 'button';
- boldBtn.textContent = 'B';
- boldBtn.title = shortcutTitle('bold', 'Bold');
- boldBtn.addEventListener('mousedown', (event) => {
- event.preventDefault();
- applyFormat('bold');
- });
-
- const italicBtn = document.createElement('button');
- italicBtn.type = 'button';
- italicBtn.textContent = 'I';
- italicBtn.title = shortcutTitle('italic', 'Italic');
- italicBtn.style.fontStyle = 'italic';
- italicBtn.addEventListener('mousedown', (event) => {
- event.preventDefault();
- applyFormat('italic');
- });
-
- const underlineBtn = document.createElement('button');
- underlineBtn.type = 'button';
- underlineBtn.textContent = 'U';
- underlineBtn.title = shortcutTitle('underline', 'Underline');
- underlineBtn.style.textDecoration = 'underline';
- underlineBtn.addEventListener('mousedown', (event) => {
- event.preventDefault();
- applyFormat('underline');
- });
-
- const strikeBtn = document.createElement('button');
- strikeBtn.type = 'button';
- strikeBtn.textContent = 'S';
- strikeBtn.title = shortcutTitle('strikethrough', 'Strikethrough');
- strikeBtn.style.textDecoration = 'line-through';
- strikeBtn.addEventListener('mousedown', (event) => {
- event.preventDefault();
- applyFormat('strikeThrough');
- });
-
- const clearFormatBtn = document.createElement('button');
- clearFormatBtn.type = 'button';
- clearFormatBtn.className = 'be-clear-format-btn';
- clearFormatBtn.textContent = 'Tx';
- clearFormatBtn.title = shortcutTitle('clearFormat', 'Clear formatting');
- clearFormatBtn.addEventListener('mousedown', (event) => {
- event.preventDefault();
- clearFormat();
- });
-
- const colorTriggerBtn = document.createElement('button');
- colorTriggerBtn.type = 'button';
- colorTriggerBtn.className = 'be-color-trigger';
- colorTriggerBtn.title = 'Text color';
- colorTriggerBtn.innerHTML = '<span class="be-color-trigger-letter">A</span><span class="be-color-trigger-bar" style="background:' + (lastAppliedColor || 'currentColor') + '"></span>';
- colorTriggerBtn.addEventListener('mousedown', (event) => event.preventDefault());
- colorTriggerBtn.addEventListener('click', () => openColorPopover(colorTriggerBtn));
-
- toolbar.appendChild(boldBtn);
- toolbar.appendChild(italicBtn);
- toolbar.appendChild(underlineBtn);
- toolbar.appendChild(strikeBtn);
- toolbar.appendChild(clearFormatBtn);
- toolbar.appendChild(colorTriggerBtn);
-
- document.body.appendChild(toolbar);
- const rect = range.getBoundingClientRect();
- positionFloating(toolbar, rect, { placement: 'above', gap: 6 });
- };
-
- el.addEventListener('mouseup', () => setTimeout(showToolbar, 0));
- el.addEventListener('keyup', (event) => {
- if (event.shiftKey || event.key === 'Shift') {
- setTimeout(showToolbar, 0);
- }
- });
- el.addEventListener('blur', () => {
- setTimeout(() => {
- if (toolbar && toolbar.contains(document.activeElement)) {
- return;
- }
- if (colorPopover && colorPopover.contains(document.activeElement)) {
- return;
- }
- removeToolbar();
- }, 150);
- });
- }
-
- render() {
- this.root.innerHTML = '';
- this.blocks.forEach((block, index) => {
- this.root.appendChild(this.renderBlock(block, index));
- });
-
- const addRow = document.createElement('div');
- addRow.className = 'be-add-row';
- const addButton = document.createElement('button');
- addButton.type = 'button';
- addButton.className = 'dash-btn-small be-add-button';
- addButton.innerHTML = iconSvg('plus') + 'Add block';
- addButton.addEventListener('click', () => this.openBlockMenu(addButton, this.blocks.length - 1));
- addRow.appendChild(addButton);
- this.root.appendChild(addRow);
-
- this.syncHiddenInput();
- }
-
- renderBlock(block, index) {
- const wrapper = document.createElement('div');
- const isSystem = SYSTEM_TYPES.indexOf(block.type) !== -1;
- wrapper.className = isSystem ? 'be-block be-block-system' : 'be-block';
- wrapper.dataset.blockId = block.id;
-
- const typeIcon = document.createElement('span');
- typeIcon.className = 'be-block-icon-badge';
- typeIcon.title = BLOCK_LABELS[block.type] || '';
- typeIcon.innerHTML = iconSvg(BLOCK_ICONS[block.type] || 'text');
- wrapper.appendChild(typeIcon);
-
- const toolbar = document.createElement('div');
- toolbar.className = 'be-block-toolbar';
- const titleFor = (action, fallback) => {
- if (global.StocketBaseShortcuts) {
- const entry = global.StocketBaseShortcuts.getShortcuts()[action];
- if (entry) {
- return fallback + ' (' + entry.combo + ')';
- }
- }
- return fallback;
- };
- toolbar.innerHTML =
- '<span class="be-block-type">' + iconSvg(BLOCK_ICONS[block.type] || 'text') + BLOCK_LABELS[block.type] + '</span>' +
- '<button type="button" data-action="up" title="' + titleFor('move_block_up', 'Move up') + '">' + iconSvg('arrow-up') + '</button>' +
- '<button type="button" data-action="down" title="' + titleFor('move_block_down', 'Move down') + '">' + iconSvg('arrow-down') + '</button>' +
- '<button type="button" data-action="add" title="' + titleFor('add_block', 'Add block below') + '">' + iconSvg('plus') + '</button>' +
- (isSystem ? '' : '<button type="button" data-action="remove" title="' + titleFor('remove_block', 'Remove block') + '">' + iconSvg('x') + '</button>');
-
- toolbar.querySelector('[data-action="up"]').addEventListener('click', () => this.moveBlock(block.id, -1));
- toolbar.querySelector('[data-action="down"]').addEventListener('click', () => this.moveBlock(block.id, 1));
- const removeButton = toolbar.querySelector('[data-action="remove"]');
- if (removeButton) {
- removeButton.addEventListener('click', () => this.removeBlock(block.id));
- }
- toolbar.querySelector('[data-action="add"]').addEventListener('click', (event) => {
- this.openBlockMenu(event.currentTarget, index);
- });
-
- wrapper.appendChild(toolbar);
- wrapper.appendChild(this.renderBlockBody(block));
-
- return wrapper;
- }
-
- openBlockMenu(anchorEl, afterIndex, onPick, replacingId) {
- const existing = document.querySelector('.be-block-menu');
- if (existing) {
- existing.remove();
- }
-
- const menu = document.createElement('div');
- menu.className = 'be-block-menu';
- Object.keys(BLOCK_LABELS).forEach((type) => {
- if (SYSTEM_TYPES.indexOf(type) !== -1 || (type === 'hero' && !this.isHome) || (type === 'contact_form' && (!this.allowContact || this.isHome))) {
- return;
- }
- const item = document.createElement('button');
- item.type = 'button';
- item.className = 'be-block-menu-item';
- item.innerHTML = iconSvg(BLOCK_ICONS[type] || 'text') + '<span>' + BLOCK_LABELS[type] + '</span>';
- if (!this.canUseType(type, replacingId || null)) {
- item.disabled = true;
- item.classList.add('be-block-menu-item-disabled');
- item.title = 'You can only have one ' + BLOCK_LABELS[type].toLowerCase() + ' per page.';
- menu.appendChild(item);
- return;
- }
- item.addEventListener('click', () => {
- if (onPick) {
- onPick(type);
- } else {
- this.addBlock(afterIndex, type);
- }
- menu.remove();
- });
- menu.appendChild(item);
- });
-
- document.body.appendChild(menu);
- const rect = anchorEl.getBoundingClientRect();
- positionFloating(menu, rect, { placement: 'below', gap: 4 });
-
- const closeHandler = (event) => {
- if (!menu.contains(event.target) && event.target !== anchorEl) {
- menu.remove();
- document.removeEventListener('click', closeHandler);
- }
- };
- setTimeout(() => document.addEventListener('click', closeHandler), 0);
- }
-
- renderBlockBody(block) {
- const container = document.createElement('div');
- container.className = 'be-block-body';
-
- const update = (patch) => {
- Object.assign(block.data, patch);
- this.syncHiddenInput();
- };
-
- switch (block.type) {
- case 'paragraph':
- case 'quote': {
- const el = document.createElement('div');
- el.className = 'be-editable';
- el.contentEditable = 'true';
- el.spellcheck = spellcheckEnabled();
- el.dataset.focusTarget = '1';
- el.innerHTML = block.data.text || '';
- el.addEventListener('input', () => {
- update({ text: el.innerHTML });
- if (el.textContent === '/') {
- this.openBlockMenu(el, -1, (type) => this.transformBlock(block.id, type), block.id);
- }
- });
- this.attachInlineToolbar(el, () => update({ text: el.innerHTML }));
- attachCleanPaste(el);
- container.appendChild(el);
- break;
- }
- case 'heading2':
- case 'heading3': {
- const el = document.createElement('div');
- el.className = 'be-editable be-heading';
- el.contentEditable = 'true';
- el.spellcheck = spellcheckEnabled();
- el.dataset.focusTarget = '1';
- el.textContent = block.data.text || '';
- el.addEventListener('input', () => update({ text: el.textContent }));
- attachCleanPaste(el);
- container.appendChild(el);
- break;
- }
- case 'image': {
- container.appendChild(this.buildInput('Image link', block.data.src, (val) => update({ src: val }), true));
- container.appendChild(this.buildUploadButton('image', (url) => {
- block.data.src = url;
- this.render();
- }));
- container.appendChild(this.buildInput('Describe the image (helps blind visitors and Google)', block.data.alt, (val) => update({ alt: val })));
- container.appendChild(this.buildInput('Caption', block.data.caption, (val) => update({ caption: val })));
- if (block.data.src) {
- const preview = document.createElement('img');
- preview.src = block.data.src;
- preview.className = 'be-preview-image';
- container.appendChild(preview);
- }
- break;
- }
- case 'video': {
- container.appendChild(this.buildInput('Video link', block.data.src, (val) => update({ src: val }), true));
- container.appendChild(this.buildUploadButton('video', (url) => {
- block.data.src = url;
- this.render();
- }));
- container.appendChild(this.buildInput('Caption', block.data.caption, (val) => update({ caption: val })));
- if (block.data.src) {
- const preview = document.createElement('video');
- preview.src = block.data.src;
- preview.controls = true;
- preview.className = 'be-preview-video';
- container.appendChild(preview);
- }
- break;
- }
- case 'list': {
- const styleSelect = document.createElement('select');
- ['unordered', 'ordered'].forEach((style) => {
- const option = document.createElement('option');
- option.value = style;
- option.textContent = style === 'unordered' ? 'Bullet list' : 'Numbered list';
- option.selected = block.data.style === style;
- styleSelect.appendChild(option);
- });
- styleSelect.addEventListener('change', () => update({ style: styleSelect.value }));
- container.appendChild(styleSelect);
-
- const itemsWrap = document.createElement('div');
- (block.data.items || ['']).forEach((item, itemIndex) => {
- const row = document.createElement('div');
- row.className = 'be-list-row';
- const input = document.createElement('input');
- input.type = 'text';
- input.value = item;
- input.addEventListener('input', () => {
- block.data.items[itemIndex] = input.value;
- this.syncHiddenInput();
- });
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.textContent = '−';
- removeBtn.addEventListener('click', () => {
- block.data.items.splice(itemIndex, 1);
- this.render();
- });
- row.appendChild(input);
- row.appendChild(removeBtn);
- itemsWrap.appendChild(row);
- });
- const addItemBtn = document.createElement('button');
- addItemBtn.type = 'button';
- addItemBtn.textContent = '+ Add item';
- addItemBtn.addEventListener('click', () => {
- block.data.items.push('');
- this.render();
- });
- container.appendChild(itemsWrap);
- container.appendChild(addItemBtn);
- break;
- }
- case 'code': {
- container.appendChild(this.buildInput('Code language (like html or php)', block.data.language, (val) => update({ language: val })));
- const textarea = document.createElement('textarea');
- textarea.className = 'be-code';
- textarea.value = block.data.code || '';
- textarea.spellcheck = false;
- textarea.addEventListener('input', () => update({ code: textarea.value }));
- container.appendChild(textarea);
- break;
- }
- case 'table': {
- const table = document.createElement('table');
- table.className = 'be-table';
- const rows = block.data.rows || [['', '']];
- rows.forEach((row, rowIndex) => {
- const tr = document.createElement('tr');
- row.forEach((cell, cellIndex) => {
- const td = document.createElement('td');
- const input = document.createElement('input');
- input.type = 'text';
- input.value = cell;
- input.addEventListener('input', () => {
- block.data.rows[rowIndex][cellIndex] = input.value;
- this.syncHiddenInput();
- });
- td.appendChild(input);
- tr.appendChild(td);
- });
- table.appendChild(tr);
- });
- const tableScroll = document.createElement('div');
- tableScroll.className = 'be-table-scroll';
- tableScroll.appendChild(table);
- container.appendChild(tableScroll);
-
- const controls = document.createElement('div');
- controls.className = 'be-table-controls';
- const addRowBtn = document.createElement('button');
- addRowBtn.type = 'button';
- addRowBtn.textContent = '+ Row';
- addRowBtn.addEventListener('click', () => {
- const cols = (block.data.rows[0] || ['', '']).length;
- block.data.rows.push(new Array(cols).fill(''));
- this.render();
- });
- const addColBtn = document.createElement('button');
- addColBtn.type = 'button';
- addColBtn.textContent = '+ Column';
- addColBtn.addEventListener('click', () => {
- block.data.rows.forEach((row) => row.push(''));
- this.render();
- });
- controls.appendChild(addRowBtn);
- controls.appendChild(addColBtn);
- container.appendChild(controls);
- break;
- }
- case 'checklist': {
- const itemsWrap = document.createElement('div');
- (block.data.items || []).forEach((item, itemIndex) => {
- const row = document.createElement('div');
- row.className = 'be-checklist-row';
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.checked = !!item.checked;
- checkbox.addEventListener('change', () => {
- block.data.items[itemIndex].checked = checkbox.checked;
- this.syncHiddenInput();
- });
- const input = document.createElement('input');
- input.type = 'text';
- input.value = item.text;
- input.addEventListener('input', () => {
- block.data.items[itemIndex].text = input.value;
- this.syncHiddenInput();
- });
- row.appendChild(checkbox);
- row.appendChild(input);
- itemsWrap.appendChild(row);
- });
- const addBtn = document.createElement('button');
- addBtn.type = 'button';
- addBtn.textContent = '+ Add item';
- addBtn.addEventListener('click', () => {
- block.data.items.push({ text: '', checked: false });
- this.render();
- });
- container.appendChild(itemsWrap);
- container.appendChild(addBtn);
- break;
- }
- case 'callout': {
- const styleSelect = document.createElement('select');
- ['info', 'warning', 'success', 'danger'].forEach((style) => {
- const option = document.createElement('option');
- option.value = style;
- option.textContent = style;
- option.selected = block.data.style === style;
- styleSelect.appendChild(option);
- });
- styleSelect.addEventListener('change', () => update({ style: styleSelect.value }));
- container.appendChild(styleSelect);
-
- const text = document.createElement('div');
- text.className = 'be-editable';
- text.contentEditable = 'true';
- text.spellcheck = spellcheckEnabled();
- text.dataset.focusTarget = '1';
- text.textContent = block.data.text || '';
- text.addEventListener('input', () => update({ text: text.textContent }));
- attachCleanPaste(text);
- container.appendChild(text);
- break;
- }
- case 'embed': {
- container.appendChild(this.buildInput('Link to embed (like a YouTube video)', block.data.url, (val) => update({ url: val }), true));
- break;
- }
- case 'separator':
- container.appendChild(document.createElement('hr'));
- break;
- case 'contact_form': {
- container.appendChild(this.buildInput('Heading above the form (optional), like Contact us', block.data.title || '', (val) => update({ title: val }), true));
- const note = document.createElement('div');
- note.className = 'be-contact-note';
- note.innerHTML = iconSvg('mail') + '<span>Visitors see a message form here. '
- + (this.allowContact
- ? 'Messages go to the email you set in Settings.'
- : 'The contact form is turned off in Settings, so visitors won\'t see it yet.')
- + ' You can have one per page.</span>';
- container.appendChild(note);
- break;
- }
- case 'hero': {
- container.appendChild(this.buildInput('Small text above the title (leave empty for "Home")', block.data.eyebrow || '', (val) => update({ eyebrow: val }), true));
- container.appendChild(this.buildInput('Title (leave empty for your store name)', block.data.title || '', (val) => update({ title: val })));
- container.appendChild(this.buildInput('Subtitle (leave empty for your store description)', block.data.subtitle || '', (val) => update({ subtitle: val })));
- const toggle = document.createElement('label');
- toggle.className = 'be-hero-toggle';
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.checked = block.data.show_eyebrow !== false;
- checkbox.addEventListener('change', () => update({ show_eyebrow: checkbox.checked }));
- toggle.appendChild(checkbox);
- toggle.appendChild(document.createTextNode(' Show the small label'));
- container.appendChild(toggle);
- break;
- }
- case 'home_featured':
- case 'home_products': {
- const note = document.createElement('div');
- note.className = 'be-contact-note be-system-note';
- note.innerHTML = iconSvg(BLOCK_ICONS[block.type]) + '<span>'
- + (block.type === 'home_featured'
- ? 'Your featured products go here. You choose them in Settings, Layout.'
- : 'All your products go here. You can change how they load in Settings, Layout.')
- + ' You can move this block, but it always stays on the page.</span>';
- container.appendChild(note);
- break;
- }
- default:
- break;
- }
-
- return container;
- }
-
- buildUploadButton(kind, onUploaded) {
- const wrapper = document.createElement('div');
- wrapper.className = 'be-upload-wrapper';
-
- const fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = kind === 'image' ? 'image/jpeg,image/png,image/gif,image/webp' : 'video/mp4,video/webm';
- fileInput.style.display = 'none';
-
- const button = document.createElement('button');
- button.type = 'button';
- button.textContent = 'Upload ' + kind;
- button.addEventListener('click', () => {
- if (window.StocketBaseMedia) {
- window.StocketBaseMedia.open({ type: kind === 'image' ? 'image' : 'video', onSelect: onUploaded });
- } else {
- fileInput.click();
- }
- });
-
- const status = document.createElement('span');
- status.className = 'be-upload-status';
-
- fileInput.addEventListener('change', () => {
- const file = fileInput.files[0];
- if (!file) {
- return;
- }
- status.textContent = 'Uploading…';
- uploadFile(
- file,
- (url) => {
- status.textContent = '';
- onUploaded(url);
- },
- (error) => {
- status.textContent = error;
- }
- );
- });
-
- wrapper.appendChild(button);
- wrapper.appendChild(fileInput);
- wrapper.appendChild(status);
- return wrapper;
- }
-
- buildInput(placeholder, value, onInput, autofocus) {
- const input = document.createElement('input');
- input.type = 'text';
- input.placeholder = placeholder;
- input.value = value || '';
- if (autofocus) {
- input.dataset.focusTarget = '1';
- }
- input.addEventListener('input', () => onInput(input.value));
- return input;
- }
- }
-
- function ensureBlockEditorIconStyles() {
- if (document.getElementById('be-icon-styles')) {
- return;
- }
- const style = document.createElement('style');
- style.id = 'be-icon-styles';
- style.textContent =
- '.be-block-type{display:inline-flex;align-items:center;gap:6px;}' +
- '.be-block-toolbar button svg.icon{display:block;}' +
- '.be-block-menu-item{display:flex;align-items:center;gap:10px;width:100%;text-align:left;}' +
- '.be-block-menu-item svg.icon{flex-shrink:0;}';
- document.head.appendChild(style);
- }
-
- document.addEventListener('DOMContentLoaded', function () {
- const root = document.getElementById('block-editor-root');
- if (root) {
- ensureBlockEditorIconStyles();
- window.stocketbaseBlockEditor = new BlockEditor(root);
- }
- });
- })(window);
-