@opentiny/tiny-engine-canvas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.eslintrc.js +42 -0
  2. package/README.md +7 -0
  3. package/canvas.html +212 -0
  4. package/dist/index.js +48919 -0
  5. package/index.html +13 -0
  6. package/package.json +30 -0
  7. package/public/favicon.ico +0 -0
  8. package/src/Design.vue +53 -0
  9. package/src/assets/logo.png +0 -0
  10. package/src/canvas.js +34 -0
  11. package/src/components/builtin/CanvasBox.vue +22 -0
  12. package/src/components/builtin/CanvasCol.vue +89 -0
  13. package/src/components/builtin/CanvasCollection.js +278 -0
  14. package/src/components/builtin/CanvasCollection.vue +106 -0
  15. package/src/components/builtin/CanvasIcon.vue +30 -0
  16. package/src/components/builtin/CanvasImg.vue +18 -0
  17. package/src/components/builtin/CanvasPlaceholder.vue +26 -0
  18. package/src/components/builtin/CanvasRow.vue +67 -0
  19. package/src/components/builtin/CanvasRowColContainer.vue +42 -0
  20. package/src/components/builtin/CanvasSlot.vue +22 -0
  21. package/src/components/builtin/CanvasText.vue +18 -0
  22. package/src/components/builtin/builtin.json +955 -0
  23. package/src/components/builtin/helper.js +46 -0
  24. package/src/components/builtin/index.js +33 -0
  25. package/src/components/common/index.js +158 -0
  26. package/src/components/container/CanvasAction.vue +554 -0
  27. package/src/components/container/CanvasContainer.vue +244 -0
  28. package/src/components/container/CanvasDivider.vue +246 -0
  29. package/src/components/container/CanvasDragItem.vue +38 -0
  30. package/src/components/container/CanvasFooter.vue +86 -0
  31. package/src/components/container/CanvasMenu.vue +214 -0
  32. package/src/components/container/CanvasResize.vue +195 -0
  33. package/src/components/container/CanvasResizeBorder.vue +219 -0
  34. package/src/components/container/container.js +791 -0
  35. package/src/components/container/keyboard.js +147 -0
  36. package/src/components/container/shortCutPopover.vue +181 -0
  37. package/src/components/render/CanvasEmpty.vue +14 -0
  38. package/src/components/render/RenderMain.js +408 -0
  39. package/src/components/render/context.js +53 -0
  40. package/src/components/render/render.js +689 -0
  41. package/src/components/render/runner.js +140 -0
  42. package/src/i18n/en.json +5 -0
  43. package/src/i18n/zh.json +5 -0
  44. package/src/i18n.js +21 -0
  45. package/src/index.js +96 -0
  46. package/src/locale.js +19 -0
  47. package/src/lowcode.js +104 -0
  48. package/src/main.js +17 -0
  49. package/test/form.json +690 -0
  50. package/test/group.json +99 -0
  51. package/test/jsslot.json +427 -0
  52. package/vite.config.js +73 -0
@@ -0,0 +1,214 @@
1
+ <template>
2
+ <div v-show="menuState.show" ref="menuDom" class="context-menu" :style="menuState.position">
3
+ <ul class="menu-item">
4
+ <li
5
+ v-for="(item, index) in menus"
6
+ :key="index"
7
+ :class="item.items ? 'li-item' : ''"
8
+ @click="doOperation(item)"
9
+ @mouseover="current = item"
10
+ >
11
+ <div>
12
+ <span>{{ item.name }}</span>
13
+ <span v-if="item.items"><icon-right></icon-right></span>
14
+ </div>
15
+ <ul v-if="item.items && current === item" class="sub-menu menu-item">
16
+ <template v-for="(subItem, subIndex) in item.items" :key="subIndex">
17
+ <li v-if="subItem?.check?.() !== false" @click.stop="doOperation(subItem)">
18
+ {{ subItem.name }}
19
+ </li>
20
+ </template>
21
+ </ul>
22
+ </li>
23
+ </ul>
24
+ <save-new-block :boxVisibility="boxVisibility" fromCanvas @close="close"></save-new-block>
25
+ </div>
26
+ </template>
27
+
28
+ <script lang="jsx">
29
+ import { ref, reactive, nextTick } from 'vue'
30
+ import { getConfigure, getController, getCurrent, copyNode, removeNodeById } from './container'
31
+ import { useLayout, useModal, useCanvas } from '@opentiny/tiny-engine-controller'
32
+ import { SaveNewBlock } from '@opentiny/tiny-engine-common'
33
+ import { iconRight } from '@opentiny/vue-icon'
34
+
35
+ const menuState = reactive({
36
+ position: null,
37
+ show: false,
38
+ menus: []
39
+ })
40
+
41
+ const current = ref(null)
42
+ const menuDom = ref(null)
43
+
44
+ export const closeMenu = () => {
45
+ menuState.show = false
46
+ current.value = null
47
+ }
48
+
49
+ export const openMenu = (offset, event) => {
50
+ const { x, y } = offset
51
+ const { getScale } = useLayout()
52
+ menuState.position = {
53
+ // 位置处于画布右侧边缘时需要调整显示方向 TODO
54
+ left: event.clientX * getScale() + x + 2 + 'px',
55
+ top: event.clientY * getScale() + y + 'px'
56
+ }
57
+ menuState.show = sessionStorage.getItem('pageInfo') ? true : false
58
+
59
+ nextTick(() => {
60
+ if (menuDom.value) {
61
+ const { bottom, height, top } = menuDom.value.getBoundingClientRect()
62
+ if (bottom > document.body?.clientHeight) {
63
+ menuState.position.top = top - height + 'px'
64
+ }
65
+ }
66
+ })
67
+ }
68
+
69
+ export default {
70
+ components: {
71
+ SaveNewBlock,
72
+ IconRight: iconRight()
73
+ },
74
+ setup(props, { emit }) {
75
+ const menus = ref([
76
+ { name: '修改属性', code: 'config' },
77
+ {
78
+ name: '插入',
79
+ items: [
80
+ { name: '向前', code: 'insert', value: 'top' },
81
+ {
82
+ name: '中间',
83
+ code: 'insert',
84
+ value: 'in',
85
+ check() {
86
+ const { componentName } = getCurrent()?.schema || {}
87
+ return getConfigure(componentName)?.isContainer
88
+ }
89
+ },
90
+ { name: '向后', code: 'insert', value: 'bottom' }
91
+ ]
92
+ },
93
+ {
94
+ name: '添加父级',
95
+ items: [
96
+ { name: '文字提示', code: 'wrap', value: 'TinyTooltip' },
97
+ { name: '弹出框', code: 'wrap', value: 'TinyPopover' }
98
+ ]
99
+ },
100
+ { name: '删除', code: 'del' },
101
+ { name: '复制', code: 'copy' },
102
+ { name: '绑定事件', code: 'bindEvent' }
103
+ ])
104
+
105
+ const boxVisibility = ref(false)
106
+
107
+ // 计算上下文菜单位置,右键时显示,否则关闭
108
+
109
+ const operations = {
110
+ del() {
111
+ removeNodeById(getCurrent().schema.id)
112
+ },
113
+ copy() {
114
+ copyNode(getCurrent().schema.id)
115
+ },
116
+ config() {
117
+ useLayout().activeSetting('props')
118
+ },
119
+ bindEvent() {
120
+ useLayout().activeSetting('event')
121
+ },
122
+ insert({ value }) {
123
+ emit('insert', value)
124
+ },
125
+ wrap({ value, name }) {
126
+ const componentName = value || name
127
+ const { schema, parent } = getCurrent()
128
+ const index = parent.children.indexOf(schema)
129
+ const wrapSchema = {
130
+ componentName,
131
+ id: null,
132
+ props: {},
133
+ children: [schema]
134
+ }
135
+
136
+ parent.children.splice(index, 1, wrapSchema)
137
+
138
+ getController().addHistory()
139
+ },
140
+ createBlock() {
141
+ if (useCanvas().isSaved()) {
142
+ boxVisibility.value = true
143
+ } else {
144
+ useModal().message({
145
+ message: '请先保存当前页面',
146
+ status: 'error'
147
+ })
148
+ }
149
+ }
150
+ }
151
+
152
+ const close = () => {
153
+ boxVisibility.value = false
154
+ }
155
+ const doOperation = (item) => {
156
+ if (item?.code) {
157
+ operations[item.code](item)
158
+ closeMenu()
159
+ }
160
+ }
161
+
162
+ return {
163
+ menuState,
164
+ menus,
165
+ doOperation,
166
+ boxVisibility,
167
+ close,
168
+ current,
169
+ menuDom
170
+ }
171
+ }
172
+ }
173
+ </script>
174
+
175
+ <style lang="less" scoped>
176
+ .context-menu {
177
+ position: fixed;
178
+ z-index: 10;
179
+ }
180
+ .menu-item {
181
+ width: 140px;
182
+ line-height: 20px;
183
+ border-radius: 6px;
184
+ padding: 8px 0;
185
+ background-color: var(--ti-lowcode-canvas-menu-bg);
186
+ box-shadow: 0 1px 15px 0 rgb(0 0 0 / 20%);
187
+ display: flex;
188
+ flex-direction: column;
189
+ .li-item {
190
+ border-bottom: 1px solid var(--ti-lowcode-canvas-menu-border-color);
191
+ }
192
+ li {
193
+ & > div {
194
+ display: flex;
195
+ width: 100%;
196
+ justify-content: space-between;
197
+ }
198
+ font-size: 12px;
199
+ color: var(--ti-lowcode-toolbar-breadcrumb-color);
200
+ padding: 6px 15px;
201
+ &:hover {
202
+ color: var(--ti-lowcode-toolbar-icon-color);
203
+ background: var(--ti-lowcode-canvas-menu-hover-color);
204
+ }
205
+ position: relative;
206
+ }
207
+ &.sub-menu {
208
+ width: 100px;
209
+ position: absolute;
210
+ right: -100px;
211
+ top: -2px;
212
+ }
213
+ }
214
+ </style>
@@ -0,0 +1,195 @@
1
+ <template>
2
+ <div ref="resizeDom" class="canvas-size-controller" :style="sizeStyle">
3
+ <slot></slot>
4
+ <div ref="resize" class="canvas-resize-handle" @mousedown.stop="onMouseDown">
5
+ <div class="handle right-handle">
6
+ <div class="gutter-handle"></div>
7
+ <div class="tab-handle"></div>
8
+ </div>
9
+ </div>
10
+ </div>
11
+ </template>
12
+ <script>
13
+ import { ref, computed, watch, nextTick } from 'vue'
14
+ import { useLayout } from '@opentiny/tiny-engine-controller'
15
+ import { canvasState } from './container'
16
+
17
+ export default {
18
+ setup() {
19
+ const sizeStyle = computed(() => {
20
+ const { width, maxWidth, minWidth, scale } = useLayout().getDimension()
21
+ return {
22
+ width,
23
+ maxWidth,
24
+ minWidth,
25
+ height: `${100 / scale}%`,
26
+ transform: `scale(${scale}) translateY(-${(100 / scale - 100) / 2}%)`
27
+ }
28
+ })
29
+
30
+ const mouseDown = ref(false)
31
+ const resizeDom = ref(null)
32
+
33
+ const onMouseMove = (event) => {
34
+ if (mouseDown.value) {
35
+ event.preventDefault()
36
+ calculateSize(event)
37
+ }
38
+ }
39
+
40
+ const calculateSize = ({ movementX }) => {
41
+ const dimension = useLayout().getDimension()
42
+ const { maxWidth, minWidth, width } = dimension
43
+ const newWidth = parseInt(width) + movementX * 2
44
+
45
+ // 鼠标往返移动到边界时再触发反向宽度调整
46
+ useLayout().setDimension({
47
+ width: `${parseInt(Math.min(Math.max(newWidth, parseInt(minWidth)), parseInt(maxWidth)), 10)}px`
48
+ })
49
+ }
50
+
51
+ const onMouseDown = () => {
52
+ const iframe = canvasState.iframe
53
+
54
+ if (iframe) {
55
+ iframe.style['pointer-events'] = 'none'
56
+ bindEvents()
57
+ mouseDown.value = true
58
+ }
59
+ }
60
+
61
+ const onMouseUp = () => {
62
+ const iframe = canvasState.iframe
63
+
64
+ if (iframe) {
65
+ iframe.style['pointer-events'] = 'auto'
66
+ mouseDown.value = false
67
+ unbindEvents()
68
+ }
69
+ }
70
+
71
+ const bindEvents = () => {
72
+ document.addEventListener('mousemove', onMouseMove, { passive: false })
73
+ document.addEventListener('mouseup', onMouseUp)
74
+ }
75
+
76
+ const unbindEvents = () => {
77
+ document.removeEventListener('mousemove', onMouseMove, { passive: false })
78
+ document.removeEventListener('mouseup', onMouseUp)
79
+ }
80
+
81
+ const setScale = () => {
82
+ useLayout().setDimension({ scale: 1 })
83
+ nextTick(() => {
84
+ const canvasWrap = document.querySelector('#canvas-wrap')
85
+ const rate = canvasWrap.offsetWidth / resizeDom.value.offsetWidth
86
+ useLayout().setDimension({
87
+ scale: rate > 1 ? 1 : rate
88
+ })
89
+ })
90
+ }
91
+
92
+ watch(() => useLayout().getDimension().width, setScale, { flush: 'post' })
93
+
94
+ watch(() => useLayout().getPluginState().fixedPanels, setScale, { flush: 'post' })
95
+
96
+ watch(
97
+ () => useLayout().getPluginState().render,
98
+ (value) => !value && setScale(),
99
+ { flush: 'post' }
100
+ )
101
+
102
+ return {
103
+ onMouseDown,
104
+ onMouseMove,
105
+ sizeStyle,
106
+ resizeDom
107
+ }
108
+ }
109
+ }
110
+ </script>
111
+ <style lang="less" scoped>
112
+ .canvas-size-controller {
113
+ height: 100%;
114
+
115
+ .canvas-resize-handle {
116
+ position: relative;
117
+
118
+ .handle {
119
+ position: absolute;
120
+ top: 0;
121
+ bottom: 0;
122
+ z-index: 13;
123
+
124
+ &::before {
125
+ content: '';
126
+ position: absolute;
127
+ top: 0;
128
+ left: -3px;
129
+ width: 4px;
130
+ height: 100%;
131
+ background: var(--ti-lowcode-canvas-tab-handle-hover-bg);
132
+ display: none;
133
+ }
134
+
135
+ &:hover::before {
136
+ display: block;
137
+ }
138
+
139
+ &:hover {
140
+ .tab-handle {
141
+ background: var(--ti-lowcode-canvas-tab-handle-hover-bg);
142
+ }
143
+
144
+ .tab-handle:before,
145
+ .tab-handle:after {
146
+ background: #ffffff;
147
+ }
148
+ }
149
+
150
+ .gutter-handle {
151
+ position: absolute;
152
+ top: 0;
153
+ left: -3px;
154
+ width: 4px;
155
+ height: 100%;
156
+ cursor: col-resize;
157
+ pointer-events: all;
158
+ }
159
+
160
+ .tab-handle {
161
+ position: fixed;
162
+ top: 50%;
163
+ width: 14px;
164
+ height: 38px;
165
+ margin-top: -19px;
166
+ background: var(--ti-lowcode-canvas-iframe-scrollbar-thumb-color);
167
+ cursor: col-resize;
168
+ pointer-events: all;
169
+ border-top-right-radius: 3px;
170
+ border-bottom-right-radius: 3px;
171
+
172
+ &::before,
173
+ &::after {
174
+ content: '';
175
+ position: absolute;
176
+ top: 8px;
177
+ bottom: 8px;
178
+ left: 8px;
179
+ width: 1px;
180
+ background: var(--ti-lowcode-canvas-tab-handle-color);
181
+ }
182
+
183
+ &::before {
184
+ left: 5px;
185
+ }
186
+ }
187
+ }
188
+
189
+ .right-handle {
190
+ right: -14px;
191
+ width: 14px;
192
+ }
193
+ }
194
+ }
195
+ </style>
@@ -0,0 +1,219 @@
1
+ <template>
2
+ <div
3
+ v-if="state.visible"
4
+ :class="['resize-border', `resize-${state.direction}`]"
5
+ @mousedown="handleResizeStart"
6
+ ></div>
7
+ </template>
8
+
9
+ <script>
10
+ import { reactive, watch } from 'vue'
11
+ import { getCurrent, updateRect, selectState, querySelectById } from './container'
12
+
13
+ export default {
14
+ props: {
15
+ iframe: {
16
+ type: Object,
17
+ default: () => ({})
18
+ }
19
+ },
20
+ setup(props) {
21
+ const state = reactive({
22
+ top: 0,
23
+ left: 0,
24
+ width: 0,
25
+ height: 0,
26
+ visible: false,
27
+ direction: 'vertical',
28
+ startPosition: {
29
+ x: 0,
30
+ y: 0,
31
+ width: 0,
32
+ height: 0
33
+ }
34
+ })
35
+
36
+ const isFirstChild = (parent, schema) => {
37
+ return parent.children[0].id === schema.id
38
+ }
39
+
40
+ const handleResize = (event, type) => {
41
+ let { clientX, clientY } = event
42
+
43
+ if (type === 'iframe' && props.iframe) {
44
+ const iframeRect = props.iframe.getBoundingClientRect()
45
+ clientX += iframeRect.left
46
+ clientY += iframeRect.top
47
+ }
48
+
49
+ const { parent, schema } = getCurrent()
50
+
51
+ if (!schema.props) {
52
+ schema.props = {}
53
+ }
54
+
55
+ if (state.direction === 'horizontal') {
56
+ let dis = state.startPosition.x - clientX
57
+
58
+ if (isFirstChild(parent, schema)) {
59
+ dis = -dis
60
+ }
61
+
62
+ let newWidth = state.startPosition.width + dis
63
+
64
+ const parentDomNode = querySelectById(parent.id)
65
+ const parentWidth = parseInt(window.getComputedStyle(parentDomNode).width, 10)
66
+
67
+ // 最大宽度不能大于父组件宽度
68
+ if (newWidth >= parentWidth) {
69
+ newWidth = parentWidth
70
+ }
71
+
72
+ // 最小宽度32
73
+ if (newWidth <= 32) {
74
+ newWidth = 32
75
+ }
76
+
77
+ schema.props.flexBasis = `${newWidth}px`
78
+ schema.props.widthType = 'fixed'
79
+ }
80
+
81
+ if (state.direction === 'vertical') {
82
+ let target = schema.componentName === 'CanvasRow' ? schema : parent
83
+ if (!target.props) {
84
+ target.props = {}
85
+ }
86
+ const dis = clientY - state.startPosition.y
87
+ const minHeight = state.startPosition.height + dis
88
+ target.props.minHeight = `${minHeight}px`
89
+ }
90
+
91
+ updateRect()
92
+ }
93
+
94
+ const handleResizeOverIframe = (event) => {
95
+ handleResize(event, 'iframe')
96
+ }
97
+
98
+ const handleResizeEnd = () => {
99
+ window.removeEventListener('mousemove', handleResize, true)
100
+ window.removeEventListener('mouseup', handleResizeEnd, true)
101
+ if (props.iframe) {
102
+ const iframeWin = props.iframe.contentWindow
103
+ iframeWin.removeEventListener('mousemove', handleResizeOverIframe, true)
104
+ iframeWin.removeEventListener('mouseup', handleResizeEnd, true)
105
+ }
106
+ updateRect()
107
+ }
108
+
109
+ const handleResizeStart = () => {
110
+ const { top, left, width, height } = selectState
111
+ const { parent, schema } = getCurrent()
112
+
113
+ let startX = left
114
+
115
+ if (isFirstChild(parent, schema)) {
116
+ startX += parseInt(width, 10)
117
+ }
118
+
119
+ state.startPosition = {
120
+ x: startX,
121
+ y: top + parseInt(height, 10),
122
+ width: parseInt(width, 10),
123
+ height: parseInt(height, 10)
124
+ }
125
+
126
+ window.addEventListener('mousemove', handleResize, true)
127
+ window.addEventListener('mouseup', handleResizeEnd, true)
128
+
129
+ // 需要同时监听 iframe 和 app 的 mousemove 、mouseup 事件,因为鼠标移动过快可能会造成在 iframe 上面 mousemove 了
130
+ if (props.iframe) {
131
+ const iframeWin = props.iframe.contentWindow
132
+ iframeWin.addEventListener('mousemove', handleResizeOverIframe, true)
133
+ iframeWin.addEventListener('mouseup', handleResizeEnd, true)
134
+ }
135
+ }
136
+
137
+ watch(
138
+ () => selectState,
139
+ () => {
140
+ const { top, left, width, height, componentName } = selectState
141
+ const { parent, schema } = getCurrent()
142
+
143
+ if (!['CanvasRow', 'CanvasCol'].includes(componentName)) {
144
+ state.visible = false
145
+ return
146
+ }
147
+
148
+ state.visible = true
149
+
150
+ if (componentName === 'CanvasRow') {
151
+ state.top = `${top - 10 + height}px`
152
+ state.left = `${left}px`
153
+ state.width = `${width}px`
154
+ state.height = '20px'
155
+ state.direction = 'vertical'
156
+ return
157
+ }
158
+
159
+ if (componentName === 'CanvasCol') {
160
+ state.direction = parent.children.length > 1 ? 'horizontal' : 'vertical'
161
+ if (state.direction == 'vertical') {
162
+ // 选中的是 col,但是只有一个 col,所以出现的是 Row 的高度调整边框
163
+ state.top = `${top - 10 + height}px`
164
+ state.left = `${left}px`
165
+ state.width = `${width}px`
166
+ state.height = '20px'
167
+ } else {
168
+ const extraWidth = isFirstChild(parent, schema) ? width : 0
169
+ state.top = `${top}px`
170
+ state.left = `${left - 10 + extraWidth}px`
171
+ state.width = '20px'
172
+ state.height = `${height}px`
173
+ }
174
+ }
175
+ },
176
+ { deep: true }
177
+ )
178
+
179
+ return {
180
+ state,
181
+ handleResizeStart
182
+ }
183
+ }
184
+ }
185
+ </script>
186
+
187
+ <style lang="less" scoped>
188
+ .resize-border {
189
+ position: fixed;
190
+ display: flex;
191
+ justify-content: center;
192
+ align-items: center;
193
+ z-index: 3;
194
+ top: v-bind('state.top');
195
+ left: v-bind('state.left');
196
+ width: v-bind('state.width');
197
+ height: v-bind('state.height');
198
+ &::after {
199
+ content: '';
200
+ display: block;
201
+ border: 1px solid var(--ti-lowcode-common-primary-color);
202
+ }
203
+ &.resize-vertical {
204
+ cursor: ns-resize;
205
+ &::after {
206
+ min-width: 50%;
207
+ height: 4px;
208
+ }
209
+ }
210
+
211
+ &.resize-horizontal {
212
+ cursor: ew-resize;
213
+ &::after {
214
+ width: 4px;
215
+ height: 100%;
216
+ }
217
+ }
218
+ }
219
+ </style>