import dayjs, { Dayjs } from 'dayjs' import { Event, IEvent } from './Event' import { Ressource, IRessource } from './Ressource' import { HorizontalResizer } from './components/horizontal-resizer' import { LitElement, html, customElement, property, css, TemplateResult } from 'lit-element'; import { styleMap } from 'lit-html/directives/style-map'; import syncronizeElementsScrolling from './utils/syncroScroll'; import Selectable from './utils/selectable'; interface TimelineOptions { ressources?: Array items?: Array } interface TimelineContent { ressources: Array items: Array } interface TimeInterval { start: number, end: number, slots: Array } type dayjsUnit = "y"|"M"|"d"|"h"|"m"|'s' type UnitLegend = { [k in dayjsUnit]: string; }; interface legendItem { colspan:number title:string } // TODO define std zoom level // TODO add selectable Slot // TODO Improve Selected Ressource style // TODO enable to rearrange between different component. @customElement('jc-timeline') class Timeline extends LitElement { static styles = css` body{ font-family:Roboto; } div { box-sizing: border-box; } .jc-timeline-content, .jc-timeline-header{ width:100%; position:relative; display: flex; flex-direction:row; height:max-content; align-items: stretch; } .jc-timeline-rows-title, .jc-timeline-rows > tr > td{ padding: 8px; min-width:40px; } .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; height: 1rem; position:relative; box-sizing: border-box; } i.jc-spacer:after{ content: " "; position:absolute; background-repeat: no-repeat; background-size: 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-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{ width:var(--width, 200px); overflow: hidden; border-collapse:collapse; } .jc-timeline-grid-title-container, .jc-timeline-grid-container{ position:relative; width: 600px; display: block; overflow: hidden; } .jc-timeline-grid-container{ overflow-x: auto; } .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 { white-space: nowrap; cursor: grab; user-select: none; /* supported by Chrome and Opera */ -webkit-user-select: none; /* Safari */ -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; } .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; top:0px; height:calc( 100% - 8px); border-left: 2px solid white; 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 lightgray; border-left-style: dotted; border-right:0; text-align: center; 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{ border-left-style: solid; } .jc-timeslots{ position:absolute; top:0px; left:0px; bottom:0px; overflow: hidden; } .jc-timeslot{ position:absolute; white-space: nowrap; overflow-x:hidden; background-color:var(--default-background); color:#fff; border-radius:3px; padding:4px; margin:2px 0px; z-index:1; cursor:auto; } .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; 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{ padding-top: 2px; height: calc( 100% - 8px); } .jc-ressource > span { pointer-events: none; } .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 ); 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); background-color: var(--default-background, SteelBlue); border-radius: 4px 0 0 0; } `; @property({ type: Array }) private rows: Array @property({ type: Array }) private items: Array private selectedList: Array @property({ type: Number }) private ressourceWidth: number @property({ type: Object }) private _start: Dayjs @property({ type: String }) public get start(): string { return this._start.toISOString(); } public set start(value: string){ this._start = dayjs(value); this.updateLegend(); } private _end: Dayjs @property({ type: String }) public get end(): string { return this._end.toISOString(); } public set end(value: string){ this._end = dayjs(value); 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 }) private 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>; constructor(options: TimelineOptions = {}) { super() this.rows = options.ressources ? options.ressources.map(Ressource.toRessource) : []; this.items = options.items ? options.items.map(Event.toTimeSlot) : [] this._start = dayjs().startOf("day"); this._end = this._start.endOf("day"); this.rowsTitle = "Ressources" this.ressourceWidth = 200 this.selectedList = []; this.legend = []; this.defaultBackground = ""; 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){ this.legendUnitFormat = {...this.legendUnitFormat, ...legend} this.updateLegend() } setLegendUnitFormat(unit:dayjsUnit, format:string){ this.legendUnitFormat[unit] = format; this.updateLegend() } addRessources(list:Array){ return list.map(r=>this.addRessource(r)); } // Ressource management addRessource(ressource: IRessource): IRessource | undefined { if (this.rows.filter(o => o.id == ressource.id).length > 0) { return } const r = Ressource.toRessource(ressource) if (r.parent !== undefined) { const idx = this.rows.indexOf(r.parent) if (idx > -1) { this.rows[idx].children.push(r); this.rows.splice(idx + 1, 0, r); } else { return } } else { this.rows = [...this.rows, r] } this.updateTimeslotPosition(r) return r; } removeRessourceById(id: string): TimelineContent { return this._removeRessourceById(id); } _removeRessourceById(id: string, depth = 0){ const output: TimelineContent = { ressources: [], items: [] }; for (let i = 0; i < this.rows.length; i) { const ressource = this.rows[i]; if (ressource.id === id) { 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 !== ressource.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 { return this.rows; } getRessourceFromId(id:string):Ressource | null{ const tmp = this.rows.filter(r=>r.id===id) return tmp.length > 0 ? tmp[0] : null; } setRowsTitle(title: string) { this.rowsTitle = title; } addTimeSlots(list:Array){ return list.map((e)=>this.addTimeSlot(e)); } // TimeSlot management addTimeSlot(slot: IEvent): Event | null { if (this.items.filter(o => o.id == slot.id).length > 0) { return null } const ressource = this.rows.find(r => r.id === slot.ressourceId); if (ressource === undefined) { return null } const timeslot = Event.toTimeSlot(slot); this.items = [...this.items, timeslot] // Update timeslot status timeslot.isDisplayed = timeslot.end > this._start.toDate() || timeslot.start < this._end.toDate(); this.updateTimeslotPosition(ressource) return timeslot; } removeTimeslotById(id: string): Array { const output = this.items.filter(o => o.id === id); this.items = this.items.filter(o => o.id !== id); return output } updateTimeslotById(id: string): Event | null { const output = this.removeTimeslotById(id) if (output.length > 0) { this.addTimeSlot(output[0]) return output[0]; } else { return null; } } private updateTimeslotPosition(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 = [] 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 = [] 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; } }) }) } getTimeSlots(): Array { return this.items; } updateLegend() { const legend = []; const legendUnitList:Array = [ "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 = []; 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; } _handleResizeX(e: CustomEvent) { e.stopPropagation(); this.ressourceWidth += e.detail if (this.ressourceWidth < 0) { this.ressourceWidth = 0 } } _grabHeader(e: MouseEvent) { 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) } } _getEventResizerHandler(slot: Event, direction: "end" | "start") { return (evt: MouseEvent) => { evt.stopPropagation(); evt.preventDefault() const startPos = evt.clientX; const localSlot = slot const localDir = direction; const startDate = slot[direction]; const resizeListener = (e: MouseEvent) => { const newDate = dayjs(startDate).add(Math.round((e.clientX - startPos) / this.slotWidth) * this.slotDuration, "m").toDate(); if (direction === "start" ? (newDate < localSlot.end) : (localSlot.start < newDate)) { localSlot[localDir] = newDate; this.updateTimeslotById(slot.id); } } const mouseUpListener = (e: MouseEvent) => { window.removeEventListener("mousemove", resizeListener) window.removeEventListener("mouseup", mouseUpListener) localSlot.moving = false; this.updateTimeslotById(slot.id); } localSlot.moving = true window.addEventListener("mousemove", resizeListener) window.addEventListener("mouseup", mouseUpListener) } } _getEventGrabHandler(slot: Event, editable: boolean, ressourceEditable: boolean, callback: (e: MouseEvent, wasModified:boolean) => void) { return (evt: MouseEvent) => { evt.stopPropagation(); evt.preventDefault(); const startPos = evt.clientX; let hasChanged = false; const localSlot = slot; // Register all current selected timeslot let localSlots: Array = 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.updateTimeslotPosition(oldRessource); return true; } } return false; } : (e: MouseEvent) => { return false }; const moveListener = (e: MouseEvent) => { // force the handling of potential position move const a = updatePosition(e); if (updateRessource(e) || a) { hasChanged = true; this.updateTimeslotById(localSlot.id); } } const mouseUpListener = (e: MouseEvent) => { window.removeEventListener("mousemove", moveListener); window.removeEventListener("mouseup", mouseUpListener); localSlot.moving = false; this.updateTimeslotById(slot.id); callback(e,hasChanged); } localSlot.moving = true; window.addEventListener("mousemove", moveListener) window.addEventListener("mouseup", mouseUpListener) } } private _clearSelectedItems(){ this.selectedList.map(e => { e.selected = false; this.updateTimeslotById(e.id) }); this.selectedList = [] } private _clearSelectionhandler = ()=>{ 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){ }else if (e.ctrlKey) { this.selectedList.splice(idx, 1); item.selected = false; this.updateTimeslotById(item.id) } else { this._clearSelectedItems() } } else { if (this.selectedList.length > 0 && ! e.ctrlKey ) { this._clearSelectedItems() } item.selected = true; this.selectedList.push(item) this.updateTimeslotById(item.id) } const myEvent = new CustomEvent('item-selected', { detail: { items: this.selectedList }, bubbles: true, composed: true }); this.dispatchEvent(myEvent); //window.addEventListener("click",this._clearSelectionhandler); }; } firstUpdated() { const root = this.shadowRoot; if (root !== null) { const gridContainer = root.querySelector(".jc-timeline-grid-container") as HTMLBaseElement; syncronizeElementsScrolling([gridContainer, root.querySelector(".jc-timeline-grid-title-container") as HTMLBaseElement], "h") syncronizeElementsScrolling([gridContainer, root.querySelector(".jc-timeline-rows") as HTMLBaseElement], "v") } if (this.defaultBackground === "") { this.style.setProperty("--default-background", "SteelBlue"); } } // RENDERING renderTimeslot(slot: Event): TemplateResult { if (!slot.isDisplayed) { return html`` } let rowTop = 0 let ressource: Ressource; let i: number; for (i = 0; i < this.rows.length && this.rows[i].id !== slot.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(slot.start).diff(this._start, "m") * minute2pixel ; const right = - dayjs(slot.end).diff(this._end, "m") * minute2pixel ; const style = { height: this.rowHeight - 4 + "px", top: rowTop + slot.offset * this.rowHeight + "px", left: left + "px", right: right + "px", backgroundColor:"" }; const bgColor = slot.bgColor ? slot.bgColor : ressource.eventBgColor; if (bgColor) { style.backgroundColor = bgColor; } // Show collapsed ressource if (! ressource.show) { style.height = "" style.top = rowTop - 6 + "px"; return html`
` } let content: TemplateResult = html`${slot.title}` const resizer = slot.editable === null ? ressource.eventEditable : slot.editable; const editableRessource = slot.ressourceEditable === null ? ressource.eventRessourceEditable : slot.ressourceEditable; if (resizer) { content = html`
${content}
`; } return html`
${content}
`; } _getCollapseRessourceHandler(item:Ressource):(e:MouseEvent)=>void{ return (e:MouseEvent) => { item.collapseChildren = ! item.collapseChildren; this.updateTimeslotPosition(item); // Force rows refresh TODO improve this rerendering this.rows = [...this.rows]; }; } _onRessourceDragStart(item:Ressource){ return (event:DragEvent)=>{ event.dataTransfer?.setData("text", item.id); } } _onRessourceDragEnter(event:DragEvent){ if (event.target instanceof HTMLElement){ const tgt = event.target; tgt.classList.add("target"); }else if(event.target instanceof Node){ } } _onRessourceDragLeave(event:DragEvent){ if (event.target instanceof HTMLElement){ event.target.classList.remove("target"); } } _onRessourceDrop(event:DragEvent){ 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.childOf(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].childOf(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.addTimeSlots(movedContent.items); } } } 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`
${Array(depth).fill(0).map(i => html``)}${hasChild ? html`` : html``} ${item.title}
`; } renderGridRow(columns: Array, rowId = -1, height = 30): TemplateResult { return html`${columns.map((d,i) => html` `)}` } render():TemplateResult { const nCol = Math.floor(this._end.diff(this._start, 'm') / this.slotDuration) + 1; const columns: Array = [] 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`
${this.rowsTitle}
${columns.map(o=>html``)} ${this.legend.map(arr=>html`${arr.map(o=>html``)}`)}
${o.title}
${this.rows.length > 0 ? displayedRows.map(o=>this.renderRessource(o.r)) : html``}
No ressource
${columns.map(o=>html``)} ${this.rows.length > 0 ? displayedRows.map(o => this.renderGridRow(columns, o.i, o.r.height)) : this.renderGridRow(columns)}
${this.items.map(slot => this.renderTimeslot(slot))}
`; } } export default Timeline;