UNPKG

nt-vl-gantt

Version:

Interactive JavaScript Gantt chart/task scheduling component

39 lines 154 kB
{ "version": 3, "file": "svelteGantt.css", "sources": [ "../../Gantt.svelte", "../../Column.svelte", "../../ColumnHeaderRow.svelte", "../../Columns.svelte", "../../Milestone.svelte", "../../Row.svelte", "../../Task.svelte", "../../TimeRange.svelte", "../../TimeRangeHeader.svelte", "../../ChangeValue.svelte", "../../DeleteSure.svelte", "../../Form.svelte", "../../Menu.svelte", "../../Modal.svelte", "../../Arrow.svelte", "../../Dependency.svelte", "../../GanttDependencies.svelte", "../../Table.svelte", "../../TableRow.svelte", "../../TableTreeCell.svelte", "../../RangePips.svelte", "../../RangeSlider.svelte", "../../Toast.svelte", "../../TooltipFromAction.svelte", "../../ContextMenu.svelte", "../../Resizer.svelte" ], "sourcesContent": [ "<script>\r\n import _ from \"lodash\";\r\n import { onMount, setContext, tick, onDestroy } from 'svelte';\r\n import { writable, derived } from 'svelte/store';\r\n import moment from 'moment';\r\n // import { tooltip } from './tooltip/tooltip';\r\n import Tooltip from \"./tooltip/TooltipFromAction.svelte\";\r\n import Modal,{getModal} from './modal/Modal.svelte';\r\n import {notifications} from './toast/notifications.js'\r\n\timport Toast from './toast/Toast.svelte'\r\n \r\n import Form from \"./modal/Form.svelte\";\r\n import Menu from \"./modal/Menu.svelte\"\r\n let ganttElement;\r\n let mainHeaderContainer;\r\n let mainContainer;\r\n let rowContainer;\r\n let scrollables = [];\r\n let mounted = false;\r\n let tooltipComponent;\r\n\r\n import { rowStore, taskStore, timeRangeStore, allTasks, allRows, allTimeRanges, rowTaskCache } from './core/store';\r\n import { Task, Row, TimeRange, TimeRangeHeader, Milestone } from './entities';\r\n import { Columns, ColumnHeader } from './column';\r\n import { Resizer } from \"./ui\";\r\n\r\n import { GanttUtils, getPositionByDate } from \"./utils/utils\";\r\n import { getRelativePos, debounce, throttle } from \"./utils/domUtils\";\r\n import { SelectionManager } from \"./utils/selectionManager\";\r\n import { GanttApi } from \"./core/api\";\r\n import { TaskFactory, reflectTask } from \"./core/task\";\r\n import { RowFactory } from \"./core/row\";\r\n import { TimeRangeFactory } from \"./core/timeRange\";\r\n import { DragDropManager } from \"./core/drag\";\r\n import { findByPosition, findByDate } from './core/column';\r\n import { onEvent, onDelegatedEvent, offDelegatedEvent } from './core/events';\r\n\r\n export let rows;\r\n export let tasks = [];\r\n export let timeRanges = [];\r\n $: if(mounted) initRows(rows);\r\n $: if(mounted) initTasks(tasks);\r\n $: if(mounted) initTimeRanges(timeRanges);\r\n\r\n export let rowPadding = 6;\r\n export let rowHeight = 52;\r\n const _rowHeight = writable(rowHeight);\r\n const _rowPadding = writable(rowPadding);\r\n\r\n export let from;\r\n export let to;\r\n const _from = writable(from);\r\n const _to = writable(to);\r\n $: $_from = from;\r\n $: $_to = to;\r\n\r\n export let minWidth = 800;\r\n export let fitWidth = false;\r\n const _minWidth = writable(minWidth);\r\n const _fitWidth = writable(fitWidth);\r\n $: {\r\n $_minWidth = minWidth;\r\n $_fitWidth = fitWidth;\r\n } \r\n\r\n export let classes = [];\r\n export let totalDays=10;\r\n export let dayValue=0;\r\n // export let currentDay=1;\r\n\r\n export let fromDate;\r\n export let toDate;\r\n const _fromDate = writable(fromDate);\r\n const _toDate = writable(toDate);\r\n $: $_fromDate = fromDate;\r\n $: $_toDate = toDate;\r\n\r\n export let headers = [{unit: 'day', format: 'MMMM Do'}, {unit: 'hour', format: 'H:mm'}];\r\n export let zoomLevels = [\r\n\t\t{\r\n\t\t\theaders: [\r\n\t\t\t\t{ unit: 'day', format: 'DD.MM.YYYY' },\r\n\t\t\t\t{ unit: 'hour', format: 'HH' }\r\n\t\t\t],\r\n\t\t\tminWidth: 800,\r\n\t\t\tfitWidth: true\r\n\t\t},\r\n\t\t{\r\n\t\t\theaders: [\r\n\t\t\t\t{ unit: 'hour', format: 'ddd D/M, H A' },\r\n\t\t\t\t{ unit: 'minute', format: 'mm', offset: 15 }\r\n\t\t\t],\r\n\t\t\tminWidth: 5000,\r\n\t\t\tfitWidth: false\r\n\t\t}\r\n\t];\r\n export let taskContent = null;\r\n export let tableWidth = 100;\r\n export let resizeHandleWidth = 10;\r\n export let onTaskButtonClick = null;\r\n\r\n export let magnetUnit = 'minute';\r\n export let magnetOffset = 5;\r\n export let columnUnit = 'minute';\r\n export let columnOffset = 5;\r\n\r\n // export until Svelte3 implements Svelte2's setup(component) hook\r\n export let ganttTableModules = [];\r\n export let ganttBodyModules = [];\r\n\r\n export let reflectOnParentRows = true;\r\n export let reflectOnChildRows = false;\r\n\r\n export let columnStrokeColor;\r\n export let columnStrokeWidth;\r\n\r\n const visibleWidth = writable();\r\n const visibleHeight = writable();\r\n const headerHeight = writable();\r\n const _width = derived([visibleWidth, _minWidth, _fitWidth], ([visible, min, stretch]) => {\r\n return stretch && visible > min ? visible : min;\r\n });\r\n \r\n export const columnService = {\r\n getColumnByDate(date) {\r\n const pair = findByDate(columns, date);\r\n return !pair[0] ? pair[1] : pair[0];\r\n },\r\n getColumnByPosition(x) {\r\n const pair = findByPosition(columns, x);\r\n return !pair[0] ? pair[1] : pair[0];\r\n },\r\n getPositionByDate (date) {\r\n if(!date) return null;\r\n const column = this.getColumnByDate(date);\r\n \r\n let durationTo = date.diff(column.from, 'milliseconds');\r\n const position = durationTo / column.duration * column.width;\r\n\r\n //multiples - skip every nth col, use other duration\r\n return column.left + position;\r\n },\r\n getDateByPosition (x) {\r\n const column = this.getColumnByPosition(x);\r\n x = x - column.left;\r\n\r\n let positionDuration = column.duration / column.width * x;\r\n const date = moment(column.from).add(positionDuration, 'milliseconds');\r\n\r\n return date;\r\n },\r\n /**\r\n * \r\n * @param {Moment} date - Date\r\n * @returns {Moment} rounded date passed as parameter\r\n */\r\n roundTo(date) {\r\n let value = date.get(magnetUnit);\r\n value = Math.round(value / magnetOffset);\r\n date.set(magnetUnit, value * magnetOffset);\r\n\r\n //round all smaller units to 0\r\n const units = ['millisecond', 'second', 'minute', 'hour', 'date', 'month', 'year'];\r\n const indexOf = units.indexOf(magnetUnit);\r\n for (let i = 0; i < indexOf; i++) {\r\n date.set(units[i], 0)\r\n }\r\n return date\r\n }\r\n }\r\n\r\n const columnWidth = writable(getPositionByDate($_from.clone().add(columnOffset, columnUnit), $_from, $_to, $_width) | 0);\r\n $: $columnWidth = getPositionByDate($_from.clone().add(columnOffset, columnUnit), $_from, $_to, $_width) | 0;\r\n let columnCount = Math.ceil($_width / $columnWidth);\r\n $: columnCount = Math.ceil($_width / $columnWidth);\r\n let columns = getColumns($_from, columnCount, columnOffset, columnUnit, $columnWidth);\r\n $: columns = getColumns($_from, columnCount, columnOffset, columnUnit, $columnWidth);\r\n\r\n function getColumns(from, count, offset, unit, width) {\r\n let columns = [];\r\n let columnFrom = from.clone();\r\n let left = 0;\r\n for (let i = 0; i < count; i++) {\r\n const from = columnFrom.clone();\r\n const to = columnFrom.add(offset, unit);\r\n const duration = to.diff(from, 'milliseconds');\r\n\r\n columns.push({\r\n width: width,\r\n from,\r\n left,\r\n duration\r\n });\r\n left += width;\r\n columnFrom = to;\r\n }\r\n \r\n return columns;\r\n }\r\n\r\n const dimensionsChanged = derived([columnWidth, _from, _to], () => ({}));\r\n $: {\r\n if($dimensionsChanged) {\r\n refreshTasks();\r\n refreshTimeRanges();\r\n }\r\n }\r\n\r\n setContext('dimensions', {\r\n from: _from,\r\n to: _to,\r\n width: _width,\r\n visibleWidth,\r\n visibleHeight,\r\n headerHeight,\r\n dimensionsChanged\r\n });\r\n\r\n setContext('options', {\r\n taskContent,\r\n rowPadding: _rowPadding,\r\n rowHeight: _rowHeight,\r\n resizeHandleWidth: resizeHandleWidth,\r\n reflectOnParentRows,\r\n reflectOnChildRows,\r\n onTaskButtonClick\r\n });\r\n\r\n const hoveredRow = writable();\r\n const selectedRow = writable();\r\n\r\n const ganttContext = { \r\n scrollables, \r\n hoveredRow, \r\n selectedRow \r\n };\r\n setContext('gantt', ganttContext);\r\n\r\n onMount(() => {\r\n Object.assign(ganttContext, {\r\n rowContainer,\r\n mainContainer,\r\n mainHeaderContainer\r\n });\r\n api.registerEvent('tasks', 'move');\r\n api.registerEvent('tasks', 'select');\r\n api.registerEvent('tasks', 'switchRow');\r\n api.registerEvent('tasks', 'moveEnd');\r\n api.registerEvent('tasks', 'change');\r\n api.registerEvent('tasks', 'changed');\r\n api.registerEvent('tasks', 'mouseover');\r\n api.registerEvent('tasks', 'mouseout');\r\n api.registerEvent('tasks', 'dblclick');\r\n api.registerEvent(\"tasks\", \"contextmenu\");\r\n api.registerEvent('rows', 'mouseover');\r\n api.registerEvent(\"rows\", \"dblclick\");\r\n api.registerEvent('gantt', 'viewChanged');\r\n api.registerEvent('button', 'move');\r\n \r\n \r\n\r\n mounted = true;\r\n });\r\n\tonDelegatedEvent(\"mouseover\", \"data-task-id\", (event, data, target) => {\r\n const start = target.getAttribute(\"start\");\r\n\r\n const end = target.getAttribute(\"end\");\r\n \r\n const duration = target.getAttribute(\"duration\");\r\n\r\n tooltipComponent = new Tooltip({\r\n props: {\r\n start: start,\r\n end: end,\r\n duration: duration,\r\n x: event.pageX,\r\n y: event.pageY,\r\n },\r\n target: document.body,\r\n });\r\n \r\n const taskId = +data;\r\n if (event.ctrlKey) {\r\n selectionManager.toggleSelection(taskId);\r\n } else {\r\n selectionManager.selectSingle(taskId);\r\n }\r\n const object = {\r\n\t\t\ttaskId: data,\r\n\t\t\tevent: event\r\n\t\t}\r\n api.tasks.raise.mouseover($taskStore.entities[taskId]);\r\n\t});\r\n onDelegatedEvent(\"mouseout\", \"data-task-id\", (event, data, target) => {\r\n tooltipComponent.$destroy();\r\n\t});\r\n\r\n\r\n let selection\r\n\t\r\n\t// Callback function provided to the `open` function, it receives the value given to the `close` function call, or `undefined` if the Modal was closed with escape or clicking the X, etc.\r\n\tfunction setSelection(res){\r\n\t\tselection=res\r\n\t}\r\n let taskData={\r\n model:null,\r\n dataTaskId:0,\r\n start: \"00:00\",\r\n end: \"00:00\",\r\n duration:ConvertMinutesToHHmm(0),\r\n currentDay:1,\r\n value:0,\r\n valueUnit:''\r\n }; \r\n onDelegatedEvent(\"contextmenu\", \"data-task-id\", (event, data, target) => {\r\n const dataTaskId=data;\r\n const start = target.getAttribute(\"start\");\r\n const end = target.getAttribute(\"end\");\r\n const duration = target.getAttribute(\"duration\");\r\n const currentDay = parseInt(target.getAttribute(\"currentDay\"));\r\n const value = parseInt(target.getAttribute(\"value\"));\r\n const valueUnit = target.getAttribute(\"valueUnit\");\r\n\r\n\r\n\r\n taskData = {\r\n dataTaskId:dataTaskId,\r\n\t\t\tstart: start,\r\n\t\t\tend: end,\r\n duration:ConvertMinutesToHHmm(duration),\r\n currentDay:currentDay,\r\n value:value,\r\n valueUnit:valueUnit\r\n\t\t}\r\n getModal('task-meu-modal').open(setSelection);\r\n\t\tconst taskId = +data;\r\n\t\tif (event.ctrlKey) {\r\n\t\t\tselectionManager.toggleSelection(taskId);\r\n\t\t} else {\r\n\t\t\tselectionManager.selectSingle(taskId);\r\n\t\t}\r\n\r\n\t\tconst object = {\r\n\t\t\ttaskId: data,\r\n\t\t\tevent: event\r\n\t\t}\r\n\r\n\t\tapi.tasks.raise.contextmenu(object);\r\n\t\tevent.preventDefault();\r\n\t});\r\n\r\n\r\n function ConvertMinutesToHHmm(duratio) {\r\n function D(J) {\r\n return (J < 10 ? \"0\" : \"\") + J;\r\n }\r\n var h = D(Math.floor(duratio / 60)); //((num-(d*1440))/60);\r\n var m = D(Math.round(duratio % 60));\r\n return h + \":\" + m;\r\n }\r\n onDelegatedEvent('dblclick', 'data-task-id', (event, data, target) => {\r\n const dataTaskId=data;\r\n const start = target.getAttribute(\"start\");\r\n const end = target.getAttribute(\"end\");\r\n const duration = target.getAttribute(\"duration\");\r\n const currentDay = parseInt(target.getAttribute(\"currentDay\"));\r\n const value = parseInt(target.getAttribute(\"value\"));\r\n const valueUnit = target.getAttribute(\"valueUnit\");\r\n\r\n\r\n\r\n taskData = {\r\n dataTaskId:dataTaskId,\r\n\t\t\tstart: start,\r\n\t\t\tend: end,\r\n duration:ConvertMinutesToHHmm(duration),\r\n currentDay:currentDay,\r\n value:value,\r\n valueUnit:valueUnit\r\n\t\t}\r\n getModal().open();\r\n const taskId = +data;\r\n if (event.ctrlKey) {\r\n selectionManager.toggleSelection(taskId);\r\n } else {\r\n selectionManager.selectSingle(taskId);\r\n }\r\n const object = {\r\n\t\t\ttaskId: data,\r\n\t\t\tevent: event\r\n\t\t}\r\n api.tasks.raise.dblclick($taskStore.entities[taskId]);\r\n });\r\n\r\n onDelegatedEvent('click', 'data-task-id', (event, data, target) => {\r\n const taskId = +data;\r\n if (event.ctrlKey) {\r\n selectionManager.toggleSelection(taskId);\r\n } else {\r\n selectionManager.selectSingle(taskId);\r\n }\r\n const object = {\r\n\t\t\ttaskId: data,\r\n\t\t\tevent: event\r\n\t\t}\r\n api.tasks.raise.select($taskStore.entities[taskId]);\r\n });\r\n\r\n\r\n onDelegatedEvent('click', 'data-row-id', (event, data, target) => {\r\n $selectedRow = +data;\r\n });\r\n\tonDelegatedEvent(\"mouseover\", \"data-row-id\", (event, data, target) => {\r\n\t\tconst object = {\r\n\t\t\trowId: data,\r\n\t\t\tevent: event\r\n\t\t}\r\n\t\tapi.rows.raise.mouseover(object);\r\n\t});\r\n onDelegatedEvent(\"dblclick\", \"data-row-id\", (event, data, target) => {\r\n\t\tvar date = columnService.getDateByPosition(event.offsetX);\r\n\t\tconst rowId = +data;\r\n var object = {date: date, rowId: rowId};\r\n \r\n if (event.ctrlKey) {\r\n selectionManager.toggleSelection(rowId);\r\n\t\t\t\r\n } else {\r\n selectionManager.selectSingle(rowId);\r\n }\r\n \r\n api.rows.raise.dblclick(object);\r\n \r\n \r\n });\r\n \r\n onDestroy(() => {\r\n offDelegatedEvent('click', 'data-task-id');\r\n offDelegatedEvent('dblclick', 'data-row-id');\r\n offDelegatedEvent('dblclick', 'data-task-id');\r\n offDelegatedEvent('contextmenu', 'data-task-id');\r\n offDelegatedEvent('mouseover', 'data-task-id');\r\n offDelegatedEvent('mouseout', 'data-task-id');\r\n\r\n offDelegatedEvent('click', 'data-row-id');\r\n\r\n });\r\n\r\n let __scrollTop = 0;\r\n let __scrollLeft = 0;\r\n function scrollable(node) {\r\n const onscroll = event => {\r\n const { scrollTop, scrollLeft } = node;\r\n\r\n scrollables.forEach(scrollable => {\r\n if (scrollable.orientation === \"horizontal\") {\r\n scrollable.node.scrollLeft = scrollLeft;\r\n } else {\r\n scrollable.node.scrollTop = scrollTop;\r\n }\r\n });\r\n\r\n __scrollTop = scrollTop;\r\n __scrollLeft = scrollLeft;\r\n };\r\n\r\n node.addEventListener(\"scroll\", onscroll);\r\n return {\r\n destroy() {\r\n node.removeEventListener(\"scroll\", onscroll, false);\r\n }\r\n };\r\n }\r\n\r\n function horizontalScrollListener(node) {\r\n scrollables.push({ node, orientation: \"horizontal\" });\r\n }\r\n\r\n function onResize(event) {\r\n tableWidth = event.detail.left;\r\n }\r\n\r\n let zoomLevel = 0;\r\n let zooming = false;\r\n async function onwheel(event) {\r\n if (event.ctrlKey) {\r\n event.preventDefault();\r\n\r\n const prevZoomLevel = zoomLevel;\r\n if (event.deltaY > 0) {\r\n zoomLevel = Math.max(zoomLevel - 1, 0);\r\n } else {\r\n zoomLevel = Math.min(zoomLevel + 1, zoomLevels.length - 1);\r\n }\r\n\r\n if (prevZoomLevel != zoomLevel && zoomLevels[zoomLevel]) {\r\n const options = {\r\n columnUnit: columnUnit,\r\n columnOffset: columnOffset,\r\n minWidth: $_minWidth,\r\n ...zoomLevels[zoomLevel]\r\n };\r\n\r\n const scale = options.minWidth / $_width;\r\n const node = mainContainer;\r\n const mousepos = getRelativePos(node, event);\r\n const before = node.scrollLeft + mousepos.x;\r\n const after = before * scale;\r\n const scrollLeft = after - mousepos.x + node.clientWidth / 2;\r\n\r\n\r\n columnUnit = options.columnUnit;\r\n columnOffset = options.columnOffset;\r\n $_minWidth = options.minWidth;\r\n\r\n if(options.headers)\r\n headers = options.headers;\r\n\r\n if(options.fitWidth)\r\n $_fitWidth = options.fitWidth;\r\n\r\n api.gantt.raise.viewChanged();\r\n zooming = true;\r\n await tick();\r\n node.scrollLeft = scrollLeft;\r\n zooming = false;\r\n }\r\n }\r\n }\r\n\r\n function onDateSelected(event) {\r\n $_from = event.detail.from.clone();\r\n $_to = event.detail.to.clone();\r\n }\r\n\r\n function initRows(rowsData) {\r\n const rows = rowFactory.createRows(rowsData);\r\n rowStore.addAll(rows);\r\n }\r\n\r\n async function initTasks(taskData) {\r\n await tick();\r\n\r\n const tasks = [];\r\n const opts = { rowPadding: $_rowPadding };\r\n taskData.forEach(t => {\r\n const task = taskFactory.createTask(t);\r\n const row = $rowStore.entities[task.model.resourceId];\r\n task.reflections = [];\r\n\r\n if(reflectOnChildRows && row.allChildren) {\r\n row.allChildren.forEach(r => {\r\n const reflectedTask = reflectTask(task, r, opts);\r\n task.reflections.push(reflectedTask.model.id);\r\n tasks.push(reflectedTask);\r\n });\r\n }\r\n\r\n if(reflectOnParentRows && row.allParents.length > 0) {\r\n row.allParents.forEach(r => {\r\n const reflectedTask = reflectTask(task, r, opts);\r\n task.reflections.push(reflectedTask.model.id);\r\n tasks.push(reflectedTask);\r\n });\r\n }\r\n\r\n tasks.push(task);\r\n });\r\n taskStore.addAll(tasks);\r\n }\r\n\r\n function initTimeRanges(timeRangeData) {\r\n const timeRanges = timeRangeData.map(timeRange => {\r\n return timeRangeFactory.create(timeRange);\r\n });\r\n timeRangeStore.addAll(timeRanges);\r\n }\r\n \r\n function onModuleInit(module) {\r\n \r\n }\r\n\r\n async function tickWithoutCSSTransition() {\r\n disableTransition = false;\r\n await tick();\r\n ganttElement.offsetHeight; // force a reflow\r\n disableTransition = true;\r\n }\r\n\r\n export const api = new GanttApi();\r\n const selectionManager = new SelectionManager();\r\n\r\n export const taskFactory = new TaskFactory(columnService);\r\n $: {\r\n taskFactory.rowPadding = $_rowPadding;\r\n taskFactory.rowEntities = $rowStore.entities;\r\n }\r\n\r\n export const rowFactory = new RowFactory();\r\n $: rowFactory.rowHeight = rowHeight;\r\n\r\n export const dndManager = new DragDropManager(rowStore);\r\n export const timeRangeFactory = new TimeRangeFactory(columnService);\r\n\r\n export const utils = new GanttUtils();\r\n $: {\r\n utils.from = $_from;\r\n utils.to = $_to;\r\n utils.width = $_width;\r\n utils.magnetOffset = magnetOffset;\r\n utils.magnetUnit = magnetUnit;\r\n }\r\n\r\n setContext('services', {\r\n utils,\r\n api,\r\n dndManager,\r\n selectionManager,\r\n columnService\r\n });\r\n\r\n export function refreshTimeRanges() {\r\n \r\n timeRangeStore._update(({ids, entities}) => {\r\n ids.forEach(id => {\r\n const timeRange = entities[id];\r\n const newLeft = columnService.getPositionByDate(timeRange.model.from) | 0;\r\n const newRight = columnService.getPositionByDate(timeRange.model.to) | 0;\r\n\r\n timeRange.left = newLeft;\r\n timeRange.width = newRight - newLeft;\r\n });\r\n return { ids, entities };\r\n });\r\n }\r\n\r\n export function refreshTasks() {\r\n $allTasks.forEach(task => {\r\n const newLeft = columnService.getPositionByDate(task.model.from) | 0;\r\n const newRight = columnService.getPositionByDate(task.model.to) | 0;\r\n\r\n task.left = newLeft;\r\n task.width = newRight - newLeft;\r\n });\r\n\r\n taskStore.refresh();\r\n }\r\n\r\n export function getRowContainer() {\r\n return rowContainer;\r\n }\r\n\r\n export function selectTask(id) {\r\n const task = $taskStore.entities[id];\r\n if (task) {\r\n selectionManager.selectSingle(task);\r\n }\r\n }\r\n\r\n export function unselectTasks() {\r\n selectionManager.clearSelection();\r\n }\r\n\r\n export function scrollToRow(id, scrollBehavior = 'auto') {\r\n const { scrollTop, clientHeight } = mainContainer;\r\n \r\n const index = $allRows.findIndex(r => r.model.id == id);\r\n if(index === -1) return;\r\n const targetTop = index * rowHeight;\r\n\r\n if(targetTop < scrollTop) {\r\n mainContainer.scrollTo({\r\n top: targetTop,\r\n behavior: scrollBehavior\r\n });\r\n }\r\n\r\n if(targetTop > scrollTop + clientHeight) {\r\n mainContainer.scrollTo({\r\n top: targetTop + rowHeight - clientHeight,\r\n behavior: scrollBehavior\r\n });\r\n }\r\n }\r\n export let model;\r\n export function handleManualTimeSubmit() {\r\n const newLeft = columnService.getPositionByDate(moment(taskData.start, 'HH:mm')) | 0;\r\n const newRight = columnService.getPositionByDate(moment(taskData.end, 'HH:mm')) | 0;\r\n const left = newLeft;\r\n const width = newRight - newLeft;\r\n const task = $taskStore.entities[taskData.dataTaskId];\r\n model=task.model;\r\n Object.assign(model, {\r\n from: moment(taskData.start, 'HH:mm').add(taskData.currentDay-1, 'day'),\r\n to: moment(taskData.end, 'HH:mm').add(taskData.currentDay-1, 'day'),\r\n currentDay:taskData.currentDay,\r\n value:taskData.value,\r\n valueUnit:taskData.valueUnit\r\n });\r\n const newTask = {\r\n ...task,\r\n left: left,\r\n width: width,\r\n model,\r\n };\r\n taskStore.update(newTask);\r\n refreshTasks();\r\n notifications.success('Successfully Updated!', 3000);\r\n getModal().close();\r\n\t}\r\n\r\n\texport function scrollToTask(id, scrollBehavior = 'auto') {\r\n const { scrollLeft, scrollTop, clientWidth, clientHeight } = mainContainer;\r\n \r\n const task = $taskStore.entities[id];\r\n if(!task) return;\r\n const targetLeft = task.left;\r\n const rowIndex = $allRows.findIndex(r => r.model.id == task.model.resourceId);\r\n const targetTop = rowIndex * rowHeight;\r\n \r\n const options = {\r\n top: undefined,\r\n left: undefined,\r\n behavior: scrollBehavior\r\n };\r\n\r\n if(targetLeft < scrollLeft) {\r\n options.left = targetLeft;\r\n }\r\n\r\n if(targetLeft > scrollLeft + clientWidth) {\r\n options.left = targetLeft + task.width - clientWidth;\r\n }\r\n\r\n if(targetTop < scrollTop) {\r\n options.top = targetTop;\r\n }\r\n\r\n if(targetTop > scrollTop + clientHeight) {\r\n options.top = targetTop + rowHeight - clientHeight;\r\n }\r\n \r\n mainContainer.scrollTo(options);\r\n }\r\n\r\n export function updateTask(model) {\r\n const task = taskFactory.createTask(model);\r\n taskStore.upsert(task);\r\n }\r\n\r\n export function updateTasks(taskModels) {\r\n const tasks = taskModels.map(model => taskFactory.createTask(model));\r\n taskStore.upsertAll(tasks);\r\n }\r\n\r\n export function updateRow(model) {\r\n const row = rowFactory.createRow(model);\r\n rowStore.upsert(row);\r\n }\r\n\r\n export function updateRows(rowModels) {\r\n const rows = rowModels.map(model => rowFactory.createRow(model));\r\n rowStore.upsertAll(rows);\r\n }\r\n\r\n export function getRow(resourceId) {\r\n return $rowStore.entities[resourceId];\r\n }\r\n\r\n export function getTask(id) {\r\n return $taskStore.entities[id];\r\n }\r\n\r\n export function getTasks(resourceId) {\r\n if ($rowTaskCache[resourceId]) {\r\n return $rowTaskCache[resourceId].map(id => $taskStore.entities[id]);\r\n }\r\n return null;\r\n }\r\n\r\n export function getAllTasksIds() {\r\n return $rowTaskCache;\r\n }\r\n\r\n export function getAllTasksEntities() {\r\n return $taskStore.entities;\r\n }\r\n export function getAllTasksStores() {\r\n return $taskStore;\r\n }\r\n export function copyTask() {\r\n }\r\n export function copySelectedTasks() {\r\n }\r\n \r\n export let dayTasks=[];\r\n\r\n export function copyDayTasks() {\r\n dayTasks=[];\r\n visibleTasks.forEach(task => {\r\n if (task.model.from.isSame(from,\"days\")) {\r\n dayTasks.push(task);\r\n }\r\n });\r\n notifications.success('Copied!', 1500);\r\n }\r\n export function pasteDayTasks() {\r\n const copiedTasks= _.cloneDeep(dayTasks);\r\n if (copiedTasks && copiedTasks.length>0) {\r\n copiedTasks.forEach(task => {\r\n if (!(task.model.from.isSame(from,\"days\"))) {\r\n task.model.id=5000 +Date.now()+ Math.floor(Math.random() * 1000)\r\n task.model.from=task.model.from.set('year', from.get('year')).set('month', from.get('month')).set('date', from.get('date')).set('day', from.get('day'));\r\n task.model.to=task.model.to.set('year', from.get('year')).set('month', from.get('month')).set('date', from.get('date')).set('day', from.get('day'));\r\n task.model.currentDay=dayValue;\r\n taskStore.add(task);\r\n refreshTasks();\r\n }\r\n });\r\n }\r\n notifications.success('Pasted!', 1500);\r\n \r\n }\r\n export function copyWholeTasks() {\r\n }\r\n \r\n\r\n\r\n let filteredRows = [];\r\n $: filteredRows = $allRows.filter(row => !row.hidden);\r\n\r\n let rightScrollbarVisible;\r\n $: rightScrollbarVisible = rowContainerHeight > $visibleHeight;\r\n\r\n let rowContainerHeight;\r\n $: rowContainerHeight = filteredRows.length * rowHeight;\r\n\r\n let startIndex;\r\n $: startIndex = Math.floor(__scrollTop / rowHeight);\r\n\r\n let endIndex;\r\n $: endIndex = Math.min(startIndex + Math.ceil($visibleHeight / rowHeight), filteredRows.length - 1);\r\n\r\n let paddingTop = 0;\r\n $: paddingTop = startIndex * rowHeight;\r\n\r\n let paddingBottom = 0;\r\n $: paddingBottom = (filteredRows.length - endIndex - 1) * rowHeight;\r\n\r\n let visibleRows = [];\r\n $: visibleRows = filteredRows.slice(startIndex, endIndex + 1);\r\n\r\n export let visibleTasks;\r\n $: {\r\n const tasks = [];\r\n visibleRows.forEach(row => {\r\n if ($rowTaskCache[row.model.id]) {\r\n $rowTaskCache[row.model.id].forEach(id => {\r\n tasks.push($taskStore.entities[id]);\r\n });\r\n }\r\n });\r\n visibleTasks = tasks;\r\n }\r\n\r\n let disableTransition = true;\r\n $: if($dimensionsChanged) tickWithoutCSSTransition();\r\n\r\n\r\n \r\n</script>\r\n<Toast />\r\n<Modal >\r\n <Form id=\"task-form-modal\" taskData={taskData} {totalDays} on:submit={handleManualTimeSubmit}/>\r\n</Modal>\r\n\r\n<Modal id=\"task-meu-modal\">\r\n <Menu taskData={taskData} on:submit={handleManualTimeSubmit}/>\r\n</Modal>\r\n<div class=\"sg-gantt {classes}\" class:sg-disable-transition={!disableTransition} bind:this={ganttElement} on:click={onEvent} on:mouseover={onEvent} on:contextmenu={onEvent} on:mouseout={onEvent} on:dblclick={onEvent}>\r\n\r\n {#each ganttTableModules as module}\r\n <svelte:component this={module} {rowContainerHeight} {paddingTop} {paddingBottom} tableWidth={tableWidth} {...$$restProps} on:init=\"{onModuleInit}\" {visibleRows} />\r\n\r\n <Resizer x={tableWidth} on:resize=\"{onResize}\" container={ganttElement}></Resizer>\r\n {/each}\r\n\r\n <div class=\"sg-timeline sg-view\">\r\n <div class=\"sg-header\" bind:this={mainHeaderContainer} bind:clientHeight=\"{$headerHeight}\" class:right-scrollbar-visible=\"{rightScrollbarVisible}\">\r\n <div class=\"sg-header-scroller\" use:horizontalScrollListener>\r\n <div class=\"header-container\" style=\"width:{$_width}px\">\r\n <ColumnHeader {headers} {columnUnit} {columnOffset} {dayValue} on:dateSelected=\"{onDateSelected}\" />\r\n {#each $allTimeRanges as timeRange (timeRange.id)}\r\n <TimeRangeHeader {...timeRange} />\r\n {/each}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div class=\"sg-timeline-body\" bind:this={mainContainer} use:scrollable class:zooming=\"{zooming}\" on:wheel=\"{onwheel}\"\r\n bind:clientHeight=\"{$visibleHeight}\" bind:clientWidth=\"{$visibleWidth}\">\r\n <div class=\"content\" style=\"width:{$_width}px\">\r\n <Columns columns={columns} {columnStrokeColor} {columnStrokeWidth}/>\r\n <div class=\"sg-rows\" bind:this={rowContainer} style=\"height:{rowContainerHeight}px;\">\r\n <div style=\"transform: translateY({paddingTop}px);\">\r\n {#each visibleRows as row (row.model.id)}\r\n <Row row={row} />\r\n {/each}\r\n </div>\r\n </div>\r\n <div class=\"sg-foreground\">\r\n {#each $allTimeRanges as timeRange (timeRange.id)}\r\n <TimeRange {...timeRange} />\r\n {/each}\r\n\r\n {#each visibleTasks as task (task.model.id)}\r\n <Task left={task.left} value={task.model.value} currentDay={task.model.currentDay} from={task.model.from} to={task.model.to} duration={moment.duration(task.model.to.diff(task.model.from)).asMinutes()}\r\n width={task.width} height={task.height} top={task.top} {...task} />\r\n {/each}\r\n </div>\r\n {#each ganttBodyModules as module}\r\n <svelte:component this={module} {paddingTop} {paddingBottom} {visibleRows} {...$$restProps} on:init=\"{onModuleInit}\" />\r\n {/each}\r\n </div>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<style>\r\n .sg-disable-transition :global(.sg-task),\r\n .sg-disable-transition :global(.sg-milestone) {\r\n transition: transform 0s, background-color 0.2s, width 0s !important;\r\n }\r\n\r\n :global(.sg-view:not(:first-child)) {\r\n margin-left: 5px;\r\n }\r\n \r\n /* This class should take into account varying widths of the scroll bar */\r\n .right-scrollbar-visible {\r\n padding-right: 17px;\r\n }\r\n\r\n .sg-timeline {\r\n flex: 1 1 0%;\r\n display: flex;\r\n flex-direction: column;\r\n overflow-x: auto;\r\n }\r\n\r\n .sg-gantt {\r\n display: flex;\r\n\r\n width: 100%;\r\n height: 100%;\r\n position: relative;\r\n }\r\n\r\n .sg-foreground {\r\n box-sizing: border-box;\r\n overflow: hidden;\r\n top: 0;\r\n left: 0;\r\n position: absolute;\r\n width: 100%;\r\n height: 100%;\r\n z-index: 1;\r\n pointer-events: none;\r\n }\r\n\r\n .sg-rows {\r\n width: 100%;\r\n box-sizing: border-box;\r\n overflow: hidden;\r\n }\r\n\r\n .sg-timeline-body {\r\n overflow: auto;\r\n flex: 1 1 auto;\r\n }\r\n\r\n .sg-header {\r\n \r\n }\r\n\r\n .header-container {\r\n }\r\n\r\n .sg-header-scroller {\r\n border-right: 1px solid #efefef;\r\n overflow: hidden;\r\n position: relative;\r\n }\r\n\r\n .content {\r\n position: relative;\r\n }\r\n\r\n :global(*) {\r\n box-sizing: border-box;\r\n }\r\n\t.green {\r\n\t\tbackground: #0f0;\r\n\t}\r\n button {\r\n /* top: 460px;\r\n left: 585px; */\r\n width: 197px;\r\n height: 34px;\r\n background: #BCDD06 0% 0% no-repeat padding-box;\r\n border-radius: 4px;\r\n opacity: 1;\r\n }\r\n</style>", "<script>\n export let left;\n export let width;\n</script>\n\n<div class=\"column\" style=\"left:{left}px;width:{width}px\"></div>\n<style>\n .column {\n position: absolute;\n height: 100%;\n box-sizing: border-box;\n }\n \n .column {\n border-right: #efefef 1px solid;\n }\n</style>", "<script>\n import { createEventDispatcher, getContext } from 'svelte';\n import moment from 'moment';\n\n\n const dispatch = createEventDispatcher();\n\n import { duration as momentDuration } from 'moment';\n\n const { from, to, width } = getContext('dimensions');\n \n export let header;\n export let baseWidth;\n export let baseDuration;\n export let dayValue;\n\n export let columnWidth;\n $: {\n const offset = header.offset || 1;\n const duration = momentDuration(offset, header.unit).asMilliseconds();\n const ratio = duration / baseDuration;\n columnWidth = baseWidth * ratio;\n }\n\n export let columnCount;\n $: {\n columnCount = Math.ceil($width / columnWidth);\n if(!isFinite(columnCount)){\n console.error('columnCount is not finite');\n columnCount = 0;\n }\n }\n\n let _headers = [];\n $: {\n const headers = [];\n \n let headerTime = $from.clone().startOf(header.unit);\n\n const offset = header.offset || 1;\n\n for(let i = 0; i < columnCount; i++){\n if (header.unit === 'day') {\n headers.push({\n width: Math.min(columnWidth, $width), \n label: headerTime.format(header.format)+ \" ( Day \"+ dayValue + \" )\",\n from: headerTime.clone(),\n to: headerTime.clone().add(offset, header.unit),\n unit: header.unit\n });\n } else {\n headers.push({\n width: Math.min(columnWidth, $width), \n label: headerTime.format(header.format),\n from: headerTime.clone(),\n to: headerTime.clone().add(offset, header.unit),\n unit: header.unit\n });\n }\n \n headerTime.add(offset, header.unit);\n }\n _headers = headers;\n }\n</script>\n\n<div class=\"column-header-row\">\n {#each _headers as _header}\n <div class=\"column-header-cell\" class:sticky={header.sticky} style=\"width:{_header.width}px\" on:click=\"{() => dispatch('dateSelected', { from: _header.from, to: _header.to, unit: _header.unit })}\">\n <div class=\"column-header-cell-label\">{_header.label || 'N/A'}</div>\n </div>\n {/each}\n</div>\n<style>\n .column-header-row {\n box-sizing: border-box;\n white-space: nowrap;\n height: 32px;\n }\n\n .column-header-cell {\n display: inline-block;\n height: 100%;\n box-sizing: border-box;\n text-overflow: clip;\n /* vertical-align: top; */\n text-align: center;\n\n display: inline-flex;\n justify-content: center;\n align-items: center;\n font-size: 1em; \n font-size: 14px;\n font-weight: 300;\n transition: background 0.2s;\n\n cursor: pointer; \n user-select: none;\n\n border-right: #efefef 1px solid;\n border-bottom: #efefef 1px solid;\n }\n\n .column-header-cell:hover {\n background: #f9f9f9;\n }\n\n .column-header-cell.sticky > .column-header-cell-label {\n position: sticky;\n left: 1rem;\n }\n</style>", "<script>\n import { getContext, onMount } from 'svelte';\n \n import Column from './Column.svelte';\n /**\n * Container component for columns rendered as gantt body background\n */\n export let columns = [];\n\n export let columnStrokeWidth = 1;\n export let columnStrokeColor = '#efefef';\n\n function lineAt(ctx, x) {\n ctx.beginPath();\n ctx.moveTo(x, 0);\n ctx.lineTo(x, 20);\n ctx.stroke();\n }\n\n function createBackground(columns) {\n const canvas = document.createElement('canvas');\n canvas.width = (columns.length - 1) * columns[0].width;\n canvas.height = 20;\n\n const ctx = canvas.getContext('2d');\n ctx.shadowColor = \"rgba(128,128,128,0.5)\";\n ctx.shadowOffsetX = 0;\n ctx.shadowOffsetY = 0;\n ctx.shadowBlur = 0.5;\n ctx.lineWidth = columnStrokeWidth;\n ctx.lineCap = \"square\";\n ctx.strokeStyle = columnStrokeColor;\n ctx.translate(0.5, 0.5);\n\n columns.forEach(column => {\n lineAt(ctx, column.left);\n });\n\n const dataURL = canvas.toDataURL();\n return `url(\"${dataURL}\")`;\n }\n\n let backgroundImage;\n $: {\n backgroundImage = createBackground(columns.slice(0,5));\n }\n</script>\n\n<div class=\"sg-columns\" style=\"background-image:{backgroundImage};\">\n\t{#each columns as column}\n\t<Column left={column.left} width={column.width} />\n\t{/each}\n</div>\n<style>\n .sg-columns {\n position: absolute;\n height: 100%;\n width: 100%;\n overflow: hidden;\n\n background-repeat: repeat;\n background-position-x: -1px;\n }\n</style>", "<script>\n import { beforeUpdate, onMount, getContext } from 'svelte';\n\n let milestoneElement;\n\n import { Draggable } from '../core/drag';\n import { rowStore, taskStore } from '../core/store';\n const { rowPadding } = getContext('options');\n const { selectionManager, api, rowContainer, dndManager, columnService} = getContext('services');\n\n export let left;\n export let top;\n export let model;\n export let height = 20;\n\n const selection = selectionManager.selection;\n\n let dragging = false;\n let x = null;\n let y = null;\n $: {\n if(!dragging){\n x = left, y = top;\n }\n }\n\n function drag(node) {\n const ondrop = ({ x, y, currWidth, event, dragging }) => {\n let rowChangeValid = true;\n //row switching\n if(dragging){\n const sourceRow = $rowStore.entities[model.resourceId];\n const targetRow = dndManager.getTarget('row', event);\n if(targetRow){\n model.resourceId = targetRow.model.id;\n api.tasks.raise.switchRow(this, targetRow, sourceRow);\n }\n else{\n rowChangeValid = false;\n }\n }\n \n dragging = false;\n const task = $taskStore.entities[model.id];\n if(rowChangeValid) {\n const newFrom = utils.roundTo(columnService.getDateByPosition(x)); \n const newLeft = columnService.getPositionByDate(newFrom);\n\n Object.assign(model, {\n from: newFrom\n });\n \n $taskStore.update({\n ...task,\n left: newLeft,\n top: rowPadding + $rowStore.entities[model.resourceId].y,\n model\n });\n }\n else {\n // reset position\n $taskStore.update({\n ...task\n });\n }\n }\n\n const draggable = new Draggable(node, {\n onDown: ({x, y}) => {\n //this.set({x, y});\n }, \n onDrag: (pos) => {\n x = pos.x, y = pos.y, dragging = true;\n },\n dragAllowed: () => {\n return row.model.enableDragging && model.enableDragging;\n },\n resizeAllowed: false,\n onDrop: ondrop, \n container: rowContainer, \n getX: () => x,\n getY: () => y\n });\n\n return {\n destroy() { draggable.destroy(); }\n }\n }\n\n onMount(() => {\n x = left = columnService.getPositionByDate(model.from); \n y = top = row.y + $rowPadding;; \n height = row.height - 2 * $rowPadding;\n });\n\n export function select(event) {\n if(event.ctrlKey){\n selectionManager.toggleSelection(model.id);\n }\n else{\n selectionManager.selectSingle(model.id);\n }\n \n if(selected){\n api.tasks.raise.select(model);\n }\n }\n\n let selected = false;\n $: selected = $selection.indexOf(model.id) !== -1;\n\n let row;\n $: row = $rowStore.entities[model.resourceId];\n</script>\n\n<div bind:this={milestoneElement}\n class=\"sg-milestone {model.classes}\" \n style=\"transform: translate({x}px, {y}px);height:{height}px;width:{height}px\"\n use:drag \n on:click=\"{select}\"\n class:selected=\"{selected}\"\n class:moving=\"{dragging}\">\n <div class=\"inside\"></div>\n <!-- <span class=\"debug\">x:{x|0} y:{y|0}, x:{left|0} y:{top|0}</span> -->\n</div>\n\n<style>\n .sg-milestone {\n\t\tposition: absolute; \n top: 0;\n bottom: 0;\n\n white-space: nowrap;\n /* overflow: hidden; */\n\n height: 20px;\n width: 20px;\n\n min-width: 40px;\n margin-left: -20px;\n display: flex;\n align-items: center;\n flex-direction: column;\n\n transition: background-color 0.2s, opacity 0.2s;\n }\n\n .sg-milestone .inside {\n position: relative;\n }\n\n .sg-milestone .inside:before {\n position: absolute;\n top: 0;\n left: 0;\n content: ' ';\n height: 28px;\n width: 28px;\n transform-origin: 0 0;\n transform: rotate(45deg); \n /* //after -45 */\n background-color: #feac31;\n border-color: #feac31;\n }\n\n .sg-milestone:not(.moving) {\n transition: transform 0.2s, background-color 0.2s, width 0.2s;\n }\n\n .sg-milestone.moving{\n z-index: 1;\n }\n\n .sg-milestone.selected {\n outline: 2px solid rgba(3, 169, 244, 0.5);\n outline-offset: 3px;\n z-index: 1;\n }\n</style>", "<script>\n import { getContext } from 'svelte';\n export let row;\n let rowElement;\n\n const { rowHeight } = getContext('options');\n const { hoveredRow, selectedRow } = getContext('gantt');\n</script>\n\n<div class=\"sg-row {row.model.classes}\" data-row-id=\"{row.model.id}\" class:sg-hover={$hoveredRow == row.model.id} class:sg-selected={$selectedRow == row.model.id} bind:this={rowElement} style=\"height:{$rowHeight}px\">\n {#if row.model.contentHtml}\n {@html row.model.contentHtml}\n {/if}\n</div> \n<style>\n .sg-row {\n position: relative;\n width: 100%;\n box-sizing: border-box;\n border-bottom: #efefef 1px solid;\n\n }\n</style>", "<script>\n import {\n beforeUpdate,\n afterUpdate,\n getContext,\n onMount,\n onDestroy,\n tick,\n } from \"svelte\";\n import moment from \"moment\";\n\n import { setCursor } from \"src/utils/domUtils\";\n import { taskStore, rowStore } from \"../core/store\";\n import { Draggable } from \"../core/drag\";\n import { reflectTask } from \"src/core/task\";\n export let model;\n export let height;\n export let left;\n export let top;\n export let width;\n export let from;\n export let to;\n export let duration;\n export let value;\n export let currentDay;\n export let reflected = false;\n\n let animating = true;\n\n let _dragging = false;\n let _resizing = false;\n\n let _position = {\n x: left,\n y: top,\n width: width,\n };\n let _time = {\n start: from,\n end: to,\n duration: duration,\n };\n\n $: updatePosition(left, top, width);\n function updatePosition(x, y, width) {\n if (!_dragging && !_resizing) {\n _position.x = x;\n _position.y = y; //row.y + 6;\n _position.width = width;\n // should NOT animate on resize/update of columns\n }\n }\n $: updateTime(from, to, duration);\n function updateTime(start, end, duration) {\n if (!_dragging && !_resizing) {\n _time.start = start;\n _time.end = end; //row.y + 6;\n _time.duration = duration;\n // should NOT animate on resize/update of columns\n }\n }\n const { dimensionsChanged } = getContext(\"dimensions\");\n const { rowContainer } = getContext(\"gantt\");\n const {\n taskContent,\n resizeHandleWidth,\n rowPadding,\n onTaskButtonClick,\n reflectOnParentRows,\n reflectOnChildRows,\n } = getContext(\"options\");\n const { dndManager, api, utils, selectionManager, columnService } =\n getContext(\"services\");\n\n function drag(node) {\n const ondrop = (event) => {\n let rowChangeValid = true;\n //row switching\n const sourceRow = $rowStore.entities[model.resourceId];\n if (event.dragging) {\n const targetRow = dndManager.getTarget(\"row\", event.mouseEvent);\n if (targetRow) {\n model.resourceId = targetRow.model.id;\n api.tasks.raise.switchRow(this, targetRow, sourceRow);\n } else {\n rowChangeValid = false;\n }\n }\n\n _dragging = _resizing = false;\n\n const task = $taskStore.entities[model.id];\n\n if (rowChangeValid) {\n const prevFrom = model.from;\n const prevTo = model.to;\n const newFrom = (model.from = utils.roundTo(\n columnService.getDateByPosition(event.x)\n ));\n const newTo = (model.to = utils.roundTo(\n columnService.getDateByPosition(event.x + event.width)\n ));\n const duration = moment.duration(newTo.diff(newFrom)).asMinutes();\n const newLeft = columnService.getPositionByDate(newFrom) | 0;\n const newRight = columnService.getPositionByDate(newTo) | 0;\n\n const targetRow = $rowStore.entities[model.resourceId];\n const left = newLeft;\n const width = newRight - newLeft;\n const top = $rowPadding + targetRow.y;\n updatePosition(left, top, width);\n updateTime(newFrom, newTo, duration);\n\n const newTask = {\n ...task,\n left: left,\