Patch #43976 » 0001-Convert-responsive.js-to-stimulus-controller.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 890 | 890 |
}); |
| 891 | 891 |
} |
| 892 | 892 | |
| 893 |
function isMobile() {
|
|
| 894 |
const element = document.getElementsByClassName('.js-flyout-menu-toggle-button')[0];
|
|
| 895 |
if (typeof element !== 'undefined') {
|
|
| 896 |
return isVisible(element); |
|
| 897 |
} else {
|
|
| 898 |
return false; |
|
| 899 |
} |
|
| 900 |
} |
|
| 901 | ||
| 902 |
function isVisible(element) {
|
|
| 903 |
try {
|
|
| 904 |
if (typeof element.checkVisibility === 'function') {
|
|
| 905 |
return element.checkVisibility(); |
|
| 906 |
} |
|
| 907 |
return !!( element.offsetWidth || element.offsetHeight || element.getClientRects().length ); |
|
| 908 |
} catch (error) {
|
|
| 909 |
console.error('Error checking visibility:', error);
|
|
| 910 |
return false; |
|
| 911 |
} |
|
| 912 |
} |
|
| 913 | ||
| 893 | 914 |
$(document).ready(function(){
|
| 894 | 915 |
$(".drdn .autocomplete").val('');
|
| 895 | 916 | |
| app/assets/javascripts/responsive.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
// generic layout specific responsive stuff goes here |
|
| 8 | ||
| 9 |
function openFlyout() {
|
|
| 10 |
$('html').addClass('flyout-is-active');
|
|
| 11 |
$('#main, #header').on('click.close-flyout', function(e){
|
|
| 12 |
e.preventDefault(); |
|
| 13 |
e.stopPropagation(); |
|
| 14 |
closeFlyout(); |
|
| 15 |
}); |
|
| 16 |
} |
|
| 17 | ||
| 18 |
function closeFlyout() {
|
|
| 19 |
$('html').removeClass('flyout-is-active');
|
|
| 20 |
$('#main, #header').off('click.close-flyout');
|
|
| 21 |
} |
|
| 22 | ||
| 23 | ||
| 24 |
function isMobile() {
|
|
| 25 |
return $('.js-flyout-menu-toggle-button').is(":visible");
|
|
| 26 |
} |
|
| 27 | ||
| 28 |
function setupFlyout() {
|
|
| 29 |
var mobileInit = false, |
|
| 30 |
desktopInit = false; |
|
| 31 | ||
| 32 |
/* click handler for mobile menu toggle */ |
|
| 33 |
$('.js-flyout-menu-toggle-button').on('click', function(e) {
|
|
| 34 |
e.preventDefault(); |
|
| 35 |
e.stopPropagation(); |
|
| 36 |
if($('html').hasClass('flyout-is-active')) {
|
|
| 37 |
closeFlyout(); |
|
| 38 |
} else {
|
|
| 39 |
openFlyout(); |
|
| 40 |
} |
|
| 41 |
}); |
|
| 42 | ||
| 43 |
/* bind resize handler */ |
|
| 44 |
$(window).resize(function() {
|
|
| 45 |
initMenu(); |
|
| 46 |
}) |
|
| 47 | ||
| 48 |
/* menu init function for dom detaching and appending on mobile / desktop view */ |
|
| 49 |
function initMenu() {
|
|
| 50 | ||
| 51 |
var _initMobileMenu = function() {
|
|
| 52 |
/* only init mobile menu, if it hasn't been done yet */ |
|
| 53 |
if(!mobileInit) {
|
|
| 54 | ||
| 55 |
$('#main-menu > ul').detach().appendTo('.js-project-menu');
|
|
| 56 |
$('#top-menu > ul').detach().appendTo('.js-general-menu');
|
|
| 57 |
$('#sidebar > *').detach().appendTo('.js-sidebar');
|
|
| 58 |
$('#account > ul').detach().appendTo('.js-profile-menu');
|
|
| 59 | ||
| 60 |
mobileInit = true; |
|
| 61 |
desktopInit = false; |
|
| 62 |
} |
|
| 63 |
} |
|
| 64 | ||
| 65 |
var _initDesktopMenu = function() {
|
|
| 66 |
if(!desktopInit) {
|
|
| 67 | ||
| 68 |
$('.js-project-menu > ul').detach().appendTo('#main-menu');
|
|
| 69 |
$('.js-general-menu > ul').detach().appendTo('#top-menu');
|
|
| 70 |
$('.js-sidebar > *').detach().appendTo('#sidebar');
|
|
| 71 |
$('.js-profile-menu > ul').detach().appendTo('#account');
|
|
| 72 | ||
| 73 |
desktopInit = true; |
|
| 74 |
mobileInit = false; |
|
| 75 |
} |
|
| 76 |
} |
|
| 77 | ||
| 78 |
if(isMobile()) {
|
|
| 79 |
_initMobileMenu(); |
|
| 80 |
} else {
|
|
| 81 |
_initDesktopMenu(); |
|
| 82 |
} |
|
| 83 |
} |
|
| 84 | ||
| 85 |
// init menu on page load |
|
| 86 |
initMenu(); |
|
| 87 |
} |
|
| 88 | ||
| 89 |
$(document).ready(setupFlyout); |
|
| app/helpers/application_helper.rb | ||
|---|---|---|
| 1768 | 1768 |
'rails-ujs', |
| 1769 | 1769 |
'tribute-5.1.3.min' |
| 1770 | 1770 |
) |
| 1771 |
tags << javascript_include_tag('application-legacy', 'responsive')
|
|
| 1771 |
tags << javascript_include_tag('application-legacy')
|
|
| 1772 | 1772 |
unless User.current.pref.warn_on_leaving_unsaved == '0' |
| 1773 | 1773 |
warn_text = escape_javascript(l(:text_warn_on_leaving_unsaved)) |
| 1774 | 1774 |
tags << |
| app/javascript/controllers/command_controller.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 | ||
| 7 |
import { Controller } from "@hotwired/stimulus"
|
|
| 8 | ||
| 9 |
// Connects to data-controller="command" |
|
| 10 |
export default class extends Controller {
|
|
| 11 |
execute(e) {
|
|
| 12 |
e.preventDefault(); |
|
| 13 |
this.dispatch(e.params.name); |
|
| 14 |
} |
|
| 15 |
} |
|
| app/javascript/controllers/relay_controller.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Copyright (C) 2024- Justin Searls |
|
| 3 |
* copied from https://justin.searls.co/posts/a-decoupled-approach-to-relaying-events-between-stimulus-controllers/ |
|
| 4 |
*/ |
|
| 5 | ||
| 6 |
import { Controller } from '@hotwired/stimulus'
|
|
| 7 | ||
| 8 |
export default class RelayController extends Controller {
|
|
| 9 |
forward (e) {
|
|
| 10 |
const subscribers = this.element.querySelectorAll(`[data-relay-events*='${e.type}']`)
|
|
| 11 | ||
| 12 |
subscribers.forEach(el => {
|
|
| 13 |
el.dispatchEvent(new CustomEvent(e.type, {
|
|
| 14 |
detail: e.detail, |
|
| 15 |
params: e.params |
|
| 16 |
})) |
|
| 17 |
}) |
|
| 18 |
} |
|
| 19 |
} |
|
| app/javascript/controllers/responsive_controller.js | ||
|---|---|---|
| 1 |
/** |
|
| 2 |
* Redmine - project management software |
|
| 3 |
* Copyright (C) 2006- Jean-Philippe Lang |
|
| 4 |
* This code is released under the GNU General Public License. |
|
| 5 |
*/ |
|
| 6 |
import { Controller } from "@hotwired/stimulus"
|
|
| 7 | ||
| 8 |
// generic layout specific responsive stuff goes here |
|
| 9 |
// Connects to data-controller="responsive" |
|
| 10 |
export default class extends Controller {
|
|
| 11 |
static values = { isMobile: Boolean }
|
|
| 12 | ||
| 13 |
initialize() {
|
|
| 14 |
this.mediaQueryList = window.matchMedia('screen and (max-width: 899px)');
|
|
| 15 |
this.mediaQueryList.addEventListener('change', (e) => {
|
|
| 16 |
this.isMobileValue = e.matches |
|
| 17 |
}) |
|
| 18 |
} |
|
| 19 | ||
| 20 |
connect() {
|
|
| 21 |
this.handlers = [] |
|
| 22 |
this.html = document.querySelector('html')
|
|
| 23 | ||
| 24 |
// init menu on page load |
|
| 25 |
this.isMobileValue = this.mediaQueryList.matches |
|
| 26 |
} |
|
| 27 | ||
| 28 |
/* click handler for mobile menu toggle */ |
|
| 29 |
toggle(e) {
|
|
| 30 |
e.preventDefault(); |
|
| 31 |
e.stopPropagation(); |
|
| 32 |
if(this.html.classList.contains(this.isFlyoutActive)) {
|
|
| 33 |
this.closeFlyout(); |
|
| 34 |
} else {
|
|
| 35 |
this.openFlyout(); |
|
| 36 |
} |
|
| 37 |
} |
|
| 38 | ||
| 39 |
isMobileValueChanged(value) {
|
|
| 40 |
if (value) {
|
|
| 41 |
this.replace('#main-menu > ul', '.js-project-menu')
|
|
| 42 |
this.replace('#top-menu > ul', '.js-general-menu')
|
|
| 43 |
this.replace('#sidebar > *' , '.js-sidebar')
|
|
| 44 |
this.replace('#account > ul', '.js-profile-menu')
|
|
| 45 |
} else {
|
|
| 46 |
this.replace('.js-project-menu > ul', '#main-menu')
|
|
| 47 |
this.replace('.js-general-menu > ul', '#top-menu')
|
|
| 48 |
this.replace('.js-sidebar > *' , '#sidebar')
|
|
| 49 |
this.replace('.js-profile-menu > ul', '#account')
|
|
| 50 |
} |
|
| 51 |
} |
|
| 52 | ||
| 53 |
replace(from, to) {
|
|
| 54 |
const fromElem = document.querySelectorAll(from) |
|
| 55 |
const toElem = document.querySelector(to) |
|
| 56 | ||
| 57 |
if (toElem !== null) {
|
|
| 58 |
const toChildren = toElem.children |
|
| 59 |
toElem.replaceChildren(...toChildren, ...fromElem); |
|
| 60 |
} |
|
| 61 |
} |
|
| 62 | ||
| 63 |
openFlyout() {
|
|
| 64 |
this.html.classList.add(this.isFlyoutActive); |
|
| 65 |
this.handlers = ['#main', '#header'].map((selector) => {
|
|
| 66 |
return addHandler(document, 'click', (e) => {
|
|
| 67 |
if (e.target.matches('.js-flyout-menu-toggle-button')) return;
|
|
| 68 |
if (e.target.closest(selector) === null) return; |
|
| 69 | ||
| 70 |
this.closeFlyout() |
|
| 71 |
}) |
|
| 72 |
}); |
|
| 73 |
} |
|
| 74 | ||
| 75 |
closeFlyout() {
|
|
| 76 |
this.html.classList.remove(this.isFlyoutActive); |
|
| 77 |
this.handlers.forEach(handler => handler()); |
|
| 78 |
} |
|
| 79 | ||
| 80 |
get isFlyoutActive() {
|
|
| 81 |
return 'flyout-is-active'; |
|
| 82 |
} |
|
| 83 |
} |
|
| 84 | ||
| 85 |
function addHandler(element, ...args) {
|
|
| 86 |
element.addEventListener(...args); |
|
| 87 |
return () => element.removeEventListener(...args); |
|
| 88 |
} |
|
| app/views/layouts/base.html.erb | ||
|---|---|---|
| 20 | 20 |
</head> |
| 21 | 21 |
<body class="<%= body_css_classes %>" data-text-formatting="<%= Setting.text_formatting %>"> |
| 22 | 22 |
<%= call_hook :view_layouts_base_body_top %> |
| 23 |
<div id="wrapper"> |
|
| 23 |
<div id="wrapper" data-controller="relay">
|
|
| 24 | 24 | |
| 25 |
<div class="flyout-menu js-flyout-menu"> |
|
| 25 |
<div class="flyout-menu js-flyout-menu" data-controller="responsive" data-action="command:toggle->responsive#toggle" data-relay-events="command:toggle">
|
|
| 26 | 26 | |
| 27 | 27 |
<% if User.current.logged? || !Setting.login_required? %> |
| 28 | 28 |
<div class="flyout-menu__search"> |
| ... | ... | |
| 67 | 67 | |
| 68 | 68 |
<div id="header"> |
| 69 | 69 | |
| 70 |
<a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a> |
|
| 70 |
<a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button" data-controller="command" data-action="command#execute command:toggle->relay#forward" data-command-name-param="toggle"></a>
|
|
| 71 | 71 | |
| 72 | 72 |
<% if User.current.logged? || !Setting.login_required? %> |
| 73 | 73 |
<div id="quick-search"> |