Browse Source

implement & fix sticky legend

tripeur 4 years ago
parent
commit
af9d55204e
8 changed files with 1690 additions and 1277 deletions
  1. 101 57
      dev/index.ts
  2. 5 5
      lib/Timeline.d.ts
  3. 247 114
      lib/Timeline.js
  4. 0 0
      lib/Timeline.js.map
  5. 191 184
      lib/styles/Timeline.style.js
  6. 1 1
      lib/styles/Timeline.style.js.map
  7. 952 730
      src/Timeline.ts
  8. 193 186
      src/styles/Timeline.style.ts

+ 101 - 57
dev/index.ts

@@ -1,25 +1,28 @@
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { formats } from 'dayjs/locale/*';
-import Ressource from '../src/Ressource';
-import TimeLine from '../src/Timeline'
-import '../src/Ressource';
-import { IEvent } from '../src/Event';
-import dayjs from 'dayjs';
+import { formats } from "dayjs/locale/*";
+import Ressource from "../src/Ressource";
+import TimeLine from "../src/Timeline";
+import "../src/Ressource";
+import { IEvent } from "../src/Event";
+import dayjs from "dayjs";
 
-
- // TimeLine.extendStyle.push(new_css)
-const emptyTimeline:TimeLine = document.createElement("jc-timeline") as TimeLine;
+// TimeLine.extendStyle.push(new_css)
+const emptyTimeline: TimeLine = document.createElement(
+  "jc-timeline"
+) as TimeLine;
 let title = document.createElement("h2");
-title.innerText = "Empty Timeline"
-document.body.appendChild(title)
+title.innerText = "Empty Timeline";
+document.body.appendChild(title);
 document.body.appendChild(emptyTimeline);
+emptyTimeline.setLegendUnitFormat("d", "dddd D MMMM");
 
-emptyTimeline.setAttribute("legendSpan","1");
-emptyTimeline.setAttribute("slotWidth","40");
-
-
+emptyTimeline.setAttribute("legendSpan", "2");
+emptyTimeline.setAttribute("slotduration", "30");
+emptyTimeline.setAttribute("slotWidth", "30");
+emptyTimeline.end = "2021-09-12T15:59:59.999Z";
+emptyTimeline.start = "2021-09-10T13:00:00.000Z";
 
