Project

General

Profile

Patch #38740 » replace_gantt_entry_click.diff

Yasu Saku, 2024-10-11 01:02

View differences:

app/assets/javascripts/gantt.js
233 233
  };
234 234
}
235 235

  
236
ganttEntryClick = function(e){
237
  var icon_expander = e.currentTarget;
238
  var subject = $(icon_expander.parentElement);
239
  var subject_left = parseInt(subject.css('left')) + parseInt(icon_expander.offsetWidth);
240
  var target_shown = null;
241
  var target_top = 0;
242
  var total_height = 0;
243
  var out_of_hierarchy = false;
244
  var iconChange = null;
245
  if(subject.hasClass('open'))
246
    iconChange = function(element){
247
      var expander = $(element).find('.expander')
248
      expander.switchClass('icon-expanded', 'icon-collapsed');
249
      $(element).removeClass('open');
250
      if (expander.find('svg').length === 1) {
251
        updateSVGIcon(expander[0], 'angle-right')
236
ganttEntryClick = function (e) {
237
  const iconExpanderElem = e.currentTarget;
238
  const subjectElem = iconExpanderElem.parentElement;
239
  const expanderWidth = iconExpanderElem.offsetWidth;
240
  const recursive = e.ctrlKey;
241

  
242
  function toggleClass(elem, class1, class2) {
243
    if (!elem) return;
244
    elem.classList.remove(class1);
245
    elem.classList.add(class2);
246
  }
247

  
248
  function getLeft(elem) {
249
    if (!elem) return 0;
250
    return elem.offsetLeft || parseInt(elem.style.left);
251
  }
252

  
253
  function getTop(elem) {
254
    if (!elem) return 0;
255
    return elem.offsetTop || parseInt(elem.style.top);
256
  }
257

  
258
  function nextAll(elem, tagName) {
259
    const nextAllElems = [];
260
    let targetElem = elem.nextElementSibling;
261
    while (targetElem) {
262
      if (!tagName || targetElem.tagName === tagName) {
263
        nextAllElems.push(targetElem);
252 264
      }
253
    };
254
  else
255
    iconChange = function(element){
256
      var expander = $(element).find('.expander')
257
      expander.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
258
      $(element).addClass('open');
259
      if (expander.find('svg').length === 1) {
260
        updateSVGIcon(expander[0], 'angle-down')
265
      targetElem = targetElem.nextElementSibling;
266
    }
267
    return nextAllElems;
268
  }
269

  
270
  class GanttItem {
271
    constructor(elem) {
272
      this.elem = elem;
273
      this.iconExpander = elem.querySelector(":scope > .icon.expander");
274
      this.left =
275
        getLeft(this.elem) + (this.iconExpander ? expanderWidth : 0);
276
      this.top = getTop(this.elem);
277
      this.isShown =
278
        this.elem.offsetWidth > 0 || this.elem.offsetHeight > 0;
279
      this.json = JSON.parse(this.elem.dataset.collapseExpand);
280
      this.numberOfRows = this.elem.dataset.numberOfRows;
281
      this.isCollapsed =
282
        this.iconExpander?.classList.contains("icon-collapsed");
283

  
284
      const selector =
285
        `[data-collapse-expand="${this.json.obj_id}"]` +
286
        `[data-number-of-rows="${this.numberOfRows}"]`;
287
      this.taskBars = document.querySelectorAll(
288
        `#gantt_area form > ${selector}`
289
      );
290
      this.selectedColumns = document.querySelectorAll(
291
        `td.gantt_selected_column ${selector}`
292
      );
293
    }
294

  
295
    toggleIcon(force = undefined) {
296
      if (this.isCollapsed === undefined) return false;
297

  
298
      this.elem.classList.remove("open");
299
      const svgIcon = this.iconExpander.getElementsByTagName("svg");
300

  
301
      if ((force === true && !(force === false)) || this.isCollapsed) {
302
        toggleClass(this.iconExpander, "icon-collapsed", "icon-expanded");
303
        this.elem.classList.add("open");
304
        if (svgIcon.length === 1) {
305
          updateSVGIcon(this.iconExpander, 'angle-down');
306
        }
307
      } else {
308
        toggleClass(this.iconExpander, "icon-expanded", "icon-collapsed");
309
        if (svgIcon.length === 1) {
310
          updateSVGIcon(this.iconExpander, 'angle-right');
311
        }
261 312
      }
262
    };
263
  iconChange(subject);
264
  subject.nextAll('div').each(function(_, element){
265
    var el = $(element);
266
    var json = el.data('collapse-expand');
267
    var number_of_rows = el.data('number-of-rows');
268
    var el_task_bars = '#gantt_area form > div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
269
    var el_selected_columns = 'td.gantt_selected_column div[data-collapse-expand="' + json.obj_id + '"][data-number-of-rows="' + number_of_rows + '"]';
270
    if(out_of_hierarchy || parseInt(el.css('left')) <= subject_left){
271
      out_of_hierarchy = true;
272
      if(target_shown == null) return false;
273

  
274
      var new_top_val = parseInt(el.css('top')) + total_height * (target_shown ? -1 : 1);
275
      el.css('top', new_top_val);
276
      $([el_task_bars, el_selected_columns].join()).each(function(_, el){
277
        $(el).css('top', new_top_val);
278
      });
313

  
314
      this.isCollapsed = !this.isCollapsed;
279 315
      return true;
280 316
    }
281 317

  
282
    var is_shown = el.is(':visible');
283
    if(target_shown == null){
284
      target_shown = is_shown;
285
      target_top = parseInt(el.css('top'));
286
      total_height = 0;
318
    #setDisplayStyle(displayStyle) {
319
      this.taskBars.forEach((taskBar) => {
320
        taskBar.style.display = displayStyle;
321
      });
322
      this.selectedColumns.forEach((selectedColumn) => {
323
        selectedColumn.style.display = displayStyle;
324
      });
325
      this.elem.style.display = displayStyle;
326
    }
327

  
328
    hide() {
329
      this.#setDisplayStyle("none");
330
      this.isShown = false;
331
    }
332

  
333
    show() {
334
      this.#setDisplayStyle("");
335
      this.isShown = true;
287 336
    }
288
    if(is_shown == target_shown){
289
      $(el_task_bars).each(function(_, task) {
290
        var el_task = $(task);
291
        if(!is_shown)
292
          el_task.css('top', target_top + total_height);
293
        if(!el_task.hasClass('tooltip'))
294
          el_task.toggle(!is_shown);
337

  
338
    move(top) {
339
      this.top = top;
340
      this.taskBars.forEach((taskBar) => {
341
        taskBar.style.top = `${top}px`;
295 342
      });
296
      $(el_selected_columns).each(function (_, attr) {
297
        var el_attr = $(attr);
298
        if (!is_shown)
299
          el_attr.css('top', target_top + total_height);
300
          el_attr.toggle(!is_shown);
343
      this.selectedColumns.forEach((selectedColumn) => {
344
        selectedColumn.style.top = `${top}px`;
301 345
      });
302
      if(!is_shown)
303
        el.css('top', target_top + total_height);
304
      iconChange(el);
305
      el.toggle(!is_shown);
306
      total_height += parseInt(json.top_increment);
346
      this.elem.style.top = `${top}px`;
307 347
    }
308
  });
348
  }
349

  
350
  const subject = new GanttItem(subjectElem);
351
  subject.toggleIcon();
352

  
353
  let totalHeight = 0;
354
  let outOfHierarchyTop = null;
355
  let firstItemTop = null;
356
  let collapsedStateHierarchy = new Map();
357
  collapsedStateHierarchy.set(subject.left, subject.isCollapsed);
358
  let prevItemLeft = subject.left;
359

  
360
  function updateGanttItemPositionAndView (ganttItem) {
361
    if (outOfHierarchyTop || ganttItem.left <= subject.left) {
362
      if (!outOfHierarchyTop) outOfHierarchyTop = ganttItem.top;
363

  
364
      const newTop =
365
        ganttItem.top +
366
        (subject.isCollapsed
367
          ? -outOfHierarchyTop + subject.top + subject.json.top_increment
368
          : totalHeight);
369

  
370
      ganttItem.move(newTop);
371
      return;
372
    }
373

  
374
    // Clear the collapsed state for levels deeper than the current hierarchy
375
    // level.
376
    if (prevItemLeft > ganttItem.left) {
377
      for (const left of collapsedStateHierarchy.keys()) {
378
        if (left >= ganttItem.left) collapsedStateHierarchy.delete(left);
379
      }
380
    }
381

  
382
    // Update the stored left value for the next loop
383
    prevItemLeft = ganttItem.left;
384

  
385
    if (!firstItemTop) {
386
      firstItemTop = subject.top + subject.json.top_increment;
387
    }
388

  
389
    if (
390
      (recursive && subject.isCollapsed) ||
391
      (!recursive && collapsedStateHierarchy.values().some((i) => i))
392
    ) {
393
      if (ganttItem.isShown) ganttItem.hide();
394
    } else {
395
      if (!ganttItem.isShown) ganttItem.show();
396
      ganttItem.move(firstItemTop + totalHeight);
397
      totalHeight += ganttItem.json.top_increment;
398
    }
399

  
400
    if (ganttItem.iconExpander) {
401
      collapsedStateHierarchy.set(ganttItem.left, ganttItem.isCollapsed);
402
      if (recursive && ganttItem.isCollapsed !== subject.isCollapsed) {
403
        ganttItem.toggleIcon();
404
      }
405
    }
406
  }
407

  
408
  // Get all subsequent DIV elements, convert to GanttItem,
409
  // and update their positions and view states.
410
  nextAll(subjectElem, "DIV")
411
    .map((elem) => new GanttItem(elem))
412
    .forEach(updateGanttItemPositionAndView);
413

  
309 414
  drawGanttHandler();
310 415
};
311 416

  
(5-5/5)