474 lines
21 KiB
Plaintext
474 lines
21 KiB
Plaintext
/**
|
|
* 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: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="14" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect></svg>'
|
|
};
|
|
}
|
|
|
|
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 = '<i class="fas fa-th-large"></i> 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 = '<label class="small font-weight-bold text-secondary mb-1">Columns per Row</label>';
|
|
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 = '<label class="small font-weight-bold text-primary mb-1"><i class="fas fa-palette"></i> Card Style Variant</label>';
|
|
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 = '<label class="small font-weight-bold text-secondary mb-1">Global CSS Class(es)</label>';
|
|
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 = '<label class="small font-weight-bold text-secondary mb-1">Global HTML ID</label>';
|
|
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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
|
|
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 = '<i class="fas fa-plus"></i> 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 = '<i class="fas fa-arrow-up"></i>';
|
|
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 = '<i class="fas fa-arrow-down"></i>';
|
|
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 = '<i class="fas fa-trash-alt"></i>';
|
|
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 = `<i class="fas fa-id-card text-info mr-1"></i> Card #${index + 1}`;
|
|
itemBox.appendChild(itemHeader);
|
|
|
|
// 1. Card Title
|
|
const titleGroup = document.createElement('div');
|
|
titleGroup.className = 'form-group mb-2';
|
|
titleGroup.innerHTML = '<label class="small font-weight-bold text-secondary mb-1">Card Title</label>';
|
|
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 = '<label class="small font-weight-bold text-secondary mb-1">Card Description</label>';
|
|
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 = '<label class="small font-weight-bold text-secondary mb-1">Card Icon / Image URL</label>';
|
|
|
|
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 = '<i class="fas fa-images"></i> 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 = '<label class="small font-weight-bold text-info mb-1"><i class="fas fa-sync-alt"></i> Hover Icon / Image URL (Optional - Swaps on Hover)</label>';
|
|
|
|
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 = '<i class="fas fa-images"></i> 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 = '<label class="small font-weight-bold text-secondary mb-1"><i class="fas fa-sliders-h"></i> Card Custom Attributes</label>';
|
|
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 };
|