-const timeline:TimeLine = document.createElement("jc-timeline") as TimeLine;
+const timeline: TimeLine = document.createElement("jc-timeline") as TimeLine;
 timeline.customStyle = `.bubble{
     height: 20px;
     width: 20px;
@@ -39,68 +42,109 @@ bubble.red{
 }
 bubble.orange{
     background: orange;
-}`
+}`;
 // timeline.defaultBackground = "maroon";
 
-const rows:Array<Ressource> = [ 
-    {id:'1',title:"Ressource 1 DarkSlate",eventBgColor:"DarkSlateBlue"},
-    {id:'3',title:"Ressource 3"},
-    {id:'4',title:"Ressource 4"},
-    {id:'5',title:"Ressource 5"}].map(i=> new Ressource(i));
+const rows: Array<Ressource> = [
+  { id: "1", title: "Ressource 1 DarkSlate", eventBgColor: "DarkSlateBlue" },
+  { id: "3", title: "Ressource 3" },
+  { id: "4", title: "Ressource 4" },
+  { id: "5", title: "Ressource 5" },
+].map((i) => new Ressource(i));
 rows[2].parent = rows[1];
 rows[0].parent = rows[1];
 const data = dayjs().startOf("hour").hour(9);
-const timeslots:Array<IEvent> = [
-    {id:'1', title:"Fixed ressource" ,ressourceId:'3', start:data.subtract(3,"h").toDate(), end:data.toDate(), ressourceEditable:false,bgColor:"darkgreen"},
-    {id:'2', title:"Fixed time" , ressourceId:'3', start:data.toDate(), end:data.endOf("hour").add(1,"h").toDate(), editable:false,bgColor:"FireBrick"},
-    {id:'3', ressourceId:'4', start:data.add(1,"h").toDate(), end:data.endOf("hour").add(2,"h").toDate(),content:'<div class="bubble">0</div>'},
-    {id:'4', ressourceId:'3', start:data.startOf("day").subtract(1,"h").toDate(), end:dayjs().endOf("hour").add(1,"h").toDate()}
-]
-for(let i = 5; i < 10; i++){
-    timeslots.push({
-        id : i.toFixed(0),
-        title: "event " + i,
-        ressourceId:'1',
-        start:data.add(i,'h').toDate(),
-        end:data.add(i,'h').endOf("hour").add(1,"h").toDate(),
-    })
+const timeslots: Array<IEvent> = [
+  {
+    id: "1",
+    title: "Fixed ressource",
+    ressourceId: "3",
+    start: data.subtract(3, "h").toDate(),
+    end: data.toDate(),
+    ressourceEditable: false,
+    bgColor: "darkgreen",
+  },
+  {
+    id: "2",
+    title: "Fixed time",
+    ressourceId: "3",
+    start: data.toDate(),
+    end: data.endOf("hour").add(1, "h").toDate(),
+    editable: false,
+    bgColor: "FireBrick",
+  },
+  {
+    id: "3",
+    ressourceId: "4",
+    start: data.add(1, "h").toDate(),
+    end: data.endOf("hour").add(2, "h").toDate(),
+    content: '<div class="bubble">0</div>',
+  },
+  {
+    id: "4",
+    ressourceId: "3",
+    start: data.startOf("day").subtract(1, "h").toDate(),
+    end: dayjs().endOf("hour").add(1, "h").toDate(),
+  },
+];
+for (let i = 5; i < 10; i++) {
+  timeslots.push({
+    id: i.toFixed(0),
+    title: "event " + i,
+    ressourceId: "1",
+    start: data.add(i, "h").toDate(),
+    end: data.add(i, "h").endOf("hour").add(1, "h").toDate(),
+  });
 }
 timeline.addRessource(rows[1]);
 timeline.addRessource(rows[0]);
 timeline.addRessource(rows[2]);
 timeline.addRessource(rows[3]);
 
-timeline.addEvents(timeslots)
-timeline.addEventListener("item-selected",(e)=>{console.log((e as CustomEvent).detail)})
-timeline.addEventListener("change-event",(e)=>{console.log((e as CustomEvent).detail)})
-timeline.addEventListener("reorder-ressource",(e)=>{console.log((e as CustomEvent).detail)})
+timeline.addEvents(timeslots);
+timeline.addEventListener("item-selected", (e) => {
+  console.log((e as CustomEvent).detail);
+});
+timeline.addEventListener("change-event", (e) => {
+  console.log((e as CustomEvent).detail);
+});
+timeline.addEventListener("reorder-ressource", (e) => {
+  console.log((e as CustomEvent).detail);
+});
 
 title = document.createElement("h2");
-title.innerText = "Generic Timeline"
+title.innerText = "Generic Timeline";
 document.body.appendChild(title);
 document.body.appendChild(timeline);
 
-const nestedTimeline:TimeLine = document.createElement("jc-timeline") as TimeLine;
+const nestedTimeline: TimeLine = document.createElement(
+  "jc-timeline"
+) as TimeLine;
 
 title = document.createElement("h2");
-title.innerText = "Multiple level Timeline"
+title.innerText = "Multiple level Timeline";
 document.body.appendChild(title);
 document.body.appendChild(nestedTimeline);
 
-const ressources = Array(8).fill(0).map((_,i)=>new Ressource({id:""+i,title:"level "+i}));
-for(let i = 0 ; i < ressources.length-1;i++){
-    ressources[i+1].parent=ressources[i]
+const ressources = Array(8)
+  .fill(0)
+  .map((_, i) => new Ressource({ id: "" + i, title: "level " + i }));
+for (let i = 0; i < ressources.length - 1; i++) {
+  ressources[i + 1].parent = ressources[i];
 }
-ressources.forEach(r=>nestedTimeline.addRessource(r));
-ressources[0].collapseChildren = true
+ressources.forEach((r) => nestedTimeline.addRessource(r));
+ressources[0].collapseChildren = true;
 const nItem = 32;
-const timeslots2 = Array(nItem).fill(0).map((_,i)=>{
+const timeslots2 = Array(nItem)
+  .fill(0)
+  .map((_, i) => {
     return {
-        id:""+i,
-        ressourceId:"" + (i % 8),
-        title:"Item "+i,
-        start:data.add(i/2 - 5, 'h').toDate(),
-        end:data.add(i/2 - 4, 'h').toDate(),
-        bgColor:"hsl(" + Math.round(i / (nItem-1) * 360) + ", 100%, 50%)"
-    }});
-nestedTimeline.addEvents(timeslots2);
+      id: "" + i,
+      ressourceId: "" + (i % 8),
+      title: "Item " + i,
+      start: data.add(i / 2 - 5, "h").toDate(),
+      end: data.add(i / 2 - 4, "h").toDate(),
+      bgColor: "hsl(" + Math.round((i / (nItem - 1)) * 360) + ", 100%, 50%)",
+    };
+  });
+nestedTimeline.addEvents(timeslots2);

+ 5 - 5
lib/Timeline.d.ts

@@ -1,7 +1,7 @@
-import { LitElement, TemplateResult, CSSResult } from 'lit-element';
-import { Event, IEvent } from './Event';
-import { Ressource, IRessource } from './Ressource';
-export { HorizontalResizer } from './components/horizontal-resizer';
+import { LitElement, TemplateResult, CSSResult } from "lit-element";
+import { Event, IEvent } from "./Event";
+import { Ressource, IRessource } from "./Ressource";
+export { HorizontalResizer } from "./components/horizontal-resizer";
 export interface TimelineOptions {
     ressources?: Array<IRessource>;
     items?: Array<IEvent>;
@@ -11,7 +11,7 @@ interface TimelineContent {
     items: Array<Event>;
     index: number;
 }
-declare type dayjsUnit = "y" | "M" | "d" | "h" | "m" | 's';
+declare type dayjsUnit = "y" | "M" | "d" | "h" | "m" | "s";
 export declare type UnitLegend = {
     [k in dayjsUnit]: string;
 };

+ 247 - 114
lib/Timeline.js

@@ -29,7 +29,14 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         this._legendSpan = 2;
         this.rowHeight = 32;
         this.slotWidth = 20;
-        this.legendUnitFormat = { "y": "YYYY", "M": "MMMM", "d": 'D', "h": "H[h]", "m": "m'", 's': "s[s]" };
+        this.legendUnitFormat = {
+            y: "YYYY",
+            M: "MMMM",
+            d: "D",
+            h: "H[h]",
+            m: "m'",
+            s: "s[s]",
+        };
         this._clearSelectionHandler = (_e) => {
             this.clearSelectedItems();
             window.removeEventListener("click", this._clearSelectionHandler);
@@ -108,7 +115,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         this.updateLegend();
     }
     addRessources(list) {
-        return list.map(r => this.addRessource(r));
+        return list.map((r) => this.addRessource(r));
     }
     addRessource(ressource) {
         var _a;
@@ -118,7 +125,8 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         }
         const r = Ressource_1.Ressource.toRessource(ressource);
         if (r.parent !== undefined) {
-            r.parent = (_a = this.getRessourceFromId(r.parent.id)) !== null && _a !== void 0 ? _a : this.addRessource(r.parent);
+            r.parent =
+                (_a = this.getRessourceFromId(r.parent.id)) !== null && _a !== void 0 ? _a : this.addRessource(r.parent);
             const idx = this.rows.indexOf(r.parent);
             if (idx > -1) {
                 this.rows[idx].children.push(r);
@@ -146,7 +154,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 output.index = i;
                 output.ressources.push(ressource);
                 if (ressource.parent && depth === 0) {
-                    ressource.parent.children = ressource.parent.children.filter(o => o.id !== id);
+                    ressource.parent.children = ressource.parent.children.filter((o) => o.id !== id);
                 }
                 this.rows.splice(i, 1);
             }
@@ -159,15 +167,15 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 i++;
             }
         }
-        output.items.push(...this.items.filter(i => i.ressourceId === id));
-        this.items = this.items.filter(i => i.ressourceId !== id);
+        output.items.push(...this.items.filter((i) => i.ressourceId === id));
+        this.items = this.items.filter((i) => i.ressourceId !== id);
         return output;
     }
     getRessources() {
         return this.rows;
     }
     getRessourceFromId(id) {
-        const tmp = this.rows.filter(r => r.id === id);
+        const tmp = this.rows.filter((r) => r.id === id);
         return tmp.length > 0 ? tmp[0] : null;
     }
     updateRessource(id, key, value) {
@@ -182,7 +190,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             }
             else {
                 ressource[key] = value;
-                this.rows = this.rows.map(r => r.id == ressource.id ? ressource : r);
+                this.rows = this.rows.map((r) => r.id == ressource.id ? ressource : r);
             }
             this._updateEventPosition(ressource);
             return ressource;
@@ -193,7 +201,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         this.rowsTitle = title;
     }
     getEventById(id) {
-        return this.items.find(o => o.id == id);
+        return this.items.find((o) => o.id == id);
     }
     addEvents(list) {
         return list.map((e) => this.addEvent(e));
@@ -203,19 +211,21 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         if (existingEvent) {
             return existingEvent;
         }
-        const ressource = this.rows.find(r => r.id === event.ressourceId);
+        const ressource = this.rows.find((r) => r.id === event.ressourceId);
         if (ressource === undefined) {
             return undefined;
         }
         const timeslot = Event_1.Event.toTimeSlot(event);
         this.items = [...this.items, timeslot];
-        timeslot.isDisplayed = timeslot.end > this._start.toDate() || timeslot.start < this._end.toDate();
+        timeslot.isDisplayed =
+            timeslot.end > this._start.toDate() ||
+                timeslot.start < this._end.toDate();
         this._updateEventPosition(ressource);
         return timeslot;
     }
     removeEventById(id) {
-        const output = this.items.filter(o => o.id === id);
-        this.items = this.items.filter(o => o.id !== id);
+        const output = this.items.filter((o) => o.id === id);
+        this.items = this.items.filter((o) => o.id !== id);
         return output;
     }
     updateEventById(id) {
@@ -229,7 +239,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         }
     }
     _updateEventPosition(ressource) {
-        const timeslots = this.items.filter(i => i.ressourceId === ressource.id);
+        const timeslots = this.items.filter((i) => i.ressourceId === ressource.id);
         if (timeslots.length === 0) {
             ressource.height = this.rowHeight + (ressource.collapseChildren ? 5 : 0);
             return;
@@ -243,7 +253,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 points.push(t);
             }
         };
-        timeslots.forEach(element => {
+        timeslots.forEach((element) => {
             populateInterval(element.start);
             populateInterval(element.end);
         });
@@ -255,31 +265,35 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             intervals.push({
                 start: points[i],
                 end: points[i + 1],
-                slots: timeslots.filter(slot => (slot.start.getTime() <= startTime && endTime <= slot.end.getTime()))
+                slots: timeslots.filter((slot) => slot.start.getTime() <= startTime && endTime <= slot.end.getTime()),
             });
         }
         const lineCount = intervals.reduce((acc, interval) => Math.max(acc, interval.slots.length), 0);
-        ressource.height = this.rowHeight * Math.max(lineCount, 1) + (ressource.collapseChildren ? 5 : 0);
+        ressource.height =
+            this.rowHeight * Math.max(lineCount, 1) +
+                (ressource.collapseChildren ? 5 : 0);
         const sortTimeslots = (a, b) => {
             const t = a.start.getTime() - b.start.getTime();
             if (t === 0) {
                 const tend = b.end.getTime() - a.end.getTime();
-                return tend === 0 ? ('' + a.id).localeCompare(b.id) : tend;
+                return tend === 0 ? ("" + a.id).localeCompare(b.id) : tend;
             }
             return t;
         };
-        timeslots.forEach(slot => slot.offset = -1);
+        timeslots.forEach((slot) => (slot.offset = -1));
         timeslots.sort(sortTimeslots);
         timeslots[0].offset = 0;
         const potentialOffset = [];
         for (let i = 0; i < lineCount; i++) {
             potentialOffset.push(i);
         }
-        intervals.forEach(intervals => {
+        intervals.forEach((intervals) => {
             intervals.slots.sort(sortTimeslots);
-            const usedOffset = intervals.slots.map(o => o.offset).filter(i => i > -1);
-            const availableOffset = potentialOffset.filter(i => !usedOffset.includes(i));
-            intervals.slots.forEach(slot => {
+            const usedOffset = intervals.slots
+                .map((o) => o.offset)
+                .filter((i) => i > -1);
+            const availableOffset = potentialOffset.filter((i) => !usedOffset.includes(i));
+            intervals.slots.forEach((slot) => {
                 if (slot.offset === -1) {
                     slot.offset = availableOffset.shift() || 0;
                 }
@@ -291,25 +305,40 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
     }
     updateLegend() {
         const legend = [];
-        const legendUnitList = ["y", "M", "d", "h", "m", 's'];
+        const legendUnitList = ["y", "M", "d", "h", "m", "s"];
         const legendMinUnitSpan = this.slotDuration * this.legendSpan;
+        const deltaT = this._end.diff(this._start, "m");
+        const nCol = Math.floor(deltaT / legendMinUnitSpan) + 1;
         for (const legendUnit of legendUnitList) {
-            let currentDate = dayjs_1.default(this._start);
+            const currentDate = dayjs_1.default(this._start);
+            const format = this.legendUnitFormat[legendUnit];
             let nextColumn = currentDate.add(legendMinUnitSpan, "m");
             const isLegendPossible = this._end.diff(this._start, legendUnit) > 0 &&
-                (nextColumn.format(this.legendUnitFormat[legendUnit]) !== currentDate.format(this.legendUnitFormat[legendUnit])
-                    || currentDate.add(1, legendUnit).diff(currentDate, "m") >= legendMinUnitSpan);
+                (nextColumn.format(format) !== currentDate.format(format) ||
+                    currentDate.add(1, legendUnit).diff(currentDate, "m") >=
+                        legendMinUnitSpan);
             if (isLegendPossible) {
+                let currentHeader = currentDate.format(format);
                 const row = [];
                 let i = 0;
-                while (currentDate.isBefore(this._end)) {
+                for (let j = 0; j < nCol; j++) {
                     i += this.legendSpan;
-                    if (nextColumn.diff(currentDate, legendUnit) > 0) {
-                        row.push({ colspan: i, title: '' + currentDate.format(this.legendUnitFormat[legendUnit]) });
+                    nextColumn = nextColumn.add(legendMinUnitSpan, "m");
+                    const nextHeader = nextColumn.format(format);
+                    if (currentHeader !== nextHeader) {
+                        row.push({
+                            colspan: i,
+                            title: currentHeader,
+                        });
                         i = 0;
-                        currentDate = nextColumn;
+                        currentHeader = nextHeader;
                     }
-                    nextColumn = nextColumn.add(legendMinUnitSpan, "m");
+                }
+                if (i > 0) {
+                    row.push({
+                        colspan: i,
+                        title: currentHeader,
+                    });
                 }
                 legend.push(row);
             }
@@ -330,7 +359,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             const headerContainer = root.querySelector(".jc-timeline-grid-title-container");
             let lastPosX = e.clientX;
             const scroll = function (e) {
-                const scrollLeft = (lastPosX - e.clientX);
+                const scrollLeft = lastPosX - e.clientX;
                 headerContainer.scrollLeft += scrollLeft;
                 gridContainer.scrollLeft += scrollLeft;
                 lastPosX = e.clientX;
@@ -352,8 +381,13 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             const localDir = direction;
             const initialDate = slot[direction];
             const resizeListener = (e) => {
-                const newDate = dayjs_1.default(initialDate).add(Math.round((e.clientX - startPos) / this.slotWidth) * this.slotDuration, "m").toDate();
-                if (direction === "start" ? (newDate < localSlot.end) : (localSlot.start < newDate)) {
+                const newDate = dayjs_1.default(initialDate)
+                    .add(Math.round((e.clientX - startPos) / this.slotWidth) *
+                    this.slotDuration, "m")
+                    .toDate();
+                if (direction === "start"
+                    ? newDate < localSlot.end
+                    : localSlot.start < newDate) {
                     if (localSlot[localDir] !== newDate) {
                         localSlot[localDir] = newDate;
                         this.updateEventById(slot.id);
@@ -367,7 +401,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 this.updateEventById(localSlot.id);
                 if (initialDate !== localSlot[localDir]) {
                     const cEvt = new CustomEvent("event-change", {
-                        detail: { items: [localSlot] }
+                        detail: { items: [localSlot] },
                     });
                     this.dispatchEvent(cEvt);
                 }
@@ -384,35 +418,50 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             const startPos = evt.clientX;
             let hasChanged = false;
             const localSlot = slot;
-            let localSlots = this.selectedList.filter(s => s instanceof Event_1.Event).map(s => s);
+            let localSlots = this.selectedList
+                .filter((s) => s instanceof Event_1.Event)
+                .map((s) => s);
             if (!localSlots.includes(localSlot)) {
                 localSlots = [localSlot];
             }
-            const startDates = localSlots.map(slot => slot.start);
-            const endDates = localSlots.map(slot => slot.end);
-            const updatePosition = editable ? (e) => {
-                const changeTime = Math.round((e.clientX - startPos) / this.slotWidth) * this.slotDuration;
-                return localSlots.map((slot, index) => {
-                    const prevStart = slot.start;
-                    slot.start = dayjs_1.default(startDates[index]).add(changeTime, "m").toDate();
-                    slot.end = dayjs_1.default(endDates[index]).add(changeTime, "m").toDate();
-                    return prevStart.getTime() !== slot.start.getTime();
-                }).reduce((prev, curr) => prev || curr);
-            } : (_e) => { return false; };
-            const updateRessource = ressourceEditable ? (e) => {
-                var _a, _b, _c;
-                const rowId = (_c = (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.elementsFromPoint(e.clientX, e.clientY).find((e) => e.tagName == "TD")) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.getAttribute('row-id');
-                if (rowId) {
-                    const ressourceId = this.rows[Number(rowId)].id;
-                    if (ressourceId !== localSlot.ressourceId) {
-                        const oldRessource = this.getRessourceFromId(localSlot.ressourceId);
-                        localSlot.ressourceId = ressourceId;
-                        this._updateEventPosition(oldRessource);
-                        return true;
+            const startDates = localSlots.map((slot) => slot.start);
+            const endDates = localSlots.map((slot) => slot.end);
+            const updatePosition = editable
+                ? (e) => {
+                    const changeTime = Math.round((e.clientX - startPos) / this.slotWidth) *
+                        this.slotDuration;
+                    return localSlots
+                        .map((slot, index) => {
+                        const prevStart = slot.start;
+                        slot.start = dayjs_1.default(startDates[index])
+                            .add(changeTime, "m")
+                            .toDate();
+                        slot.end = dayjs_1.default(endDates[index]).add(changeTime, "m").toDate();
+                        return prevStart.getTime() !== slot.start.getTime();
+                    })
+                        .reduce((prev, curr) => prev || curr);
+                }
+                : (_e) => {
+                    return false;
+                };
+            const updateRessource = ressourceEditable
+                ? (e) => {
+                    var _a, _b, _c;
+                    const rowId = (_c = (_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.elementsFromPoint(e.clientX, e.clientY).find((e) => e.tagName == "TD")) === null || _b === void 0 ? void 0 : _b.parentElement) === null || _c === void 0 ? void 0 : _c.getAttribute("row-id");
+                    if (rowId) {
+                        const ressourceId = this.rows[Number(rowId)].id;
+                        if (ressourceId !== localSlot.ressourceId) {
+                            const oldRessource = this.getRessourceFromId(localSlot.ressourceId);
+                            localSlot.ressourceId = ressourceId;
+                            this._updateEventPosition(oldRessource);
+                            return true;
+                        }
                     }
+                    return false;
                 }
-                return false;
-            } : (_e) => { return false; };
+                : (_e) => {
+                    return false;
+                };
             const moveListener = (e) => {
                 const a = updatePosition(e);
                 if (updateRessource(e) || a) {
@@ -427,7 +476,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 this.updateEventById(slot.id);
                 if (hasChanged) {
                     const cEvt = new CustomEvent("event-change", {
-                        detail: { items: localSlots }
+                        detail: { items: localSlots },
                     });
                     this.dispatchEvent(cEvt);
                 }
@@ -443,7 +492,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             selectable.selected = false;
             this.updateEventById(selectable.id);
         });
-        this.rows.forEach((selectable) => selectable.selected = false);
+        this.rows.forEach((selectable) => (selectable.selected = false));
         this.selectedList = [];
     }
     _getEventClickHandler(clickedItem) {
@@ -471,10 +520,10 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 this.selectedList.push(item);
                 this.updateEventById(item.id);
             }
-            const myEvent = new CustomEvent('item-selected', {
+            const myEvent = new CustomEvent("item-selected", {
                 detail: { items: this.selectedList },
                 bubbles: true,
-                composed: true
+                composed: true,
             });
             this.dispatchEvent(myEvent);
         };
@@ -484,7 +533,10 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         if (root !== null) {
             const gridContainer = root.querySelector(".jc-timeline-grid-container");
             const simplebar = new simplebar_1.default(gridContainer).getScrollElement();
-            syncroScroll_1.default([simplebar, root.querySelector(".jc-timeline-grid-title-container")], "h");
+            syncroScroll_1.default([
+                simplebar,
+                root.querySelector(".jc-timeline-grid-title-container"),
+            ], "h");
             syncroScroll_1.default([simplebar, root.querySelector(".jc-timeline-rows")], "v");
         }
         if (this.defaultBackground === "") {
@@ -513,7 +565,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             top: rowTop + evt.offset * this.rowHeight + "px",
             left: left + "px",
             right: right + "px",
-            backgroundColor: ""
+            backgroundColor: "",
         };
         const bgColor = evt.bgColor ? evt.bgColor : ressource.eventBgColor;
         if (bgColor) {
@@ -522,21 +574,41 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         if (!ressource.show) {
             style.height = "";
             style.top = rowTop - 6 + "px";
-            return lit_element_1.html `<div class="jc-timeslot empty" style="${style_map_1.styleMap(style)}"></div>`;
+            return lit_element_1.html `<div
+        class="jc-timeslot empty"
+        style="${style_map_1.styleMap(style)}"
+      ></div>`;
         }
-        let content = lit_element_1.html `<div class="jc-timeslot-title">${evt.title}</div>${evt.content ? unsafe_html_1.unsafeHTML(evt.content) : ""}`;
+        let content = lit_element_1.html `<div class="jc-timeslot-title">
+        ${evt.title}
+      </div>
+      ${evt.content ? unsafe_html_1.unsafeHTML(evt.content) : ""}`;
         const resizer = evt.editable === null ? ressource.eventEditable : evt.editable;
-        const editableRessource = evt.ressourceEditable === null ? ressource.eventRessourceEditable : evt.ressourceEditable;
+        const editableRessource = evt.ressourceEditable === null
+            ? ressource.eventRessourceEditable
+            : evt.ressourceEditable;
         if (resizer) {
-            content = lit_element_1.html `<div class="jc-timeslot-resizer-start" @mousedown="${this._getEventResizerHandler(evt, "start")}"></div>${content}
-                            <div class="jc-timeslot-resizer-end" @mousedown="${this._getEventResizerHandler(evt, "end")}"></div>`;
+            content = lit_element_1.html `<div
+          class="jc-timeslot-resizer-start"
+          @mousedown="${this._getEventResizerHandler(evt, "start")}"
+        ></div>
+        ${content}
+        <div
+          class="jc-timeslot-resizer-end"
+          @mousedown="${this._getEventResizerHandler(evt, "end")}"
+        ></div>`;
         }
-        return lit_element_1.html `<div class="jc-timeslot ${evt.moving ? "moving" : ""} ${evt.selected ? "selected" : ""}" 
-            start="${evt.start.getHours()}" 
-            end="${evt.end.getHours()}" 
-            style="${style_map_1.styleMap(style)}"
-            @mousedown="${this._getEventGrabHandler(evt, resizer, editableRessource, this._getEventClickHandler(evt))}"
-            >${content}</div>`;
+        return lit_element_1.html `<div
+      class="jc-timeslot ${evt.moving ? "moving" : ""} ${evt.selected
+            ? "selected"
+            : ""}"
+      start="${evt.start.getHours()}"
+      end="${evt.end.getHours()}"
+      style="${style_map_1.styleMap(style)}"
+      @mousedown="${this._getEventGrabHandler(evt, resizer, editableRessource, this._getEventClickHandler(evt))}"
+    >
+      ${content}
+    </div>`;
     }
     _getCollapseRessourceHandler(item) {
         return (_e) => {
@@ -569,7 +641,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
             event.target.classList.remove("target");
             const srcId = (_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData("text");
             const destinationId = (_b = event.target.parentElement) === null || _b === void 0 ? void 0 : _b.getAttribute("ressourceId");
-            if (srcId && destinationId && (destinationId !== srcId)) {
+            if (srcId && destinationId && destinationId !== srcId) {
                 const src = this.getRessourceFromId(srcId);
                 const destination = this.getRessourceFromId(destinationId);
                 if (destination.descendantOf(src)) {
@@ -581,11 +653,11 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 }
                 else {
                     movedContent.ressources[0].parent = destination.parent;
-                    let idx = this.rows.findIndex(v => v.id === destinationId);
+                    let idx = this.rows.findIndex((v) => v.id === destinationId);
                     if (event.target.classList.contains("jc-ressource-below")) {
                         idx += 1;
-                        while ((idx < this.rows.length)
-                            && this.rows[idx].descendantOf(destination)) {
+                        while (idx < this.rows.length &&
+                            this.rows[idx].descendantOf(destination)) {
                             idx += 1;
                         }
                     }
@@ -595,7 +667,7 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
                 this.addRessources(movedContent.ressources);
                 this.addEvents(movedContent.items);
                 this.dispatchEvent(new CustomEvent("reorder-ressource", {
-                    detail: { ressources: this.rows }
+                    detail: { ressources: this.rows },
                 }));
             }
         }
@@ -606,60 +678,121 @@ let Timeline = class Timeline extends lit_element_1.LitElement {
         const hasChild = item.children.length > 0;
         const collapseHandler = this._getCollapseRessourceHandler(item);
         return lit_element_1.html `<tr>
