diff --git a/sisvietnamvn_main/sisvietnam_backup.dmp b/sisvietnamvn_main/sisvietnam_backup.dmp index ac0a01f4..d6d708f3 100644 Binary files a/sisvietnamvn_main/sisvietnam_backup.dmp and b/sisvietnamvn_main/sisvietnam_backup.dmp differ diff --git a/sisvietnamvn_main/src/main/resources/static/css/custom.css b/sisvietnamvn_main/src/main/resources/static/css/custom.css index 1f017ed5..80f76ec1 100644 --- a/sisvietnamvn_main/src/main/resources/static/css/custom.css +++ b/sisvietnamvn_main/src/main/resources/static/css/custom.css @@ -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 { diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js index dcedaed5..7e5eeb17 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-config.js @@ -709,242 +709,7 @@ class SISHeroBannerTool { } } -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', - 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); - - // 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 = ' 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 = ' 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'; - 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 = ''; - 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 = ' 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(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); diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/nav.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/nav.js new file mode 100644 index 00000000..da0d3dd7 --- /dev/null +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/nav.js @@ -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: '' + }; + } + + 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 }; diff --git a/sisvietnamvn_main/src/main/resources/templates/manage/pages/form.html b/sisvietnamvn_main/src/main/resources/templates/manage/pages/form.html index 9c643f0c..556ee341 100644 --- a/sisvietnamvn_main/src/main/resources/templates/manage/pages/form.html +++ b/sisvietnamvn_main/src/main/resources/templates/manage/pages/form.html @@ -441,6 +441,7 @@ + diff --git a/sisvietnamvn_main/src/main/resources/templates/page.html b/sisvietnamvn_main/src/main/resources/templates/page.html index 524e039c..bec79e75 100644 --- a/sisvietnamvn_main/src/main/resources/templates/page.html +++ b/sisvietnamvn_main/src/main/resources/templates/page.html @@ -208,11 +208,13 @@ - +