perf: 优化甘特图

This commit is contained in:
kuaifan 2022-03-15 18:39:46 +08:00
parent b83ce02849
commit 5adbd6e8f1
6 changed files with 387 additions and 455 deletions

View File

@ -1,5 +1,5 @@
<template>
<div class="wook-gantt">
<div class="common-gantt">
<div class="gantt-left" :style="{width:menuWidth+'px'}">
<div class="gantt-title">
<div class="gantt-title-text">{{$L('任务名称')}}</div>
@ -26,7 +26,7 @@
<li v-for="(item, key) in dateNum" :key="key" :style="dateStyle(key)">
<div class="date-format">
<div class="format-day">{{dateFormat(key, 'day')}}</div>
<div v-if="dateWidth > 46" class="format-wook">{{dateFormat(key, 'wook')}}</div>
<div v-if="dateWidth > 46" class="format-week">{{dateFormat(key, 'week')}}</div>
</div>
</li>
</ul>
@ -39,7 +39,7 @@
class="timeline-item"
:style="itemStyle(item)"
@mousedown="itemMouseDown($event, item)">
<div class="timeline-title">{{item.label}}</div>
<div class="timeline-title" :title="item.label">{{item.label}}</div>
<div class="timeline-resizer"></div>
</div>
</li>
@ -184,7 +184,7 @@ export default {
let date = new Date(new Date().getTime() + j * 86400000)
if (type == 'day') {
return date.getDate();
} else if (type == 'wook') {
} else if (type == 'week') {
return this.$L(`星期${'日一二三四五六'.charAt(date.getDay())}`);
} else {
return date;
@ -291,7 +291,6 @@ export default {
type: type,
clientX: e.clientX,
value: item[type],
time: item.time,
};
this.mouseItem = item;
this.dateMove = null;
@ -299,18 +298,16 @@ export default {
itemMouseMove(e) {
if (this.mouseItem != null) {
e.preventDefault();
var diff = e.clientX - this.mouseBak.clientX;
const {start, end} = this.mouseBak.time;
let date = new Date();
let nowTime = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0).getTime();
let diffStartDay = (start - nowTime) / 1000 / 60 / 60 / 24;
let diffEndDay = (end - nowTime) / 1000 / 60 / 60 / 24;
let width = this.dateWidth * (diffEndDay - diffStartDay);
width += this.mouseBak.value + diff;
if (width <= 0) {
return false;
const diff = this.mouseBak.value + (e.clientX - this.mouseBak.clientX);
if (this.mouseBak.type === 'moveW') {
const oneWidthTime = 86400000 / this.dateWidth;
const {start, end} = this.mouseItem.time;
let moveTime = diff * oneWidthTime;
if (end + moveTime - start <= 0) {
return
}
}
this.$set(this.mouseItem, this.mouseBak.type, this.mouseBak.value + diff);
this.$set(this.mouseItem, this.mouseBak.type, diff);
} else if (this.dateMove != null) {
e.preventDefault();
let moveX = (this.dateMove.clientX - e.clientX) * 5;
@ -340,9 +337,6 @@ export default {
this.$set(this.mouseItem, 'moveW', 0);
isM = true;
}
if (this.mouseItem.time && this.mouseItem.time.start > this.mouseItem.time.end) {
return false;
}
//
if (isM) {
this.$emit("on-change", this.mouseItem)
@ -371,3 +365,265 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.common-gantt {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: self-start;
color: #747a81;
* {
box-sizing: border-box;
}
.gantt-left {
flex-grow:0;
flex-shrink:0;
height: 100%;
background-color: #ffffff;
position: relative;
display: flex;
flex-direction: column;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1px;
background-color: rgba(237, 241, 242, 0.75);
}
.gantt-title {
height: 76px;
flex-grow: 0;
flex-shrink: 0;
background-color: #F9FAFB;
padding-left: 12px;
overflow: hidden;
.gantt-title-text {
line-height: 100px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
}
.gantt-item {
transform: translateZ(0);
max-height: 100%;
overflow: auto;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
> li {
height: 40px;
border-bottom: 1px solid rgba(237, 241, 242, 0.75);
position: relative;
display: flex;
align-items: center;
padding-left: 12px;
&:hover {
.item-icon {
display: flex;
}
}
.item-overdue {
flex-grow:0;
flex-shrink:0;
color: #ffffff;
margin-right: 4px;
background-color: #ff0000;
padding: 1px 3px;
border-radius: 3px;
font-size: 12px;
line-height: 18px;
}
.item-title {
flex: 1;
padding-right: 12px;
cursor: default;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.complete {
text-decoration: line-through;
}
&.overdue {
font-weight: 600;
}
}
.item-icon {
display: none;
align-items: center;
justify-content: center;
width: 32px;
margin-right: 2px;
font-size: 16px;
color: #888888;
}
}
}
}
.gantt-right {
flex: 1;
height: 100%;
background-color: #ffffff;
position: relative;
overflow: hidden;
.gantt-chart {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: translateZ(0);
.gantt-month {
display: flex;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
height: 26px;
line-height: 20px;
font-size: 14px;
background-color: #F9FAFB;
> li {
flex-grow: 0;
flex-shrink: 0;
height: 100%;
position: relative;
overflow: hidden;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: rgba(237, 241, 242, 0.75);
}
.month-format {
overflow: hidden;
white-space: nowrap;
padding: 6px 6px 0;
}
}
}
.gantt-date {
display: flex;
align-items: center;
position: absolute;
top: 26px;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
cursor: move;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #F9FAFB;
}
> li {
flex-grow: 0;
flex-shrink: 0;
height: 100%;
position: relative;
overflow: hidden;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: rgba(237, 241, 242, 0.75);
}
.date-format {
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 44px;
.format-day {
line-height: 28px;
font-size: 18px;
}
.format-week {
line-height: 16px;
font-weight: 300;
font-size: 13px;
}
}
}
}
.gantt-timeline {
position: absolute;
top: 76px;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
overflow-x: hidden;
overflow-y: auto;
> li {
cursor: default;
height: 40px;
border-bottom: 1px solid rgba(237, 241, 242, 0.75);
position: relative;
.timeline-item {
position: absolute;
top: 0;
touch-action: none;
pointer-events: auto;
padding: 4px;
margin-top: 4px;
background: #e74c3c;
border-radius: 18px;
color: #fff;
display: flex;
align-items: center;
will-change: contents;
height: 32px;
.timeline-title {
touch-action: none;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 4px;
margin-right: 10px;
}
.timeline-resizer {
height: 22px;
touch-action: none;
width: 8px;
background: rgba(255,255,255,0.1);
cursor: ew-resize;
flex-shrink: 0;
will-change: visibility;
position: absolute;
top: 5px;
right: 5px;
}
}
}
}
}
}
}
</style>

View File

@ -386,24 +386,6 @@
getDialogUnread(dialog) {
return dialog ? (dialog.unread || dialog.is_mark_unread || 0) : 0
},
/**
* 克隆对象
* @param myObj
* @returns {*}
*/
cloneData(myObj) {
if (typeof (myObj) !== 'object') return myObj;
if (myObj === null) return myObj;
//
if (typeof myObj.length === 'number') {
let [...myNewObj] = myObj;
return myNewObj;
} else {
let {...myNewObj} = myObj;
return myNewObj;
}
},
});
/**

View File

@ -1,20 +1,26 @@
<template>
<div class="project-gstc-gantt">
<GanttView :lists="lists" :menuWidth="windowWidth ? 180 : 260" :itemWidth="80" @on-change="updateTime" @on-click="clickItem"/>
<Dropdown class="project-gstc-dropdown-filtr" :style="windowWidth?{left:'142px'}:{}" @on-click="tapProject">
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId>0}" type="md-funnel" />
<GanttView
:lists="lists"
:menuWidth="menuWidth"
:itemWidth="80"
@on-change="onChange"
@on-click="onClick"/>
<Dropdown class="project-gstc-dropdown-filtr" :style="dropStyle" trigger="click" @on-click="onSwitchColumn">
<Icon class="project-gstc-dropdown-icon" :class="{filtr:filtrProjectId > 0}" type="md-funnel" />
<DropdownMenu slot="list">
<DropdownItem :name="0" :class="{'dropdown-active':filtrProjectId==0}">{{$L('全部')}}</DropdownItem>
<DropdownItem v-for="(item, index) in projectLabel"
:key="index"
:name="item.id"
:class="{'dropdown-active':filtrProjectId==item.id}">
{{item.name}}
<span v-if="item.tasks">({{item.tasks.length}})</span>
<DropdownItem :name="0" :class="{'dropdown-active':filtrProjectId == 0}">{{ $L('全部') }}</DropdownItem>
<DropdownItem
v-for="(item, index) in projectColumn"
:key="index"
:name="item.id"
:class="{'dropdown-active':filtrProjectId == item.id}">
{{ item.name }}
<span v-if="item.tasks">({{ item.tasks.length }})</span>
</DropdownItem>
</DropdownMenu>
</Dropdown>
<div class="project-gstc-edit" :class="{info:editShowInfo, visible:editData&&editData.length > 0}">
<div class="project-gstc-edit" :class="{info:editShowInfo, visible:editData && editData.length > 0}">
<div class="project-gstc-edit-info">
<Table size="small" max-height="600" :columns="editColumns" :data="editData"></Table>
<div class="project-gstc-edit-btns">
@ -33,85 +39,83 @@
</template>
<script>
import GanttView from "./GanttView";
import {mapState} from "vuex";
import GanttView from "../../../components/GanttView";
/**
* 甘特图
*/
export default {
name: 'ProjectGantt',
components: {GanttView },
components: {GanttView},
props: {
projectLabel: {
projectColumn: {
default: []
},
lineTaskData: {
default: []
},
levelList: {},
},
data () {
data() {
return {
loadFinish: false,
lists: [],
editColumns: [],
editData: [],
editShowInfo: false,
editLoad: 0,
filtrProjectId: 0,
editColumns: [
{
title: this.$L('任务名称'),
key: 'label',
minWidth: 150,
ellipsis: true,
}, {
title: this.$L('原计划时间'),
minWidth: 135,
align: 'center',
render: (h, {row}) => {
if (row.notime === true) {
return h('span', '-');
}
return h('div', {
style: {},
}, [
h('div', $A.formatDate('Y-m-d H:i', Math.round(row.baktime.start / 1000))),
h('div', $A.formatDate('Y-m-d H:i', Math.round(row.baktime.end / 1000)))
]);
}
}, {
title: this.$L('新计划时间'),
minWidth: 135,
align: 'center',
render: (h, {row}) => {
return h('div', {
style: {},
}, [
h('div', $A.formatDate('Y-m-d H:i', Math.round(row.newTime.start / 1000))),
h('div', $A.formatDate('Y-m-d H:i', Math.round(row.newTime.end / 1000)))
]);
}
}
],
editData: [],
editLoad: 0,
editShowInfo: false,
}
},
mounted() {
this.editColumns = [
{
title: this.$L('任务名称'),
key: 'label',
minWidth: 150,
ellipsis: true,
}, {
title: this.$L('原计划时间'),
minWidth: 135,
align: 'center',
render: (h, params) => {
if (params.row.notime === true) {
return h('span', '-');
}
return h('div', {
style: {},
}, [
h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.backTime.start / 1000))),
h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.backTime.end / 1000)))
]);
}
}, {
title: this.$L('新计划时间'),
minWidth: 135,
align: 'center',
render: (h, params) => {
return h('div', {
style: {},
}, [
h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.newTime.start / 1000))),
h('div', $A.formatDate('Y-m-d H:i', Math.round(params.row.newTime.end / 1000)))
]);
}
}
];
//
this.initData();
this.loadFinish = true;
},
computed: {
...mapState(['userId', 'windowWidth', 'taskPriority']),
menuWidth() {
return this.windowWidth < 1440 ? 180 : 260;
},
dropStyle() {
return this.windowWidth < 1440 ? {left: '142px'} : {};
}
},
watch:{
projectLabel: {
watch: {
projectColumn: {
handler() {
this.initData();
},
@ -120,13 +124,18 @@ export default {
},
methods: {
verifyLists(item){
initData() {
this.lists = [];
this.projectColumn && this.projectColumn.some(this.checkAdd);
},
checkAdd(item) {
if (this.filtrProjectId > 0) {
if (item.id != this.filtrProjectId) {
return;
}
}
item.tasks && item.tasks.forEach((taskData) => {
item.tasks && item.tasks.some(taskData => {
let notime = !taskData.start_at || !taskData.end_at;
let times = this.getTimeObj(taskData);
let start = times.start;
@ -134,10 +143,10 @@ export default {
//
let color = '#058ce4';
if (taskData.complete_at) {
return;
return;
} else {
//
this.levelList.some(level => {
this.taskPriority.some(level => {
if (level.priority === taskData.p_level) {
color = level.color;
return true;
@ -145,11 +154,11 @@ export default {
});
}
//
let tempTime = { start, end };
let findData = this.editData.find((t) => { return t.id == taskData.id });
let tempTime = {start, end};
let bakTime = $A.cloneJSON(tempTime)
let findData = this.editData.find(({id}) => id == taskData.id);
if (findData) {
findData.backTime = $A.cloneData(tempTime)
tempTime = $A.cloneData(findData.newTime);
tempTime = $A.cloneJSON(findData.newTime);
}
//
this.lists.push({
@ -159,108 +168,80 @@ export default {
overdue: taskData.overdue,
time: tempTime,
notime: notime,
style: { background: color },
baktime: bakTime,
style: {background: color},
});
});
},
initData() {
this.lists = [];
this.projectLabel && this.projectLabel.forEach((item) => {
this.verifyLists(item);
});
if (this.lists&&this.lists.length == 0) {
this.lineTaskData && this.lineTaskData.forEach((item) => {
this.verifyLists(item);
});
}
//
if (this.lists&&this.lists.length == 0 && this.filtrProjectId == 0) {
$A.modalWarning({
content: '任务列表为空,请先添加任务。',
onOk: () => {
this.$emit('on-close');
},
});
}
},
updateTime(item) {
let original = this.getRawTime(item.id);
if (Math.abs(original.end - item.time.end) > 1000 || Math.abs(original.start - item.time.start) > 1000) {
onChange(item) {
const {time, baktime} = item;
if (Math.abs(baktime.end - time.end) > 1000 || Math.abs(baktime.start - time.start) > 1000) {
//1)
let backTime = $A.cloneData(original);
let newTime = $A.cloneData(item.time);
let findData = this.editData.find(({id}) => id == item.id);
if (findData) {
findData.newTime = newTime;
findData.newTime = time;
} else {
this.editData.push({
id: item.id,
label: item.label,
notime: item.notime,
backTime,
newTime,
baktime: item.baktime,
newTime: time,
})
}
}
},
clickItem(item) {
onClick(item) {
this.$store.dispatch("openTask", item);
},
editSubmit(save) {
this.editData&&this.editData.forEach((item) => {
this.editData && this.editData.forEach(item => {
let task = this.lists.find(({id}) => id == item.id)
if (save) {
this.editLoad++;
let timeStart = $A.formatDate('Y-m-d H:i', Math.round(item.newTime.start / 1000));
let timeEnd = $A.formatDate('Y-m-d H:i', Math.round(item.newTime.end / 1000));
let dataJson = {
task_id: item.id,
times:[timeStart,timeEnd],
times: [timeStart, timeEnd],
};
this.$store.dispatch("taskUpdate", dataJson).then(({msg}) => {
$A.messageSuccess(msg);
this.editLoad--;
if (typeof successCallback === "function") successCallback();
this.editLoad === 0 && $A.messageSuccess(msg);
task && this.$set(task, 'baktime', $A.cloneJSON(task.time));
}).catch(({msg}) => {
$A.modalError(msg);
this.editLoad--;
this.editLoad === 0 && $A.modalError(msg);
task && this.$set(task, 'time', $A.cloneJSON(task.baktime));
})
} else {
this.lists.some((task) => {
if (task.id == item.id) {
this.$set(task, 'time', item.backTime);
return true;
}
})
task && this.$set(task, 'time', $A.cloneJSON(task.baktime));
}
});
this.editData = [];
},
getRawTime(taskId) {
let times = null;
this.lists.some((taskData) => {
if (taskData.id == taskId) {
times = this.getTimeObj(taskData);
}
});
return times;
let task = this.lists.find(({id}) => id == taskId)
return task ? this.getTimeObj(task) : null;
},
getTimeObj(taskData) {
let start = $A.Time(taskData.start_at) || $A.Time(taskData.created_at);
let end = $A.Time(taskData.end_at) || ($A.Time(taskData.created_at) + 86400);
if (end == start) {
end = Math.round(new Date($A.formatDate('Y-m-d 23:59:59', end)).getTime()/1000);
end = Math.round(new Date($A.formatDate('Y-m-d 23:59:59', end)).getTime() / 1000);
}
end = Math.max(end, start + 60);
start*= 1000;
end*= 1000;
start *= 1000;
end *= 1000;
return {start, end};
},
tapProject(e) {
onSwitchColumn(e) {
this.filtrProjectId = $A.runNum(e);
this.initData();
},

View File

@ -314,11 +314,8 @@
</div>
</div>
<div v-else-if="tabTypeActive === 'gantt'" class="project-gantt">
<ProjectGantt
:lineData="ganttColumnList[0].tasks"
:projectLabel="ganttColumnList"
:lineTaskData="ganttColumnList[0].tasks"
:levelList="taskPriority"/>
<!--甘特图-->
<ProjectGantt :projectColumn="columnList"/>
</div>
<!--项目设置-->
<Modal
@ -556,8 +553,6 @@ export default {
'userId',
'cacheDialogs',
'taskPriority',
'projectId',
'projectLoad',
'cacheTasks',
@ -685,26 +680,6 @@ export default {
return list;
},
ganttColumnList() {
const {projectId, cacheColumns} = this;
return cacheColumns.filter((row) => {
row.tasks = row.tasks.filter(task => {
return task.column_id == row.id && !task.complete_at;
}).sort((a, b) => {
if (a.sort != b.sort) {
return a.sort - b.sort;
}
return a.id - b.id;
});
return row.project_id == projectId && row.tasks.length > 0;
}).sort((a, b) => {
if (a.sort != b.sort) {
return a.sort - b.sort;
}
return a.id - b.id;
});
},
myList() {
const {allTask, taskCompleteTemps, sortField, sortType} = this;
let array = allTask.filter(task => this.myFilter(task));

View File

@ -2,6 +2,7 @@
@import "file-content";
@import "project-archived";
@import "project-dialog";
@import "project-gantt";
@import "project-list";
@import "project-log";
@import "project-management";
@ -14,5 +15,3 @@
@import "task-menu";
@import "task-priority";
@import "team-management";
@import "project-gantt";
@import "gantt-view";

View File

@ -1,261 +0,0 @@
.wook-gantt {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: self-start;
color: #747a81;
* {
box-sizing: border-box;
}
.gantt-left {
flex-grow:0;
flex-shrink:0;
height: 100%;
background-color: #ffffff;
position: relative;
display: flex;
flex-direction: column;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 1px;
background-color: rgba(237, 241, 242, 0.75);
}
.gantt-title {
height: 76px;
flex-grow: 0;
flex-shrink: 0;
background-color: #F9FAFB;
padding-left: 12px;
overflow: hidden;
.gantt-title-text {
line-height: 100px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
}
.gantt-item {
transform: translateZ(0);
max-height: 100%;
overflow: auto;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
> li {
height: 40px;
border-bottom: 1px solid rgba(237, 241, 242, 0.75);
position: relative;
display: flex;
align-items: center;
padding-left: 12px;
&:hover {
.item-icon {
display: flex;
}
}
.item-overdue {
flex-grow:0;
flex-shrink:0;
color: #ffffff;
margin-right: 4px;
background-color: #ff0000;
padding: 1px 3px;
border-radius: 3px;
font-size: 12px;
line-height: 18px;
}
.item-title {
flex: 1;
padding-right: 12px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.complete {
text-decoration: line-through;
}
&.overdue {
font-weight: 600;
}
}
.item-icon {
display: none;
align-items: center;
justify-content: center;
width: 32px;
margin-right: 2px;
font-size: 16px;
color: #888888;
cursor: pointer;
}
}
}
}
.gantt-right {
flex: 1;
height: 100%;
background-color: #ffffff;
position: relative;
overflow: hidden;
.gantt-chart {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: translateZ(0);
.gantt-month {
display: flex;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
height: 26px;
line-height: 20px;
font-size: 14px;
background-color: #F9FAFB;
> li {
flex-grow: 0;
flex-shrink: 0;
height: 100%;
position: relative;
overflow: hidden;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: rgba(237, 241, 242, 0.75);
}
.month-format {
overflow: hidden;
white-space: nowrap;
padding: 6px 6px 0;
}
}
}
.gantt-date {
display: flex;
align-items: center;
position: absolute;
top: 26px;
left: 0;
right: 0;
bottom: 0;
z-index: 2;
cursor: move;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: #F9FAFB;
}
> li {
flex-grow: 0;
flex-shrink: 0;
height: 100%;
position: relative;
overflow: hidden;
&:after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 100%;
background-color: rgba(237, 241, 242, 0.75);
}
.date-format {
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 44px;
.format-day {
line-height: 28px;
font-size: 18px;
}
.format-wook {
line-height: 16px;
font-weight: 300;
font-size: 13px;
}
}
}
}
.gantt-timeline {
position: absolute;
top: 76px;
left: 0;
right: 0;
bottom: 0;
z-index: 3;
overflow-x: hidden;
overflow-y: auto;
> li {
cursor: default;
height: 40px;
border-bottom: 1px solid rgba(237, 241, 242, 0.75);
position: relative;
.timeline-item {
position: absolute;
top: 0;
touch-action: none;
pointer-events: auto;
padding: 4px;
margin-top: 4px;
background: #e74c3c;
border-radius: 18px;
color: #fff;
display: flex;
align-items: center;
will-change: contents;
height: 32px;
.timeline-title {
touch-action: none;
flex-grow: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 4px;
margin-right: 10px;
cursor:pointer;
}
.timeline-resizer {
height: 22px;
touch-action: none;
width: 8px;
background: rgba(255,255,255,0.1);
cursor: ew-resize;
flex-shrink: 0;
will-change: visibility;
position: absolute;
top: 5px;
right: 5px;
}
}
}
}
}
}
}