-                        <td class="${item.selected ? "jc-ressource-selected" : ""}" style="${style}" ressourceId="${item.id}" @click="${this._getEventClickHandler(item)}">
-                            <div class="jc-ressource-above"></div>    
-                            <div class="jc-ressource" draggable="true" @dragstart="${this._onRessourceDragStart(item)}">
-                            ${Array(depth).fill(0).map(_i => lit_element_1.html `<i class="jc-spacer"></i>`)}${hasChild ? lit_element_1.html `<i class="jc-spacer ${item.collapseChildren ? "extend" : "collapse"}" @click="${collapseHandler}"></i>` : lit_element_1.html `<i class="jc-spacer"></i>`}
-                            <span>${item.title}</span>
-                            </div>
-                            <div class="jc-ressource-below"></div>
-                        </td>
-                    </tr>`;
+      <td
+        class="${item.selected ? "jc-ressource-selected" : ""}"
+        style="${style}"
+        ressourceId="${item.id}"
+        @click="${this._getEventClickHandler(item)}"
+      >
+        <div class="jc-ressource-above"></div>
+        <div
+          class="jc-ressource"
+          draggable="true"
+          @dragstart="${this._onRessourceDragStart(item)}"
+        >
+          ${Array(depth)
+            .fill(0)
+            .map((_i) => lit_element_1.html `<i class="jc-spacer"></i>`)}${hasChild
+            ? lit_element_1.html `<i
+                class="jc-spacer ${item.collapseChildren
+                ? "extend"
+                : "collapse"}"
+                @click="${collapseHandler}"
+              ></i>`
+            : lit_element_1.html `<i class="jc-spacer"></i>`}
+          <span>${item.title}</span>
+        </div>
+        <div class="jc-ressource-below"></div>
+      </td>
+    </tr>`;
     }
     renderGridRow(columns, rowId = -1, height = 30) {
-        return lit_element_1.html `<tr row-id="${rowId}">${columns.map((d, i) => lit_element_1.html `<td style="height:${height}px;" class="jc-slot ${(i % this.legendSpan) === 0 ? "jc-major-slot" : ""}" start="${d.toISOString()}">&nbsp;</td>`)}</tr>`;
+        return lit_element_1.html `<tr row-id="${rowId}">
+      ${columns.map((d, i) => lit_element_1.html `<td
+            style="height:${height}px;"
+            class="jc-slot ${i % this.legendSpan === 0 ? "jc-major-slot" : ""}"
+            start="${d.toISOString()}"
+          >
+            &nbsp;
+          </td>`)}
+    </tr>`;
     }
     render() {
-        const nCol = Math.floor(this._end.diff(this._start, 'm') / this.slotDuration) + 1;
+        const nCol = Math.floor(this._end.diff(this._start, "m") / this.slotDuration) + 1;
         const columns = [];
         for (let i = 0; i < nCol; i++) {
-            columns.push(this._start.add(this.slotDuration * i, 'm'));
+            columns.push(this._start.add(this.slotDuration * i, "m"));
         }
-        const displayedRows = this.rows.map((r, i) => { return { i: i, r: r }; }).filter(o => o.r.show);
+        const displayedRows = this.rows
+            .map((r, i) => {
+            return { i: i, r: r };
+        })
+            .filter((o) => o.r.show);
         return lit_element_1.html `
-        <style>${this.customStyle}</style>
+      <style>
+        ${this.customStyle}
+      </style>
       <div class="jc-timeline-header">
-        <div class="jc-timeline-rows-title" style=${style_map_1.styleMap({ minWidth: this.ressourceWidth + "px", width: this.ressourceWidth + "px" })}>${this.rowsTitle}</div>
-        <horizontal-resizer @resize-x="${this._handleResizeX}"></horizontal-resizer>
+        <div
+          class="jc-timeline-rows-title"
+          style=${style_map_1.styleMap({
+            minWidth: this.ressourceWidth + "px",
+            width: this.ressourceWidth + "px",
+        })}
+        >
+          ${this.rowsTitle}
+        </div>
+        <horizontal-resizer
+          @resize-x="${this._handleResizeX}"
+        ></horizontal-resizer>
         <div class="jc-timeline-grid-title-container">
-          <table @mousedown="${this._grabHeader}"  style="width:${nCol * this.slotWidth}px;">
-              <colgroup>${columns.map(_o => lit_element_1.html `<col style="min-width:${this.slotWidth}px">`)}</colgroup>
-              <tbody>
-                  ${this.legend.map(arr => lit_element_1.html `<tr class="jc-timeline-grid-title">${arr.map(o => lit_element_1.html `<th colspan="${o.colspan}">${o.title}</th>`)}</tr>`)}
-              </tbody>
+          <table
+            @mousedown="${this._grabHeader}"
+            style="width:${nCol * this.slotWidth}px;"
+          >
+            <colgroup>
+              ${columns.map((_o) => lit_element_1.html `<col style="min-width:${this.slotWidth}px" />`)}
+            </colgroup>
+            <tbody>
+              ${this.legend.map((arr) => lit_element_1.html `<tr class="jc-timeline-grid-title">
+                    ${arr.map((o) => lit_element_1.html `<th colspan="${o.colspan}"><div>${o.title}</div></th>`)}
+                  </tr>`)}
+            </tbody>
           </table>
         </div>
       </div>
       <div class="jc-timeline-content">
-        <table class="jc-timeline-rows"
+        <table
+          class="jc-timeline-rows"
           style="${style_map_1.styleMap({ "--width": this.ressourceWidth + "px" })}"
           @dragover="${(e) => e.preventDefault()}"
           @dragenter="${this._onRessourceDragEnter}"
           @dragleave="${this._onRessourceDragLeave}"
-          @drop="${this._onRessourceDrop}">
-          ${this.rows.length > 0 ? displayedRows.map(o => this.renderRessource(o.r)) : lit_element_1.html `<tr class="empty"><td>No ressource</td></tr>`}
+          @drop="${this._onRessourceDrop}"
+        >
+          ${this.rows.length > 0
+            ? displayedRows.map((o) => this.renderRessource(o.r))
+            : lit_element_1.html `<tr class="empty">
+                <td>No ressource</td>
+              </tr>`}
         </table>
-        <horizontal-resizer @resize-x="${this._handleResizeX}"></horizontal-resizer>
+        <horizontal-resizer
+          @resize-x="${this._handleResizeX}"
+        ></horizontal-resizer>
         <div class="jc-timeline-grid-container">
           <table style="width:${nCol * this.slotWidth}px;">
-              <colgroup>${columns.map(_o => lit_element_1.html `<col style="min-width:${this.slotWidth}px">`)}</colgroup>
-              
-              <tbody>
-                  ${this.rows.length > 0 ? displayedRows.map(o => this.renderGridRow(columns, o.i, o.r.height)) : this.renderGridRow(columns)}
-              </tbody>
+            <colgroup>
+              ${columns.map((_o) => lit_element_1.html `<col style="min-width:${this.slotWidth}px" />`)}
+            </colgroup>
+
+            <tbody>
+              ${this.rows.length > 0
+            ? displayedRows.map((o) => this.renderGridRow(columns, o.i, o.r.height))
+            : this.renderGridRow(columns)}
+            </tbody>
           </table>
           <div class="jc-timeslots" style="width:${nCol * this.slotWidth}px;">
-            ${this.items.map(slot => this.renderTimeslot(slot))}
+            ${this.items.map((slot) => this.renderTimeslot(slot))}
           </div>
         </div>
       </div>
@@ -706,7 +839,7 @@ __decorate([
     lit_element_1.property({ type: String })
 ], Timeline.prototype, "defaultBackground", null);
 Timeline = __decorate([
-    lit_element_1.customElement('jc-timeline')
+    lit_element_1.customElement("jc-timeline")
 ], Timeline);
 exports.default = Timeline;
 //# sourceMappingURL=Timeline.js.map

File diff suppressed because it is too large
+ 0 - 0
lib/Timeline.js.map


+ 191 - 184
lib/styles/Timeline.style.js

@@ -3,96 +3,97 @@ Object.defineProperty(exports, "__esModule", { value: true });
 exports.TimelineStyle = void 0;
 const lit_element_1 = require("lit-element");
 exports.TimelineStyle = lit_element_1.css `
-* {
-    font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  * {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+      Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
     font-size: 16px;
     font-weight: 400;
-}
-div {
+  }
+  div {
     box-sizing: border-box;
-}
-.jc-timeline-content,
-.jc-timeline-header{
-    width:100%;
-    position:relative;
+  }
+  .jc-timeline-content,
+  .jc-timeline-header {
+    width: 100%;
+    position: relative;
     display: flex;
-    flex-direction:row;
-    height:max-content;
+    flex-direction: row;
+    height: max-content;
     align-items: stretch;
-}
+  }
 
-.jc-timeline-rows-title,
-.jc-timeline-rows > tr > td{        
+  .jc-timeline-rows-title,
+  .jc-timeline-rows > tr > td {
     padding: 8px;
-    min-width:40px;
+    min-width: 40px;
     box-sizing: border-box;
-}
-.jc-timeline-rows > tr > td {
-    max-width:calc( var(--width) - 8px );
-    padding: 0px; 
-    vertical-align:top;
-}
-.jc-timeline-rows > tr.empty > td{
+  }
+  .jc-timeline-rows > tr > td {
+    max-width: calc(var(--width) - 8px);
+    padding: 0px;
+    vertical-align: top;
+  }
+  .jc-timeline-rows > tr.empty > td {
     padding: 6px 0px 4px 8px;
-}
-i.jc-spacer {
-    display:inline-block;
-    width : 1rem;
+  }
+  i.jc-spacer {
+    display: inline-block;
+    width: 1rem;
     height: 1rem;
-    position:relative;
+    position: relative;
     box-sizing: border-box;
-}
-i.jc-spacer:after{
+  }
+  i.jc-spacer:after {
     content: " ";
-    position:absolute;
+    position: absolute;
     background-repeat: no-repeat;
     background-size: 1.05rem;
-    width:  1.05rem;
+    width: 1.05rem;
     height: 1.05rem;
-}
-.jc-spacer.extend,
-.jc-spacer.collapse    {
-    cursor:pointer;
-}
-i.jc-spacer.extend:after{
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iYmxhY2siIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xOSAzSDVjLTEuMTEgMC0yIC45LTIgMnYxNGMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHptLTgtMmgydi00aDR2LTJoLTRWN2gtMnY0SDd2Mmg0eiIvPjwvc3ZnPg==")
-}
-i.jc-spacer.collapse:after{
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHpNNyAxMWgxMHYySDd6Ii8+PC9zdmc+")
-}
+  }
+  .jc-spacer.extend,
+  .jc-spacer.collapse {
+    cursor: pointer;
+  }
+  i.jc-spacer.extend:after {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iYmxhY2siIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xOSAzSDVjLTEuMTEgMC0yIC45LTIgMnYxNGMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHptLTgtMmgydi00aDR2LTJoLTRWN2gtMnY0SDd2Mmg0eiIvPjwvc3ZnPg==");
+  }
+  i.jc-spacer.collapse:after {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHpNNyAxMWgxMHYySDd6Ii8+PC9zdmc+");
+  }
 
-.jc-timeline-rows > tr{
+  .jc-timeline-rows > tr {
     box-sizing: border-box;
     white-space: nowrap;
     border: 1px solid grey;
     border-style: solid none;
-}
-.jc-timeline-rows,
-.jc-timeline-rows-title{
+  }
+  .jc-timeline-rows,
+  .jc-timeline-rows-title {
     height: inherit;
-    width:var(--width, 200px);
+    width: var(--width, 200px);
     overflow: hidden;
-    border-collapse:collapse;
-}
-.jc-timeline-grid-title-container,
-.jc-timeline-grid-container{
-    position:relative;
+    border-collapse: collapse;
+  }
+  .jc-timeline-grid-title-container,
+  .jc-timeline-grid-container {
+    position: relative;
     width: 600px;
     display: block;
     overflow: hidden;
-}
-.jc-timeline-grid-container{
+  }
+  .jc-timeline-grid-container {
     overflow-x: auto;
-}
-.jc-timeline-grid-title-container table,
-.jc-timeline-grid-container table {
-    width:100%;
+  }
+  .jc-timeline-grid-title-container table,
+  .jc-timeline-grid-container table {
+    width: 100%;
     table-layout: fixed;
     border-collapse: collapse;
     box-sizing: border-box;
-}
+  }
 
-.jc-timeline-grid-title-container {
+  .jc-timeline-grid-title-container {
     white-space: nowrap;
     cursor: grab;
     user-select: none; /* supported by Chrome and Opera */
@@ -100,149 +101,155 @@ i.jc-spacer.collapse:after{
     -khtml-user-select: none; /* Konqueror HTML */
     -moz-user-select: none; /* Firefox */
     -ms-user-select: none; /* Internet Explorer/Edge */
-}
-.jc-timeline-grid-title:first-child > th{
-    border-top:0;
-    border-left-color:#ffff;
-    border-right-color:#ffff;
-}    
-.jc-timeline-grid-title:first-child > th:before,
-.jc-timeline-grid-title:first-child > th:last-child:after {
-    content:" ";
+  }
+  .jc-timeline-grid-title:first-child > th {
+    border-top: 0;
+    border-left-color: #ffff;
+    border-right-color: #ffff;
+  }
+  .jc-timeline-grid-title:not(:last-child) > th > div {
+    position: sticky;
+    width: min-content;
+    padding: 0 4px;
+    position: -webkit-sticky;
+    left: 0;
+  }
+  .jc-timeline-grid-title:first-child > th:before,
+  .jc-timeline-grid-title:first-child > th:last-child:after {
+    content: " ";
     display: block;
-    position:absolute;
-    left:-1px;
-    bottom:0px;
+    position: absolute;
+    left: -1px;
+    bottom: 0px;
     height: 8px;
     border-left: 1px solid lightgrey;
-    z-index:2;
-}
-.jc-timeline-grid-title:first-child > th:last-child:after
-{
-    left:auto;
-    right:-1px;
-}
-.jc-timeline-grid-title:first-child:last-child >th{
-    padding:8px 0;
-}
-.jc-timeline-grid-title:last-child > th{
-    border-bottom:none;
-}
-.jc-timeline-grid-title > th,
-.jc-slot {      
-    height:100%;
+    z-index: 2;
+  }
+  .jc-timeline-grid-title:first-child > th:last-child:after {
+    left: auto;
+    right: -1px;
+  }
+  .jc-timeline-grid-title:first-child:last-child > th {
+    padding: 8px 0;
+  }
+  .jc-timeline-grid-title:last-child > th {
+    border-bottom: none;
+  }
+  .jc-timeline-grid-title > th,
+  .jc-slot {
+    height: 100%;
     border: solid 1px lightgrey;
     border-left-style: dotted;
-    border-right:0;
+    border-right: 0;
     text-align: center;
-    position:relative;
+    position: relative;
     box-sizing: border-box;
-}
-.jc-timeline-grid-title > th:last-child,
-.jc-slot:last-child{
-    border-right:solid 1px lightgray;
-}
-.jc-timeline-grid-title > th,
-.jc-major-slot{
+  }
+  .jc-timeline-grid-title > th:last-child,
+  .jc-slot:last-child {
+    border-right: solid 1px lightgray;
+  }
+  .jc-timeline-grid-title > th,
+  .jc-major-slot {
     border-left-style: solid;
-}
-.jc-timeslots{
-    position:absolute;
-    top:0px;
-    left:0px;
-    bottom:0px;
+  }
+  .jc-timeslots {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    bottom: 0px;
     overflow: hidden;
-}
-.jc-timeslot{
-    position:absolute;
-    background-color:var(--default-background);
-    color:#fff;
-    border-radius:3px;
-    margin:2px 0px;
-    z-index:auto;
+  }
+  .jc-timeslot {
+    position: absolute;
+    background-color: var(--default-background);
+    color: #fff;
+    border-radius: 3px;
+    margin: 2px 0px;
+    z-index: auto;
     cursor: grab;
-}
-.jc-timeslot-title{
-    font-size:14px;
-    font-weight:600;
-    padding:4px;
+  }
+  .jc-timeslot-title {
+    font-size: 14px;
+    font-weight: 600;
+    padding: 4px;
     white-space: nowrap;
     overflow-x: hidden;
-}
-.jc-timeslot.empty{
-    height:5px;
-    padding:2px 2px;
-    margin:0px;
-    cursor:pointer;
-}
-.jc-timeslot.moving{
-    opacity:0.7;
-    cursor:grabbing;
-}
-.jc-timeslot.selected:before{
-    border:solid 2px black;
-    border-radius:3px;
-    position:absolute;
-    top:0;
-    bottom:0;
-    left:0;
-    right:0;
-    content:" ";
-}
-.jc-timeslot-resizer-start,
-.jc-timeslot-resizer-end{
-    position:absolute;
-    top:0;
-    bottom:0;
-    width:4px;
-    min-width:4px;
-    display:block;
+  }
+  .jc-timeslot.empty {
+    height: 5px;
+    padding: 2px 2px;
+    margin: 0px;
+    cursor: pointer;
+  }
+  .jc-timeslot.moving {
+    opacity: 0.7;
+    cursor: grabbing;
+  }
+  .jc-timeslot.selected:before {
+    border: solid 2px black;
+    border-radius: 3px;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    content: " ";
+  }
+  .jc-timeslot-resizer-start,
+  .jc-timeslot-resizer-end {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 4px;
+    min-width: 4px;
+    display: block;
     cursor: ew-resize;
-}
-.jc-timeslot-resizer-start,
-.jc-timeslot-resizer-end{
-    display:block;
-}
-.jc-timeslot-resizer-start{
-    left:0px;
-}
-.jc-timeslot-resizer-end{
-    right:0px;
-}
-.jc-timeline-rows > tr >td{
-    height:100%;
-}
-.jc-ressource{
+  }
+  .jc-timeslot-resizer-start,
+  .jc-timeslot-resizer-end {
+    display: block;
+  }
+  .jc-timeslot-resizer-start {
+    left: 0px;
+  }
+  .jc-timeslot-resizer-end {
+    right: 0px;
+  }
+  .jc-timeline-rows > tr > td {
+    height: 100%;
+  }
+  .jc-ressource {
     padding-top: 2px;
-    height: calc( 100% - 8px);
-}
-.jc-ressource > span {
+    height: calc(100% - 8px);
+  }
+  .jc-ressource > span {
     pointer-events: none;
-}
-.jc-ressource.target{
+  }
+  .jc-ressource.target {
     background-color: lightgrey;
-}
-.jc-ressource-selected{
-    border:1px solid var(--default-background, SteelBlue);
-    border-right:0;
-    border-left:0;
-    background-color:#4682b46b;
-}
-.jc-ressource-above{
-    height:4px;
-}
-.jc-ressource-above.target{
-    margin-left: calc( var(--depth) * 16px );
+  }
+  .jc-ressource-selected {
+    border: 1px solid var(--default-background, SteelBlue);
+    border-right: 0;
+    border-left: 0;
+    background-color: #4682b46b;
+  }
+  .jc-ressource-above {
+    height: 4px;
+  }
+  .jc-ressource-above.target {
+    margin-left: calc(var(--depth) * 16px);
     background-color: var(--default-background, SteelBlue);
     border-radius: 0 0 0 4px;
-}
-.jc-ressource-below{
-    height:4px;
-}
-.jc-ressource-below.target{
-    margin-left: calc( var(--depth) * 16px);
+  }
+  .jc-ressource-below {
+    height: 4px;
+  }
+  .jc-ressource-below.target {
+    margin-left: calc(var(--depth) * 16px);
     background-color: var(--default-background, SteelBlue);
     border-radius: 4px 0 0 0;
-}
+  }
 `;
 //# sourceMappingURL=Timeline.style.js.map

+ 1 - 1
lib/styles/Timeline.style.js.map

@@ -1 +1 @@
-{"version":3,"file":"Timeline.style.js","sourceRoot":"","sources":["../../src/styles/Timeline.style.ts"],"names":[],"mappings":";;;AAAA,6CAA6C;AAEhC,QAAA,aAAa,GAAa,iBAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkPzC,CAAC"}
+{"version":3,"file":"Timeline.style.js","sourceRoot":"","sources":["../../src/styles/Timeline.style.ts"],"names":[],"mappings":";;;AAAA,6CAA6C;AAEhC,QAAA,aAAa,GAAc,iBAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyP1C,CAAC"}

+ 952 - 730
src/Timeline.ts

@@ -1,777 +1,999 @@
-import dayjs, { Dayjs } from 'dayjs'
-import { LitElement, html, customElement, property, TemplateResult, internalProperty, CSSResult } from 'lit-element';
-import { styleMap } from 'lit-html/directives/style-map';
-import { unsafeHTML } from 'lit-html/directives/unsafe-html';
-import SimpleBar from 'simplebar';
-import {SimpleBarStyle} from './styles/SimplbeBar.styles';
-
-import { Event, IEvent } from './Event'
-import { Ressource, IRessource } from './Ressource'
-
-export { HorizontalResizer } from './components/horizontal-resizer'
-import syncronizeElementsScrolling from './utils/syncroScroll';
-import Selectable from './utils/selectable';
-import { TimelineStyle } from './styles/Timeline.style';
-
+import dayjs, { Dayjs } from "dayjs";
+import {
+  LitElement,
+  html,
+  customElement,
+  property,
+  TemplateResult,
+  internalProperty,
+  CSSResult,
+} from "lit-element";
+import { styleMap } from "lit-html/directives/style-map";
+import { unsafeHTML } from "lit-html/directives/unsafe-html";
+import SimpleBar from "simplebar";
+import { SimpleBarStyle } from "./styles/SimplbeBar.styles";
+
+import { Event, IEvent } from "./Event";
+import { Ressource, IRessource } from "./Ressource";
+
+export { HorizontalResizer } from "./components/horizontal-resizer";
+import syncronizeElementsScrolling from "./utils/syncroScroll";
+import Selectable from "./utils/selectable";
+import { TimelineStyle } from "./styles/Timeline.style";
 
 export interface TimelineOptions {
-    ressources?: Array<IRessource>
-    items?: Array<IEvent>
+  ressources?: Array<IRessource>;
+  items?: Array<IEvent>;
 }
 
 interface TimelineContent {
-    ressources: Array<Ressource>
-    items: Array<Event>
-    index:number
+  ressources: Array<Ressource>;
+  items: Array<Event>;
+  index: number;
 }
 interface TimeInterval {
-    start: number,
-    end: number,
-    slots: Array<Event>
+  start: number;
+  end: number;
+  slots: Array<Event>;
 }
-type dayjsUnit = "y"|"M"|"d"|"h"|"m"|'s'
+type dayjsUnit = "y" | "M" | "d" | "h" | "m" | "s";
 export type UnitLegend = {
-    [k in dayjsUnit]: string;
+  [k in dayjsUnit]: string;
 };
 interface legendItem {
-    colspan:number
-    title:string
+  colspan: number;
+  title: string;
 }
 // TODO define std zoom level
-// TODO add selectable Slot 
+// TODO add selectable Slot
 // TODO enable to rearrange between different component.
 /**
  * This is a component that can display event in a Timeline.
- * 
+ *
  */
-@customElement('jc-timeline')
+@customElement("jc-timeline")
 class Timeline extends LitElement {
-    
-    static get styles():CSSResult[] {
-        return [TimelineStyle,SimpleBarStyle] 
-    }
-    customStyle:string  
-    @internalProperty() // important for the refresh
-    private rows: Array<Ressource>
-    @internalProperty() // important for the refresh
-    private items: Array<Event>
-    private selectedList: Array<Selectable>
-
-    @property({ type: Number })
-    public ressourceWidth: number
-
-    @property({ type: Object })
-    private _start: Dayjs
-    @property({ type: String })
-    public get start(): string {
-        return this._start.toISOString();
-    }
-    public set start(value: string){
-        const d = dayjs(value);
-        if( d.isValid()){
-            this._start = d < this._end ? d : this._end;
-            this.updateLegend();
-        }else{
-            console.log("Invalid Date :" ,value)
-        }
-    }
-    
-    private _end: Dayjs
-    @property({ type: String })
-    public get end(): string {
-        return this._end.toISOString();
-    }
-    public set end(value: string){
-        const d = dayjs(value);
-        if( d.isValid()){
-            this._end = this._start < d ? d:this._start;
-            this.updateLegend();
-        }
-    }
-
-    private _slotDuration = 30; // in minute
-    @property({ type: Number })
-    public get slotDuration(): number {
-        return this._slotDuration;
-    }
-    public set slotDuration(value: number) {
-        this._slotDuration = value;
-        this.updateLegend();
-    }
-    
-    private _legendSpan = 2; // in slot count
-    @property({ type: Number })
-    public get legendSpan(): number {
-        return this._legendSpan;
-    }
-    public set legendSpan(value: number) {
-        this._legendSpan = value;
-        this.updateLegend();
-    }
-
-    @property({ type: Number })
-    private rowHeight = 32 // in px
-
-    @property({ type: Number })
-    public slotWidth = 20 // in px
-
-    @property({ type: String })
-    private rowsTitle: string
-
-    private legendUnitFormat:UnitLegend = {"y":"YYYY","M":"MMMM","d":'D',"h":"H[h]","m":"m'",'s':"s[s]"}
-
-    @property({ type: Array })
-    private legend: Array<Array<legendItem>>;
-    
-    constructor(options: TimelineOptions = {}) {
-        super()
-        this.rows = [];
-        this.items = [];
-        this._start = dayjs().startOf("day");
-        this._end = this._start.endOf("day");
-        this.rowsTitle = "Ressources"
-        this.ressourceWidth = 200
-        this.selectedList = [];
-        this.legend = [];
-        this.defaultBackground = "";
-        this.customStyle = "";  
-    
-        if (options.ressources){
-            this.addRessources(options.ressources)
-        }
-        if (options.items){
-            this.addEvents(options.items)
-        }
-        this.updateLegend();
-        this.render();
-    }
-
-    @property({type:String})
-    set defaultBackground(value: string) {
-        this.style.setProperty("--default-background", value);
-    }
-    get defaultBackground(): string {
-        return this.style.getPropertyValue("--default-background");
-    }
-    setLegendUnitFormatAll(legend:Partial<UnitLegend>):void{
-        this.legendUnitFormat  = {...this.legendUnitFormat, ...legend}
-        this.updateLegend()
-    }
-    setLegendUnitFormat(unit:dayjsUnit, format:string):void{
-        this.legendUnitFormat[unit] = format;
-        this.updateLegend()
-    }
-    addRessources(list:Array<IRessource>):Array<Ressource>{
-        return list.map(r=>this.addRessource(r));
-    }
-    // Ressource management
-    /**
-     * Add the ressource the the timeline or return the existing ressource with the same id
-     * @param ressource 
-     * @returns The Ressource object registered to in the timeline 
-     */
-    addRessource(ressource: IRessource): Ressource {
-        const existingRessource = this.getRessourceFromId(ressource.id)
-        if (existingRessource) {
-            return existingRessource
+  static get styles(): CSSResult[] {
+    return [TimelineStyle, SimpleBarStyle];
+  }
+  customStyle: string;
+  @internalProperty() // important for the refresh
+  private rows: Array<Ressource>;
+  @internalProperty() // important for the refresh
+  private items: Array<Event>;
+  private selectedList: Array<Selectable>;
+
+  @property({ type: Number })
+  public ressourceWidth: number;
+
+  @property({ type: Object })
+  private _start: Dayjs;
+  @property({ type: String })
+  public get start(): string {
+    return this._start.toISOString();
+  }
+  public set start(value: string) {
+    const d = dayjs(value);
+    if (d.isValid()) {
+      this._start = d < this._end ? d : this._end;
+      this.updateLegend();
+    } else {
+      console.log("Invalid Date :", value);
+    }
+  }
+
+  private _end: Dayjs;
+  @property({ type: String })
+  public get end(): string {
+    return this._end.toISOString();
+  }
+  public set end(value: string) {
+    const d = dayjs(value);
+    if (d.isValid()) {
+      this._end = this._start < d ? d : this._start;
+      this.updateLegend();
+    }
+  }
+
+  private _slotDuration = 30; // in minute
+  @property({ type: Number })
+  public get slotDuration(): number {
+    return this._slotDuration;
+  }
+  public set slotDuration(value: number) {
+    this._slotDuration = value;
+    this.updateLegend();
+  }
+
+  private _legendSpan = 2; // in slot count
+  @property({ type: Number })
+  public get legendSpan(): number {
+    return this._legendSpan;
+  }
+  public set legendSpan(value: number) {
+    this._legendSpan = value;
+    this.updateLegend();
+  }
+
+  @property({ type: Number })
+  private rowHeight = 32; // in px
+
+  @property({ type: Number })
+  public slotWidth = 20; // in px
+
+  @property({ type: String })
+  private rowsTitle: string;
+
+  private legendUnitFormat: UnitLegend = {
+    y: "YYYY",
+    M: "MMMM",
+    d: "D",
+    h: "H[h]",
+    m: "m'",
+    s: "s[s]",
+  };
+
+  @property({ type: Array })
+  private legend: Array<Array<legendItem>>;
+
+  constructor(options: TimelineOptions = {}) {
+    super();
+    this.rows = [];
+    this.items = [];
+    this._start = dayjs().startOf("day");
+    this._end = this._start.endOf("day");
+    this.rowsTitle = "Ressources";
+    this.ressourceWidth = 200;
+    this.selectedList = [];
+    this.legend = [];
+    this.defaultBackground = "";
+    this.customStyle = "";
+
+    if (options.ressources) {
+      this.addRessources(options.ressources);
+    }
+    if (options.items) {
+      this.addEvents(options.items);
+    }
+    this.updateLegend();
+    this.render();
+  }
+
+  @property({ type: String })
+  set defaultBackground(value: string) {
+    this.style.setProperty("--default-background", value);
+  }
+  get defaultBackground(): string {
+    return this.style.getPropertyValue("--default-background");
+  }
+  setLegendUnitFormatAll(legend: Partial<UnitLegend>): void {
+    this.legendUnitFormat = { ...this.legendUnitFormat, ...legend };
+    this.updateLegend();
+  }
+  setLegendUnitFormat(unit: dayjsUnit, format: string): void {
+    this.legendUnitFormat[unit] = format;
+    this.updateLegend();
+  }
+  addRessources(list: Array<IRessource>): Array<Ressource> {
+    return list.map((r) => this.addRessource(r));
+  }
+  // Ressource management
+  /**
+   * Add the ressource the the timeline or return the existing ressource with the same id
+   * @param ressource
+   * @returns The Ressource object registered to in the timeline
+   */
+  addRessource(ressource: IRessource): Ressource {
+    const existingRessource = this.getRessourceFromId(ressource.id);
+    if (existingRessource) {
+      return existingRessource;
+    }
+    const r = Ressource.toRessource(ressource);
+
+    if (r.parent !== undefined) {
+      r.parent =
+        this.getRessourceFromId(r.parent.id) ?? this.addRessource(r.parent);
+      const idx = this.rows.indexOf(r.parent as Ressource);
+      if (idx > -1) {
+        this.rows[idx].children.push(r);
+        this.rows.splice(idx + 1, 0, r);
+      } else {
+        throw new Error("Not able to create ressource parent.\n" + r.id);
+      }
+    } else {
+      this.rows = [...this.rows, r];
+    }
+
+    this.addRessources(r.children);
+    this._updateEventPosition(r);
+    return r;
+  }
+  removeRessourceById(id: string): TimelineContent {
+    return this._removeRessourceById(id);
+  }
+  _removeRessourceById(id: string, depth = 0): TimelineContent {
+    const output: TimelineContent = { ressources: [], items: [], index: 0 };
+
+    for (let i = 0; i < this.rows.length; i) {
+      const ressource = this.rows[i];
+      if (ressource.id === id) {
+        output.index = i;
+        output.ressources.push(ressource);
+        // remove the top level children from it's parent.
+        if (ressource.parent && depth === 0) {
+          ressource.parent.children = ressource.parent.children.filter(
+            (o) => o.id !== id
+          );
+        }
+        this.rows.splice(i, 1);
+      } else if (ressource.parentId === id) {
+        const partialOutput = this._removeRessourceById(
+          ressource.id,
+          depth + 1
+        );
+        output.ressources.push(...partialOutput.ressources);
+        output.items.push(...partialOutput.items);
+      } else {
+        i++;
+      }
+    }
+    // Recover deleted items
+    output.items.push(...this.items.filter((i) => i.ressourceId === id));
+    // Update items list
+    this.items = this.items.filter((i) => i.ressourceId !== id);
+    return output;
+  }
+
+  getRessources(): Array<Ressource> {
+    return this.rows;
+  }
+  getRessourceFromId(id: string): Ressource | null {
+    const tmp = this.rows.filter((r) => r.id === id);
+    return tmp.length > 0 ? tmp[0] : null;
+  }
+  updateRessource<K extends keyof Ressource>(
+    id: string,
+    key: K,
+    value: Ressource[K]
+  ): Ressource | null {
+    const ressource = this.getRessourceFromId(id);
+    if (ressource) {
+      if (key == "parent") {
+        const content = this.removeRessourceById(id);
+        ressource[key] = value;
+        this.addRessource(ressource);
+        this.addRessources(content.ressources);
+        this.addEvents(content.items);
+      } else {
+        ressource[key] = value;
+        this.rows = this.rows.map((r) =>
+          r.id == ressource.id ? ressource : r
+        );
+      }
+      this._updateEventPosition(ressource);
+      return ressource;
+    }
+    return null;
+  }
+  setRowsTitle(title: string): void {
+    this.rowsTitle = title;
+  }
+  getEventById(id: string): Event | undefined {
+    return this.items.find((o) => o.id == id);
+  }
+  addEvents(list: Array<IEvent>): Array<Event | undefined> {
+    return list.map((e) => this.addEvent(e));
+  }
+  // TimeSlot management
+  /**
+   * Add the event the the timeline or return the existing event with the same id
+   * return undefined if the event ressource is not defined in the timeline
+   * @param event
+   * @returns
+   */
+  addEvent(event: IEvent): Event | undefined {
+    const existingEvent = this.getEventById(event.id);
+    if (existingEvent) {
+      return existingEvent;
+    }
+    const ressource = this.rows.find((r) => r.id === event.ressourceId);
+    if (ressource === undefined) {
+      return undefined;
+    }
+    const timeslot = Event.toTimeSlot(event);
+    this.items = [...this.items, timeslot];
+    // Update timeslot status
+    timeslot.isDisplayed =
+      timeslot.end > this._start.toDate() ||
+      timeslot.start < this._end.toDate();
+    this._updateEventPosition(ressource);
+    return timeslot;
+  }
+
+  removeEventById(id: string): Array<Event> {
+    const output = this.items.filter((o) => o.id === id);
+    this.items = this.items.filter((o) => o.id !== id);
+    return output;
+  }
+  updateEventById(id: string): Event | null {
+    const output = this.removeEventById(id);
+    if (output.length > 0) {
+      this.addEvent(output[0]);
+      return output[0];
+    } else {
+      return null;
+    }
+  }
+  private _updateEventPosition(ressource: Ressource): void {
+    const timeslots = this.items.filter((i) => i.ressourceId === ressource.id);
+    if (timeslots.length === 0) {
+      ressource.height = this.rowHeight + (ressource.collapseChildren ? 5 : 0);
+      return;
+    }
+    const start = this._start.toDate().getTime();
+    const end = this._end.toDate().getTime();
+    // List potential interval
+    const points = [start, end];
+    const populateInterval = (d: Date) => {
+      const t = d.getTime();
+      if (start < t && t < end && !points.includes(t)) {
+        points.push(t);
+      }
+    };
+    timeslots.forEach((element) => {
+      populateInterval(element.start);
+      populateInterval(element.end);
+    });
+    points.sort();
+    // Count maximum number of interval intersection
+    const intervals: Array<TimeInterval> = [];
+    for (let i = 0; i < points.length - 1; i++) {
+      const startTime = points[i];
+      const endTime = points[i + 1];
+      intervals.push({
+        start: points[i],
+        end: points[i + 1],
+        slots: timeslots.filter(
+          (slot) =>
+            slot.start.getTime() <= startTime && endTime <= slot.end.getTime()
+        ),
+      });
+    }
+    // Update rows height
+    const lineCount = intervals.reduce(
+      (acc, interval) => Math.max(acc, interval.slots.length),
+      0
+    );
+    ressource.height =
+      this.rowHeight * Math.max(lineCount, 1) +
+      (ressource.collapseChildren ? 5 : 0); // to avoid collapse rows
+    // Solve the offset positioning of all items
+    const sortTimeslots = (a: Event, b: Event): number => {
+      const t = a.start.getTime() - b.start.getTime();
+      if (t === 0) {
+        const tend = b.end.getTime() - a.end.getTime();
+        return tend === 0 ? ("" + a.id).localeCompare(b.id) : tend;
+      }
+      return t;
+    };
+    // Remove all items offset
+    timeslots.forEach((slot) => (slot.offset = -1));
+    timeslots.sort(sortTimeslots);
+    timeslots[0].offset = 0;
+    const potentialOffset: Array<number> = [];
+    for (let i = 0; i < lineCount; i++) {
+      potentialOffset.push(i);
+    }
+    intervals.forEach((intervals) => {
+      intervals.slots.sort(sortTimeslots);
+      const usedOffset = intervals.slots
+        .map((o) => o.offset)
+        .filter((i) => i > -1);
+      const availableOffset = potentialOffset.filter(
+        (i) => !usedOffset.includes(i)
+      );
+      intervals.slots.forEach((slot) => {
+        if (slot.offset === -1) {
+          slot.offset = availableOffset.shift() || 0;
+        }
+      });
+    });
+  }
+  getEvents(): Array<Event> {
+    return this.items;
+  }
+
+  updateLegend(): void {
+    const legend: Array<Array<legendItem>> = [];
+    const legendUnitList: Array<dayjsUnit> = ["y", "M", "d", "h", "m", "s"];
+    const legendMinUnitSpan = this.slotDuration * this.legendSpan;
+    const deltaT = this._end.diff(this._start, "m");
+    const nCol = Math.floor(deltaT / legendMinUnitSpan) + 1;
+
+    for (const legendUnit of legendUnitList) {
+      const currentDate = dayjs(this._start);
+      const format = this.legendUnitFormat[legendUnit];
+      let nextColumn = currentDate.add(legendMinUnitSpan, "m");
+      // Check is the starting and end date can accomodate fews cell of the given unit for this unit of time
+      const isLegendPossible =
+        // Enough time to fit 1 cell AND
+        this._end.diff(this._start, legendUnit) > 0 &&
+        // Starting & Ending date have different legend
+        (nextColumn.format(format) !== currentDate.format(format) ||
+          // OR 2 consecutive legends can accomodate at least 1 grid cell
+          currentDate.add(1, legendUnit).diff(currentDate, "m") >=
+            legendMinUnitSpan);
+
+      if (isLegendPossible) {
+        let currentHeader = currentDate.format(format);
+        const row: Array<legendItem> = [];
+        let i = 0;
+        for (let j = 0; j < nCol; j++) {
+          i += this.legendSpan;
+          nextColumn = nextColumn.add(legendMinUnitSpan, "m");
+          const nextHeader = nextColumn.format(format);
+          if (currentHeader !== nextHeader) {
+            row.push({
+              colspan: i,
+              title: currentHeader,
+            });
+            i = 0;
+            currentHeader = nextHeader;
+          }
         }
-        const r = Ressource.toRessource(ressource)
-        
-        if (r.parent !== undefined) {
-            r.parent = this.getRessourceFromId(r.parent.id) ?? this.addRessource(r.parent);
-            const idx = this.rows.indexOf(r.parent as Ressource)
-            if (idx > -1) {
-                this.rows[idx].children.push(r);
-                this.rows.splice(idx + 1, 0, r);
-            } else {
-                throw new Error("Not able to create ressource parent.\n" + r.id)
-            }
-        } else {
-            this.rows = [...this.rows, r]
+        // IF the last title cover some column add it to the legend
+        if (i > 0) {
+          row.push({
+            colspan: i,
+            title: currentHeader,
+          });
+        }
+        legend.push(row);
+      }
+    }
+    this.legend = legend;
+  }
+  private _handleResizeX(e: CustomEvent<number>): void {
+    e.stopPropagation();
+    this.ressourceWidth += e.detail;
+    if (this.ressourceWidth < 0) {
+      this.ressourceWidth = 0;
+    }
+  }
+  private _grabHeader(e: MouseEvent): void {
+    const root = this.shadowRoot;
+    if (root !== null) {
+      const gridContainer = root.querySelector(
+        ".jc-timeline-grid-container"
+      ) as HTMLBaseElement;
+      const headerContainer = root.querySelector(
+        ".jc-timeline-grid-title-container"
+      ) as HTMLBaseElement;
+      let lastPosX = e.clientX;
+      const scroll = function (e: MouseEvent) {
+        const scrollLeft = lastPosX - e.clientX;
+        headerContainer.scrollLeft += scrollLeft;
+        gridContainer.scrollLeft += scrollLeft;
+        lastPosX = e.clientX;
+      };
+      const mouseUpListener = function (_e: MouseEvent) {
+        window.removeEventListener("mousemove", scroll);
+        window.removeEventListener("mouseup", mouseUpListener);
+      };
+      window.addEventListener("mousemove", scroll);
+      window.addEventListener("mouseup", mouseUpListener);
+    }
+  }
+  private _getEventResizerHandler(slot: Event, direction: "end" | "start") {
+    return (evt: MouseEvent): void => {
+      evt.stopPropagation();
+      evt.preventDefault();
+      const startPos = evt.clientX;
+      const localSlot = slot;
+      const localDir = direction;
+      const initialDate = slot[direction];
+      const resizeListener = (e: MouseEvent) => {
+        const newDate = dayjs(initialDate)
+          .add(
+            Math.round((e.clientX - startPos) / this.slotWidth) *
+              this.slotDuration,
+            "m"
+          )
+          .toDate();
+        if (
+          direction === "start"
+            ? newDate < localSlot.end
+            : localSlot.start < newDate
+        ) {
+          if (localSlot[localDir] !== newDate) {
+            localSlot[localDir] = newDate;
+            this.updateEventById(slot.id);
+          }
         }
-    
-        this.addRessources(r.children);
-        this._updateEventPosition(r)
-        return r;
-    }
-    removeRessourceById(id: string): TimelineContent {
-        return this._removeRessourceById(id);
-    }
-    _removeRessourceById(id: string, depth = 0):TimelineContent{
-        const output: TimelineContent = { ressources: [], items: [], index:0 };
-
-        for (let i = 0; i < this.rows.length; i) {
-            const ressource = this.rows[i];
-            if (ressource.id === id) {
-                output.index = i;
-                output.ressources.push(ressource);
-                // remove the top level children from it's parent.
-                if (ressource.parent && depth === 0 ){
-                    ressource.parent.children = ressource.parent.children.filter(o=> o.id !== id)
-                }
-                this.rows.splice(i, 1);
-            } else if (ressource.parentId === id) {
-                const partialOutput = this._removeRessourceById(ressource.id, depth + 1);
-                output.ressources.push(...partialOutput.ressources)
-                output.items.push(...partialOutput.items)
-            } else {
-                i++
+      };
+
+      const mouseUpListener = (_e: MouseEvent): void => {
+        window.removeEventListener("mousemove", resizeListener);
+        window.removeEventListener("mouseup", mouseUpListener);
+        localSlot.moving = false;
+        this.updateEventById(localSlot.id);
+
+        if (initialDate !== localSlot[localDir]) {
+          const cEvt = new CustomEvent("event-change", {
+            detail: { items: [localSlot] },
+          });
+          this.dispatchEvent(cEvt);
+        }
+      };
+      localSlot.moving = true;
+      window.addEventListener("mousemove", resizeListener);
+      window.addEventListener("mouseup", mouseUpListener);
+    };
+  }
+  private _getEventGrabHandler(
+    slot: Event,
+    editable: boolean,
+    ressourceEditable: boolean,
+    callback: (e: MouseEvent, wasModified: boolean) => void
+  ) {
+    return (evt: MouseEvent): void => {
+      evt.stopPropagation();
+      evt.preventDefault();
+
+      const startPos: number = evt.clientX;
+      let hasChanged = false;
+      const localSlot: Event = slot;
+      // Register all current selected timeslot
+      let localSlots: Array<Event> = this.selectedList
+        .filter((s) => s instanceof Event)
+        .map((s) => s as Event);
+      if (!localSlots.includes(localSlot)) {
+        localSlots = [localSlot];
+      }
+      const startDates = localSlots.map((slot) => slot.start);
+      const endDates = localSlots.map((slot) => slot.end);
+
+      const updatePosition = editable
+        ? (e: MouseEvent) => {
+            const changeTime =
+              Math.round((e.clientX - startPos) / this.slotWidth) *
+              this.slotDuration;
+            return localSlots
+              .map((slot, index) => {
+                const prevStart = slot.start;
+                slot.start = dayjs(startDates[index])
+                  .add(changeTime, "m")
+                  .toDate();
+                slot.end = dayjs(endDates[index]).add(changeTime, "m").toDate();
+                return prevStart.getTime() !== slot.start.getTime();
+              })
+              .reduce((prev, curr) => prev || curr);
+          }
+        : (_e: MouseEvent) => {
+            return false;
+          };
+
+      const updateRessource = ressourceEditable
+        ? (e: MouseEvent) => {
+            const rowId = this.shadowRoot
+              ?.elementsFromPoint(e.clientX, e.clientY)
+              .find((e) => e.tagName == "TD")
+              ?.parentElement?.getAttribute("row-id");
+            if (rowId) {
+              const ressourceId = this.rows[Number(rowId)].id;
+              if (ressourceId !== localSlot.ressourceId) {
+                const oldRessource = this.getRessourceFromId(
+                  localSlot.ressourceId
+                ) as Ressource;
+                localSlot.ressourceId = ressourceId;
+                this._updateEventPosition(oldRessource);
+                return true;
+              }
             }
-        }
-        // Recover deleted items
-        output.items.push(...this.items.filter(i => i.ressourceId === id))
-        // Update items list
-        this.items = this.items.filter(i => i.ressourceId !== id)
-        return output;
-    }
-    
-    getRessources(): Array<Ressource> {
-        return this.rows;
-    }
-    getRessourceFromId(id:string):Ressource | null{
-        const tmp = this.rows.filter(r=>r.id===id)
-        return tmp.length > 0 ? tmp[0] : null;
-    }
-    updateRessource<K extends keyof Ressource>(id:string,key:K,value:Ressource[K]):Ressource|null{
-        const ressource = this.getRessourceFromId(id);
-        if(ressource){
-          if (key == "parent") {
-            const content = this.removeRessourceById(id);
-            ressource[key] = value;
-            this.addRessource(ressource)
-            this.addRessources(content.ressources);
-            this.addEvents(content.items);
+            return false;
+          }
+        : (_e: MouseEvent) => {
+            return false;
+          };
+
+      const moveListener = (e: MouseEvent) => {
+        // Evaluate the updatePosition to avoid || collapse
+        const a = updatePosition(e);
+        if (updateRessource(e) || a) {
+          hasChanged = true;
+          this.updateEventById(localSlot.id);
+        }
+      };
+
+      const mouseUpListener = (e: MouseEvent) => {
+        window.removeEventListener("mousemove", moveListener);
+        window.removeEventListener("mouseup", mouseUpListener);
+        localSlot.moving = false;
+        this.updateEventById(slot.id);
+        if (hasChanged) {
+          const cEvt = new CustomEvent("event-change", {
+            detail: { items: localSlots },
+          });
+          this.dispatchEvent(cEvt);
+        }
+        callback(e, hasChanged);
+      };
+      localSlot.moving = true;
+      window.addEventListener("mousemove", moveListener);
+      window.addEventListener("mouseup", mouseUpListener);
+    };
+  }
+  clearSelectedItems(): void {
+    this.items.forEach((selectable) => {
+      selectable.selected = false;
+      this.updateEventById(selectable.id);
+    });
+    this.rows.forEach((selectable) => (selectable.selected = false));
+    this.selectedList = [];
+  }
+  private _clearSelectionHandler = (_e: MouseEvent) => {
+    this.clearSelectedItems();
+    window.removeEventListener("click", this._clearSelectionHandler);
+  };
+  private _getEventClickHandler(clickedItem: Selectable) {
+    const item = clickedItem;
+    return (e: MouseEvent, wasModified = false) => {
+      e.stopPropagation();
+      const idx = this.selectedList.indexOf(item);
+      if (idx > -1) {
+        if (!wasModified) {
+          if (e.ctrlKey) {
+            this.selectedList.splice(idx, 1);
+            item.selected = false;
+            this.updateEventById(item.id);
           } else {
-            ressource[key] = value;
-            this.rows = this.rows.map(r=>r.id==ressource.id ? ressource : r);
+            this.clearSelectedItems();
           }
-          this._updateEventPosition(ressource);
-          return ressource 
-        }
-        return null
-    }
-    setRowsTitle(title: string):void {
-        this.rowsTitle = title;
-    }
-    getEventById(id:string):Event | undefined{
-        return this.items.find(o=>o.id==id);
-    }
-    addEvents(list:Array<IEvent>):Array<Event | undefined>{
-        return list.map((e)=>this.addEvent(e));
-    }
-    // TimeSlot management
-    /**
-     * Add the event the the timeline or return the existing event with the same id
-     * return undefined if the event ressource is not defined in the timeline
-     * @param event 
-     * @returns 
-     */
-    addEvent(event: IEvent): Event | undefined {
-        const existingEvent = this.getEventById(event.id)
-        if (existingEvent) {
-            return existingEvent
-        }
-        const ressource = this.rows.find(r => r.id === event.ressourceId);
-        if (ressource === undefined) {
-            return undefined
         }
-        const timeslot = Event.toTimeSlot(event);
-        this.items = [...this.items, timeslot]
-        // Update timeslot status
-        timeslot.isDisplayed = timeslot.end > this._start.toDate() || timeslot.start < this._end.toDate();
-        this._updateEventPosition(ressource)
-        return timeslot;
-    }
-
-    removeEventById(id: string): Array<Event> {
-        const output = this.items.filter(o => o.id === id);
-        this.items = this.items.filter(o => o.id !== id);
-        return output
-    }
-    updateEventById(id: string): Event | null {
-        const output = this.removeEventById(id)
-        if (output.length > 0) {
-            this.addEvent(output[0])
-            return output[0];
+      } else {
+        if (this.selectedList.length > 0 && !e.ctrlKey) {
+          this.clearSelectedItems();
+        }
+        item.selected = true;
+        this.selectedList.push(item);
+        this.updateEventById(item.id);
+      }
+      const myEvent = new CustomEvent("item-selected", {
+        detail: { items: this.selectedList },
+        bubbles: true,
+        composed: true,
+      });
+      this.dispatchEvent(myEvent);
+    };
+  }
+  firstUpdated(): void {
+    const root = this.shadowRoot;
+    if (root !== null) {
+      const gridContainer = root.querySelector(
+        ".jc-timeline-grid-container"
+      ) as HTMLBaseElement;
+      const simplebar = new SimpleBar(
+        gridContainer
+      ).getScrollElement() as HTMLBaseElement;
+      syncronizeElementsScrolling(
+        [
+          simplebar,
+          root.querySelector(
+            ".jc-timeline-grid-title-container"
+          ) as HTMLBaseElement,
+        ],
+        "h"
+      );
+      syncronizeElementsScrolling(
+        [simplebar, root.querySelector(".jc-timeline-rows") as HTMLBaseElement],
+        "v"
+      );
+    }
+    if (this.defaultBackground === "") {
+      this.style.setProperty("--default-background", "SteelBlue");
+    }
+  }
+  // RENDERING
+  renderTimeslot(evt: Event): TemplateResult {
+    if (!evt.isDisplayed) {
+      return html``;
+    }
+    let rowTop = 0;
+    let ressource: Ressource;
+    let i: number;
+    for (
+      i = 0;
+      i < this.rows.length && this.rows[i].id !== evt.ressourceId;
+      i++
+    ) {
+      ressource = this.rows[i];
+      if (ressource.show) {
+        rowTop += ressource.height ? ressource.height : this.rowHeight;
+      }
+    }
+    ressource = this.rows[i];
+    const minute2pixel = this.slotWidth / this.slotDuration;
+    const left = dayjs(evt.start).diff(this._start, "m") * minute2pixel;
+    const right = -dayjs(evt.end).diff(this._end, "m") * minute2pixel;
+    const style = {
+      height: this.rowHeight - 4 + "px",
+      top: rowTop + evt.offset * this.rowHeight + "px",
+      left: left + "px",
+      right: right + "px",
+      backgroundColor: "",
+    };
+    const bgColor = evt.bgColor ? evt.bgColor : ressource.eventBgColor;
+    if (bgColor) {
+      style.backgroundColor = bgColor;
+    }
+
+    // Show collapsed ressource
+    if (!ressource.show) {
+      style.height = "";
+      style.top = rowTop - 6 + "px";
+      return html`<div
+        class="jc-timeslot empty"
+        style="${styleMap(style)}"
+      ></div>`;
+    }
+
+    let content: TemplateResult = html`<div class="jc-timeslot-title">
+        ${evt.title}
+      </div>
+      ${evt.content ? unsafeHTML(evt.content) : ""}`;
+    const resizer =
+      evt.editable === null ? ressource.eventEditable : evt.editable;
+    const editableRessource =
+      evt.ressourceEditable === null
+        ? ressource.eventRessourceEditable
+        : evt.ressourceEditable;
+    if (resizer) {
+      content = html`<div
+          class="jc-timeslot-resizer-start"
+          @mousedown="${this._getEventResizerHandler(evt, "start")}"
+        ></div>
+        ${content}
+        <div
+          class="jc-timeslot-resizer-end"
+          @mousedown="${this._getEventResizerHandler(evt, "end")}"
+        ></div>`;
+    }
+    return html`<div
+      class="jc-timeslot ${evt.moving ? "moving" : ""} ${evt.selected
+        ? "selected"
+        : ""}"
+      start="${evt.start.getHours()}"
+      end="${evt.end.getHours()}"
+      style="${styleMap(style)}"
+      @mousedown="${this._getEventGrabHandler(
+        evt,
+        resizer,
+        editableRessource,
+        this._getEventClickHandler(evt)
+      )}"
+    >
+      ${content}
+    </div>`;
+  }
+  private _getCollapseRessourceHandler(
+    item: Ressource
+  ): (e: MouseEvent) => void {
+    return (_e: MouseEvent) => {
+      item.collapseChildren = !item.collapseChildren;
+      this._updateEventPosition(item);
+      // Force rows refresh TODO improve this rerendering
+      this.rows = [...this.rows];
+    };
+  }
+  private _onRessourceDragStart(item: Ressource): (event: DragEvent) => void {
+    return (event: DragEvent): void => {
+      event.dataTransfer?.setData("text", item.id);
+    };
+  }
+  private _onRessourceDragEnter(event: DragEvent): void {
+    if (event.target instanceof HTMLElement) {
+      const tgt = event.target;
+      tgt.classList.add("target");
+    }
+  }
+  private _onRessourceDragLeave(event: DragEvent): void {
+    if (event.target instanceof HTMLElement) {
+      event.target.classList.remove("target");
+    }
+  }
+  private _onRessourceDrop(event: DragEvent): void {
+    event.preventDefault();
+    if (event.target instanceof HTMLElement) {
+      event.target.classList.remove("target");
+      const srcId = event.dataTransfer?.getData("text");
+      const destinationId = event.target.parentElement?.getAttribute(
+        "ressourceId"
+      );
+      if (srcId && destinationId && destinationId !== srcId) {
+        // Check if destination is not child of parent
+        const src = this.getRessourceFromId(srcId) as Ressource;
+        const destination = this.getRessourceFromId(destinationId) as Ressource;
+        if (destination.descendantOf(src)) {
+          return;
+        }
+        // Remove src item from the current Ressource
+        const movedContent = this.removeRessourceById(src.id);
+
+        // Update the moved ressource position
+        if (event.target.classList.contains("jc-ressource")) {
+          movedContent.ressources[0].parent = destination;
         } else {
-            return null;
-        }
-    }
-    private _updateEventPosition(ressource: Ressource): void {
-        const timeslots = this.items.filter(i => i.ressourceId === ressource.id);
-        if (timeslots.length === 0) {
-            ressource.height = this.rowHeight + (ressource.collapseChildren ? 5:0);
-            return
-        }
-        const start = this._start.toDate().getTime();
-        const end = this._end.toDate().getTime();
-        // List potential interval
-        const points = [start, end]
-        const populateInterval = (d: Date) => {
-            const t = d.getTime()
-            if (start < t && t < end && !points.includes(t)) {
-                points.push(t)
-            }
-        }
-        timeslots.forEach(element => {
-            populateInterval(element.start);
-            populateInterval(element.end);
-        });
-        points.sort(); 
-        // Count maximum number of interval intersection
-        const intervals: Array<TimeInterval> = []
-        for (let i = 0; i < points.length - 1; i++) {
-            const startTime = points[i];
-            const endTime = points[i+1];
-            intervals.push({
-                start: points[i],
-                end: points[i+1],
-                slots: timeslots.filter(slot => (slot.start.getTime() <= startTime && endTime <= slot.end.getTime()))
-            })
-        }
-        // Update rows height
-        const lineCount = intervals.reduce((acc, interval) => Math.max(acc, interval.slots.length), 0);
-        ressource.height = this.rowHeight * Math.max(lineCount, 1) + (ressource.collapseChildren ? 5:0); // to avoid collapse rows
-        // Solve the offset positioning of all items
-        const sortTimeslots = (a: Event, b: Event): number => {
-            const t = a.start.getTime() - b.start.getTime();
-            if (t === 0) {
-                const tend = b.end.getTime() - a.end.getTime();
-                return tend === 0 ? ('' + a.id).localeCompare(b.id) : tend
-            }
-            return t;
-        }
-        // Remove all items offset
-        timeslots.forEach(slot => slot.offset = -1);
-        timeslots.sort(sortTimeslots);
-        timeslots[0].offset = 0;
-        const potentialOffset: Array<number> = []
-        for (let i = 0; i < lineCount; i++) {
-            potentialOffset.push(i)
-        }
-        intervals.forEach(intervals => {
-            intervals.slots.sort(sortTimeslots);
-            const usedOffset = intervals.slots.map(o => o.offset).filter(i => i > -1);
-            const availableOffset = potentialOffset.filter(i => !usedOffset.includes(i));
-            intervals.slots.forEach(slot => {
-                if (slot.offset === -1) {
-                    slot.offset = availableOffset.shift() || 0;
-                }
-            })
-
-        })
-    }
-    getEvents(): Array<Event> {
-        return this.items;
-    }
-
-    updateLegend():void {
-        const legend:Array<Array<legendItem>> = [];
-        const legendUnitList:Array<dayjsUnit> = [ "y","M","d","h","m",'s'];
-        const legendMinUnitSpan = this.slotDuration * this.legendSpan;
-        for (const legendUnit of legendUnitList) {
-            let currentDate = dayjs(this._start);
-            let nextColumn = currentDate.add(legendMinUnitSpan,"m");
-            // Check is the starting and end date can accomodate fews cell of the given unit for this unit of time
-            const isLegendPossible = 
-                    // Enough time to fit 1 cell AND
-                    this._end.diff(this._start, legendUnit) > 0 && 
-                        // Starting & Ending date have different legend
-                        (nextColumn.format(this.legendUnitFormat[legendUnit])!==currentDate.format(this.legendUnitFormat[legendUnit]) 
-                        // OR 2 consecutive legends can accomodate at least 1 grid cell
-                        || currentDate.add(1,legendUnit).diff(currentDate,"m") >= legendMinUnitSpan)
-
-            if(isLegendPossible){ 
-                const row:Array<legendItem> = [];
-                let i = 0;
-                while(currentDate.isBefore(this._end)){
-                    i+=this.legendSpan;
-                    if(nextColumn.diff(currentDate,legendUnit) > 0  ){                    
-                        row.push({colspan:i,title:'' + currentDate.format(this.legendUnitFormat[legendUnit])})
-                        i = 0;
-                        currentDate = nextColumn;
-                    }
-                    nextColumn = nextColumn.add(legendMinUnitSpan,"m");
-                }
-                legend.push(row);
-            }
-        }
-        this.legend = legend;
-    }
-    private _handleResizeX(e: CustomEvent<number>):void {
-        e.stopPropagation();
-        this.ressourceWidth += e.detail
-        if (this.ressourceWidth < 0) {
-            this.ressourceWidth = 0
-        }
-    }
-    private _grabHeader(e: MouseEvent):void {
-        const root = this.shadowRoot;
-        if (root !== null) {
-            const gridContainer = root.querySelector(".jc-timeline-grid-container") as HTMLBaseElement;
-            const headerContainer = root.querySelector(".jc-timeline-grid-title-container") as HTMLBaseElement;
-            let lastPosX = e.clientX;
-            const scroll = function (e: MouseEvent) {
-                const scrollLeft = (lastPosX - e.clientX)
-                headerContainer.scrollLeft += scrollLeft
-                gridContainer.scrollLeft += scrollLeft
-                lastPosX = e.clientX
-            }
-            const mouseUpListener = function (_e: MouseEvent) {
-                window.removeEventListener("mousemove", scroll)
-                window.removeEventListener("mouseup", mouseUpListener)
-            }
-            window.addEventListener("mousemove", scroll)
-            window.addEventListener("mouseup", mouseUpListener)
-        }
-    }
-    private _getEventResizerHandler(slot: Event, direction: "end" | "start") {
-        return (evt: MouseEvent):void => {
-            evt.stopPropagation();
-            evt.preventDefault()
-            const startPos = evt.clientX;
-            const localSlot = slot
-            const localDir = direction;
-            const initialDate = slot[direction];
-            const resizeListener = (e: MouseEvent) => {
-                const newDate = dayjs(initialDate).add(Math.round((e.clientX - startPos) / this.slotWidth) * this.slotDuration, "m").toDate();
-                if (direction === "start" ? (newDate < localSlot.end) : (localSlot.start < newDate)) {
-                    if(localSlot[localDir] !== newDate){
-                        localSlot[localDir] = newDate
-                        this.updateEventById(slot.id);
-                    }
-                }
-            }
-
-            const mouseUpListener = (_e: MouseEvent):void => {
-                window.removeEventListener("mousemove", resizeListener)
-                window.removeEventListener("mouseup", mouseUpListener)
-                localSlot.moving = false;
-                this.updateEventById(localSlot.id);
-
-                if(initialDate!==localSlot[localDir]){
-                    const cEvt =  new CustomEvent("event-change",{
-                        detail: { items:[localSlot] }
-                    })
-                    this.dispatchEvent(cEvt);
-                }
-            }
-            localSlot.moving = true
-            window.addEventListener("mousemove", resizeListener)
-            window.addEventListener("mouseup", mouseUpListener)
-        }
-    }
-    private _getEventGrabHandler(slot: Event, editable: boolean, ressourceEditable: boolean, callback: (e: MouseEvent, wasModified:boolean) => void) {
-        return (evt: MouseEvent):void => {
-            evt.stopPropagation();
-            evt.preventDefault();
-
-            const startPos:number = evt.clientX;
-            let hasChanged = false;
-            const localSlot:Event = slot;
-            // Register all current selected timeslot     
-            let localSlots: Array<Event> = this.selectedList.filter(s => s instanceof Event).map(s => s as Event);
-            if (! localSlots.includes(localSlot)){
-                localSlots = [localSlot]
-            }
-            const startDates = localSlots.map(slot => slot.start);
-            const endDates = localSlots.map(slot => slot.end);
-
-            const updatePosition = editable ? (e: MouseEvent) => {
-                const changeTime = Math.round((e.clientX - startPos) / this.slotWidth) * this.slotDuration;
-                return localSlots.map((slot, index) => {
-                    const prevStart = slot.start;
-                    slot.start = dayjs(startDates[index]).add(changeTime, "m").toDate();
-                    slot.end = dayjs(endDates[index]).add(changeTime, "m").toDate();
-                    return prevStart.getTime()!==slot.start.getTime()
-                }).reduce((prev,curr)=>prev || curr)
-            } : (_e: MouseEvent) => { return false };
-
-            const updateRessource = ressourceEditable ? (e: MouseEvent) => {
-                const rowId = this.shadowRoot?.elementsFromPoint(e.clientX, e.clientY)
-                    .find((e) => e.tagName == "TD")?.parentElement?.getAttribute('row-id');
-                if (rowId) {
-                    const ressourceId = this.rows[Number(rowId)].id;
-                    if (ressourceId !== localSlot.ressourceId) {
-                        const oldRessource = this.getRessourceFromId(localSlot.ressourceId) as Ressource;
-                        localSlot.ressourceId = ressourceId;
-                        this._updateEventPosition(oldRessource);
-                        return true;
-                    }
-                }
-                return false;
-            } : (_e: MouseEvent) => { return false };
-
-            const moveListener = (e: MouseEvent) => {
-                // Evaluate the updatePosition to avoid || collapse
-                const a = updatePosition(e);
-                if (updateRessource(e) || a) {
-                    hasChanged = true;
-                    this.updateEventById(localSlot.id);
-                }
-            }
-
-            const mouseUpListener = (e: MouseEvent) => {
-                window.removeEventListener("mousemove", moveListener);
-                window.removeEventListener("mouseup", mouseUpListener);
-                localSlot.moving = false;
-                this.updateEventById(slot.id);
-                if(hasChanged){
-                    const cEvt =  new CustomEvent("event-change",{
-                        detail: { items:localSlots }
-                    })
-                    this.dispatchEvent(cEvt);
-                }
-                callback(e,hasChanged);
-            }
-            localSlot.moving = true;
-            window.addEventListener("mousemove", moveListener)
-            window.addEventListener("mouseup", mouseUpListener)
-        }
-    }
-    clearSelectedItems():void{
-        this.items.forEach((selectable) => {
-            selectable.selected = false;
-            this.updateEventById(selectable.id)
-        });
-        this.rows.forEach((selectable) => selectable.selected = false);
-        this.selectedList = []
-    }
-    private _clearSelectionHandler = (_e: MouseEvent)=>{
-        this.clearSelectedItems()
-        window.removeEventListener("click",this._clearSelectionHandler);
-    }
-    private _getEventClickHandler(clickedItem: Selectable) {
-        const item = clickedItem;
-        return (e: MouseEvent, wasModified = false) => {
-            e.stopPropagation();
-            const idx = this.selectedList.indexOf(item)
-            if (idx > -1) {
-                if(! wasModified){
-                    if (e.ctrlKey) {
-                        this.selectedList.splice(idx, 1);
-                        item.selected = false;
-                        this.updateEventById(item.id)
-                    } else {
-                        this.clearSelectedItems()
-                    }
-                }
-            } else {
-                if (this.selectedList.length > 0 && ! e.ctrlKey ) {
-                    this.clearSelectedItems()
-                } 
-                item.selected = true;
-                this.selectedList.push(item)
-                this.updateEventById(item.id)
-            }
-            const myEvent = new CustomEvent('item-selected', {
-                detail: { items: this.selectedList },
-                bubbles: true,
-                composed: true
-            });
-            this.dispatchEvent(myEvent);
-        };
-    }
-    firstUpdated():void {
-        const root = this.shadowRoot;
-        if (root !== null) {
-            const gridContainer = root.querySelector(".jc-timeline-grid-container") as HTMLBaseElement;
-            const simplebar = new SimpleBar(gridContainer).getScrollElement() as HTMLBaseElement;
-            syncronizeElementsScrolling([simplebar,
-                root.querySelector(".jc-timeline-grid-title-container") as HTMLBaseElement], "h")
-            syncronizeElementsScrolling([simplebar,
-                root.querySelector(".jc-timeline-rows") as HTMLBaseElement], "v")
-                
-                    
-        }
-        if (this.defaultBackground === "") {
-            this.style.setProperty("--default-background", "SteelBlue");
-        }
-    }
-    // RENDERING
-    renderTimeslot(evt: Event): TemplateResult {
-        if (!evt.isDisplayed) {
-            return html``
-        }
-        let rowTop = 0
-        let ressource: Ressource;
-        let i: number;
-        for (i = 0; i < this.rows.length && this.rows[i].id !== evt.ressourceId; i++) {
-            ressource = this.rows[i];
-            if (ressource.show){
-                rowTop += ressource.height ? ressource.height : this.rowHeight;
+          movedContent.ressources[0].parent = destination.parent;
+          let idx = this.rows.findIndex((v) => v.id === destinationId);
+          if (event.target.classList.contains("jc-ressource-below")) {
+            idx += 1;
+            while (
+              idx < this.rows.length &&
+              this.rows[idx].descendantOf(destination)
+            ) {
+              idx += 1;
             }
-        }
-        ressource = this.rows[i];
-        const minute2pixel = this.slotWidth / this.slotDuration;
-        const left = dayjs(evt.start).diff(this._start, "m") * minute2pixel ;
-        const right = - dayjs(evt.end).diff(this._end, "m") * minute2pixel ;
-        const style = {
-            height: this.rowHeight - 4 + "px",
-            top: rowTop + evt.offset * this.rowHeight + "px",
-            left: left + "px",
-            right: right + "px",
-            backgroundColor:""
-        };
-        const bgColor = evt.bgColor ? evt.bgColor : ressource.eventBgColor;
-        if (bgColor) {
-            style.backgroundColor = bgColor;
-        }
-
-        // Show collapsed ressource
-        if (! ressource.show) {
-            style.height = ""
-            style.top = rowTop - 6 + "px";
-            return html`<div class="jc-timeslot empty" style="${styleMap(style)}"></div>`
-        }
-
-        let content: TemplateResult = html`<div class="jc-timeslot-title">${evt.title}</div>${evt.content ? unsafeHTML(evt.content):""}`
-        const resizer = evt.editable === null ? ressource.eventEditable : evt.editable;
-        const editableRessource = evt.ressourceEditable === null ? ressource.eventRessourceEditable : evt.ressourceEditable;
-        if (resizer) {
-            content = html`<div class="jc-timeslot-resizer-start" @mousedown="${this._getEventResizerHandler(evt, "start")}"></div>${content}
-                            <div class="jc-timeslot-resizer-end" @mousedown="${this._getEventResizerHandler(evt, "end")}"></div>`;
-        }
-        return html`<div class="jc-timeslot ${evt.moving ? "moving" : ""} ${evt.selected ? "selected" : ""}" 
-            start="${evt.start.getHours()}" 
-            end="${evt.end.getHours()}" 
-            style="${styleMap(style)}"
-            @mousedown="${this._getEventGrabHandler(evt, resizer, editableRessource, this._getEventClickHandler(evt))}"
-            >${content}</div>`;
-
-
-    }
-    private _getCollapseRessourceHandler(item:Ressource):(e:MouseEvent)=>void{
-        return (_e:MouseEvent) => {
-            item.collapseChildren = ! item.collapseChildren;
-            this._updateEventPosition(item);
-            // Force rows refresh TODO improve this rerendering
-            this.rows = [...this.rows];
-        };
-    }
-    private _onRessourceDragStart(item:Ressource):(event:DragEvent)=>void{
-        return (event:DragEvent):void=>{
-          event.dataTransfer?.setData("text", item.id);
-        }
-    }
-    private _onRessourceDragEnter(event:DragEvent):void{
-        if (event.target instanceof HTMLElement){
-            const tgt = event.target;
-            tgt.classList.add("target");
-        }
-    }
-    private _onRessourceDragLeave(event:DragEvent):void{
-        if (event.target instanceof HTMLElement){
-            event.target.classList.remove("target");
-        }
-    }
-    private _onRessourceDrop(event:DragEvent):void{
-        event.preventDefault()
-        if (event.target instanceof HTMLElement){
-            event.target.classList.remove("target");
-            const srcId = event.dataTransfer?.getData("text");
-            const destinationId = event.target.parentElement?.getAttribute("ressourceId")
-            if (srcId && destinationId && (destinationId !== srcId) ){
-                // Check if destination is not child of parent
-                const src = this.getRessourceFromId(srcId) as Ressource;
-                const destination = this.getRessourceFromId(destinationId) as Ressource;
-                if(destination.descendantOf(src)){
-                    return
-                }      
-                // Remove src item from the current Ressource
-                const movedContent = this.removeRessourceById(src.id);
-                
-                // Update the moved ressource position
-                if (event.target.classList.contains("jc-ressource")){
-                    movedContent.ressources[0].parent = destination;
-                }else {
-                    movedContent.ressources[0].parent = destination.parent;
-                    let idx = this.rows.findIndex(v=>v.id === destinationId); 
-                    if (event.target.classList.contains("jc-ressource-below")){
-                        idx += 1;
-                        while((idx < this.rows.length) 
-                            && this.rows[idx].descendantOf(destination)){
-                            idx += 1;
-                        } 
-                    }
-                    const arr = this.rows
-                    this.rows = [...arr.splice(0, idx), src, ...arr];
-                }
-                // Add moved children and associated slots
-                this.addRessources(movedContent.ressources);
-                this.addEvents(movedContent.items);
-                this.dispatchEvent(new CustomEvent("reorder-ressource",{
-                    detail:{ressources:this.rows}
-                }))
-            }
-        }
-    }
-    renderRessource(item: Ressource): TemplateResult {
-        const depth = item.depth;
-        const style = `--depth:${depth};` + (item.height ? `height:${item.height}px;` : "");
-        const hasChild = item.children.length > 0;
-        const collapseHandler = this._getCollapseRessourceHandler(item);
-        return html`<tr>
-                        <td class="${item.selected ? "jc-ressource-selected":""}" style="${style}" ressourceId="${item.id}" @click="${this._getEventClickHandler(item)}">
-                            <div class="jc-ressource-above"></div>    
-                            <div class="jc-ressource" draggable="true" @dragstart="${this._onRessourceDragStart(item)}">
-                            ${Array(depth).fill(0).map(_i => html`<i class="jc-spacer"></i>`)}${hasChild ? html`<i class="jc-spacer ${item.collapseChildren ? "extend" : "collapse"}" @click="${collapseHandler}"></i>` : html`<i class="jc-spacer"></i>`}
-                            <span>${item.title}</span>
-                            </div>
-                            <div class="jc-ressource-below"></div>
-                        </td>
-                    </tr>`;
-
-    }
-    private renderGridRow(columns: Array<Dayjs>, rowId = -1, height = 30): TemplateResult {
-        return html`<tr row-id="${rowId}">${columns.map((d,i) => html`<td style="height:${height}px;" class="jc-slot ${(i % this.legendSpan) === 0 ? "jc-major-slot" :""}" start="${d.toISOString()}">&nbsp;</td>`)}</tr>`
-    }
-    render():TemplateResult {
-        const nCol = Math.floor(this._end.diff(this._start, 'm') / this.slotDuration) + 1;
-        const columns: Array<Dayjs> = []
-        for (let i = 0; i < nCol; i++) {
-            columns.push(this._start.add(this.slotDuration * i, 'm'))
-        }
-        const displayedRows = this.rows.map((r,i)=>{return{i:i,r:r}}).filter(o=>o.r.show);
-        return html`
-        <style>${this.customStyle}</style>
+          }
+          const arr = this.rows;
+          this.rows = [...arr.splice(0, idx), src, ...arr];
+        }
+        // Add moved children and associated slots
+        this.addRessources(movedContent.ressources);
+        this.addEvents(movedContent.items);
+        this.dispatchEvent(
+          new CustomEvent("reorder-ressource", {
+            detail: { ressources: this.rows },
+          })
+        );
+      }
+    }
+  }
+  renderRessource(item: Ressource): TemplateResult {
+    const depth = item.depth;
+    const style =
+      `--depth:${depth};` + (item.height ? `height:${item.height}px;` : "");
+    const hasChild = item.children.length > 0;
+    const collapseHandler = this._getCollapseRessourceHandler(item);
+    return html`<tr>
+      <td
+        class="${item.selected ? "jc-ressource-selected" : ""}"
+        style="${style}"
+        ressourceId="${item.id}"
+        @click="${this._getEventClickHandler(item)}"
+      >
+        <div class="jc-ressource-above"></div>
+        <div
+          class="jc-ressource"
+          draggable="true"
+          @dragstart="${this._onRessourceDragStart(item)}"
+        >
+          ${Array(depth)
+            .fill(0)
+            .map((_i) => html`<i class="jc-spacer"></i>`)}${hasChild
+            ? html`<i
+                class="jc-spacer ${item.collapseChildren
+                  ? "extend"
+                  : "collapse"}"
+                @click="${collapseHandler}"
+              ></i>`
+            : html`<i class="jc-spacer"></i>`}
+          <span>${item.title}</span>
+        </div>
+        <div class="jc-ressource-below"></div>
+      </td>
+    </tr>`;
+  }
+  private renderGridRow(
+    columns: Array<Dayjs>,
+    rowId = -1,
+    height = 30
+  ): TemplateResult {
+    return html`<tr row-id="${rowId}">
+      ${columns.map(
+        (d, i) =>
+          html`<td
+            style="height:${height}px;"
+            class="jc-slot ${i % this.legendSpan === 0 ? "jc-major-slot" : ""}"
+            start="${d.toISOString()}"
+          >
+            &nbsp;
+          </td>`
+      )}
+    </tr>`;
+  }
+  render(): TemplateResult {
+    const nCol =
+      Math.floor(this._end.diff(this._start, "m") / this.slotDuration) + 1;
+    const columns: Array<Dayjs> = [];
+    for (let i = 0; i < nCol; i++) {
+      columns.push(this._start.add(this.slotDuration * i, "m"));
+    }
+    const displayedRows = this.rows
+      .map((r, i) => {
+        return { i: i, r: r };
+      })
+      .filter((o) => o.r.show);
+    return html`
+      <style>
+        ${this.customStyle}
+      </style>
       <div class="jc-timeline-header">
-        <div class="jc-timeline-rows-title" style=${styleMap({ minWidth: this.ressourceWidth + "px", width: this.ressourceWidth + "px" })}>${this.rowsTitle}</div>
-        <horizontal-resizer @resize-x="${this._handleResizeX}"></horizontal-resizer>
+        <div
+          class="jc-timeline-rows-title"
+          style=${styleMap({
+            minWidth: this.ressourceWidth + "px",
+            width: this.ressourceWidth + "px",
+          })}
+        >
+          ${this.rowsTitle}
+        </div>
+        <horizontal-resizer
+          @resize-x="${this._handleResizeX}"
+        ></horizontal-resizer>
         <div class="jc-timeline-grid-title-container">
-          <table @mousedown="${this._grabHeader}"  style="width:${nCol * this.slotWidth}px;">
-              <colgroup>${columns.map(_o=>html`<col style="min-width:${this.slotWidth}px">`)}</colgroup>
-              <tbody>
-                  ${this.legend.map(arr=>html`<tr class="jc-timeline-grid-title">${arr.map(o=>html`<th colspan="${o.colspan}">${o.title}</th>`)}</tr>`)}
-              </tbody>
+          <table
+            @mousedown="${this._grabHeader}"
+            style="width:${nCol * this.slotWidth}px;"
+          >
+            <colgroup>
+              ${columns.map(
+                (_o) => html`<col style="min-width:${this.slotWidth}px" />`
+              )}
+            </colgroup>
+            <tbody>
+              ${this.legend.map(
+                (arr) =>
+                  html`<tr class="jc-timeline-grid-title">
+                    ${arr.map(
+                      (o) => html`<th colspan="${o.colspan}"><div>${o.title}</div></th>`
+                    )}
+                  </tr>`
+              )}
+            </tbody>
           </table>
         </div>
       </div>
       <div class="jc-timeline-content">
-        <table class="jc-timeline-rows"
+        <table
+          class="jc-timeline-rows"
           style="${styleMap({ "--width": this.ressourceWidth + "px" })}"
-          @dragover="${(e:DragEvent)=>e.preventDefault()}"
+          @dragover="${(e: DragEvent) => e.preventDefault()}"
           @dragenter="${this._onRessourceDragEnter}"
           @dragleave="${this._onRessourceDragLeave}"
-          @drop="${this._onRessourceDrop}">
-          ${this.rows.length > 0 ? displayedRows.map(o=>this.renderRessource(o.r)) : html`<tr class="empty"><td>No ressource</td></tr>`}
+          @drop="${this._onRessourceDrop}"
+        >
+          ${this.rows.length > 0
+            ? displayedRows.map((o) => this.renderRessource(o.r))
+            : html`<tr class="empty">
+                <td>No ressource</td>
+              </tr>`}
         </table>
-        <horizontal-resizer @resize-x="${this._handleResizeX}"></horizontal-resizer>
+        <horizontal-resizer
+          @resize-x="${this._handleResizeX}"
+        ></horizontal-resizer>
         <div class="jc-timeline-grid-container">
           <table style="width:${nCol * this.slotWidth}px;">
-              <colgroup>${columns.map(_o=>html`<col style="min-width:${this.slotWidth}px">`)}</colgroup>
-              
-              <tbody>
-                  ${this.rows.length > 0 ? displayedRows.map(o => this.renderGridRow(columns, o.i, o.r.height)) : this.renderGridRow(columns)}
-              </tbody>
+            <colgroup>
+              ${columns.map(
+                (_o) => html`<col style="min-width:${this.slotWidth}px" />`
+              )}
+            </colgroup>
+
+            <tbody>
+              ${this.rows.length > 0
+                ? displayedRows.map((o) =>
+                    this.renderGridRow(columns, o.i, o.r.height)
+                  )
+                : this.renderGridRow(columns)}
+            </tbody>
           </table>
           <div class="jc-timeslots" style="width:${nCol * this.slotWidth}px;">
-            ${this.items.map(slot => this.renderTimeslot(slot))}
+            ${this.items.map((slot) => this.renderTimeslot(slot))}
           </div>
         </div>
       </div>
     `;
-    }
+  }
 }
 
 export default Timeline;

