Files
sisvietnamvn_01/sisvietnamvn_Trang chính thức hiện tại/Edit Page - SIS Vietnam - Manage_files/hover-card.js.download
T

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 };