|
|
@@ -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()}"> </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()}"
|
|
|
+ >
|
|
|
+
|
|
|
+ </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;
|