+ 193 - 186
src/styles/Timeline.style.ts

@@ -1,96 +1,97 @@
 import { css, CSSResult } from "lit-element";
 
-export const TimelineStyle:CSSResult = css`
-* {
-    font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+export const TimelineStyle: CSSResult = css`
+  * {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
+      Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
     font-size: 16px;
     font-weight: 400;
-}
-div {
+  }
+  div {
     box-sizing: border-box;
-}
-.jc-timeline-content,
-.jc-timeline-header{
-    width:100%;
-    position:relative;
+  }
+  .jc-timeline-content,
+  .jc-timeline-header {
+    width: 100%;
+    position: relative;
     display: flex;
-    flex-direction:row;
-    height:max-content;
+    flex-direction: row;
+    height: max-content;
     align-items: stretch;
-}
+  }
 
-.jc-timeline-rows-title,
-.jc-timeline-rows > tr > td{        
+  .jc-timeline-rows-title,
+  .jc-timeline-rows > tr > td {
     padding: 8px;
-    min-width:40px;
+    min-width: 40px;
     box-sizing: border-box;
-}
-.jc-timeline-rows > tr > td {
-    max-width:calc( var(--width) - 8px );
-    padding: 0px; 
-    vertical-align:top;
-}
-.jc-timeline-rows > tr.empty > td{
+  }
+  .jc-timeline-rows > tr > td {
+    max-width: calc(var(--width) - 8px);
+    padding: 0px;
+    vertical-align: top;
+  }
+  .jc-timeline-rows > tr.empty > td {
     padding: 6px 0px 4px 8px;
-}
-i.jc-spacer {
-    display:inline-block;
-    width : 1rem;
+  }
+  i.jc-spacer {
+    display: inline-block;
+    width: 1rem;
     height: 1rem;
-    position:relative;
+    position: relative;
     box-sizing: border-box;
-}
-i.jc-spacer:after{
+  }
+  i.jc-spacer:after {
     content: " ";
-    position:absolute;
+    position: absolute;
     background-repeat: no-repeat;
     background-size: 1.05rem;
-    width:  1.05rem;
+    width: 1.05rem;
     height: 1.05rem;
-}
-.jc-spacer.extend,
-.jc-spacer.collapse    {
-    cursor:pointer;
-}
-i.jc-spacer.extend:after{
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iYmxhY2siIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xOSAzSDVjLTEuMTEgMC0yIC45LTIgMnYxNGMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHptLTgtMmgydi00aDR2LTJoLTRWN2gtMnY0SDd2Mmg0eiIvPjwvc3ZnPg==")
-}
-i.jc-spacer.collapse:after{
-    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHpNNyAxMWgxMHYySDd6Ii8+PC9zdmc+")
-}
+  }
+  .jc-spacer.extend,
+  .jc-spacer.collapse {
+    cursor: pointer;
+  }
+  i.jc-spacer.extend:after {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iYmxhY2siIHdpZHRoPSIxOHB4IiBoZWlnaHQ9IjE4cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik0xOSAzSDVjLTEuMTEgMC0yIC45LTIgMnYxNGMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHptLTgtMmgydi00aDR2LTJoLTRWN2gtMnY0SDd2Mmg0eiIvPjwvc3ZnPg==");
+  }
+  i.jc-spacer.collapse:after {
+    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNWgxNHYxNHpNNyAxMWgxMHYySDd6Ii8+PC9zdmc+");
+  }
 
