/** * 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: '' }; } 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 = ' 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 = ''; 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 = ''; 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 = ''; 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 = ''; 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 = ' 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 = ' 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 = ` `; 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 = ''; 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 = ' 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 += `${label}`; }); previewBox.innerHTML = html || 'No nav links added'; }; 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 };