Project

General

Profile

Feature #44608 » 0002-Auto-fit-Gantt-column-widths.patch

Katsuya HIDAKA, 2026-10-14 11:13

View differences:

app/assets/stylesheets/gantt.css
51 51
  max-inline-size: max(100px, calc(100% - 100px));
52 52
}
53 53

  
54
/* On a narrow chart, dragging may make the subject column wider than the available space. */
54
/* On a narrow chart, dragging or auto-fit may make the subject column wider than the available space. */
55 55
.gantt-chart.is-manually-resized [data-gantt-column="subjects"] {
56 56
  flex: 0 0 var(--gantt-subject-display-width);
57 57
  inline-size: var(--gantt-subject-display-width);
......
60 60

  
61 61
.gantt-chart .ui-resizable-e {
62 62
  cursor: ew-resize;
63
  z-index: 30;
63 64
}
64 65

  
65 66
.gantt-pane {
......
218 219
  white-space: nowrap;
219 220
}
220 221

  
222
/* Applied temporarily when auto-fitting a column to measure its header and row content widths. */
223
.gantt-column-measuring .gantt-pane > header,
224
.gantt-column-measuring .gantt-row {
225
  inline-size: max-content;
226
}
227

  
221 228
.gantt-row-hover {
222 229
  background-color: rgba(var(--oc-yellow-1-rgb), 0.5);
223 230
}
app/helpers/gantt_helper.rb
259 259
      'subjects', min_width: 100, width: 'var(--gantt-subject-width)',
260 260
      data: {
261 261
        'gantt--chart-target': 'subjectColumn',
262
        # When the subject column is dragged on a narrow chart,
262
        # When the subject column is dragged or auto-fitted on a narrow chart,
263 263
        # let the user's chosen width take precedence over automatic shrinking.
264 264
        action: %w(
265 265
          gantt--column:resize-start->gantt--chart#handleSubjectColumnResizeStart
......
284 284

  
285 285
    tag.div(**options, &)
286 286
  end
287

  
288
  def gantt_column_resize_handle_tag
289
    tag.div(
290
      class: 'ui-resizable-handle ui-resizable-e',
291
      data: {
292
        'gantt--column-target': 'handle',
293
        action: 'dblclick->gantt--column#fitWidth:prevent'
294
      },
295
      aria: {hidden: true}
296
    )
297
  end
287 298
end
app/javascript/controllers/gantt/column_controller.js
44 44
}
45 45

  
46 46
export default class extends Controller {
47
  static targets = ["handle"]
48

  
47 49
  static values = {
48 50
    minWidth: Number,
49 51
    // Local value
......
89 91
    return this.element.dataset.ganttColumn
90 92
  }
91 93

  
94
  fitWidth() {
95
    const extraPadding = 10
96
    let width = 0
97

  
98
    // Temporarily apply this class to measure the content width needed to fit the column.
99
    this.element.classList.add("gantt-column-measuring")
100
    try {
101
      this.element.querySelectorAll(".gantt-pane > header, .gantt-row:not([hidden])").forEach((content) => {
102
        width = Math.max(width, content.getBoundingClientRect().width)
103
      })
104
    } finally {
105
      this.element.classList.remove("gantt-column-measuring")
106
    }
107

  
108
    width = Math.max(this.minWidthValue, Math.ceil(width) + extraPadding)
109
    this.dispatch("resize-start")
110
    this.#setWidth(width)
111
    this.#saveWidth(width)
112
  }
113

  
92 114
  #setupResizable() {
93 115
    const options = {
94
      handles: "e",
116
      handles: { e: this.handleTarget },
95 117
      minWidth: this.minWidthValue,
96
      zIndex: 30,
97 118
      start: () => {
98 119
        this.dispatch("resize-start")
99 120
      },
100 121
      resize: (_event, ui) => {
101
        this.element.style.setProperty("--gantt-column-width", `${ui.size.width}px`)
102
        this.element.style.removeProperty("width")
103
        this.dispatch("resize", { detail: { width: ui.size.width } })
122
        this.#setWidth(ui.size.width)
104 123
      },
105 124
      stop: (_event, ui) => {
106 125
        if (ui.originalSize.width !== ui.size.width) {
......
112 131
    this.#$element.resizable(options)
113 132
  }
114 133

  
134
  #setWidth(width) {
135
    this.element.style.setProperty("--gantt-column-width", `${width}px`)
136
    this.element.style.removeProperty("width")
137
    this.dispatch("resize", { detail: { width } })
138
  }
139

  
115 140
  #restoreWidth() {
116 141
    const width = this.#columnWidthStore.get(this.#columnName)
117 142

  
app/views/gantts/_chart.html.erb
23 23
        <% end %>
24 24
      </div>
25 25
    </section>
26
    <%= gantt_column_resize_handle_tag %>
26 27
  <% end %>
27 28

  
28 29
  <%
......
39 40
          <%= gantt.selected_column_content(column: column, top: layout.content_top, zoom: layout.zoom, g_width: layout.chart_width).html_safe %>
40 41
        </div>
41 42
      </section>
43
      <%= gantt_column_resize_handle_tag %>
42 44
    <% end %>
43 45
  <% end %>
44 46

  
test/system/gantts_test.rb
155 155
    assert_selector(subject_column) { column_width('subjects') > narrow_width }
156 156
  end
157 157

  
158
  test 'automatic column width fitting' do
159
    Issue.find(3).update!(
160
      subject: 'Cannot save a recipe after changing the ingredients and selecting a different category'
161
    )
162
    visit_gantt
163
    assert_selector '.gantt-column[data-gantt-column="subjects"].ui-resizable'
164

  
165
    expected_width = page.evaluate_script(<<~JS)
166
      (() => {
167
        const row = document.querySelector('[data-gantt-column="subjects"] .gantt-row[data-gantt-row-key="issue-3"]');
168
        const range = document.createRange();
169
        range.selectNodeContents(row);
170
        return Math.ceil(range.getBoundingClientRect().right - row.getBoundingClientRect().left) + 10;
171
      })()
172
    JS
173

  
174
    find('.gantt-column[data-gantt-column="subjects"] .ui-resizable-e').double_click
175
    assert_equal expected_width, column_width('subjects')
176

  
177
    page.refresh
178
    assert_selector '.gantt-column[data-gantt-column="subjects"].ui-resizable'
179
    assert_equal expected_width, column_width('subjects')
180
  end
181

  
158 182
  test 'context menu and tooltip interactions' do
159 183
    visit_gantt
160 184

  
(2-2/3)