Project

General

Profile

Feature #23980 » 0003-Support-expand-collapse-with-svg-icons.patch

Takashi Kato, 2022-03-21 22:32

View differences:

app/controllers/icon_sets_controller.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2022  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
class IconSetsController < ApplicationController
21
  def show
22
    icons = fetch(params[:id])
23
    if icons.present?
24
      render :json => icons, :content_type => 'application/json'
25
    else
26
      render_404
27
    end
28
  end
29

  
30
  private
31

  
32
  def fetch(name)
33
    IconSet.fetch(name)
34
  end
35
end
app/views/calendars/show.html.erb
8 8
<div id="query_form_with_buttons" class="hide-when-print">
9 9
<div id="query_form_content">
10 10
  <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
11
    <legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>"><%= l(:label_filter_plural) %></legend>
11
    <%= tag.legend l(:label_filter_plural), onclick: "toggleFieldset(this);", class: collapsible(@query.new_record?) %>
12 12
    <div style="<%= @query.new_record? ? "" : "display: none;" %>">
13 13
      <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
14 14
    </div>
app/views/gantts/show.html.erb
14 14
<div id="query_form_with_buttons" class="hide-when-print">
15 15
<div id="query_form_content">
16 16
  <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
17
    <legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>"><%= l(:label_filter_plural) %></legend>
17
    <%= tag.legend l(:label_filter_plural), onclick: 'toggleFieldset(this);', class: collapsible(@query.new_record?) %>
18 18
    <div style="<%= @query.new_record? ? "" : "display: none;" %>">
19 19
      <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
20 20
    </div>
21 21
  </fieldset>
22 22

  
23 23
  <fieldset id="options" class="collapsible collapsed">
24
    <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
24
    <%= tag.legend l(:label_options), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
25 25
    <div style="display: none;">
26 26
      <table>
27 27
        <tr>
app/views/imports/_issues_mapping.html.erb
6 6
</fieldset>
7 7

  
8 8
<fieldset class="box tabular collapsible collapsed">
9
  <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_relations_mapping) %></legend>
9
  <%= tag.legend l(:label_relations_mapping), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
10 10
  <div id="relations-mapping" style="display: none;">
11 11
    <%= render :partial => 'issues_relations_mapping' %>
12 12
  </div>
app/views/issues/_list.html.erb
24 24
    <% reset_cycle %>
25 25
    <tr class="group open">
26 26
      <td colspan="<%= query.inline_columns.size + 2 %>">
27
        <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
27
        <%= expander %>
28 28
        <span class="name"><%= group_name %></span> <span class="badge badge-count count"><%= group_count %></span> <span class="totals"><%= group_totals %></span>
29 29
        <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
30 30
                             "toggleAllRowGroups(this)", :class => 'toggle-all') %>
app/views/layouts/base.html.erb
14 14
<%= javascript_heads %>
15 15
<%= heads_for_theme %>
16 16
<%= heads_for_auto_complete(@project) %>
17
<%= preload_icon_link('common') %>
17 18
<%= call_hook :view_layouts_base_html_head %>
18 19
<!-- page specific tags -->
19 20
<%= yield :header_tags -%>
app/views/projects/_list.html.erb
14 14
    <% reset_cycle %>
15 15
    <tr class="group open">
16 16
      <td colspan="<%= @query.inline_columns.size %>">
17
        <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
17
        <%= expander %>
18 18
        <span class="name"><%= group_name %></span>
19 19
        <% if group_count %>
20 20
        <span class="count"><%= group_count %></span>
app/views/queries/_query_form.html.erb
5 5
<div id="query_form_with_buttons" class="hide-when-print">
6 6
<div id="query_form_content">
7 7
  <fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
8
    <legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>"><%= l(:label_filter_plural) %></legend>
8
    <%= content_tag('legend', l(:label_filter_plural), :onclick => "toggleFieldset(this);", :class => collapsible(@query.new_record?)) %>
9 9
    <div style="<%= @query.new_record? ? "" : "display: none;" %>">
10 10
      <%= render :partial => 'queries/filters', :locals => {:query => @query} %>
11 11
    </div>
......
13 13

  
14 14
  <% if @query.available_columns.any? %>
15 15
    <fieldset id="options" class="collapsible collapsed">
16
      <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
