Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Degradation with Complex Dependency Relationships #403

Open
NoirFeigur opened this issue Aug 5, 2024 · 2 comments
Open

Performance Degradation with Complex Dependency Relationships #403

NoirFeigur opened this issue Aug 5, 2024 · 2 comments

Comments

@NoirFeigur
Copy link

Describe the bug
When dealing with items that have complex dependency relationships, the gantt-schedule-timeline-calendar component experiences significant lag. Dragging items causes the page to freeze and sometimes crash.

<template>
  <div>
    <div id="toolbox"></div>
    <div id="gstc"></div>
  </div>
</template>

<script>
import GSTC from 'gantt-schedule-timeline-calendar';
import 'gantt-schedule-timeline-calendar/dist/style.css';
import { Plugin as TimelinePointer } from 'gantt-schedule-timeline-calendar/dist/plugins/timeline-pointer.esm.min';
import { Plugin as Selection } from 'gantt-schedule-timeline-calendar/dist/plugins/selection.esm.min';
import { Plugin as ItemMovement } from 'gantt-schedule-timeline-calendar/dist/plugins/item-movement.esm.min';
import { Plugin as ItemResizing } from 'gantt-schedule-timeline-calendar/dist/plugins/item-resizing.esm.min';
import { Plugin as CalendarScroll } from 'gantt-schedule-timeline-calendar/dist/plugins/calendar-scroll.esm.min';
import { Plugin as HighlightWeekends } from 'gantt-schedule-timeline-calendar/dist/plugins/highlight-weekends.esm.min';
import { Plugin as ProgressBar } from 'gantt-schedule-timeline-calendar/dist/plugins/progress-bar.esm.min';
import { Plugin as TimeBookmarks } from 'gantt-schedule-timeline-calendar/dist/plugins/time-bookmarks.esm.min';
import { Plugin as DependencyLines } from 'gantt-schedule-timeline-calendar/dist/plugins/dependency-lines.esm.min';
import { Plugin as ExportImage } from 'gantt-schedule-timeline-calendar/dist/plugins/export-image.esm.min';
import { Plugin as ExportPDF } from 'gantt-schedule-timeline-calendar/dist/plugins/export-pdf.esm.min';