-.jc-timeline-rows > tr{
+  .jc-timeline-rows > tr {
     box-sizing: border-box;
     white-space: nowrap;
     border: 1px solid grey;
     border-style: solid none;
-}
-.jc-timeline-rows,
-.jc-timeline-rows-title{
+  }
+  .jc-timeline-rows,
+  .jc-timeline-rows-title {
     height: inherit;
-    width:var(--width, 200px);
+    width: var(--width, 200px);
     overflow: hidden;
-    border-collapse:collapse;
-}
-.jc-timeline-grid-title-container,
-.jc-timeline-grid-container{
-    position:relative;
+    border-collapse: collapse;
+  }
+  .jc-timeline-grid-title-container,
+  .jc-timeline-grid-container {
+    position: relative;
     width: 600px;
     display: block;
     overflow: hidden;
-}
-.jc-timeline-grid-container{
+  }
+  .jc-timeline-grid-container {
     overflow-x: auto;
-}
-.jc-timeline-grid-title-container table,
-.jc-timeline-grid-container table {
-    width:100%;
+  }
+  .jc-timeline-grid-title-container table,
+  .jc-timeline-grid-container table {
+    width: 100%;
     table-layout: fixed;
     border-collapse: collapse;
     box-sizing: border-box;
-}
+  }
 
