Patch #43976 » 0002-Add-tab-buttons-controller.patch
| app/assets/javascripts/application-legacy.js | ||
|---|---|---|
| 487 | 487 |
} |
| 488 | 488 |
} |
| 489 | 489 | |
| 490 |
function moveTabRight(el) {
|
|
| 491 |
var lis = $(el).parents('div.tabs').first().find('ul').children();
|
|
| 492 |
var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
|
|
| 493 |
var tabsWidth = 0; |
|
| 494 |
var i = 0; |
|
| 495 |
lis.each(function() {
|
|
| 496 |
if ($(this).is(':visible')) {
|
|
| 497 |
tabsWidth += $(this).outerWidth(true); |
|
| 498 |
} |
|
| 499 |
}); |
|
| 500 |
if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
|
|
| 501 |
$(el).siblings('.tab-left').removeClass('disabled');
|
|
| 502 |
while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
|
|
| 503 |
var w = lis.eq(i).width(); |
|
| 504 |
lis.eq(i).hide(); |
|
| 505 |
if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
|
|
| 506 |
$(el).addClass('disabled');
|
|
| 507 |
} |
|
| 508 |
} |
|
| 509 | ||
| 510 |
function moveTabLeft(el) {
|
|
| 511 |
var lis = $(el).parents('div.tabs').first().find('ul').children();
|
|
| 512 |
var i = 0; |
|
| 513 |
while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
|
|
| 514 |
if (i > 0) {
|
|
| 515 |
lis.eq(i-1).show(); |
|
| 516 |
$(el).siblings('.tab-right').removeClass('disabled');
|
|
| 517 |
} |
|
| 518 |
if (i <= 1) {
|
|
| 519 |
$(el).addClass('disabled');
|
|
| 520 |
} |
|
| 521 |
} |
|
| 522 | ||
| 523 |
function displayTabsButtons() {
|
|
| 524 |
var lis; |
|
| 525 |
var tabsWidth; |
|
| 526 |
var el; |
|
| 527 |
var numHidden; |
|
| 528 |
$('div.tabs').each(function() {
|
|
| 529 |
el = $(this); |
|
| 530 |
lis = el.find('ul').children();
|
|
| 531 |
tabsWidth = 0; |
|
| 532 |
numHidden = 0; |
|
| 533 |
lis.each(function(){
|
|
| 534 |
if ($(this).is(':visible')) {
|
|
| 535 |
tabsWidth += $(this).outerWidth(true); |
|
| 536 |
} else {
|
|
| 537 |
numHidden++; |
|
| 538 |
} |
|
| 539 |
}); |
|
| 540 |
var bw = $(el).find('div.tabs-buttons').outerWidth(true);
|
|
| 541 |
if ((tabsWidth < el.width() - bw) && (lis.length === 0 || lis.first().is(':visible'))) {
|
|
| 542 |
el.find('div.tabs-buttons').hide();
|
|
| 543 |
} else {
|
|
| 544 |
el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
|
|
| 545 |
} |
|
| 546 |
}); |
|
| 547 |
} |
|
| 548 | ||
| 549 | 490 |
function setPredecessorFieldsVisibility() {
|
| 550 | 491 |
var relationType = $('#relation_relation_type');
|
| 551 | 492 |
if (relationType.val() == "precedes" || relationType.val() == "follows") {
|
| ... | ... | |
| 1082 | 1023 |
}); |
| 1083 | 1024 |
} |
| 1084 | 1025 | |
| 1085 |
function setupTabs() {
|
|
| 1086 |
if($('.tabs').length > 0) {
|
|
| 1087 |
displayTabsButtons(); |
|
| 1088 |
$(window).resize(displayTabsButtons); |
|
| 1089 |
} |
|
| 1090 |
} |
|
| 1091 | ||
| 1092 | 1026 |
function setupFilePreviewNavigation() {
|
| 1093 | 1027 |
// only bind arrow keys when preview navigation is present |
| 1094 | 1028 |
const element = $('.pagination.filepreview').first();
|
| ... | ... | |
| 1521 | 1455 |
$(document).ready(addFormObserversForDoubleSubmit); |
| 1522 | 1456 |
$(document).ready(defaultFocus); |
| 1523 | 1457 |
$(document).ready(setupAttachmentDetail); |
| 1524 |
$(document).ready(setupTabs); |
|
| 1525 | 1458 |
$(document).ready(setupFilePreviewNavigation); |
| 1526 | 1459 |
$(document).on('focus', '[data-auto-complete=true]', function(event) {
|
| 1527 | 1460 |
inlineAutoComplete(event.target); |
| app/assets/stylesheets/application.css | ||
|---|---|---|
| 195 | 195 |
#main-menu ul {
|
| 196 | 196 |
margin: 0; |
| 197 | 197 |
padding: 0; |
| 198 |
inline-size: 100%;
|
|
| 198 |
width: max-content;
|
|
| 199 | 199 |
min-block-size: 28px; |
| 200 | 200 |
white-space: nowrap; |
| 201 | 201 |
display: flex; |
| ... | ... | |
| 1716 | 1716 |
.version-overview table.progress td { block-size: 1.2em; }
|
| 1717 | 1717 | |
| 1718 | 1718 |
/***** Tabs *****/ |
| 1719 |
#content .tabs {block-size: 2.6em; margin-block-end: 1.2em; position: relative; overflow: hidden;}
|
|
| 1719 |
#content .tabs {block-size: 2.6em; margin-block-end: 1.2em; position: relative; overflow: hidden; border-block-end: 1px solid var(--oc-gray-4)}
|
|
| 1720 | 1720 |
#content .tabs ul {
|
| 1721 | 1721 |
margin: 0; |
| 1722 | 1722 |
position: absolute; |
| 1723 |
inset-block-end:0; |
|
| 1723 |
inset-block-end: 0;
|
|
| 1724 | 1724 |
padding-inline-start: 0.5em; |
| 1725 |
min-inline-size: 2000px; |
|
| 1726 |
inline-size: 100%; |
|
| 1727 |
border-block-end: 1px solid var(--oc-gray-4); |
|
| 1725 |
width: max-content; |
|
| 1728 | 1726 |
} |
| 1729 | 1727 |
#content .tabs ul li {
|
| 1730 | 1728 |
float:inline-start; |
| app/javascript/controllers/tab_buttons_controller.js | ||
|---|---|---|
| 1 |
import { Controller } from "@hotwired/stimulus"
|
|
| 2 | ||
| 3 |
// Connects to data-controller="tab-buttons" |
|
| 4 |
export default class extends Controller {
|
|
| 5 |
static targets = ['list', 'buttons'] |
|
| 6 |
static values = {showButton: Boolean}
|
|
| 7 | ||
| 8 |
connect() {
|
|
| 9 |
this.intersectionObserver = new IntersectionObserver(entries => {
|
|
| 10 |
entries.forEach(entry => {
|
|
| 11 |
if (entry.isIntersecting && this.showButtonValue) {
|
|
| 12 |
this.showButtonValue = false |
|
| 13 |
} |
|
| 14 |
if (!entry.isIntersecting && !this.showButtonValue) {
|
|
| 15 |
this.showButtonValue = true |
|
| 16 |
} |
|
| 17 |
}) |
|
| 18 |
}, {
|
|
| 19 |
root: this.element, |
|
| 20 |
threshold: 0.99 |
|
| 21 |
}); |
|
| 22 |
this.intersectionObserver.observe(this.listTarget); |
|
| 23 |
} |
|
| 24 | ||
| 25 |
disconnect() {
|
|
| 26 |
this.intersectionObserver?.disconnect(); |
|
| 27 |
} |
|
| 28 | ||
| 29 |
right(e) {
|
|
| 30 |
const el = e.currentTarget; |
|
| 31 |
const func = (total, el) => isVisible(el) ? total + outerWidth(el) |
|
| 32 |
: total; |
|
| 33 |
const visibleTabsWidth = this.items.reduce(func, 0) |
|
| 34 |
const availableWidth = this.element.getBoundingClientRect().width - outerWidth(this.buttonsTarget); |
|
| 35 | ||
| 36 |
if (visibleTabsWidth < availableWidth) return; |
|
| 37 | ||
| 38 |
this.enableButton(el, '.tab-left') |
|
| 39 | ||
| 40 |
const i = this.firstVisibleIndex; |
|
| 41 |
if (i === -1) return; |
|
| 42 | ||
| 43 |
const w = outerWidth(this.items[i]); |
|
| 44 |
this.items[i].style.display = 'none' |
|
| 45 | ||
| 46 |
if (visibleTabsWidth - w < availableWidth) {
|
|
| 47 |
el.classList.add('disabled');
|
|
| 48 |
} |
|
| 49 |
} |
|
| 50 | ||
| 51 |
left(e) {
|
|
| 52 |
const el = e.currentTarget; |
|
| 53 |
const i = this.firstVisibleIndex; |
|
| 54 | ||
| 55 |
if (i > 0) {
|
|
| 56 |
this.items[i - 1].style.display = '' |
|
| 57 |
this.enableButton(el, '.tab-right') |
|
| 58 |
} |
|
| 59 | ||
| 60 |
if (i <= 1) {
|
|
| 61 |
el.classList.add('disabled');
|
|
| 62 |
} |
|
| 63 |
} |
|
| 64 | ||
| 65 |
#items; |
|
| 66 | ||
| 67 |
get items() {
|
|
| 68 |
if (typeof this.#items === 'undefined') {
|
|
| 69 |
this.#items = Array.from(this.listTarget.children); |
|
| 70 |
} |
|
| 71 |
return this.#items |
|
| 72 |
} |
|
| 73 | ||
| 74 |
showButtonValueChanged(value) {
|
|
| 75 |
if (value) {
|
|
| 76 |
this.buttonsTarget.style.display = '' |
|
| 77 |
} else {
|
|
| 78 |
if (this.items.every(e => e.style.display === '')) {
|
|
| 79 |
this.buttonsTarget.style.display = 'none' |
|
| 80 |
} |
|
| 81 |
} |
|
| 82 |
} |
|
| 83 | ||
| 84 |
enableButton(el, selector) {
|
|
| 85 |
const siblings = [...el.parentNode.children].filter((child) => child.matches(selector) && child !== el); |
|
| 86 |
siblings.forEach(el => el.classList.remove('disabled'))
|
|
| 87 |
} |
|
| 88 | ||
| 89 |
get firstVisibleIndex() {
|
|
| 90 |
return this.items.findIndex(item => isVisible(item)) |
|
| 91 |
} |
|
| 92 |
} |
|
| 93 | ||
| 94 |
function outerWidth(el) {
|
|
| 95 |
const style = getComputedStyle(el); |
|
| 96 | ||
| 97 |
return ( |
|
| 98 |
el.getBoundingClientRect().width + |
|
| 99 |
parseFloat(style.marginLeft) + |
|
| 100 |
parseFloat(style.marginRight) |
|
| 101 |
); |
|
| 102 |
} |
|
| app/views/common/_tabs.html.erb | ||
|---|---|---|
| 1 | 1 |
<% default_action = false %> |
| 2 | 2 | |
| 3 |
<div class="tabs"> |
|
| 4 |
<ul> |
|
| 3 |
<div class="tabs" data-controller="tab-buttons">
|
|
| 4 |
<ul data-tab-buttons-target="list">
|
|
| 5 | 5 |
<% tabs.each do |tab| -%> |
| 6 | 6 |
<% action = get_tab_action(tab) %> |
| 7 | 7 |
<li><%= link_to l(tab[:label]), (tab[:url] || { :tab => tab[:name] }),
|
| ... | ... | |
| 11 | 11 |
<% default_action = action if tab[:name] == selected_tab %> |
| 12 | 12 |
<% end -%> |
| 13 | 13 |
</ul> |
| 14 |
<div class="tabs-buttons" style="display:none;"> |
|
| 15 |
<button class="tab-left icon-only" type="button" onclick="moveTabLeft(this);">
|
|
| 14 |
<div class="tabs-buttons" style="display:none;" data-tab-buttons-target="buttons">
|
|
| 15 |
<button class="tab-left icon-only" type="button" data-action="tab-buttons#left">
|
|
| 16 | 16 |
<%= sprite_icon("angle-left", rtl: true) %>
|
| 17 | 17 |
</button> |
| 18 |
<button class="tab-right icon-only" type="button" onclick="moveTabRight(this);">
|
|
| 18 |
<button class="tab-right icon-only" type="button" data-action="tab-buttons#right">
|
|
| 19 | 19 |
<%= sprite_icon("angle-right", rtl: true) %>
|
| 20 | 20 |
</button> |
| 21 | 21 |
</div> |
| app/views/layouts/base.html.erb | ||
|---|---|---|
| 89 | 89 |
<h1><%= page_header_title %></h1> |
| 90 | 90 | |
| 91 | 91 |
<% if display_main_menu?(@project) %> |
| 92 |
<div id="main-menu" class="tabs"> |
|
| 92 |
<div id="main-menu" class="tabs" data-controller="tab-buttons">
|
|
| 93 | 93 |
<%= render_main_menu(@project) %> |
| 94 |
<div class="tabs-buttons" style="display:none;"> |
|
| 95 |
<button class="tab-left icon-only" onclick="moveTabLeft(this); return false;">
|
|
| 94 |
<div class="tabs-buttons" style="display:none;" data-tab-buttons-target="buttons">
|
|
| 95 |
<button class="tab-left icon-only" type="button" data-action="tab-buttons#left">
|
|
| 96 | 96 |
<%= sprite_icon("angle-left", rtl: true) %>
|
| 97 | 97 |
</button> |
| 98 |
<button class="tab-right icon-only" onclick="moveTabRight(this); return false;">
|
|
| 98 |
<button class="tab-right icon-only" type="button" data-action="tab-buttons#right">
|
|
| 99 | 99 |
<%= sprite_icon("angle-right", rtl: true) %>
|
| 100 | 100 |
</button> |
| 101 | 101 |
</div> |
| lib/redmine/menu_manager.rb | ||
|---|---|---|
| 101 | 101 |
# Renders the application main menu |
| 102 | 102 |
def render_main_menu(project) |
| 103 | 103 |
if menu_name = controller.current_menu(project) |
| 104 |
render_menu(menu_name, project) |
|
| 104 |
render_menu(menu_name, project, data: {tab_buttons_target: 'list'})
|
|
| 105 | 105 |
end |
| 106 | 106 |
end |
| 107 | 107 | |
| ... | ... | |
| 110 | 110 |
menu_name.present? && Redmine::MenuManager.items(menu_name).children.present? |
| 111 | 111 |
end |
| 112 | 112 | |
| 113 |
def render_menu(menu, project=nil) |
|
| 113 |
def render_menu(menu, project=nil, options={})
|
|
| 114 | 114 |
links = [] |
| 115 | 115 |
menu_items_for(menu, project) do |node| |
| 116 | 116 |
links << render_menu_node(node, project) |
| 117 | 117 |
end |
| 118 |
links.empty? ? nil : content_tag('ul', links.join.html_safe)
|
|
| 118 |
links.empty? ? nil : content_tag('ul', links.join.html_safe, options)
|
|
| 119 | 119 |
end |
| 120 | 120 | |
| 121 | 121 |
def render_menu_node(node, project=nil) |