16
      <%= tag.legend l(:label_options), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
17 17
        <div class="hidden">
18 18
          <% if @query.available_display_types.size > 1 %>
19 19
          <div>
app/views/repositories/_dir_list_content.html.erb
7 7
<td style="padding-left: <%=18 * depth%>px;" class="<%=
8 8
           @repository.report_last_commit ? "filename" : "filename_no_report" %>">
9 9
<% if entry.is_dir? %>
10
<span class="expander icon icon-collapsed" onclick="scmEntryClick('<%= tr_id %>', '<%= escape_javascript(url_for(
11
                       :action => 'show',
12
                       :id     => @project,
13
                       :repository_id => @repository.identifier_param,
14
                       :path   => to_path_param(ent_path),
15
                       :rev    => @rev,
16
                       :depth  => (depth + 1),
17
                       :parent_id => tr_id)) %>');">&nbsp;</span>
10
<% url =  url_for(:action => 'show',
11
                  :id     => @project,
12
                  :repository_id => @repository.identifier_param,
13
                  :path   => to_path_param(ent_path),
14
                  :rev    => @rev,
15
                  :depth  => (depth + 1),
16
                  :parent_id => tr_id) %>
17
<%= tag.span '&nbsp;', class: 'expander icon icon-collapsed', onclick: "scmEntryClick('#{tr_id}', '#{escape_javascript(url)}')" %>
18 18
<% end %>
19 19
<%=  link_to ent_name,
20 20
          {:action => (entry.is_dir? ? 'show' : 'entry'), :id => @project, :repository_id => @repository.identifier_param, :path => to_path_param(ent_path), :rev => @rev},
app/views/roles/permissions.html.erb
2 2

  
3 3
<div class="hide-when-print">
4 4
  <fieldset id="filters" class="collapsible collapsed">
5
    <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_filter_plural) %></legend>
5
    <%= tag.legend l(:label_filter_plural), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
6 6
    <div style="display: none;">
7 7
      <%= form_tag({}, :method => :get) do %>
8 8
        <fieldset>
......
49 49
    <% unless mod.blank? %>
50 50
        <tr class="group open">
51 51
          <td>
52
            <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
52
            <%= expander %>
53 53
            <%= l_or_humanize(mod, :prefix => 'project_module_') %>
54 54
          </td>
55 55
          <% @roles.each do |role| %>
app/views/search/index.html.erb
24 24
</fieldset>
25 25

  
26 26
<fieldset class="collapsible collapsed">
27
  <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
27
  <%= tag.legend l(:label_options), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
28 28
  <div id="options-content" style="display:none;">
29 29
    <p><label><%= check_box_tag 'open_issues', 1, @open_issues %> <%= l(:label_search_open_issues_only) %></label></p>
30 30
    <p>
app/views/timelog/_list.html.erb
20 20
    <% reset_cycle %>
21 21
    <tr class="group open">
22 22
      <td colspan="<%= @query.inline_columns.size + 2 %>">
23
        <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
23
        <%= expander %>
24 24
        <span class="name"><%= group_name %></span>
25 25
        <% if group_count %>
26 26
        <span class="badge badge-count count"><%= group_count %></span>
app/views/trackers/fields.html.erb
20 20
    <tbody>
21 21
      <tr class="group open">
22 22
        <td colspan="<%= @trackers.size + 1 %>">
23
          <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
23
          <%= expander %>
24 24
          <%= l(:field_core_fields) %>
25 25
        </td>
26 26
      </tr>
......
44 44
      <% if @custom_fields.any? %>
45 45
        <tr class="group open">
46 46
          <td colspan="<%= @trackers.size + 1 %>">
47
            <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
47
            <%= expander %>
48 48
            <%= l(:label_custom_field_plural) %>
49 49
          </td>
50 50
        </tr>
app/views/wiki/show.html.erb
62 62
<%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
63 63

  
64 64
<fieldset class="collapsible collapsed hide-when-print">
65
  <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_attachment_plural) %> (<%= @page.attachments.length %>)</legend>
65
  <%= tag.legend "#{l(:label_attachment_plural)} (#{@page.attachments.length})", onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
66 66
  <div style="display: none;">
67 67

  
68 68
  <%= link_to_attachments @page, :thumbnails => true %>
