Project

General

Profile

Patch #43976 » 0001-Convert-responsive.js-to-stimulus-controller.patch

Takashi Kato, 2026-04-19 05:34

View differences:

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">
(1-1/3)