diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7159d..5afa6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.10] - 2025-07-06 + +### Added +- 支持多前置任务功能 +- 增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar +- 点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar + +### Changed +- 支持多前置任务功能 +- Timeline中sub-task隐藏后,关系线也随之隐藏 + ## [1.0.9] - 2025-07-04 ### Changed diff --git a/demo/App.vue b/demo/App.vue index 92d9faf..32dcd33 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -10,6 +10,7 @@ import '../src/styles/theme-variables.css' import VersionHistoryDrawer from './VersionHistoryDrawer.vue' import { useMessage } from '../src/composables/useMessage' import { useI18n } from '../src/composables/useI18n' +import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import type { Task } from '../src/models/Task' const { showMessage } = useMessage() @@ -550,11 +551,8 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => { const cleanupTaskArray = (taskArray: Task[]) => { taskArray.forEach(task => { if (task.predecessor) { - // 将predecessor字符串解析为ID数组 - const predecessorIds = task.predecessor - .split(',') - .map(id => parseInt(id.trim())) - .filter(id => !isNaN(id)) + // 使用工具函数获取前置任务ID数组 + const predecessorIds = getPredecessorIds(task.predecessor) // 过滤掉被删除的任务ID const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id)) @@ -563,7 +561,7 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => { if (validPredecessorIds.length === 0) { delete task.predecessor } else { - task.predecessor = validPredecessorIds.join(',') + task.predecessor = predecessorIdsToString(validPredecessorIds) } } diff --git a/demo/data.json b/demo/data.json index 8da5a0a..376d687 100644 --- a/demo/data.json +++ b/demo/data.json @@ -38,7 +38,7 @@ { "id": 4, "name": "调研A", - "predecessor": "2", + "predecessor": [2], "assignee": "赵六", "startDate": "2025-06-21", "endDate": "2025-06-23", @@ -51,7 +51,7 @@ { "id": 5, "name": "调研B", - "predecessor": "4", + "predecessor": [4], "assignee": "钱七", "startDate": "2025-06-24", "endDate": "2025-06-28", @@ -88,7 +88,7 @@ "estimatedHours": 80, "actualHours": 35, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 }, { @@ -101,7 +101,7 @@ "estimatedHours": 80, "actualHours": 0, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 }, { @@ -114,7 +114,7 @@ "estimatedHours": 88, "actualHours": 0, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 } ], @@ -141,7 +141,7 @@ "estimatedHours": 48, "actualHours": 0, "type": "task", - "predecessor": "7", + "predecessor": [8, 11, 12], "parentId": 13 }, { @@ -154,7 +154,7 @@ "estimatedHours": 24, "actualHours": 0, "type": "task", - "predecessor": "7", + "predecessor": [14], "parentId": 13 } ], diff --git a/demo/version-history.json b/demo/version-history.json index 5b39e5b..4daf2be 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -124,5 +124,16 @@ "增加Story删除时选择是否删除子任务的选项", "删除Story或Task时同时删除前后的关系线路" ] + }, + { + "version": "1.0.10", + "date": "2025-07-06", + "notes": [ + "升级用户体验", + "增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar", + "点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar", + "Timeline中sub-task隐藏后,关系线也随之隐藏", + "增强TaskBar可以存在多个前置任务" + ] } ] diff --git a/package.json b/package.json index 5aafb53..97fe18e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jordium-gantt-vue3", - "version": "1.0.9", + "version": "1.0.10", "type": "module", "main": "dist/jordium-gantt-vue3.cjs.js", "module": "dist/jordium-gantt-vue3.es.js", diff --git a/packageDemo/App.vue b/packageDemo/App.vue index 92d9faf..32dcd33 100644 --- a/packageDemo/App.vue +++ b/packageDemo/App.vue @@ -10,6 +10,7 @@ import '../src/styles/theme-variables.css' import VersionHistoryDrawer from './VersionHistoryDrawer.vue' import { useMessage } from '../src/composables/useMessage' import { useI18n } from '../src/composables/useI18n' +import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import type { Task } from '../src/models/Task' const { showMessage } = useMessage() @@ -550,11 +551,8 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => { const cleanupTaskArray = (taskArray: Task[]) => { taskArray.forEach(task => { if (task.predecessor) { - // 将predecessor字符串解析为ID数组 - const predecessorIds = task.predecessor - .split(',') - .map(id => parseInt(id.trim())) - .filter(id => !isNaN(id)) + // 使用工具函数获取前置任务ID数组 + const predecessorIds = getPredecessorIds(task.predecessor) // 过滤掉被删除的任务ID const validPredecessorIds = predecessorIds.filter(id => !deletedTaskIds.includes(id)) @@ -563,7 +561,7 @@ const cleanupPredecessorReferences = (deletedTaskIds: number[]) => { if (validPredecessorIds.length === 0) { delete task.predecessor } else { - task.predecessor = validPredecessorIds.join(',') + task.predecessor = predecessorIdsToString(validPredecessorIds) } } diff --git a/packageDemo/data.json b/packageDemo/data.json index 8da5a0a..376d687 100644 --- a/packageDemo/data.json +++ b/packageDemo/data.json @@ -38,7 +38,7 @@ { "id": 4, "name": "调研A", - "predecessor": "2", + "predecessor": [2], "assignee": "赵六", "startDate": "2025-06-21", "endDate": "2025-06-23", @@ -51,7 +51,7 @@ { "id": 5, "name": "调研B", - "predecessor": "4", + "predecessor": [4], "assignee": "钱七", "startDate": "2025-06-24", "endDate": "2025-06-28", @@ -88,7 +88,7 @@ "estimatedHours": 80, "actualHours": 35, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 }, { @@ -101,7 +101,7 @@ "estimatedHours": 80, "actualHours": 0, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 }, { @@ -114,7 +114,7 @@ "estimatedHours": 88, "actualHours": 0, "type": "task", - "predecessor": "5", + "predecessor": [5], "parentId": 7 } ], @@ -141,7 +141,7 @@ "estimatedHours": 48, "actualHours": 0, "type": "task", - "predecessor": "7", + "predecessor": [8, 11, 12], "parentId": 13 }, { @@ -154,7 +154,7 @@ "estimatedHours": 24, "actualHours": 0, "type": "task", - "predecessor": "7", + "predecessor": [14], "parentId": 13 } ], diff --git a/packageDemo/version-history.json b/packageDemo/version-history.json index 5b39e5b..4daf2be 100644 --- a/packageDemo/version-history.json +++ b/packageDemo/version-history.json @@ -124,5 +124,16 @@ "增加Story删除时选择是否删除子任务的选项", "删除Story或Task时同时删除前后的关系线路" ] + }, + { + "version": "1.0.10", + "date": "2025-07-06", + "notes": [ + "升级用户体验", + "增加边框粘性toolBar,表示隐藏的TaskBar和MilestoneBar", + "点击边框粘性toolBar,快速定位滑动到对应的TaskBar和MilestoneBar", + "Timeline中sub-task隐藏后,关系线也随之隐藏", + "增强TaskBar可以存在多个前置任务" + ] } ] diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index 6a7d2c7..6380190 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -4,6 +4,7 @@ import TaskList from './TaskList.vue' import Timeline from './Timeline.vue' import GanttToolbar from './GanttToolbar.vue' import { useI18n, setCustomMessages } from '../composables/useI18n' +import { formatPredecessorDisplay } from '../utils/predecessorUtils' import jsPDF from 'jspdf' import html2canvas from 'html2canvas' import type { Task } from '../models/classes/Task' @@ -222,16 +223,35 @@ const toggleTaskList = () => { isTaskListVisible.value = !isTaskListVisible.value }, 200) - // 动画结束后清理状态 + // 动画结束后清理状态,并通知Timeline容器变化 setTimeout(() => { isAnimating.value = false animationClass.value = '' + + // 手动切换TaskList后,通知Timeline重新计算半圆 + nextTick(() => { + window.dispatchEvent( + new CustomEvent('timeline-container-resized', { + detail: { source: 'manual-task-list-toggle' }, + }), + ) + }) }, 400) } // 监听Timeline的TaskList切换事件 const handleToggleTaskList = (event: CustomEvent) => { isTaskListVisible.value = event.detail + + // TaskList切换会改变Timeline容器宽度,需要通知Timeline重新计算半圆 + // 派发事件通知Timeline容器宽度发生了变化 + nextTick(() => { + window.dispatchEvent( + new CustomEvent('timeline-container-resized', { + detail: { source: 'task-list-toggle' }, + }), + ) + }) } // --- 事件链路:监听 Timeline 传递上来的拖拽/拉伸事件,并通过 props 回调暴露 --- @@ -716,7 +736,7 @@ const generateCsvContent = (tasks: Task[]): string => { result.push({ id: task.id, name: task.name || '', - predecessor: task.predecessor || '', + predecessor: formatPredecessorDisplay(task.predecessor), assignee: task.assignee || '', startDate: task.startDate || '', endDate: task.endDate || '', @@ -747,7 +767,7 @@ const generateCsvContent = (tasks: Task[]): string => { return [ escapeCSVField(task.id), escapeCSVField(task.name), - escapeCSVField(task.predecessor), + escapeCSVField(formatPredecessorDisplay(task.predecessor)), escapeCSVField(task.assignee), escapeCSVField(task.startDate), escapeCSVField(task.endDate), @@ -1181,12 +1201,6 @@ watch(
- -
- - -
- - - - diff --git a/src/components/MultiSelectPredecessor.vue b/src/components/MultiSelectPredecessor.vue new file mode 100644 index 0000000..1da4ff5 --- /dev/null +++ b/src/components/MultiSelectPredecessor.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue index 81090c3..870ac6b 100644 --- a/src/components/TaskBar.vue +++ b/src/components/TaskBar.vue @@ -9,6 +9,11 @@ interface Props { startDate: Date isParent?: boolean onDoubleClick?: (task: Task) => void + // 新增:用于粘性文字显示的滚动位置信息 + scrollLeft?: number + containerWidth?: number + // 新增:外部控制半圆隐藏状态(用于Timeline初始化等场景) + hideBubbles?: boolean } const props = defineProps() @@ -19,6 +24,7 @@ const emit = defineEmits([ 'dblclick', 'drag-end', // 新增 'resize-end', // 新增 + 'scroll-to-position', // 新增:半圆点击定位事件 ]) // 日期工具函数 - 处理时区安全的日期创建和操作 @@ -327,10 +333,404 @@ const handleTaskBarDoubleClick = (e: MouseEvent) => { } } +// 计算粘性样式 - 支持左右边界的精细控制 +const stickyStyles = computed(() => { + const scrollLeft = props.scrollLeft || 0 + const containerWidth = props.containerWidth || 0 + + if (!scrollLeft && !containerWidth) { + return { + nameLeft: '', + namePosition: '', + nameTop: '', + progressLeft: '', + progressPosition: '', + progressTop: '', + } + } + + const taskLeft = parseInt(taskBarStyle.value.left) + const taskWidth = parseInt(taskBarStyle.value.width) + const taskRight = taskLeft + taskWidth + const taskCenterX = taskLeft + taskWidth / 2 + const leftBoundary = scrollLeft + const rightBoundary = scrollLeft + containerWidth + + // 默认样式 + let nameLeft = '' + let namePosition = '' + let nameTop = '' + let progressLeft = '' + let progressPosition = '' + let progressTop = '' + + // 估算文字内容的实际位置 + const nameText = props.task.name || '' + const nameWidth = Math.max(nameText.length * 7, 40) + const progressWidth = 35 + + // 计算名称和进度在默认居中状态下的位置 + const nameLeftPos = taskCenterX - nameWidth / 2 + const nameRightPos = taskCenterX + nameWidth / 2 + const progressLeftPos = taskCenterX - progressWidth / 2 + const progressRightPos = taskCenterX + progressWidth / 2 + + // 左侧边界粘性逻辑 + const nameNeedsLeftSticky = + nameLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + + // 右侧边界粘性逻辑 + const nameNeedsRightSticky = + nameRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary + + // 名称粘性处理 + if (nameNeedsLeftSticky) { + const offset = leftBoundary - taskLeft + nameLeft = `${offset + 8}px` + namePosition = 'absolute' + nameTop = '6px' + } else if (nameNeedsRightSticky) { + const offset = rightBoundary - taskLeft - nameWidth + nameLeft = `${offset - 8}px` + namePosition = 'absolute' + nameTop = '6px' + } + + // 进度左侧边界粘性逻辑 + const progressNeedsLeftSticky = + progressLeftPos < leftBoundary && taskRight > leftBoundary && taskCenterX < leftBoundary + + // 进度右侧边界粘性逻辑 + const progressNeedsRightSticky = + progressRightPos > rightBoundary && taskLeft < rightBoundary && taskCenterX > rightBoundary + + // 进度粘性处理 + if (progressNeedsLeftSticky) { + const offset = leftBoundary - taskLeft + progressLeft = `${offset + 8}px` + progressPosition = 'absolute' + progressTop = '24px' + } else if (progressNeedsRightSticky) { + const offset = rightBoundary - taskLeft - progressWidth + progressLeft = `${offset - 8}px` + progressPosition = 'absolute' + progressTop = '24px' + } + + return { + nameLeft, + namePosition, + nameTop, + progressLeft, + progressPosition, + progressTop, + } +}) + +// 计算气泡指示器的显示状态和位置 +const bubbleIndicator = computed(() => { + const scrollLeft = props.scrollLeft || 0 + const containerWidth = props.containerWidth || 0 + + // 如果没有有效的滚动信息,不显示气泡 + if (!containerWidth || containerWidth <= 0) { + return { + show: false, + left: '0px', + side: 'left', + color: '#409eff', + animationType: 'none', + } + } + + // 如果正在初始化、强制隐藏状态或外部要求隐藏,不显示气泡 + if (isInitializing.value || bubbleHidden.value || props.hideBubbles) { + return { + show: false, + left: '0px', + side: 'left', + color: taskStatus.value.color, + animationType: 'none', + } + } + + // 获取实际的DOM位置(考虑缩放等因素) + const taskLeft = parseInt(taskBarStyle.value.left) + const taskWidth = parseInt(taskBarStyle.value.width) + const taskRight = taskLeft + taskWidth + const leftBoundary = scrollLeft + const rightBoundary = scrollLeft + containerWidth + + // 检查边界状态 + const isCompletelyOutOfLeft = taskRight <= leftBoundary + const isCompletelyOutOfRight = taskLeft >= rightBoundary + + // 只有完全超出边界时才显示半圆 + if (isCompletelyOutOfLeft) { + return { + show: true, + left: `${leftBoundary - taskLeft - 4}px`, // 圆心在边界上,向左偏移半径(4px) + side: 'left', + color: taskStatus.value.color, + animationType: 'morphToSemiCircle', + } + } + + if (isCompletelyOutOfRight) { + return { + show: true, + left: `${rightBoundary - taskLeft - 8}px`, // 右侧半圆位置调整,减少偏移量 + side: 'right', + color: taskStatus.value.color, + animationType: 'morphToSemiCircle', + } + } + + // 部分可见或完全可见时不显示 + return { + show: false, + left: '0px', + side: 'left', + color: taskStatus.value.color, + animationType: 'none', + } +}) + +// 气泡 tooltip 状态 +const showTooltip = ref(false) +const tooltipPosition = ref({ x: 0, y: 0 }) + +// 跟踪滚动状态,避免非滚动时的动画 +const isScrollingContext = ref(false) +const scrollTimeout = ref(null) +const hasManualResize = ref(false) // 跟踪是否有手动resize事件 +const isInitializing = ref(true) // 跟踪初始化状态 +const isAutoScrolling = ref(false) // 跟踪自动滚动状态(如定位到今日、点击半圆定位等) +const bubbleHidden = ref(false) // 控制半圆的强制隐藏状态 + +// 气泡点击事件 - 将TaskBar定位到Timeline中间 +const handleBubbleClick = () => { + const scrollLeft = props.scrollLeft || 0 + const containerWidth = props.containerWidth || 0 + + if (!scrollLeft && !containerWidth) return + + const taskLeft = parseInt(taskBarStyle.value.left) + const taskWidth = parseInt(taskBarStyle.value.width) + const taskCenterX = taskLeft + taskWidth / 2 + + // 计算将TaskBar中心定位到Timeline中心所需的滚动位置 + const targetScrollLeft = taskCenterX - containerWidth / 2 + + // 标记为自动滚动状态,隐藏所有半圆 + isAutoScrolling.value = true + bubbleHidden.value = true + + // 立即隐藏tooltip + showTooltip.value = false + + // 通过事件向Timeline发送滚动请求 + emit('scroll-to-position', targetScrollLeft) +} + +// 监听滚动相关的props变化,判断是否在滚动 +watch( + () => [props.scrollLeft, props.containerWidth], + (newValues, oldValues) => { + // 检查是否是真实的滚动变化(而非初始化或resize) + const [newScrollLeft, newContainerWidth] = newValues + const [oldScrollLeft, oldContainerWidth] = oldValues || [0, 0] + + // 确保数值有效 + const safeNewScrollLeft = newScrollLeft || 0 + const safeNewContainerWidth = newContainerWidth || 0 + const safeOldScrollLeft = oldScrollLeft || 0 + const safeOldContainerWidth = oldContainerWidth || 0 + + // 如果容器宽度发生变化(包括Splitter拖拽、TaskList展开收起、窗口resize等) + if (Math.abs(safeNewContainerWidth - safeOldContainerWidth) > 1 && safeOldContainerWidth > 0) { + hasManualResize.value = true + + // 容器宽度变化时,强制重新计算气泡显示状态 + // 立即触发bubbleIndicator重新计算 + nextTick(() => { + // computed会自动重新计算 + }) + + // 延长禁用动画的时间,确保各种resize操作稳定 + setTimeout(() => { + hasManualResize.value = false + }, 500) // 给容器变化留足够时间稳定 + return + } + + // 首次接收到有效的滚动数据,标记初始化完成 + if (isInitializing.value && safeNewScrollLeft >= 0 && safeNewContainerWidth > 0) { + // 延迟标记初始化完成,等待初始滚动动画结束 + setTimeout(() => { + isInitializing.value = false + }, 1000) // 给初始化滚动留足够时间 + } + + // 只有在scrollLeft变化且没有resize时才认为是滚动 + if (safeNewScrollLeft !== safeOldScrollLeft && !hasManualResize.value) { + isScrollingContext.value = true + + // 清除之前的超时 + if (scrollTimeout.value) { + clearTimeout(scrollTimeout.value) + } + + // 滚动结束后的处理 + scrollTimeout.value = setTimeout(() => { + isScrollingContext.value = false + + // 如果是自动滚动结束,恢复半圆显示 + if (isAutoScrolling.value) { + isAutoScrolling.value = false + // 延迟一点时间再显示半圆,确保滚动完全停止 + setTimeout(() => { + bubbleHidden.value = false + }, 300) + } + }, 200) + } + }, +) + +// 监听外部hideBubbles属性变化,确保Timeline的容器变化能及时反应 +watch( + () => props.hideBubbles, + (newHidden, oldHidden) => { + // 当Timeline设置hideBubbles从true变为false时,强制重新计算半圆状态 + if (oldHidden && !newHidden) { + nextTick(() => { + // 强制重新计算bubbleIndicator,确保容器宽度变化后正确显示半圆 + }) + } + }, +) + +// 监听TaskBar可见性变化,只在滚动时实现重新出现动画 +watch( + () => bubbleIndicator.value.show, + () => { + // TaskBar重新出现时,不需要动画效果 + // 半圆会自然消失,TaskBar会立即显示 + }, +) + +// 监听页面缩放和大小变化,重新计算气泡位置 +const handleResize = () => { + // timeline区域resize时,立即重新计算半圆显示状态 + hasManualResize.value = true + + // 强制重新计算bubbleIndicator + nextTick(() => { + // computed会自动重新计算 + }) + + // 短时间后恢复正常状态,允许动画 + setTimeout(() => { + hasManualResize.value = false + }, 300) // 缩短时间,快速恢复 +} + +onMounted(() => { + // 监听窗口大小变化和缩放 + window.addEventListener('resize', handleResize) + window.addEventListener('zoom', handleResize) // 某些浏览器支持 +}) + onUnmounted(() => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('resize', handleResize) + window.removeEventListener('zoom', handleResize) }) + +// 处理气泡悬停 +const handleBubbleMouseEnter = (event: MouseEvent) => { + showTooltip.value = true + + // 智能定位:右侧气泡在左侧显示tooltip,左侧气泡在右侧显示 + const isRightBubble = bubbleIndicator.value.side === 'right' + const offsetX = isRightBubble ? -180 : 15 // 右侧气泡向左偏移距离调整,与左侧距离一致 + + tooltipPosition.value = { + x: event.clientX + offsetX, + y: event.clientY - 10, + } +} + +const handleBubbleMouseLeave = () => { + showTooltip.value = false +} + +// 处理气泡点击事件 - 点击时隐藏tooltip,但不影响定位功能 +const handleBubbleMouseDown = (event: MouseEvent) => { + // 阻止mousedown事件冒泡,防止影响其他功能 + event.stopPropagation() + // 点击时隐藏tooltip + showTooltip.value = false +} + +// 格式化日期显示 +const formatDisplayDate = (dateStr: string | undefined): string => { + if (!dateStr) return '未设置' + const date = createLocalDate(dateStr) + if (!date) return '未设置' + + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +// 计算工时信息 +const workHourInfo = computed(() => { + // 这里可以根据实际的数据结构调整 + const startDate = createLocalDate(props.task.startDate) + const endDate = createLocalDate(props.task.endDate) + + let totalHours = 0 + if (startDate && endDate) { + const diffTime = Math.abs(endDate.getTime() - startDate.getTime()) + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + totalHours = diffDays * 8 // 假设每天8小时 + } + + const progress = props.task.progress || 0 + const usedHours = Math.round((totalHours * progress) / 100) + + return { + total: totalHours, + used: usedHours, + } +}) + +// Helper functions to create type-safe style objects +const getNameStyles = () => { + const styles = stickyStyles.value + const result: Record = {} + + if (styles.nameLeft) result.left = styles.nameLeft + if (styles.namePosition) result.position = styles.namePosition + if (styles.nameTop) result.top = styles.nameTop + + return result +} + +const getProgressStyles = () => { + const styles = stickyStyles.value + const result: Record = {} + + if (styles.progressLeft) result.left = styles.progressLeft + if (styles.progressPosition) result.position = styles.progressPosition + if (styles.progressTop) result.top = styles.progressTop + + return result +}