app/views/workflows/edit.html.erb
40 40
      <%= render :partial => 'form', :locals => {:name => 'always', :workflows => @workflows['always']} %>
41 41

  
42 42
      <fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
43
        <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_additional_workflow_transitions_for_author) %></legend>
43
        <%= tag.legend l(:label_additional_workflow_transitions_for_author), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
44 44
        <div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
45 45
          <%= render :partial => 'form', :locals => {:name => 'author', :workflows => @workflows['author']} %>
46 46
        </div>
......
48 48
      <%= javascript_tag "hideFieldset($('#author_workflows'))" unless @workflows['author'].present? %>
49 49

  
50 50
      <fieldset class="collapsible" style="padding: 0;">
51
        <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_additional_workflow_transitions_for_assignee) %></legend>
51
        <%= tag.legend l(:label_additional_workflow_transitions_for_assignee), onclick: 'toggleFieldset(this);', class: 'icon icon-collapsed' %>
52 52
        <div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
53 53
      <%= render :partial => 'form', :locals => {:name => 'assignee', :workflows => @workflows['assignee']} %>
54 54
        </div>
app/views/workflows/permissions.html.erb
54 54
    <tbody>
55 55
      <tr class="group open">
56 56
        <td colspan="<%= @statuses.size + 1 %>">
57
          <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
57
          <%= expander %>
58 58
          <%= l(:field_core_fields) %>
59 59
        </td>
60 60
      </tr>
......
74 74
      <% if @custom_fields.any? %>
75 75
        <tr class="group open">
76 76
          <td colspan="<%= @statuses.size + 1 %>">
77
            <span class="expander icon icon-expanded" onclick="toggleRowGroup(this);">&nbsp;</span>
77
            <%= expander %>
78 78
            <%= l(:label_custom_field_plural) %>
79 79
          </td>
80 80
        </tr>
config/routes.rb
395 395

  
396 396
  get 'robots', :to => 'welcome#robots'
397 397

  
398
  # SVG icons map
399
  resources :icon_sets, id: '/^[a-z]+$/', only: [:show], defaults: { :format => :json }
400

  
398 401
  Dir.glob File.expand_path("#{Redmine::Plugin.directory}/*") do |plugin_dir|
399 402
    file = File.join(plugin_dir, "config/routes.rb")
400 403
    if File.exist?(file)
public/javascripts/application.js
27 27
  $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
28 28
}
29 29

  
30
/**
31
 * 
32
 * @param {HTMLElement} elem 
33
 */
34
function addSVGIcon(elem, iconClass) {
35
  if (rm.icons && rm.icons.common) {
36
    const data = rm.icons.common;
37
    const svg = createSVGIcon(data[iconClass]);
38
    elem.insertBefore(svg, elem.firstChild);
39
  }
40
}
41

  
42
/**
43
 * 
44
 * @param {HTMLElement} elem 
45
 */
46
function removeSVGIcon(elem) {
47
  const icon = elem.getElementsByTagName('svg')[0]
48
  if (icon) {
49
    elem.removeChild(icon);
50
  }
51
}
52

  
53
/**
54
 * 
55
 * @param {string} iconUrl 
56
 * @returns HTMLElement
57
 */
58
function createSVGIcon(iconUrl) {
59
  const xmlns = "http://www.w3.org/2000/svg";
60
  const svg = document.createElementNS(xmlns, 'svg');
61
  const use = document.createElementNS(xmlns, 'use');
62
  use.setAttribute('href', `${iconUrl}#icon`);
63
  svg.appendChild(use);
64
  svg.setAttribute('xmlns', xmlns)
65
  svg.classList.add('s16');
66
  return svg
67
}
68

  
69
/**
70
 * 
71
 * @param {Array<HTMLElement>} elems
72
 * @param {string} from
73
 * @param {string} to
74
 */
75
function switchClass(elems, from, to) {
76
  elems.forEach(function(e) {
77
    e.classList.remove(from);
78
    removeSVGIcon(e);
79
    e.classList.add(to);
80
    addSVGIcon(e, to)
81
  })
82
}
83

  
84
/**
85
 * 
86
 * @param {Array<HTMLElement>} elems
87
 * @param {string} class1
88
 * @param {string} class2
89
 */