export default {
  name: 'GstcTask',
  data() {
    return {
      gstc: null,
      state: null,
      iterations: 100,
      addDays: 30,
      colors: ['#E74C3C', '#DA3C78', '#7E349D', '#0077C0', '#07ABA0', '#0EAC51', '#F1892D'],
      startDate: GSTC.api.date('2020-02-01'),
      endDate: GSTC.api.date('2020-03-31').endOf('day'),
      hideWeekends: false,
      snapTime: true,
      darkModeEnabled: false,
      historyStates: [],
      currentStateName: '',
    };
  },
  methods: {
    getRandomFaceImage() {
      return `./faces/face-${Math.ceil(Math.random() * 50)}.jpg`;
    },
    getRandomColor() {
      return this.colors[Math.floor(Math.random() * this.colors.length)];
    },
    getInitialRows() {
      const rows = {};
      for (let i = 0; i < this.iterations; i++) {
        const withParent = i > 0 && i % 2 === 0;
        const id = GSTC.api.GSTCID(String(i));
        rows[id] = {
          id,
          label: `John Doe ${i}`,
          parentId: withParent ? GSTC.api.GSTCID(String(i - 1)) : undefined,
          expanded: false,
          vacations: [],
          img: this.getRandomFaceImage(),
          progress: Math.floor(Math.random() * 100),
          visible: true,
        };
      }

      rows[GSTC.api.GSTCID('11')].label = 'NESTED TREE HERE';
      rows[GSTC.api.GSTCID('12')].parentId = GSTC.api.GSTCID('11');
      rows[GSTC.api.GSTCID('13')].parentId = GSTC.api.GSTCID('12');
      rows[GSTC.api.GSTCID('14')].parentId = GSTC.api.GSTCID('13');
      rows[GSTC.api.GSTCID('3')].vacations = [
        { from: this.startDate.add(5, 'days').startOf('day').valueOf(), to: this.startDate.add(5, 'days').endOf('day').valueOf() },
        { from: this.startDate.add(6, 'days').startOf('day').valueOf(), to: this.startDate.add(6, 'days').endOf('day').valueOf() },
      ];
      rows[GSTC.api.GSTCID('7')].birthday = [
        {
          from: this.startDate.add(3, 'day').startOf('day').valueOf(),
          to: this.startDate.add(3, 'day').endOf('day').valueOf(),
        },
      ];
      return rows;
    },
    generateItemsForDaysView() {
      const items = {};
      for (let i = 0; i < this.iterations; i++) {
        let rowId = GSTC.api.GSTCID(i.toString());
        let id = GSTC.api.GSTCID(i.toString());
        let startDayjs = GSTC.api
          .date(this.startDate.valueOf())
          .startOf('day')
          .add(Math.floor(Math.random() * this.addDays), 'day');
        let end = startDayjs
          .clone()
          .add(Math.floor(Math.random() * 20) + 4, 'day')
          .endOf('day')
          .valueOf();
        if (end > this.endDate.valueOf()) end = this.endDate.valueOf();
        items[id] = {
          id,
          label: `John Doe ${i}`,
          progress: Math.round(Math.random() * 100),
          style: { background: this.getRandomColor() },
          time: {
            start: startDayjs.startOf('day').valueOf(),
            end,
          },
          rowId,
          img: this.getRandomFaceImage(),
          classNames: ['additional-custom-class'],
          description: 'Lorem ipsum dolor sit amet',
        };
      }

      items[GSTC.api.GSTCID('0')].linkedWith = [GSTC.api.GSTCID('1')];
      items[GSTC.api.GSTCID('0')].label = 'Task 0 linked with 1';
      items[GSTC.api.GSTCID('0')].type = 'task';
      items[GSTC.api.GSTCID('1')].label = 'Task 1 linked with 0';
      items[GSTC.api.GSTCID('1')].type = 'task';
      items[GSTC.api.GSTCID('1')].time = { ...items[GSTC.api.GSTCID('0')].time };

      items[GSTC.api.GSTCID('0')].style = { background: this.colors[3] };
      items[GSTC.api.GSTCID('1')].style = { background: this.colors[3] };

      items[GSTC.api.GSTCID('3')].dependant = [GSTC.api.GSTCID('5')];
      items[GSTC.api.GSTCID('3')].label = 'Grab and move me into vacation area';
      items[GSTC.api.GSTCID('3')].time.start = GSTC.api.date(this.startDate.valueOf()).add(4, 'day').startOf('day').add(5, 'day').valueOf();
      items[GSTC.api.GSTCID('3')].time.end = GSTC.api.date(items[GSTC.api.GSTCID('3')].time.start).endOf('day').add(5, 'day').valueOf();

      items[GSTC.api.GSTCID('5')].time.start = GSTC.api.date(items[GSTC.api.GSTCID('3')].time.end).startOf('day').add(5, 'day').valueOf();
      items[GSTC.api.GSTCID('5')].time.end = GSTC.api.date(items[GSTC.api.GSTCID('5')].time.start).endOf('day').add(2, 'day').valueOf();
      items[GSTC.api.GSTCID('5')].dependant = [GSTC.api.GSTCID('7'), GSTC.api.GSTCID('9')];

      items[GSTC.api.GSTCID('7')].time.start = GSTC.api.date(items[GSTC.api.GSTCID('5')].time.end).startOf('day').add(3, 'day').valueOf();
      items[GSTC.api.GSTCID('7')].time.end = GSTC.api.date(items[GSTC.api.GSTCID('7')].time.start).endOf('day').add(2, 'day').valueOf();
      items[GSTC.api.GSTCID('9')].time.start = GSTC.api.date(items[GSTC.api.GSTCID('5')].time.end).startOf('day').add(2, 'day').valueOf();
      items[GSTC.api.GSTCID('9')].time.end = GSTC.api.date(items[GSTC.api.GSTCID('9')].time.start).endOf('day').add(3, 'day').valueOf();

      items[GSTC.api.GSTCID('7')].dependant = [GSTC.api.GSTCID('10')];
      items[GSTC.api.GSTCID('10')].dependant = [GSTC.api.GSTCID('11')];
      items[GSTC.api.GSTCID('11')].dependant = [GSTC.api.GSTCID('12')];
      items[GSTC.api.GSTCID('12')].dependant = [GSTC.api.GSTCID('13')];
      items[GSTC.api.GSTCID('13')].dependant = [GSTC.api.GSTCID('14')];
      items[GSTC.api.GSTCID('14')].dependant = [GSTC.api.GSTCID('15')];
      items[GSTC.api.GSTCID('15')].dependant = [GSTC.api.GSTCID('16')];
      items[GSTC.api.GSTCID('16')].dependant = [GSTC.api.GSTCID('17')];
      items[GSTC.api.GSTCID('17')].dependant = [GSTC.api.GSTCID('18')];
      items[GSTC.api.GSTCID('18')].dependant = [GSTC.api.GSTCID('19')];
      items[GSTC.api.GSTCID('19')].dependant = [GSTC.api.GSTCID('20')];
      items[GSTC.api.GSTCID('20')].dependant = [GSTC.api.GSTCID('21')];
      items[GSTC.api.GSTCID('21')].dependant = [GSTC.api.GSTCID('22')];
      items[GSTC.api.GSTCID('22')].dependant = [GSTC.api.GSTCID('23')];




      return items;
    },
    itemSlot(vido, props) {
      const { html, onChange, update } = vido;

      let imageSrc = '';
      let description = '';
      onChange((newProps) => {
        props = newProps;
        if (!props || !props.item) return;
        imageSrc = props.item.img;
        description = props.item.description;
        update();
      });

      return (content) =>
        html`<div
            class="item-image"
            style="background:url(${imageSrc}),transparent;flex-shrink:0;border-radius:100%;width:34px;height:34px;vertical-align: middle;background-size: 100%;margin: 4px 11px 0px 0px;"
          ></div>
          <div class="item-text">
            <div class="item-label">${content}</div>
            <div class="item-description" style="font-size:11px;margin-top:2px;color:#fffffff0;line-height:1em;">
              ${description}
            </div>
          </div>`;
    },
    rowSlot(vido, props) {
      const { html, onChange, update, api } = vido;

      let img = '';
      onChange((newProps) => {
        props = newProps;
        if (!props || !props.row) return;
        img = props.row.img;
        update();
      });

      return (content) => {
        if (!props || !props.column) return content;
        return api.sourceID(props.column.id) === 'label'
          ? html`<div class="row-content-wrapper" style="display:flex">
              <div class="row-content" style="flex-grow:1;">${content}</div>
              <div
                class="row-image"
                style="background:url(${img}),transparent;border-radius:100%;width:34px;height:34px;vertical-align: middle;background-size: 100%;margin: auto 10px;flex-shrink:0;"
              ></div>
            </div>`
          : content;
      };
    },
    snapStart({ startTime, vido }) {
      if (!this.snapTime) return startTime;
      const date = vido.api.time.findOrCreateMainDateAtTime(startTime.valueOf());
      return date.leftGlobalDate;
    },
    snapEnd({ endTime, vido }) {
      if (!this.snapTime) return endTime;
      const date = vido.api.time.findOrCreateMainDateAtTime(endTime.valueOf());
      return date.rightGlobalDate;
    },
    canMove(item) {
      const row = this.gstc.api.getRow(item.rowId);
      if (row.vacations) {
        for (const vacation of row.vacations) {
          const vacationStart = vacation.from;
          const vacationEnd = vacation.to;
          if (item.time.start >= vacationStart && item.time.start <= vacationEnd) {
            return false;
          }
          if (item.time.end >= vacationStart && item.time.end <= vacationEnd) {
            return false;
          }
          if (item.time.start <= vacationStart && item.time.end >= vacationEnd) {
            return false;
          }
          if (item.time.start >= vacationStart && item.time.end <= vacationEnd) {
            return false;
          }
        }
      }
      return true;
    },
    onLevelDates({ dates, level, format, time }) {
      if (time.period !== 'day') return dates;
      if (format.period !== time.period) return dates;
      if (!this.hideWeekends) return dates;
      return dates.filter((date) => date.leftGlobalDate.day() !== 0 && date.leftGlobalDate.day() !== 6);
    },
    onItemClick(ev) {
      const itemElement = ev.target.closest('.gstc__chart-timeline-items-row-item');
      const itemId = itemElement.dataset.gstcid;
      const item = this.gstc.api.getItem(itemId);
      console.log('Item click from template', item);
    },
    chartTimelineItemsRowItemTemplate({
                                        className,
                                        labelClassName,
                                        styleMap,
                                        cache,
                                        shouldDetach,
                                        cutterLeft,
                                        cutterRight,
                                        getContent,
                                        actions,
                                        slots,
                                        html,
                                        vido,
                                        props,
                                      }) {
      const detach = shouldDetach || !props || !props.item;
      return cache(
        detach
          ? null
          : slots.html(
            'outer',
            html`
                <div
                  class=${className}
                  data-gstcid=${props.item.id}
                  data-actions=${actions()}
                  style=${styleMap.directive()}
                  @click=${this.onItemClick}
                >
                  ${slots.html(
              'inner',
              html`
                      ${cutterLeft()}
                      <div class=${labelClassName}>${slots.html('content', getContent())}</div>
                      ${cutterRight()}
                    `
            )}
                </div>
              `
          )
      );
    },
    myItemSlot(vido, props) {
      const { onChange } = vido;

      function onClick() {
        console.log('Item click from slot', props.item);
      }

      onChange((changedProps) => {
        props = changedProps;
      });

      return (content) =>
        vido.html`<div class="my-item-wrapper" @click=${onClick} style="width:100%;display:flex;overflow:hidden;pointer-events:none;">${content}</div>`;
    },
    onCellCreateVacation({ time, row, vido, content }) {
      if (!row.vacations) return content;
      let isVacation = false;
      for (const vacation of row.vacations) {
        if (time.leftGlobal >= vacation.from && time.rightGlobal <= vacation.to) {
          isVacation = true;
          break;
        }
      }
      if (isVacation) {
        return vido.html`<div title="🏖️ VACATION" style="height:100%;width:100%;background:#A0A0A010;"></div>${content}`;
      }
      return content;
    },
    myVacationRowSlot(vido, props) {
      const { onChange, html, update, api, state } = vido;

      let vacationContent = [];
      onChange((changedProps) => {
        props = changedProps;
        if (!props || !props.row || !props.row.vacations) {
          vacationContent = [];
          return update();
        }
        const configTime = state.get('config.chart.time');
        vacationContent = [];
        for (const vacation of props.row.vacations) {
          if (vacation.to < configTime.leftGlobal || vacation.from > configTime.rightGlobal) continue;
          const leftPx = api.time.getViewOffsetPxFromDates(api.time.date(vacation.from));
          const rightPx = api.time.getViewOffsetPxFromDates(api.time.date(vacation.to));
          const widthPx = rightPx - leftPx - 1;
          if (widthPx < 0) continue;
          let textAlign = 'left';
          if (widthPx <= 100) textAlign = 'center';
          vacationContent.push(
            html`<div
              style="position:absolute;left:${leftPx}px;width:${widthPx}px;height:14px;white-space: nowrap;text-overflow:ellipsis;overflow:hidden;font-size:11px;background:#A0A0A0;color:white;text-align:${textAlign};"
            >
              Vacation
            </div>`
          );
        }
        update();
      });

      return (content) => html`${vacationContent}${content}`;
    },
    onCellCreateBirthday({ time, row, vido, content }) {
      if (!row.birthday) return content;
      let isBirthday = false;
      for (const birthday of row.birthday) {
        if (time.leftGlobal >= birthday.from && time.rightGlobal <= birthday.to) {
          isBirthday = true;
          break;
        }
      }
      if (isBirthday) {
        return vido.html`<div title="🎁 BIRTHDAY" style="height:100%;width:100%;font-size:18px;background:#F9B32F10;"></div>${content}`;
      }
      return content;
    },
    myBirthdayRowSlot(vido, props) {
      const { onChange, html, update, api, state } = vido;

      let birthdayContent = [];
      onChange((changedProps) => {
        props = changedProps;
        if (!props || !props.row || !props.row.birthday) {
          birthdayContent = [];
          return update();
        }
        const configTime = state.get('config.chart.time');
        birthdayContent = [];
        for (const birthday of props.row.birthday) {
          if (birthday.to < configTime.leftGlobal || birthday.from > configTime.rightGlobal) continue;
          const leftPx = api.time.getViewOffsetPxFromDates(api.time.date(birthday.from));
          const rightPx = api.time.getViewOffsetPxFromDates(api.time.date(birthday.to));
          const widthPx = rightPx - leftPx - 1;
          if (widthPx < 0) continue;
          let textAlign = 'left';
          if (widthPx <= 100) textAlign = 'center';
          birthdayContent.push(

            html`<div
              style="position:absolute;left:${leftPx}px;width:${widthPx}px;height:14px;white-space: nowrap;text-overflow:ellipsis;overflow:hidden;font-size:11px;background:#F9B32F;color:white;text-align:${textAlign};"
            >
              🎁 Birthday
            </div>`
          );
        }
        update();
      });

      return (content) => html`${birthdayContent}${content}`;
    },
    mountGSTC() {
      const columns = {
        data: {
          [GSTC.api.GSTCID('id')]: {
            id: GSTC.api.GSTCID('id'),
            data: ({ row }) => GSTC.api.sourceID(row.id),
            width: 80,
            sortable: ({ row }) => Number(GSTC.api.sourceID(row.id)),
            header: {
              content: 'ID',
            },
          },
          [GSTC.api.GSTCID('label')]: {
            id: GSTC.api.GSTCID('label'),
            data: 'label',
            sortable: 'label',
            expander: true,
            isHTML: false,
            width: 315,
            header: {
              content: 'Label',
            },
          },
          [GSTC.api.GSTCID('progress')]: {
            id: GSTC.api.GSTCID('progress'),
            data({ row, vido }) {
              return vido.html`<div style="text-align:center">${row.progress}</div>`;
            },
            width: 100,
            sortable: 'progress',
            header: {
              content: 'Progress',
            },
          },
        },
      };

      const bookmarks = {};
      for (let i = 0; i < 3; i++) {
        const id = `Bookmark ${i}`;
        bookmarks[id] = {
          time: this.startDate
            .add(Math.round(Math.random() * this.addDays), 'day')
            .startOf('day')
            .valueOf(),
          label: id,
          style: {
            background: this.getRandomColor(),
          },
        };
      }

      const itemMovementOptions = {
        threshold: {
          horizontal: 25,
          vertical: 25,
        },
        snapToTime: {
          start: this.snapStart,
          end({ endTime }) {
            return endTime;
          },
        },
        events: {
          onMove: ({ items }) => {
            for (let i = 0, len = items.after.length; i < len; i++) {
              const item = items.after[i];
              if (!this.canMove(item)) return items.before;
            }
            return items.after;
          },
        },
      };

      const itemResizeOptions = {
        threshold: 25,
        snapToTime: {
          start: this.snapStart,
          end: this.snapEnd,
        },
        events: {
          onResize: ({ items }) => {
            for (const item of items.after) {
              if (!this.canMove(item)) return items.before;
            }
            return items.after;
          },
        },
      };

      const config = {
        licenseKey: 'YOUR_LICENSE_KEY_HERE',
        innerHeight: 700,
        plugins: [
          HighlightWeekends(),
          TimelinePointer(),
          Selection({
            events: {
              onEnd: (selected) => {
                console.log('Selected', selected);
                return selected;
              },
            },
          }),
          ItemResizing(itemResizeOptions),
          ItemMovement(itemMovementOptions),
          CalendarScroll(),
          ProgressBar(),
          TimeBookmarks({ bookmarks }),
          DependencyLines({
            onLine: [
              (line) => {
                line.type = GSTC.api.sourceID(line.fromItem.id) === '3' ? 'smooth' : 'square';
                return line;
              },
            ],
          }),
          ExportImage(),
          ExportPDF(),
        ],
        list: {
          row: {
            height: 68,
          },
          rows: this.getInitialRows(),
          columns,
        },
        chart: {
          time: {
            from: this.startDate.valueOf(),
            to: this.endDate.valueOf(),
            onLevelDates: [this.onLevelDates],
          },
          item: {
            height: 50,
            gap: {
              top: 14,
            },
          },
          items: this.generateItemsForDaysView(),
          grid: {
            cell: {
              onCreate: [this.onCellCreateVacation, this.onCellCreateBirthday],
            },
          },
        },
        scroll: {
          vertical: { precise: true, byPixels: true },
          horizontal: { precise: true, byPixels: true },
        },
        slots: {
          'chart-timeline-items-row-item': { content: [this.itemSlot], inner: [this.myItemSlot] },
          'list-column-row': { content: [this.rowSlot] },
          'chart-timeline-grid-row': { content: [this.myBirthdayRowSlot, this.myVacationRowSlot] },
        },
        templates: {
          'chart-timeline-items-row-item': this.chartTimelineItemsRowItemTemplate,
        },
      };

      this.state = GSTC.api.stateFromConfig(config);
      const element = document.createElement('div');
      element.id = 'gstc';
      document.querySelector('#toolbox')?.after(element);
      this.gstc = GSTC({
        element,
        state: this.state,
      });
    },
    updateToolBox() {
      const searchBoxHTML = GSTC.lithtml.html`<input type="text" @input=${this.searchRows} placeholder="Search" />`;
      const historyStateHTML = GSTC.lithtml.html`<button @click="${this.openSaveCurrentStateDialog}">Save items</button>
        <label>Restore items:</label>
        <select @change=${this.onRestoreStateChange}>
          ${this.historyStates.map(
        (historyState) =>
          GSTC.lithtml.html`<option value=${historyState.name} ?selected=${historyState.name === this.currentStateName}>
                ${historyState.name}
              </option>`
      )}
        </select>`;

      const toolboxButtons = GSTC.lithtml.html` <div class="toolbox-row">
        <div class="toolbox-item"><button @click=${this.selectCells}>Select first cells</button></div>
        <div class="toolbox-item"><button @click=${this.scrollToFirstItem}>Scroll to first item</button></div>
        <div class="toolbox-item"><button @click=${this.downloadImage}>Download image</button></div>
        <div class="toolbox-item"><button @click=${this.downloadPdf}>PDF (current view)</button></div>
        <div class="toolbox-item"><button @click=${this.downloadPdfFull}>PDF (full)</button></div>
        <div class="toolbox-item"><button @click=${this.takeShotPdf}>Take screenshot</button></div>
        <div class="toolbox-item">-></div>
          <div class="toolbox-item"><button @click=${this.getPdf}>Get screenshots</button></div>
          <div class="toolbox-item">${historyStateHTML}</div>
          <div class="toolbox-item">
            <label>Zoom:</label>
            <select @change="${this.zoomChangeSelect}" id="zoom">
              <option value="hours">Hours</option>
              <option value="days" selected>Days</option>
              <option value="weeks">Weeks</option>
              <option value="months">Months</option>
            </select>
          </div>
          <div class="toolbox-item">
            <button @click=${this.deleteSelectedItems}>Delete selected items</button>
          </div>
        </div>
        <div class="toolbox-row">
          <div class="toolbox-item">${searchBoxHTML}</div>
          <div class="toolbox-item">
            <input type="checkbox" id="dark-mode" @change=${this.toggleDarkMode} /> <label for="dark-mode">Dark mode</label>
          </div>
          <div class="toolbox-item">
            <input type="checkbox" id="snap-time" @change=${this.toggleSnapTime} checked />
            <label for="snap-time">Snap time (item movement)</label>
          </div>
          <div class="toolbox-item">
            <input type="checkbox" id="hide-weekends" @change=${this.toggleHideWeekends} />
            <label for="hide-weekends">Hide weekends</label>
          </div>
          <div class="toolbox-item">
            <input type="checkbox" id="expand-time" @change=${this.toggleExpandTime} />
            <label for="expand-time">Expand view when item is outside</label>
          </div>
          <div class="toolbox-item">
            <input type="checkbox" id="move-out" @change=${this.toggleMoveOut} checked />
            <label for="move-out">Allow items to move outside area</label>
          </div>
          <div class="toolbox-item">
            <label for="zoom">Zoom:</label>
            <input
              id="zoom-range"
              type="range"
              min="16"
              max="26"
              value="20"
              step="0.1"
              @change=${this.zoomChangeRange}
              style="width:200px"
            />
          </div>
        </div>
      </div>`;
      GSTC.lithtml.render(toolboxButtons, document.getElementById('toolbox'));
    },
    selectCells() {
      const api = this.gstc.api;
      const allCells = api.getGridCells();
      api.plugins.Selection.selectCells([allCells[0].id, allCells[1].id]);
      console.log(api.plugins.Selection.getSelection());
    },
    scrollToFirstItem() {
      const api = this.gstc.api;
      const firstItem = this.gstc.state.get(`config.chart.items.${api.GSTCID('1')}`);
      api.scrollToTime(firstItem.time.start, false);
    },
    downloadImage() {
      this.gstc.api.plugins.ExportImage.download();
    },
    async downloadPdf() {
      await this.gstc.api.plugins.ExportPDF.download('timeline.pdf');
      console.log('PDF downloaded');
    },
    async takeShotPdf() {
      const img = await this.gstc.api.plugins.ExportPDF.takeShot();
      console.log('PDF shot taken', img);
      alert(
        `\nScreenshot taken\n\nYou can add more screenshots and then download them all together with "Get screenshots" button.`
      );
    },
    async getPdf() {
      await this.gstc.api.plugins.ExportPDF.getPDF('timeline.pdf');
      await this.gstc.api.plugins.ExportPDF.clearPDF();
      console.log('PDF downloaded');
    },
    downloadPdfFull() {
      this.gstc.api.plugins.ExportPDF.downloadFull('timeline.pdf');
    },
    toggleDarkMode(ev) {
      this.darkModeEnabled = ev.target.checked;
      const el = document.getElementById('gstc');
      if (this.darkModeEnabled) {
        el?.classList.add('gstc--dark');
        document.body.classList.add('gstc--dark');
      } else {
        el?.classList.remove('gstc--dark');
        document.body.classList.remove('gstc--dark');
      }
    },
    toggleHideWeekends(ev) {
      this.hideWeekends = ev.target.checked;
      this.gstc.api.time.recalculateTime();
    },
    toggleSnapTime(ev) {
      this.snapTime = ev.target.checked;
    },
    toggleExpandTime(ev) {
      const expandTime = ev.target.checked;
      const moveOutEl = document.getElementById('move-out');
      if (moveOutEl && expandTime) {
        moveOutEl.checked = expandTime;
        this.toggleMoveOut({ target: moveOutEl });
      }
      this.state.update('config.chart.time.autoExpandTimeFromItems', expandTime);
    },
    toggleMoveOut(ev) {
      const moveOut = ev.target.checked;
      const expandTimeEl = document.getElementById('expand-time');
      if (expandTimeEl && !moveOut) {
        expandTimeEl.checked = moveOut;
        this.toggleExpandTime({ target: expandTimeEl });
      }
      this.state.update('config.plugin.ItemMovement.allowItemsToGoOutsideTheArea', moveOut);
      this.state.update('config.plugin.ItemResizing.allowItemsToGoOutsideTheArea', moveOut);
    },
    zoomChangeSelect(ev) {
      const period = ev.target.value;
      let zoom = 20;
      let from = this.gstc.api.time.date('2020-02-01').startOf('day').valueOf();
      let to = this.gstc.api.time.date('2020-03-01').endOf('month').valueOf();
      switch (period) {
        case 'hours':
          zoom = 16;
          from = this.gstc.api.time.date('2020-02-01').startOf('day').valueOf();
          to = this.gstc.api.time.date('2020-03-01').endOf('month').valueOf();
          break;
        case 'days':
          zoom = 20;
          from = this.gstc.api.time.date('2020-02-01').startOf('day').valueOf();
          to = this.gstc.api.time.date('2020-03-01').endOf('month').valueOf();
          break;
        case 'weeks':
          zoom = 23;
          from = this.gstc.api.time.date('2020-02-01').startOf('day').valueOf();
          to = this.gstc.api.time.date('2020-08-01').endOf('month').valueOf();
          break;
        case 'months':
          zoom = 26;
          from = this.gstc.api.time.date('2020-01-01').startOf('day').valueOf();
          to = this.gstc.api.time.date('2024-01-10').endOf('year').valueOf();
          break;
      }

      this.state.update('config.chart.time', (time) => {
        time.zoom = zoom;
        time.from = from;
        time.to = to;
        return time;
      });
      const zoomRange = document.getElementById('zoom-range');
      if (zoomRange) zoomRange.value = zoom;
    },
    zoomChangeRange(ev) {
      const zoom = Number(ev.target.value);
      let period = 'days';
      let from = this.gstc.api.time.date('2020-02-01').startOf('day');
      let to = this.gstc.api.time.date('2020-03-01').endOf('month');

      if (zoom >= 16) {
        period = 'hours';
        from = this.gstc.api.time.date('2020-02-01').startOf('day');
        to = this.gstc.api.time.date('2020-03-01').endOf('month');
      }
      if (zoom >= 20) {
        period = 'days';
        from = this.gstc.api.time.date('2020-02-01').startOf('day');
        to = this.gstc.api.time.date('2020-03-01').endOf('month');
      }
      if (zoom >= 20) {
        period = 'weeks';
        from = this.gstc.api.time.date('2020-02-01').startOf('day');
        to = this.gstc.api.time.date('2020-08-01').endOf('month');
      }
      if (zoom >= 23) {
        period = 'months';
        from = this.gstc.api.time.date('2020-01-01').startOf('day');
        to = this.gstc.api.time.date('2024-01-10').endOf('year');
      }

      this.gstc.state.update('config.chart.time', (time) => {
        time.zoom = zoom;
        time.from = from.valueOf();
        time.to = to.valueOf();
        return time;
      });
      const zoomSelect = document.getElementById('zoom');
      if (zoomSelect) zoomSelect.value = period;
    },
    searchRows(event) {
      const copiedRows = this.getInitialRows();
      const search = String(event.target.value).trim();
      console.log('search', search);
      const regex = new RegExp(`[\s\S]?${search}[\s\S]?`, 'gi');
      const rowsToKeep = [];
      for (const rowId in copiedRows) {
        const row = copiedRows[rowId];
        const rowData = this.gstc.api.getRowData(rowId);
        if (regex.test(row.label)) {
          rowsToKeep.push(rowId);
          for (const childRowId of rowData.allChildren) {
            rowsToKeep.push(childRowId);
          }
          for (const parentRowId of rowData.parents) {
            rowsToKeep.push(parentRowId);
            if (search) copiedRows[parentRowId].expanded = true;
          }
        }
        regex.lastIndex = 0;
      }
      const uniqueRowsToKeep = [...new Set(rowsToKeep)];
      for (const rowId in copiedRows) {
        if (uniqueRowsToKeep.includes(rowId)) {
          copiedRows[rowId].visible = true;
        } else {
          copiedRows[rowId].visible = false;
        }
      }
      this.state.update('config.list.rows', () => {
        return copiedRows;
      });
    },
    saveCurrentState(stateName) {
      const items = GSTC.api.merge({}, this.state.get('config.chart.items'));
      this.historyStates.push({ name: stateName, state: items });
      this.currentStateName = stateName;
      this.updateToolBox();
    },
    restoreState(stateName) {
      const historyState = this.historyStates.find((s) => s.name == stateName);
      this.currentStateName = stateName;
      const clonedState = GSTC.api.merge({}, historyState.state);
      this.state.update('config.chart.items', clonedState);
      this.updateToolBox();
    },
    onRestoreStateChange(ev) {
      const name = ev.target.value;
      this.restoreState(name);
    },
    openSaveCurrentStateDialog() {
      const stateName = prompt('Enter current state name');
      this.saveCurrentState(stateName);
    },
    deleteSelectedItems() {
      const selectedItems = this.gstc.api.plugins.Selection.getSelected()['chart-timeline-items-row-item'];
      this.gstc.api.plugins.Selection.selectItems([]);
      this.state.update('config.plugin.Selection.lastSelecting.chart-timeline-items-row-item', []);
      this.state.update('config.chart.items', (items) => {
        for (const item of selectedItems) {
          delete items[item.id];
        }
        return items;
      });
    },
  },
  mounted() {
    this.mountGSTC();
    this.updateToolBox();
    const gstcEl = document.getElementById('gstc');
    gstcEl?.addEventListener('gstc-loaded', () => {
      console.log('GSTC loaded!');
      this.saveCurrentState('Initial');
    });
  },
};
</script>

<style scoped>
.gstc__chart-timeline-items-row-item {
  border-radius: 4px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: flex-start;
}

.gstc__chart-timeline-items-row-item .item-image {
  margin-right: 8px;
}

.gstc__chart-timeline-items-row-item .item-text {
  display: flex;
  flex-direction: column;
}

.gstc__chart-timeline-items-row-item .item-label {
  font-weight: bold;
}

.gstc__chart-timeline-items-row-item .item-description {
  font-size: 12px;
  color: #666;
}

.gstc__list-column-row .row-content-wrapper {
  display: flex;
  align-items: center;
}

.gstc__list-column-row .row-content {
  flex-grow: 1;
}

.gstc__list-column-row .row-image {
  margin-left: 8px;
}

.tool-box {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}

.tool-box .toolbox-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}

.tool-box .toolbox-item {
  margin-right: 10px;
}
</style>

gantt-schedule-timeline-calendar version
3.37.5

@NoirFeigur
Copy link
Author

It seems that the performance degradation might be specific to my environment, as the issue only occurs when using the DependencyLines Plugin. Other plugins do not seem to cause the same problem.

@NoirFeigur
Copy link
Author

vue3 vite5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant