Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 180 additions & 34 deletions packages/canvas/src/components/container/CanvasAction.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
<div class="drag-resize resize-bottom-left" @mousedown.stop="onMousedown($event, 'start', 'end')"></div>
<div class="drag-resize resize-bottom-right" @mousedown.stop="onMousedown($event, 'end', 'end')"></div>
</template>
<div v-if="showAction" class="corner-mark-right" :style="fixStyle">
<div v-if="showAction" ref="optionRef" class="corner-mark-right" :style="fixStyle">
<template v-if="!isModal">
<div v-if="showToParent" title="选择父级">
<icon-chevron-left @click.stop="selectParent"></icon-chevron-left>
Expand Down Expand Up @@ -74,7 +74,7 @@
</div>
</template>
<script>
import { watchPostEffect, ref, watch, computed } from 'vue'
import { watchPostEffect, ref, watch, computed, nextTick } from 'vue'
import {
IconDel,
IconSetting,
Expand All @@ -95,23 +95,26 @@ import {
dragStart,
getCurrentElement
} from './container'
import { useResource } from '@opentiny/tiny-engine-controller'
import { useLayout, useResource } from '@opentiny/tiny-engine-controller'
import { Popover } from '@opentiny/vue'
import shortCutPopover from './shortCutPopover.vue'

// 工具操作条高度
const OPTION_BAR_HEIGHT = 24
// 标签高度
const LABEL_HEIGHT = 24
// 操作条最大宽度
const MAX_OPTION_WIDTH = 110

// 画布右边滚动条宽度
const SCROLL_BAR_WIDTH = 8

// 当工具操作条和标签高度并排显示时,需要的间距 6px
const OPTION_SPACE = 6

// 选中框的边框宽度
const SELECTION_BORDER_WIDTH = 2

const STYLE_UNSET = 'unset'

export default {
components: {
IconDel: IconDel(),
Expand Down Expand Up @@ -204,6 +207,7 @@ export default {
return config?.configure?.isModal
})

const optionRef = ref(null)
const fixStyle = ref('')

let showPopover = ref(false)
Expand Down Expand Up @@ -265,45 +269,188 @@ export default {
const labelRef = ref(null)
const labelStyle = ref('')

const bottomPanelHeight = ref(0)
const topToolbarHeight = ref(0)
const positions = {
LEFT: 'left',
RIGHT: 'right',
TOP: 'top',
BOTTOM: 'bottom',
isHorizontal(position) {
return [this.LEFT, this.RIGHT].includes(position)
},
isVertical(position) {
return [this.TOP, this.BOTTOM].includes(position)
}
}

class Align {
alignLeft = false
horizontalValue = 0
alignTop = false
verticalValue = 0

constructor({ alignLeft, horizontalValue, alignTop, verticalValue }) {
this.alignLeft = alignLeft
this.horizontalValue = horizontalValue
this.alignTop = alignTop
this.verticalValue = verticalValue
}

align(position, value = 0) {
if (positions.isHorizontal(position)) {
this.alignLeft = position === positions.LEFT
this.horizontalValue = value
return this
}
if (positions.isVertical(position)) {
this.alignTop = position === positions.TOP
this.horizontalValue = value
return this
}
return this
}

toStyleValue() {
const styleObj = {}

if (this.alignLeft) {
styleObj.left = this.horizontalValue
styleObj.right = STYLE_UNSET
} else {
styleObj.right = this.horizontalValue
styleObj.left = STYLE_UNSET
}

if (this.alignTop) {
styleObj.top = this.verticalValue
styleObj.bottom = STYLE_UNSET
} else {
styleObj.bottom = this.verticalValue
styleObj.top = STYLE_UNSET
}

watchPostEffect(() => {
if (!bottomPanelHeight.value) {
bottomPanelHeight.value = document.querySelector('#tiny-bottom-panel')?.offsetHeight
return this.styleObj2Str(styleObj)
}
if (!topToolbarHeight.value) {
topToolbarHeight.value = document.querySelector('.tiny-engine-toolbar')?.offsetHeight

styleObj2Str = (styleObj) => {
return Object.entries(styleObj)
.map(([key, value]) => {
const num = Number(value)

if (Number.isNaN(num)) {
return `${key}:${value}`
}

const val = positions.isHorizontal(key) ? num - SELECTION_BORDER_WIDTH : num
return `${key}:${val}px`
})
.join(';')
}
}

const { left, top, width, height } = props.selectState
const getStyleValues = (selectState, siteCanvasWidth, siteCanvasHeight) => {
const { left, top, width, height, doc } = selectState
const labelRect = labelRef.value.getBoundingClientRect()
const optionRect = optionRef.value.getBoundingClientRect()
// 标签宽度和工具操作条宽度之和加上间距
const fullRectWidth = labelRect.width + optionRect.width + OPTION_SPACE

// 是否 将label 标签放置到底部,判断 top 距离
const isLabelAtBottom = top <= topToolbarHeight.value + LABEL_HEIGHT
const isLabelAtBottom = top <= LABEL_HEIGHT
const labelAlign = new Align({
alignLeft: true,
horizontalValue: 0,
alignTop: !isLabelAtBottom,
verticalValue: -LABEL_HEIGHT
})

// 是否将操作栏放置到底部,判断当前选中组件底部与页面底部的距离。
const isOptionAtBottom = window.innerHeight - top - height - bottomPanelHeight.value > OPTION_BAR_HEIGHT
const isOptionAtBottom = siteCanvasHeight - top - height > OPTION_BAR_HEIGHT
const optionAlign = new Align({
alignLeft: false,
horizontalValue: 0,
alignTop: !isOptionAtBottom,
verticalValue: -OPTION_BAR_HEIGHT
})

const scrollBarWidth = doc.documentElement.scrollHeight > doc.documentElement.clientHeight ? SCROLL_BAR_WIDTH : 0

// 选中组件需要最小的宽度,如果小于这个最小宽度,label 和 option 组件可能会重叠遮挡,labelRef.value.clientWidth:label 的宽度
const minWidth = MAX_OPTION_WIDTH + labelRef.value?.clientWidth || 0
let translateXDis = 0
if (width < fullRectWidth) {
// 选中框宽度小于标签宽度和工具操作条宽度之和加上间距

const siteCanvas = document.querySelector('.site-canvas')
const right = siteCanvas.getBoundingClientRect().right
// 判断是否偏右,偏右且重叠的话,需要移动 label 的位移,不能移动 option 的位移,否则有可能被遮挡
const isOverRight = right <= left + width + SCROLL_BAR_WIDTH
// 如果labe宽度大于选中框宽度,并且label右侧已经超出画布,则label对齐右侧
const isLabelAlignRight = labelRect.width > width && left + labelRect.width + scrollBarWidth > siteCanvasWidth
if (isLabelAlignRight) {
labelAlign.align(positions.RIGHT)
}

// 如果选中组件宽度小于最小宽度要求,则需要位移
if (width < minWidth) {
translateXDis = MAX_OPTION_WIDTH - width + (labelRef.value?.clientWidth || 0) + OPTION_SPACE
// 如果option宽度大于选中框宽度,并且option左侧已经超出画布,则option对齐左侧
const isOptionAlignLeft = optionRect.width > width && left + width - optionRect.width < 0
if (isOptionAlignLeft) {
optionAlign.align(positions.LEFT)
}

if (isLabelAtBottom === isOptionAtBottom) {
// 标签框和工具操作框都在顶部或者都在底部

if (left + fullRectWidth < siteCanvasWidth) {
// 都放在左侧
labelAlign.align(positions.LEFT)
optionAlign.align(positions.LEFT, labelRect.width + OPTION_SPACE)
} else {
// 都放在右侧
optionAlign.align(positions.RIGHT)
labelAlign.align(positions.RIGHT, optionRect.width + OPTION_SPACE)
}
}
} else {
if (left < 0) {
labelAlign.align(positions.LEFT, Math.min(-left, width - fullRectWidth))
}

if (left + width + scrollBarWidth > siteCanvasWidth) {
optionAlign.align(
positions.RIGHT,
Math.min(left + width + scrollBarWidth - siteCanvasWidth, width - fullRectWidth)
)
}
}

return {
labelStyleValue: labelAlign.toStyleValue(),
optionStyleValue: optionAlign.toStyleValue()
}
}

watchPostEffect(async () => {
let { left } = props.selectState
const { top, width, height, doc } = props.selectState

labelStyle.value = `top: unset; ${isLabelAtBottom ? 'bottom' : 'top'}: -${LABEL_HEIGHT}px; ${
isOverRight && isLabelAtBottom === isOptionAtBottom && `left: -${translateXDis}px;`
}`
// nextTick后ref才能获取到元素。需要把监听的依赖放在await之前,否则无法监听变化
await nextTick()

fixStyle.value = `
${translateXDis && !isOverRight ? `transform: translateX(${translateXDis}px);` : ''}
${isOptionAtBottom ? 'bottom' : 'top'}: -${OPTION_BAR_HEIGHT}px;`
if (labelRef.value && !optionRef.value) {
// 选中body的情况
labelStyle.value = `left: 0; right: unset; top: unset; bottom: 0`
return
}

if (!labelRef.value || !optionRef.value) {
return
}

const siteCanvasRect = document.querySelector('.site-canvas').getBoundingClientRect()
const scale = useLayout().getScale()

left -= ((1 - scale) / 2) * siteCanvasRect.width

const { labelStyleValue, optionStyleValue } = getStyleValues(
{ left, top, width, height, doc },
siteCanvasRect.width * scale,
siteCanvasRect.height
)

labelStyle.value = labelStyleValue
fixStyle.value = optionStyleValue
})

return {
Expand All @@ -313,6 +460,7 @@ export default {
copy,
hide,
selectParent,
optionRef,
fixStyle,
showAction,
showPopover,
Expand All @@ -329,7 +477,7 @@ export default {

<style lang="less">
.canvas-rect {
position: fixed;
position: absolute;
box-sizing: border-box;
pointer-events: none;
border: 1px solid var(--ti-lowcode-canvas-rect-border-color);
Expand Down Expand Up @@ -454,7 +602,6 @@ export default {
display: flex;
align-items: center;
position: absolute;
right: -1px;
height: 24px;
padding: 0 4px;
color: var(--ti-lowcode-canvas-corner-mark-right-color);
Expand All @@ -479,7 +626,6 @@ export default {
.corner-mark-left {
white-space: nowrap;
pointer-events: all;
left: -2px;
color: var(--ti-lowcode-canvas-select-corner-mark-left-color);
background: var(--ti-lowcode-canvas-select-corner-mark-left-bg-color);
svg {
Expand Down
26 changes: 25 additions & 1 deletion packages/canvas/src/components/container/CanvasContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,30 @@ export default {
const doc = iframe.value.contentDocument
const win = iframe.value.contentWindow

let isScrolling = false

// 以下是内部iframe监听的事件
win.addEventListener('mousedown', (event) => {
// html元素使用scroll和mouseup事件处理
if (event.target === doc.documentElement) {
isScrolling = false
return
}

insertPosition.value = false
setCurrentNode(event)
target.value = event.target
})

win.addEventListener('scroll', () => {
isScrolling = true
})

win.addEventListener('mouseup', (event) => {
if (event.target !== doc.documentElement || isScrolling) {
return
}

insertPosition.value = false
setCurrentNode(event)
target.value = event.target
Expand Down Expand Up @@ -194,7 +216,9 @@ export default {

onMounted(() => run(iframe))
onUnmounted(() => {
removeHostkeyEvent(iframe.value.contentDocument)
if (iframe.value?.contentDocument) {
removeHostkeyEvent(iframe.value.contentDocument)
}
window.removeEventListener('message', updateI18n, false)
})

Expand Down
Loading