/** * SISHoverCardTool — Dedicated Hover Cards Block Tool for Editor.js * Allows content managers to freely add, remove, reorder, and configure interactive hover cards. */ class SISHoverCardTool { static get toolbox() { return { title: 'Hover Cards', icon: '' }; } constructor({ data, api, readOnly }) { this.api = api; this.readOnly = readOnly; this.instanceId = 'hc_' + Math.random().toString(36).substring(7); this.data = { cols: parseInt(data && data.cols) || 4, styleVariant: (data && data.styleVariant) || 'v1', globalClass: (data && data.globalClass) || 'sis-hover-card-section', globalId: (data && data.globalId) || '', globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '', items: (data && data.items && Array.isArray(data.items)) ? data.items : [] }; // Add initial default item if list is empty if (this.data.items.length === 0) { this.data.items.push({ title: 'Card Title', description: 'Card description text...', imageUrl: '', active: true }); } this.wrapper = undefined; } render() { this.wrapper = document.createElement('div'); this.wrapper.className = 'p-3 bg-light border rounded mb-3 ce-hovercard-tool-wrapper'; this.wrapper.style.fontFamily = 'inherit'; const titleLabel = document.createElement('div'); titleLabel.className = 'font-weight-bold text-primary small mb-3'; titleLabel.innerHTML = ' Dedicated Hover Cards Settings'; this.wrapper.appendChild(titleLabel); // Global Settings Row const settingsRow = document.createElement('div'); settingsRow.className = 'form-row mb-3 pb-2 border-bottom'; // 1. Cols selector const colDiv = document.createElement('div'); colDiv.className = 'col-md-3 mb-2'; colDiv.innerHTML = ''; const colSelect = document.createElement('select'); colSelect.className = 'form-control form-control-sm'; [1, 2, 3, 4, 6].forEach(num => { const opt = document.createElement('option'); opt.value = num; opt.textContent = `${num} Column${num > 1 ? 's' : ''}`; if (num === this.data.cols) opt.selected = true; colSelect.appendChild(opt); }); if (this.readOnly) colSelect.disabled = true; colSelect.addEventListener('change', (e) => { this.data.cols = parseInt(e.target.value) || 4; }); this.colsSelect = colSelect; colDiv.appendChild(colSelect); settingsRow.appendChild(colDiv); // 2. Style Variant Selector (Hover 1 vs Hover 2) const variantDiv = document.createElement('div'); variantDiv.className = 'col-md-3 mb-2'; variantDiv.innerHTML = ''; const variantSelect = document.createElement('select'); variantSelect.className = 'form-control form-control-sm'; const optV1 = document.createElement('option'); optV1.value = 'v1'; optV1.textContent = 'Hover 1 (Standard Light)'; if (this.data.styleVariant === 'v1') optV1.selected = true; variantSelect.appendChild(optV1); const optV2 = document.createElement('option'); optV2.value = 'v2'; optV2.textContent = 'Hover 2 (Modern Blue & Glow)'; if (this.data.styleVariant === 'v2') optV2.selected = true; variantSelect.appendChild(optV2); if (this.readOnly) variantSelect.disabled = true; variantSelect.addEventListener('change', (e) => { this.data.styleVariant = e.target.value; }); this.variantSelect = variantSelect; variantDiv.appendChild(variantSelect); settingsRow.appendChild(variantDiv); // 2. Global CSS Class const classDiv = document.createElement('div'); classDiv.className = 'col-md-3 mb-2'; classDiv.innerHTML = ''; const classInput = document.createElement('input'); classInput.type = 'text'; classInput.className = 'form-control form-control-sm'; classInput.placeholder = 'e.g. sis-hover-card-section'; classInput.value = this.data.globalClass || ''; if (this.readOnly) classInput.disabled = true; classInput.addEventListener('input', (e) => this.data.globalClass = e.target.value.trim()); this.globalClassInput = classInput; classDiv.appendChild(classInput); settingsRow.appendChild(classDiv); // 3. Global HTML ID const idDiv = document.createElement('div'); idDiv.className = 'col-md-3 mb-2'; idDiv.innerHTML = ''; const idInput = document.createElement('input'); idInput.type = 'text'; idInput.className = 'form-control form-control-sm'; idInput.placeholder = 'e.g. hover-cards-section'; idInput.value = this.data.globalId || ''; if (this.readOnly) idInput.disabled = true; idInput.addEventListener('input', (e) => this.data.globalId = e.target.value.trim()); this.globalIdInput = idInput; idDiv.appendChild(idInput); settingsRow.appendChild(idDiv); // 4. Global Custom Attributes const attrDiv = document.createElement('div'); attrDiv.className = 'col-md-3 mb-2'; attrDiv.innerHTML = ''; const attrInput = document.createElement('input'); attrInput.type = 'text'; attrInput.className = 'form-control form-control-sm'; attrInput.placeholder = 'e.g. data-aos="fade-up" style="..."'; attrInput.value = this.data.globalAttributes || ''; if (this.readOnly) attrInput.disabled = true; attrInput.addEventListener('input', (e) => this.data.globalAttributes = e.target.value.trim()); this.globalAttrInput = attrInput; attrDiv.appendChild(attrInput); settingsRow.appendChild(attrDiv); this.wrapper.appendChild(settingsRow); // Cards items container this.itemsContainer = document.createElement('div'); this.wrapper.appendChild(this.itemsContainer); this._renderAllItems(); if (!this.readOnly) { const addButton = document.createElement('button'); addButton.type = 'button'; addButton.className = 'btn btn-sm btn-success mt-2'; addButton.innerHTML = ' Add Hover Card'; addButton.addEventListener('click', () => { this.data.items.push({ title: 'New Card Title', description: 'New card description text...', imageUrl: '', active: false }); this._renderAllItems(); }); this.wrapper.appendChild(addButton); } return this.wrapper; } _renderAllItems() { this.itemsContainer.innerHTML = ''; this.data.items.forEach((item, index) => { this._renderItem(item, index); }); } _renderItem(item, index) { const itemBox = document.createElement('div'); itemBox.className = 'mb-3 p-3 bg-white border rounded shadow-sm'; itemBox.style.position = 'relative'; // Top Control Action buttons (Move Up, Move Down, Delete) if (!this.readOnly) { const actionGroup = document.createElement('div'); actionGroup.style.position = 'absolute'; actionGroup.style.top = '10px'; actionGroup.style.right = '10px'; actionGroup.className = 'btn-group btn-group-sm'; if (index > 0) { const upBtn = document.createElement('button'); upBtn.type = 'button'; upBtn.className = 'btn btn-outline-secondary'; upBtn.innerHTML = ''; upBtn.title = 'Move Up'; upBtn.addEventListener('click', () => { const temp = this.data.items[index]; this.data.items[index] = this.data.items[index - 1]; this.data.items[index - 1] = temp; this._renderAllItems(); }); actionGroup.appendChild(upBtn); } if (index < this.data.items.length - 1) { const downBtn = document.createElement('button'); downBtn.type = 'button'; downBtn.className = 'btn btn-outline-secondary'; downBtn.innerHTML = ''; downBtn.title = 'Move Down'; downBtn.addEventListener('click', () => { const temp = this.data.items[index]; this.data.items[index] = this.data.items[index + 1]; this.data.items[index + 1] = temp; this._renderAllItems(); }); actionGroup.appendChild(downBtn); } const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn btn-danger'; removeBtn.innerHTML = ''; removeBtn.title = 'Remove Card'; removeBtn.addEventListener('click', () => { this.data.items.splice(index, 1); this._renderAllItems(); }); actionGroup.appendChild(removeBtn); itemBox.appendChild(actionGroup); } // Header Label const itemHeader = document.createElement('h6'); itemHeader.className = 'font-weight-bold text-dark mb-3'; itemHeader.innerHTML = ` Card #${index + 1}`; itemBox.appendChild(itemHeader); // 1. Card Title const titleGroup = document.createElement('div'); titleGroup.className = 'form-group mb-2'; titleGroup.innerHTML = ''; const titleInput = document.createElement('input'); titleInput.type = 'text'; titleInput.className = 'form-control form-control-sm'; titleInput.placeholder = 'e.g. Tiên phong'; titleInput.value = item.title || ''; if (this.readOnly) titleInput.disabled = true; titleInput.addEventListener('input', (e) => { item.title = e.target.value; }); titleGroup.appendChild(titleInput); itemBox.appendChild(titleGroup); // 2. Card Description const descGroup = document.createElement('div'); descGroup.className = 'form-group mb-2'; descGroup.innerHTML = ''; const descInput = document.createElement('textarea'); descInput.className = 'form-control form-control-sm'; descInput.rows = 2; descInput.placeholder = 'e.g. Phát triển và ứng dụng kỹ thuật tiên tiến...'; descInput.value = item.description || ''; if (this.readOnly) descInput.disabled = true; descInput.addEventListener('input', (e) => { item.description = e.target.value; }); descGroup.appendChild(descInput); itemBox.appendChild(descGroup); // 3. Image URL + Media Library Selector const imgGroup = document.createElement('div'); imgGroup.className = 'form-group mb-2'; imgGroup.innerHTML = ''; const inputGroup = document.createElement('div'); inputGroup.className = 'input-group input-group-sm mb-2'; const imgInput = document.createElement('input'); imgInput.type = 'text'; imgInput.className = 'form-control'; imgInput.placeholder = '/uploads/2026/07/icon.jpg'; imgInput.value = item.imageUrl || ''; if (this.readOnly) imgInput.disabled = true; const previewContainer = document.createElement('div'); previewContainer.className = 'mt-1'; const updatePreview = (url) => { previewContainer.innerHTML = ''; if (url) { const img = document.createElement('img'); img.src = url; img.style.maxHeight = '60px'; img.style.maxWidth = '100px'; img.style.objectFit = 'contain'; img.className = 'border rounded p-1 bg-light'; previewContainer.appendChild(img); } }; updatePreview(item.imageUrl); imgInput.addEventListener('input', (e) => { item.imageUrl = e.target.value.trim(); updatePreview(item.imageUrl); }); inputGroup.appendChild(imgInput); if (!this.readOnly) { const appendDiv = document.createElement('div'); appendDiv.className = 'input-group-append'; const mediaBtn = document.createElement('button'); mediaBtn.type = 'button'; mediaBtn.className = 'btn btn-outline-info'; mediaBtn.innerHTML = ' Media Library'; mediaBtn.addEventListener('click', () => { if (window.SISMediaPicker) { SISMediaPicker.open((selectedUrl, mediaObj, config) => { imgInput.value = selectedUrl; item.imageUrl = selectedUrl; if (config) { item.imageStyle = config.style || ''; item.imageClass = config.cssClass || ''; item.imageAlt = config.alt || ''; item.imageAttrs = config.customAttributes || ''; item.imageAspectRatio = config.aspectRatio || ''; } updatePreview(selectedUrl); }); } else { alert('Media modal function openSISMediaModal is not available.'); } }); appendDiv.appendChild(mediaBtn); inputGroup.appendChild(appendDiv); } imgGroup.appendChild(inputGroup); imgGroup.appendChild(previewContainer); itemBox.appendChild(imgGroup); // 4. Hover Icon / Image URL (Swapped on hover) const hoverImgGroup = document.createElement('div'); hoverImgGroup.className = 'form-group mb-2'; hoverImgGroup.innerHTML = ''; const hoverInputGroup = document.createElement('div'); hoverInputGroup.className = 'input-group input-group-sm mb-2'; const hoverImgInput = document.createElement('input'); hoverImgInput.type = 'text'; hoverImgInput.className = 'form-control'; hoverImgInput.placeholder = '/uploads/2026/07/hover-icon.jpg (optional)'; hoverImgInput.value = item.hoverImageUrl || ''; if (this.readOnly) hoverImgInput.disabled = true; const hoverPreviewContainer = document.createElement('div'); hoverPreviewContainer.className = 'mt-1'; const updateHoverPreview = (url) => { hoverPreviewContainer.innerHTML = ''; if (url) { const img = document.createElement('img'); img.src = url; img.style.maxHeight = '60px'; img.style.maxWidth = '100px'; img.style.objectFit = 'contain'; img.className = 'border rounded p-1 bg-info text-white'; hoverPreviewContainer.appendChild(img); } }; updateHoverPreview(item.hoverImageUrl); hoverImgInput.addEventListener('input', (e) => { item.hoverImageUrl = e.target.value.trim(); updateHoverPreview(item.hoverImageUrl); }); hoverInputGroup.appendChild(hoverImgInput); if (!this.readOnly) { const appendDiv = document.createElement('div'); appendDiv.className = 'input-group-append'; const mediaBtn = document.createElement('button'); mediaBtn.type = 'button'; mediaBtn.className = 'btn btn-outline-info'; mediaBtn.innerHTML = ' Media Library'; mediaBtn.addEventListener('click', () => { if (window.SISMediaPicker) { SISMediaPicker.open((selectedUrl, mediaObj, config) => { hoverImgInput.value = selectedUrl; item.hoverImageUrl = selectedUrl; if (config) { item.hoverImageStyle = config.style || ''; item.hoverImageClass = config.cssClass || ''; item.hoverImageAlt = config.alt || ''; item.hoverImageAttrs = config.customAttributes || ''; item.hoverImageAspectRatio = config.aspectRatio || ''; } updateHoverPreview(selectedUrl); }); } else { alert('Media modal function openSISMediaModal is not available.'); } }); appendDiv.appendChild(mediaBtn); hoverInputGroup.appendChild(appendDiv); } hoverImgGroup.appendChild(hoverInputGroup); hoverImgGroup.appendChild(hoverPreviewContainer); // Custom Attributes field for individual card const cardAttrDiv = document.createElement('div'); cardAttrDiv.className = 'form-group mb-2'; cardAttrDiv.innerHTML = ''; const cardAttrInput = document.createElement('input'); cardAttrInput.type = 'text'; cardAttrInput.className = 'form-control form-control-sm'; cardAttrInput.placeholder = 'e.g. data-aos="fade-up" style="background:#fff"'; cardAttrInput.value = item.attributes || ''; if (this.readOnly) cardAttrInput.disabled = true; cardAttrInput.addEventListener('input', (e) => { item.attributes = e.target.value.trim(); }); cardAttrDiv.appendChild(cardAttrInput); itemBox.appendChild(cardAttrDiv); // Default Active Radio (Only 1 option active at a time) const activeRadioDiv = document.createElement('div'); activeRadioDiv.className = 'custom-control custom-radio mt-2'; const activeRadio = document.createElement('input'); activeRadio.type = 'radio'; activeRadio.name = 'hovercard_active_' + this.instanceId; activeRadio.className = 'custom-control-input'; const radioId = 'hovercard_active_' + index + '_' + Math.random().toString(36).substring(7); activeRadio.id = radioId; activeRadio.checked = !!item.active; if (this.readOnly) activeRadio.disabled = true; activeRadio.addEventListener('change', () => { this.data.items.forEach((it, i) => { it.active = (i === index); }); }); const activeLabel = document.createElement('label'); activeLabel.className = 'custom-control-label small font-weight-bold text-secondary'; activeLabel.htmlFor = radioId; activeLabel.innerHTML = 'Default Active Card (Only 1 active at a time)'; activeRadioDiv.appendChild(activeRadio); activeRadioDiv.appendChild(activeLabel); itemBox.appendChild(activeRadioDiv); this.itemsContainer.appendChild(itemBox); } save() { return { cols: this.colsSelect ? parseInt(this.colsSelect.value) : (this.data.cols || 4), styleVariant: this.variantSelect ? this.variantSelect.value : (this.data.styleVariant || 'v1'), globalClass: this.globalClassInput ? this.globalClassInput.value.trim() : (this.data.globalClass || 'sis-hover-card-section'), globalId: this.globalIdInput ? this.globalIdInput.value.trim() : (this.data.globalId || ''), globalAttributes: this.globalAttrInput ? this.globalAttrInput.value.trim() : (this.data.globalAttributes || ''), items: this.data.items || [] }; } } // Register the plugin globally window.SISEditorPlugins = window.SISEditorPlugins || {}; window.SISEditorPlugins['hovercard'] = { class: SISHoverCardTool };