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 {
|
.education .gap-3 {
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
.education .flex {
|
.flex {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
.education .flex .h-stretch {
|
.education .flex .h-stretch {
|
||||||
height: stretch;
|
height: stretch;
|
||||||
}
|
}
|
||||||
.education .flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.education .flex-row {
|
.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.
|
* 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.accordion = { class: SISAccordionTool };
|
||||||
builtInTools.youtube = { class: SISYouTubeTool };
|
builtInTools.youtube = { class: SISYouTubeTool };
|
||||||
builtInTools.hero = { class: SISHeroBannerTool };
|
builtInTools.hero = { class: SISHeroBannerTool };
|
||||||
builtInTools.stickyNav = { class: SISStickyNavTool };
|
if (typeof SISStickyNavTool !== 'undefined') {
|
||||||
|
builtInTools.stickyNav = { class: SISStickyNavTool };
|
||||||
|
}
|
||||||
|
|
||||||
// === Merge built-in tools with any registered plugins ===
|
// === Merge built-in tools with any registered plugins ===
|
||||||
var allTools = Object.assign({}, builtInTools, window.SISEditorPlugins);
|
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/flex.js}"></script>
|
||||||
<script th:src="@{/js/manage/editor-plugins/hover-card.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/tag.js}"></script>
|
||||||
|
<script th:src="@{/js/manage/editor-plugins/nav.js}"></script>
|
||||||
|
|
||||||
<!-- SIS Standalone Media Picker Library -->
|
<!-- SIS Standalone Media Picker Library -->
|
||||||
<script th:src="@{/js/manage/sis-media-picker.js}"></script>
|
<script th:src="@{/js/manage/sis-media-picker.js}"></script>
|
||||||
|
|||||||
@@ -208,11 +208,13 @@
|
|||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<!-- 13. Sticky Nav / Sub-menu Block (UMass Theme mc--info-menu style) -->
|
<!-- 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
|
<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"
|
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: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">
|
<ul class="menu m--menu m--info-menu">
|
||||||
<li class="menu-item menu-item--expanded">
|
<li class="menu-item menu-item--expanded">
|
||||||
|
|||||||
Reference in New Issue
Block a user