90
function toggleClass(elems, class1, class2) {
91
  const _elems = Array.isArray(elems) ? elems : [elems];
92
  _elems.forEach(function(e) {
93
    removeSVGIcon(e);
94
    if (e.classList.contains(class1)) {
95
      e.classList.remove(class1);
96
      e.classList.add(class2);
97
      addSVGIcon(e, class2);
98
    } else if (e.classList.contains(class2)) {
99
      e.classList.remove(class2);
100
      e.classList.add(class1);
101
      addSVGIcon(e, class1);
102
    }
103
  })
104
}
105

  
30 106
function toggleRowGroup(el) {
31 107
  var tr = $(el).parents('tr').first();
32 108
  var n = tr.next();
33 109
  tr.toggleClass('open');
34
  $(el).toggleClass('icon-expanded icon-collapsed');
110
  toggleClass(el, 'icon-expanded', 'icon-collapsed');
35 111
  while (n.length && !n.hasClass('group')) {
36 112
    n.toggle();
37 113
    n = n.next('tr');
......
43 119
  tbody.children('tr').each(function(index) {
44 120
    if ($(this).hasClass('group')) {
45 121
      $(this).removeClass('open');
46
      $(this).find('.expander').switchClass('icon-expanded', 'icon-collapsed');
122
      switchClass($(this).find('.expander').get(), 'icon-expanded', 'icon-collapsed');
47 123
    } else {
48 124
      $(this).hide();
49 125
    }
......
55 131
  tbody.children('tr').each(function(index) {
56 132
    if ($(this).hasClass('group')) {
57 133
      $(this).addClass('open');
58
      $(this).find('.expander').switchClass('icon-collapsed', 'icon-expanded');
134
      switchClass($(this).find('.expander').get(), 'icon-collapsed', 'icon-expanded');
59 135
    } else {
60 136
      $(this).show();
61 137
    }
......
74 150
function toggleFieldset(el) {
75 151
  var fieldset = $(el).parents('fieldset').first();
76 152
  fieldset.toggleClass('collapsed');
77
  fieldset.children('legend').toggleClass('icon-expanded icon-collapsed');
153
  toggleClass(fieldset.children('legend').get(), 'icon-expanded', 'icon-collapsed');
78 154
  fieldset.children('div').toggle();
79 155
}
80 156

  
......
550 626
    var el = $('#'+id);
551 627
    if (el.hasClass('open')) {
552 628
        collapseScmEntry(id);
553
        el.find('.expander').switchClass('icon-expanded', 'icon-collapsed');
629
        switchClass(el.find('.expander').get(), 'icon-expanded', 'icon-collapsed');
554 630
        el.addClass('collapsed');
555 631
        return false;
556 632
    } else if (el.hasClass('loaded')) {
557 633
        expandScmEntry(id);
558
        el.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
634
        switchClass(el.find('.expander').get(), 'icon-collapsed', 'icon-expanded');
559 635
        el.removeClass('collapsed');
560 636
        return false;
561 637
    }
......
568 644
      success: function(data) {
569 645
        el.after(data);
570 646
        el.addClass('open').addClass('loaded').removeClass('loading');
571
        el.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
647
        switchClass(el.find('.expander').get(), 'icon-collapsed', 'icon-expanded');
572 648
      }
573 649
    });
574 650
    return true;
......
1219 1295
    tribute.attach(element);
1220 1296
}
1221 1297

  
1222

  
1223 1298
$(document).ready(setupAjaxIndicator);
1224 1299
$(document).ready(hideOnLoad);
1225 1300
$(document).ready(addFormObserversForDoubleSubmit);
......
1231 1306
$(document).on('focus', '[data-auto-complete=true]', function(event) {
1232 1307
  inlineAutoComplete(event.target);
1233 1308
});
1309

  
1310
window.addEventListener('DOMContentLoaded', function(event) {
1311
  const common_icon_path = document.getElementById('common_icon_path');
1312
  if (common_icon_path !== null) {
1313
    const url = common_icon_path.getAttribute('href');
1314
    fetch(url, {
1315
      method: 'GET'
1316
    })
1317
    .then(function(response){
1318
      response.json().then(function(data) {
1319
        rm.icons = rm.icons || {}
1320
        rm.icons.common = data;
1321
      })
1322
    });
1323
  }
1324
});
(23-23/54)