diff --git a/app/javascript/vue/mixins/moduleNameObserver.js b/app/javascript/vue/mixins/moduleNameObserver.js new file mode 100644 index 000000000..a86f60ee3 --- /dev/null +++ b/app/javascript/vue/mixins/moduleNameObserver.js @@ -0,0 +1,43 @@ +/* + * Mixin to observe and reflect DOM changes of a module's name in '.my_module-name .view-mode'. + * - Initializes a MutationObserver to watch for modifications. + * - Updates the `moduleName` data property on detected changes. + */ + +export default { + data() { + return { + observer: null, + moduleName: '', + }; + }, + mounted() { + this.observeChanges(); + }, + beforeDestroy() { + if (this.observer) { + this.observer.disconnect(); + } + }, + methods: { + observeChanges() { + const targetNode = document.querySelector('.my_module-name .view-mode'); + if (!targetNode) return; + + this.moduleName = targetNode.textContent; + + const config = { characterData: true, childList: true, subtree: true }; + + const callback = (mutationsList) => { + mutationsList.forEach((mutation) => { + if (mutation.type === 'childList' || mutation.type === 'characterData') { + this.moduleName = targetNode.textContent; + } + }); + }; + + this.observer = new MutationObserver(callback); + this.observer.observe(targetNode, config); + }, + }, +}; diff --git a/app/javascript/vue/mixins/stackableHeadersMixin.js b/app/javascript/vue/mixins/stackableHeadersMixin.js new file mode 100644 index 000000000..655b37454 --- /dev/null +++ b/app/javascript/vue/mixins/stackableHeadersMixin.js @@ -0,0 +1,103 @@ +/* + * Mixin for adjusting stackable headers on scroll. + * - Tracks scroll position to modify headers' styles & positions. + * - Observes changes in the secondary navigation's height. + * - Adjusts TinyMCE editor header offset if present. + */ + +export default { + data() { + return { + lastScrollTop: 0, + headerSticked: false, + secondaryNavigation: null, + taskSecondaryMenuHeight: 0, + }; + }, + computed: { + headerRef() { + return this.getHeader(); + }, + }, + mounted() { + this.secondaryNavigation = document.querySelector('#taskSecondaryMenu'); + + this.resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + this.taskSecondaryMenuHeight = entry.target.offsetHeight; + }); + }); + + this.resizeObserver.observe(this.secondaryNavigation); + }, + beforeDestroy() { + if (this.resizeObserver) { + this.resizeObserver.disconnect(); + } + }, + methods: { + initStackableHeaders() { + const header = this.headerRef; + const headerHeight = header.offsetHeight; + const headerTop = header.getBoundingClientRect().top; + const secondaryNavigationTop = this.secondaryNavigation.getBoundingClientRect().top; + + // TinyMCE offset calculation + let stickyNavigationHeight = this.taskSecondaryMenuHeight; + if ($('.tox-editor-header').length > 0 && $('.tox-editor-header')[0].getBoundingClientRect().top > headerTop) { + stickyNavigationHeight += headerHeight; + } + + // Add shadow to secondary navigation when it starts fly + if (this.secondaryNavigation.getBoundingClientRect().top === 0 && !this.headerSticked) { + this.secondaryNavigation.style.boxShadow = '0 9px 8px -2px rgba(0, 0, 0, 0.1)'; + this.secondaryNavigation.style.zIndex = 252; + } else { + this.secondaryNavigation.style.boxShadow = 'none'; + if (secondaryNavigationTop > 10) this.secondaryNavigation.style.zIndex = 11; + } + + if (headerTop - 5 < this.taskSecondaryMenuHeight) { // When secondary navigation touch header + this.secondaryNavigation.style.top = `${headerTop - headerHeight}px`; // Secondary navigation starts slowly disappear + header.style.boxShadow = '0 9px 8px -2px rgba(0, 0, 0, 0.1)'; // Flying shadow + header.style.zIndex = 250; + + this.headerSticked = true; + + + if (this.lastScrollTop > window.scrollY) { // When user scroll up + let newSecondaryTop = secondaryNavigationTop - (window.scrollY - this.lastScrollTop); // Calculate new top position of secondary navigation + if (newSecondaryTop > 0) newSecondaryTop = 0; + + this.secondaryNavigation.style.top = `${newSecondaryTop}px`; // Secondary navigation starts slowly appear + this.secondaryNavigation.style.zIndex = 252; + header.style.top = `${this.taskSecondaryMenuHeight + newSecondaryTop - 1}px`; // Header starts getting offset to compensate secondary navigation position + // -1 to compensate small gap between header and secondary navigation + } else { // When user scroll down + let newSecondaryTop = secondaryNavigationTop - (window.scrollY - this.lastScrollTop); // Calculate new top position of secondary navigation + if (newSecondaryTop * -1 > this.taskSecondaryMenuHeight) newSecondaryTop = this.taskSecondaryMenuHeight * -1; + + this.secondaryNavigation.style.top = `${newSecondaryTop}px`; // Secondary navigation starts slowly disappear + header.style.top = `${newSecondaryTop + this.taskSecondaryMenuHeight - 1}px`; // Header starts getting offset to compensate secondary navigation position + // -1 to compensate small gap between header and secondary navigation + if (newSecondaryTop * -1 >= this.taskSecondaryMenuHeight) this.secondaryNavigation.style.zIndex = 11; + } + } else { + // Just reset secondary navigation and header styles to initial state + this.secondaryNavigation.style.top = '0px'; + header.style.top = '0px'; + header.style.boxShadow = 'none'; + header.style.zIndex = 10; + this.headerSticked = false; + } + + // Apply TinyMCE offset + $('.tox-editor-header').css( + 'top', + stickyNavigationHeight + parseInt($(this.secondaryNavigation).css('top'), 10) + ); + + this.lastScrollTop = window.scrollY; // Save last scroll position to when user scroll up/down + }, + }, +}; diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index 194eca331..b16937c6c 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -2,10 +2,10 @@
-