Files

315 lines
14 KiB
Plaintext

/**
* SISStickyNavTool — Sticky Navigation & Sub-menu Plugin for Editor.js
* Allows content managers to build dynamic page navigation sub-menus with smooth anchor links.
*/
class SISStickyNavTool {
static get toolbox() {
return {
title: 'Sticky Nav / Sub-menu',
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'
};
}
constructor({ data, api, readOnly }) {
this.api = api;
this.readOnly = readOnly;
this.data = {
title: (data && data.title) ? data.title : 'Quick Navigation',
bgColor: (data && data.bgColor) ? data.bgColor : '#002554',
globalClass: (data && data.globalClass) || '',
globalId: (data && data.globalId) || '',
globalAttributes: (data && data.globalAttributes) || (data && data.attributes) || '',
items: (data && Array.isArray(data.items)) ? data.items : [
{ label: 'Giới thiệu', link: '#sec-intro' },
{ label: 'Chuyên khoa', link: '#sec-specialty' },
{ label: 'Quy trình', link: '#sec-process' }
],
sticky: (data && data.sticky !== undefined) ? !!data.sticky : true,
stretched: (data && data.stretched !== undefined) ? !!data.stretched : true
};
this.titleInput = null;
this.itemsContainer = null;
}
render() {
const container = document.createElement('div');
container.style.border = '1px solid #e3e6f0';
container.style.borderRadius = '8px';
container.style.padding = '14px';
container.style.background = '#fff';
container.style.marginBottom = '12px';
const headerLabel = document.createElement('label');
headerLabel.className = 'font-weight-bold text-primary small mb-2 d-block';
headerLabel.innerHTML = '<i class="fas fa-bars"></i> Sticky Page Navigation Bar Settings';
container.appendChild(headerLabel);
// Settings Row for Title, Class, ID & Attributes
const metaRow = document.createElement('div');
metaRow.className = 'form-row mb-3';
// 1. Mobile Title
const titleDiv = document.createElement('div');
titleDiv.className = 'col-md-3 mb-2';
titleDiv.innerHTML = '<label class="small font-weight-bold text-secondary mb-1 d-block">Dropdown Title</label>';
this.titleInput = document.createElement('input');
this.titleInput.type = 'text';
this.titleInput.className = 'form-control form-control-sm';
this.titleInput.placeholder = 'e.g. Quick Navigation';
this.titleInput.value = this.data.title || 'Quick Navigation';
if (this.readOnly) this.titleInput.disabled = true;
titleDiv.appendChild(this.titleInput);
metaRow.appendChild(titleDiv);
// 2. 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 d-block">CSS Class(es)</label>';
this.classInput = document.createElement('input');
this.classInput.type = 'text';
this.classInput.className = 'form-control form-control-sm';
this.classInput.placeholder = 'e.g. my-custom-nav';
this.classInput.value = this.data.globalClass || '';
if (this.readOnly) this.classInput.disabled = true;
this.classInput.addEventListener('input', (e) => {
this.data.globalClass = e.target.value.trim();
});
classDiv.appendChild(this.classInput);
metaRow.appendChild(classDiv);
// 3. 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 d-block">HTML ID</label>';
this.idInput = document.createElement('input');
this.idInput.type = 'text';
this.idInput.className = 'form-control form-control-sm';
this.idInput.placeholder = 'e.g. main-sticky-nav';
this.idInput.value = this.data.globalId || '';
if (this.readOnly) this.idInput.disabled = true;
this.idInput.addEventListener('input', (e) => {
this.data.globalId = e.target.value.trim();
});
idDiv.appendChild(this.idInput);
metaRow.appendChild(idDiv);
// 4. 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 d-block"><i class="fas fa-sliders-h"></i> Custom Attributes</label>';
this.attrInput = document.createElement('input');
this.attrInput.type = 'text';
this.attrInput.className = 'form-control form-control-sm';
this.attrInput.placeholder = 'e.g. data-aos="fade-down" style="..."';
this.attrInput.value = this.data.globalAttributes || '';
if (this.readOnly) this.attrInput.disabled = true;
this.attrInput.addEventListener('input', (e) => {
this.data.globalAttributes = e.target.value.trim();
});
attrDiv.appendChild(this.attrInput);
metaRow.appendChild(attrDiv);
container.appendChild(metaRow);
// Switches Row (Sticky + Stretched)
const switchesRow = document.createElement('div');
switchesRow.className = 'd-flex flex-wrap gap-3 mb-3';
// Sticky switch
const stickyWrapper = document.createElement('div');
stickyWrapper.className = 'custom-control custom-switch mr-3';
const stickyCheck = document.createElement('input');
stickyCheck.type = 'checkbox';
stickyCheck.className = 'custom-control-input';
stickyCheck.id = 'nav_sticky_' + Math.random().toString(36).substring(7);
stickyCheck.checked = !!this.data.sticky;
if (this.readOnly) stickyCheck.disabled = true;
const stickyLabel = document.createElement('label');
stickyLabel.className = 'custom-control-label small font-weight-bold text-primary';
stickyLabel.htmlFor = stickyCheck.id;
stickyLabel.innerHTML = '<i class="fas fa-thumbtack"></i> Sticky to Top on Scroll';
stickyCheck.addEventListener('change', () => {
this.data.sticky = stickyCheck.checked;
});
stickyWrapper.appendChild(stickyCheck);
stickyWrapper.appendChild(stickyLabel);
switchesRow.appendChild(stickyWrapper);
// Stretch switch
const stretchWrapper = document.createElement('div');
stretchWrapper.className = 'custom-control custom-switch';
this.stretchCheck = document.createElement('input');
this.stretchCheck.type = 'checkbox';
this.stretchCheck.className = 'custom-control-input';
this.stretchCheck.id = 'nav_stretch_' + Math.random().toString(36).substring(7);
this.stretchCheck.checked = !!this.data.stretched;
if (this.readOnly) this.stretchCheck.disabled = true;
const stretchLabel = document.createElement('label');
stretchLabel.className = 'custom-control-label small font-weight-bold text-secondary';
stretchLabel.htmlFor = this.stretchCheck.id;
stretchLabel.innerHTML = '<i class="fas fa-arrows-alt-h"></i> Stretch to Full Screen Width';
this.stretchCheck.addEventListener('change', () => {
this.data.stretched = this.stretchCheck.checked;
});
stretchWrapper.appendChild(this.stretchCheck);
stretchWrapper.appendChild(stretchLabel);
switchesRow.appendChild(stretchWrapper);
container.appendChild(switchesRow);
// Background Color Selection
const bgGroup = document.createElement('div');
bgGroup.className = 'form-group mb-3';
const bgLabel = document.createElement('label');
bgLabel.className = 'small font-weight-bold text-secondary mb-1 d-block';
bgLabel.innerText = 'Background Style';
this.bgSelect = document.createElement('select');
this.bgSelect.className = 'form-control form-control-sm';
this.bgSelect.innerHTML = `
<option value="#002554">Dark Blue (SIS Primary - #002554)</option>
<option value="#881C1C">Brand Red (#881C1C)</option>
<option value="#1a252f">Charcoal Dark (#1a252f)</option>
<option value="#ffffff">White (#ffffff)</option>
`;
this.bgSelect.value = this.data.bgColor || '#002554';
if (this.readOnly) this.bgSelect.disabled = true;
bgGroup.appendChild(bgLabel);
bgGroup.appendChild(this.bgSelect);
container.appendChild(bgGroup);
// Navigation Items List
const itemsLabel = document.createElement('label');
itemsLabel.className = 'small font-weight-bold text-secondary mb-2 d-block';
itemsLabel.innerText = 'Navigation Links / Anchors';
container.appendChild(itemsLabel);
this.itemsContainer = document.createElement('div');
this.itemsContainer.className = 'nav-items-list mb-2';
container.appendChild(this.itemsContainer);
const renderItemsInputs = () => {
this.itemsContainer.innerHTML = '';
this.data.items.forEach((item, index) => {
const itemRow = document.createElement('div');
itemRow.className = 'form-row mb-2 align-items-center';
const colLabel = document.createElement('div');
colLabel.className = 'col-md-5';
const labelInp = document.createElement('input');
labelInp.type = 'text';
labelInp.className = 'form-control form-control-sm';
labelInp.placeholder = 'Link Label (e.g. Giới thiệu)';
labelInp.value = item.label || '';
if (this.readOnly) labelInp.disabled = true;
labelInp.addEventListener('input', () => {
item.label = labelInp.value;
updatePreview();
});
colLabel.appendChild(labelInp);
const colLink = document.createElement('div');
colLink.className = 'col-md-5';
const linkInp = document.createElement('input');
linkInp.type = 'text';
linkInp.className = 'form-control form-control-sm';
linkInp.placeholder = 'Target ID or URL (e.g. #sec-intro)';
linkInp.value = item.link || '';
if (this.readOnly) linkInp.disabled = true;
linkInp.addEventListener('input', () => {
item.link = linkInp.value;
updatePreview();
});
colLink.appendChild(linkInp);
const colDel = document.createElement('div');
colDel.className = 'col-md-2';
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'btn btn-sm btn-outline-danger btn-block';
delBtn.innerHTML = '<i class="fas fa-trash"></i>';
if (this.readOnly) delBtn.disabled = true;
delBtn.addEventListener('click', () => {
this.data.items.splice(index, 1);
renderItemsInputs();
updatePreview();
});
colDel.appendChild(delBtn);
itemRow.appendChild(colLabel);
itemRow.appendChild(colLink);
itemRow.appendChild(colDel);
this.itemsContainer.appendChild(itemRow);
});
};
if (!this.readOnly) {
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'btn btn-sm btn-outline-primary mb-3';
addBtn.innerHTML = '<i class="fas fa-plus"></i> Add Nav Link';
addBtn.addEventListener('click', () => {
this.data.items.push({ label: 'New Link', link: '#section' });
renderItemsInputs();
updatePreview();
});
container.appendChild(addBtn);
}
// Live Preview Box
const previewBox = document.createElement('div');
previewBox.className = 'nav-preview p-2 rounded my-2 d-flex flex-wrap justify-content-center align-items-center gap-2';
previewBox.style.transition = 'background 0.3s ease';
container.appendChild(previewBox);
const updatePreview = () => {
const bg = this.bgSelect.value;
const isLight = bg === '#ffffff';
previewBox.style.background = bg;
let html = '';
this.data.items.forEach(item => {
const label = item.label || 'Link';
const textColor = isLight ? '#002554' : '#ffffff';
html += `<span class="badge px-3 py-2 mr-2 mb-1" style="background: rgba(255,255,255,0.15); color: ${textColor}; font-size: 13px; font-weight: 600;"><i class="fas fa-link mr-1"></i>${label}</span>`;
});
previewBox.innerHTML = html || '<span class="text-muted small">No nav links added</span>';
};
this.bgSelect.addEventListener('change', () => {
this.data.bgColor = this.bgSelect.value;
updatePreview();
});
renderItemsInputs();
updatePreview();
return container;
}
save() {
return {
title: this.titleInput ? this.titleInput.value : (this.data.title || 'Quick Navigation'),
bgColor: this.bgSelect ? this.bgSelect.value : this.data.bgColor,
globalClass: this.classInput ? this.classInput.value.trim() : (this.data.globalClass || ''),
globalId: this.idInput ? this.idInput.value.trim() : (this.data.globalId || ''),
globalAttributes: this.attrInput ? this.attrInput.value.trim() : (this.data.globalAttributes || ''),
items: this.data.items || [],
sticky: this.data.sticky !== undefined ? !!this.data.sticky : true,
stretched: this.stretchCheck ? this.stretchCheck.checked : !!this.data.stretched
};
}
}
// Alias class for nav
class SISNavTool extends SISStickyNavTool {}
// Register plugin globally
window.SISEditorPlugins = window.SISEditorPlugins || {};
window.SISEditorPlugins['stickyNav'] = { class: SISStickyNavTool };
window.SISEditorPlugins['nav'] = { class: SISNavTool };