refactor: modularize sticky navigation editor tool into a separate plugin file
This commit is contained in:
Binary file not shown.
@@ -485,13 +485,13 @@ figure.table table tr:hover {
|
||||
.education .gap-3 {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.education .flex {
|
||||
.flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.education .flex .h-stretch {
|
||||
height: stretch;
|
||||
}
|
||||
.education .flex-col {
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
.education .flex-row {
|
||||
|
||||
@@ -709,242 +709,7 @@ class SISHeroBannerTool {
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
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);
|
||||
|
||||
// Mobile Title Group
|
||||
const titleGroup = document.createElement('div');
|
||||
titleGroup.className = 'form-group mb-3';
|
||||
const titleLabel = document.createElement('label');
|
||||
titleLabel.className = 'small font-weight-bold text-secondary mb-1 d-block';
|
||||
titleLabel.innerText = 'Mobile Menu Dropdown Title';
|
||||
this.titleInput = document.createElement('input');
|
||||
this.titleInput.type = 'text';
|
||||
this.titleInput.className = 'form-control form-control-sm';
|
||||
this.titleInput.placeholder = 'e.g. Quick Navigation or Info For';
|
||||
this.titleInput.value = this.data.title || 'Quick Navigation';
|
||||
titleGroup.appendChild(titleLabel);
|
||||
titleGroup.appendChild(this.titleInput);
|
||||
container.appendChild(titleGroup);
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
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';
|
||||
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 || '';
|
||||
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 || '';
|
||||
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>';
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
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(blockContent) {
|
||||
return {
|
||||
title: this.titleInput ? this.titleInput.value : (this.data.title || 'Quick Navigation'),
|
||||
bgColor: this.bgSelect ? this.bgSelect.value : this.data.bgColor,
|
||||
items: this.data.items || [],
|
||||
sticky: this.data.sticky !== undefined ? !!this.data.sticky : true,
|
||||
stretched: this.stretchCheck ? this.stretchCheck.checked : !!this.data.stretched
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SIS Block Editor on a given holder element.
|
||||
@@ -1042,7 +807,9 @@ function initSISEditor(holderId, hiddenInputId, initialData, skipSubmitHandler =
|
||||
builtInTools.accordion = { class: SISAccordionTool };
|
||||
builtInTools.youtube = { class: SISYouTubeTool };
|
||||
builtInTools.hero = { class: SISHeroBannerTool };
|
||||
builtInTools.stickyNav = { class: SISStickyNavTool };
|
||||
if (typeof SISStickyNavTool !== 'undefined') {
|
||||
builtInTools.stickyNav = { class: SISStickyNavTool };
|
||||
}
|
||||
|
||||
// === Merge built-in tools with any registered plugins ===
|
||||
var allTools = Object.assign({}, builtInTools, window.SISEditorPlugins);
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -441,6 +441,7 @@
|
||||
<script th:src="@{/js/manage/editor-plugins/flex.js}"></script>
|
||||
<script th:src="@{/js/manage/editor-plugins/hover-card.js}"></script>
|
||||
<script th:src="@{/js/manage/editor-plugins/tag.js}"></script>
|
||||
<script th:src="@{/js/manage/editor-plugins/nav.js}"></script>
|
||||
|
||||
<!-- SIS Standalone Media Picker Library -->
|
||||
<script th:src="@{/js/manage/sis-media-picker.js}"></script>
|
||||
|
||||
@@ -208,11 +208,13 @@
|
||||
</th:block>
|
||||
|
||||
<!-- 13. Sticky Nav / Sub-menu Block (UMass Theme mc--info-menu style) -->
|
||||
<th:block th:if="${block['type'] == 'stickyNav'}">
|
||||
<th:block th:if="${block['type'] == 'stickyNav' or block['type'] == 'nav'}">
|
||||
<nav
|
||||
th:id="${block.data['globalId'] != null and !#strings.isEmpty(block.data['globalId']) ? block.data['globalId'] : null}"
|
||||
class="mc--menu mc--info-menu sis-sticky-nav my-3"
|
||||
th:classappend="${(block.data['stretched'] == true ? 'sis-fullwidth-breakout ' : '') + (block.data['sticky'] == true or block.data['sticky'] == null ? 'is-sticky ' : '') + (block.data['bgColor'] == '#ffffff' ? 'sis-nav-light ' : '')}"
|
||||
th:classappend="${(block.data['globalClass'] != null and !#strings.isEmpty(block.data['globalClass']) ? (block.data['globalClass'] + ' ') : '') + (block.data['stretched'] == true ? 'sis-fullwidth-breakout ' : '') + (block.data['sticky'] == true or block.data['sticky'] == null ? 'is-sticky ' : '') + (block.data['bgColor'] == '#ffffff' ? 'sis-nav-light ' : '')}"
|
||||
th:style="${'background-color: ' + (block.data['bgColor'] != null ? block.data['bgColor'] : '#002554') + ' !important;'}"
|
||||
th:data-custom-attrs="${block.data['globalAttributes'] != null and !#strings.isEmpty(block.data['globalAttributes']) ? block.data['globalAttributes'] : (block.data['attributes'] != null and !#strings.isEmpty(block.data['attributes']) ? block.data['attributes'] : null)}"
|
||||
>
|
||||
<ul class="menu m--menu m--info-menu">
|
||||
<li class="menu-item menu-item--expanded">
|
||||
|
||||
Reference in New Issue
Block a user