315 lines
14 KiB
Plaintext
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 };
|