-.jc-timeline-grid-title-container {
+  .jc-timeline-grid-title-container {
     white-space: nowrap;
     cursor: grab;
     user-select: none; /* supported by Chrome and Opera */
@@ -98,148 +99,154 @@ i.jc-spacer.collapse:after{
     -khtml-user-select: none; /* Konqueror HTML */
     -moz-user-select: none; /* Firefox */
     -ms-user-select: none; /* Internet Explorer/Edge */
-}
-.jc-timeline-grid-title:first-child > th{
-    border-top:0;
-    border-left-color:#ffff;
-    border-right-color:#ffff;
-}    
-.jc-timeline-grid-title:first-child > th:before,
-.jc-timeline-grid-title:first-child > th:last-child:after {
-    content:" ";
+  }
+  .jc-timeline-grid-title:first-child > th {
+    border-top: 0;
+    border-left-color: #ffff;
+    border-right-color: #ffff;
+  }
+  .jc-timeline-grid-title:not(:last-child) > th > div {
+    position: sticky;
+    width: min-content;
+    padding: 0 4px;
+    position: -webkit-sticky;
+    left: 0;
+  }
+  .jc-timeline-grid-title:first-child > th:before,
+  .jc-timeline-grid-title:first-child > th:last-child:after {
+    content: " ";
     display: block;
-    position:absolute;
-    left:-1px;
-    bottom:0px;
+    position: absolute;
+    left: -1px;
+    bottom: 0px;
     height: 8px;
     border-left: 1px solid lightgrey;
-    z-index:2;
-}
-.jc-timeline-grid-title:first-child > th:last-child:after
-{
-    left:auto;
-    right:-1px;
-}
-.jc-timeline-grid-title:first-child:last-child >th{
-    padding:8px 0;
-}
-.jc-timeline-grid-title:last-child > th{
-    border-bottom:none;
-}
-.jc-timeline-grid-title > th,
-.jc-slot {      
-    height:100%;
+    z-index: 2;
+  }
+  .jc-timeline-grid-title:first-child > th:last-child:after {
+    left: auto;
+    right: -1px;
+  }
+  .jc-timeline-grid-title:first-child:last-child > th {
+    padding: 8px 0;
+  }
+  .jc-timeline-grid-title:last-child > th {
+    border-bottom: none;
+  }
+  .jc-timeline-grid-title > th,
+  .jc-slot {
+    height: 100%;
     border: solid 1px lightgrey;
     border-left-style: dotted;
-    border-right:0;
+    border-right: 0;
     text-align: center;
-    position:relative;
+    position: relative;
     box-sizing: border-box;
-}
-.jc-timeline-grid-title > th:last-child,
-.jc-slot:last-child{
-    border-right:solid 1px lightgray;
-}
-.jc-timeline-grid-title > th,
-.jc-major-slot{
+  }
+  .jc-timeline-grid-title > th:last-child,
+  .jc-slot:last-child {
+    border-right: solid 1px lightgray;
+  }
+  .jc-timeline-grid-title > th,
+  .jc-major-slot {
     border-left-style: solid;
-}
-.jc-timeslots{
-    position:absolute;
-    top:0px;
-    left:0px;
-    bottom:0px;
+  }
+  .jc-timeslots {
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    bottom: 0px;
     overflow: hidden;
-}
-.jc-timeslot{
-    position:absolute;
-    background-color:var(--default-background);
-    color:#fff;
-    border-radius:3px;
-    margin:2px 0px;
-    z-index:auto;
+  }
+  .jc-timeslot {
+    position: absolute;
+    background-color: var(--default-background);
+    color: #fff;
+    border-radius: 3px;
+    margin: 2px 0px;
+    z-index: auto;
     cursor: grab;
-}
-.jc-timeslot-title{
-    font-size:14px;
-    font-weight:600;
-    padding:4px;
+  }
+  .jc-timeslot-title {
+    font-size: 14px;
+    font-weight: 600;
+    padding: 4px;
     white-space: nowrap;
     overflow-x: hidden;
-}
-.jc-timeslot.empty{
-    height:5px;
-    padding:2px 2px;
-    margin:0px;
-    cursor:pointer;
-}
-.jc-timeslot.moving{
-    opacity:0.7;
-    cursor:grabbing;
-}
-.jc-timeslot.selected:before{
-    border:solid 2px black;
-    border-radius:3px;
-    position:absolute;
-    top:0;
-    bottom:0;
-    left:0;
-    right:0;
-    content:" ";
-}
-.jc-timeslot-resizer-start,
-.jc-timeslot-resizer-end{
-    position:absolute;
-    top:0;
-    bottom:0;
-    width:4px;
-    min-width:4px;
-    display:block;
+  }
+  .jc-timeslot.empty {
+    height: 5px;
+    padding: 2px 2px;
+    margin: 0px;
+    cursor: pointer;
+  }
+  .jc-timeslot.moving {
+    opacity: 0.7;
+    cursor: grabbing;
+  }
+  .jc-timeslot.selected:before {
+    border: solid 2px black;
+    border-radius: 3px;
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    content: " ";
+  }
+  .jc-timeslot-resizer-start,
+  .jc-timeslot-resizer-end {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 4px;
+    min-width: 4px;
+    display: block;
     cursor: ew-resize;
-}
-.jc-timeslot-resizer-start,
-.jc-timeslot-resizer-end{
-    display:block;
-}
-.jc-timeslot-resizer-start{
-    left:0px;
-}
-.jc-timeslot-resizer-end{
-    right:0px;
-}
-.jc-timeline-rows > tr >td{
-    height:100%;
-}
-.jc-ressource{
+  }
+  .jc-timeslot-resizer-start,
+  .jc-timeslot-resizer-end {
+    display: block;
+  }
+  .jc-timeslot-resizer-start {
+    left: 0px;
+  }
+  .jc-timeslot-resizer-end {
+    right: 0px;
+  }
+  .jc-timeline-rows > tr > td {
+    height: 100%;
+  }
+  .jc-ressource {
     padding-top: 2px;
-    height: calc( 100% - 8px);
-}
-.jc-ressource > span {
+    height: calc(100% - 8px);
+  }
+  .jc-ressource > span {
     pointer-events: none;
-}
-.jc-ressource.target{
+  }
+  .jc-ressource.target {
     background-color: lightgrey;
-}
-.jc-ressource-selected{
-    border:1px solid var(--default-background, SteelBlue);
-    border-right:0;
-    border-left:0;
-    background-color:#4682b46b;
-}
-.jc-ressource-above{
-    height:4px;
-}
-.jc-ressource-above.target{
-    margin-left: calc( var(--depth) * 16px );
+  }
+  .jc-ressource-selected {
+    border: 1px solid var(--default-background, SteelBlue);
+    border-right: 0;
+    border-left: 0;
+    background-color: #4682b46b;
+  }
+  .jc-ressource-above {
+    height: 4px;
+  }
+  .jc-ressource-above.target {
+    margin-left: calc(var(--depth) * 16px);
     background-color: var(--default-background, SteelBlue);
     border-radius: 0 0 0 4px;
-}
-.jc-ressource-below{
-    height:4px;
-}
-.jc-ressource-below.target{
-    margin-left: calc( var(--depth) * 16px);
+  }
+  .jc-ressource-below {
+    height: 4px;
+  }
+  .jc-ressource-below.target {
+    margin-left: calc(var(--depth) * 16px);
     background-color: var(--default-background, SteelBlue);
     border-radius: 4px 0 0 0;
-}
-`;
+  }
+`;

Some files were not shown because too many files changed in this diff