添加创建元素区域
This commit is contained in:
parent
8fb6e53342
commit
0988cbae90
|
@ -9,6 +9,7 @@ export enum MutationTypes {
|
|||
SET_EDITORAREA_FOCUS = 'setEditorAreaFocus',
|
||||
SET_DISABLE_HOTKEYS_STATE = 'setDisableHotkeysState',
|
||||
SET_GRID_LINES_STATE = 'setGridLinesState',
|
||||
SET_CREATING_ELEMENT_TYPE = 'setCreatingElementType',
|
||||
SET_AVAILABLE_FONTS = 'setAvailableFonts',
|
||||
SET_TOOLBAR_STATE = 'setToolbarState',
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ export interface State {
|
|||
editorAreaFocus: boolean;
|
||||
disableHotkeys: boolean;
|
||||
showGridLines: boolean;
|
||||
creatingElementType: string;
|
||||
availableFonts: FontName[];
|
||||
toolbarState: ToolbarState;
|
||||
slides: Slide[];
|
||||
|
@ -40,6 +41,7 @@ const state: State = {
|
|||
editorAreaFocus: false,
|
||||
disableHotkeys: false,
|
||||
showGridLines: false,
|
||||
creatingElementType: '',
|
||||
availableFonts: [],
|
||||
toolbarState: 'slideStyle',
|
||||
slides: slides,
|
||||
|
|
|
@ -49,6 +49,10 @@ export const mutations: MutationTree<State> = {
|
|||
state.showGridLines = show
|
||||
},
|
||||
|
||||
[MutationTypes.SET_CREATING_ELEMENT_TYPE](state, type: string) {
|
||||
state.creatingElementType = type
|
||||
},
|
||||
|
||||
[MutationTypes.SET_AVAILABLE_FONTS](state) {
|
||||
state.availableFonts = FONT_NAMES.filter(font => isSupportFontFamily(font.en))
|
||||
},
|
||||
|
|
|
@ -81,3 +81,8 @@ export interface ImageClipedEmitData {
|
|||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateElementSelectionData {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
<template>
|
||||
<div
|
||||
class="element-create-selection"
|
||||
ref="selectionRef"
|
||||
@mousedown.stop="$event => createSelection($event)"
|
||||
>
|
||||
<div :class="['selection', elementType]" v-if="start && end" :style="position">
|
||||
|
||||
<!-- 绘制线条专用 -->
|
||||
<SvgWrapper
|
||||
v-if="elementType === 'line' && lineData"
|
||||
overflow="visible"
|
||||
:width="lineData.svgWidth"
|
||||
:height="lineData.svgHeight"
|
||||
>
|
||||
<path
|
||||
:d="lineData.path"
|
||||
stroke="#888"
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke-linecap
|
||||
stroke-linejoin
|
||||
stroke-miterlimit
|
||||
></path>
|
||||
</SvgWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, reactive, Ref, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { MutationTypes, State } from '@/store'
|
||||
|
||||
import SvgWrapper from '@/components/SvgWrapper.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'element-create-selection',
|
||||
components: {
|
||||
SvgWrapper,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const store = useStore<State>()
|
||||
const ctrlOrShiftKeyActive: Ref<boolean> = computed(() => store.getters.ctrlOrShiftKeyActive)
|
||||
const elementType = computed(() => store.state.creatingElementType)
|
||||
|
||||
const start = ref<[number, number] | null>(null)
|
||||
const end = ref<[number, number] | null>(null)
|
||||
|
||||
const selectionRef = ref<HTMLElement | null>(null)
|
||||
const offset = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
onMounted(() => {
|
||||
if(!selectionRef.value) return
|
||||
const { x, y } = selectionRef.value.getBoundingClientRect()
|
||||
offset.x = x
|
||||
offset.y = y
|
||||
})
|
||||
|
||||
const createSelection = (e: MouseEvent) => {
|
||||
let isMouseDown = true
|
||||
|
||||
const startPageX = e.pageX
|
||||
const startPageY = e.pageY
|
||||
start.value = [startPageX, startPageY]
|
||||
|
||||
document.onmousemove = e => {
|
||||
if(!isMouseDown) return
|
||||
|
||||
let currentPageX = e.pageX
|
||||
let currentPageY = e.pageY
|
||||
|
||||
if(ctrlOrShiftKeyActive.value) {
|
||||
const moveX = currentPageX - startPageX
|
||||
const moveY = currentPageY - startPageY
|
||||
|
||||
const absX = Math.abs(moveX)
|
||||
const absY = Math.abs(moveY)
|
||||
|
||||
if(elementType.value === 'shape') {
|
||||
// moveX和moveY一正一负
|
||||
const isOpposite = (moveY > 0 && moveX < 0) || (moveY < 0 && moveX > 0)
|
||||
|
||||
if(absX > absY) {
|
||||
currentPageY = isOpposite ? startPageY - moveX : startPageY + moveX
|
||||
}
|
||||
else {
|
||||
currentPageX = isOpposite ? startPageX - moveY : startPageX + moveY
|
||||
}
|
||||
}
|
||||
|
||||
else if(elementType.value === 'line') {
|
||||
if(absX > absY) currentPageY = startPageY
|
||||
else currentPageX = startPageX
|
||||
}
|
||||
}
|
||||
|
||||
end.value = [currentPageX, currentPageY]
|
||||
}
|
||||
|
||||
document.onmouseup = e => {
|
||||
document.onmousemove = null
|
||||
document.onmouseup = null
|
||||
isMouseDown = false
|
||||
|
||||
const endPageX = e.pageX
|
||||
const endPageY = e.pageY
|
||||
|
||||
const minSize = 30
|
||||
|
||||
if(Math.abs(endPageX - startPageX) >= minSize || Math.abs(endPageY - startPageY) >= minSize) {
|
||||
emit('created', {
|
||||
start: start.value,
|
||||
end: end.value,
|
||||
})
|
||||
store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lineData = computed(() => {
|
||||
if(!start.value || !end.value || elementType.value !== 'line') return null
|
||||
|
||||
const [_startX, _startY] = start.value
|
||||
const [_endX, _endY] = end.value
|
||||
const minX = Math.min(_startX, _endX)
|
||||
const maxX = Math.max(_startX, _endX)
|
||||
const minY = Math.min(_startY, _endY)
|
||||
const maxY = Math.max(_startY, _endY)
|
||||
|
||||
const svgWidth = maxX - minX >= 24 ? maxX - minX : 24
|
||||
const svgHeight = maxY - minY >= 24 ? maxY - minY : 24
|
||||
|
||||
const startX = _startX === minX ? 0 : maxX - minX
|
||||
const startY = _startY === minY ? 0 : maxY - minY
|
||||
const endX = _endX === minX ? 0 : maxX - minX
|
||||
const endY = _endY === minY ? 0 : maxY - minY
|
||||
|
||||
const path = `M${startX}, ${startY} L${endX}, ${endY}`
|
||||
|
||||
return {
|
||||
svgWidth,
|
||||
svgHeight,
|
||||
startX,
|
||||
startY,
|
||||
endX,
|
||||
endY,
|
||||
path,
|
||||
}
|
||||
})
|
||||
|
||||
const position = computed(() => {
|
||||
if(!start.value || !end.value) return {}
|
||||
|
||||
const [startX, startY] = start.value
|
||||
const [endX, endY] = end.value
|
||||
const minX = Math.min(startX, endX)
|
||||
const maxX = Math.max(startX, endX)
|
||||
const minY = Math.min(startY, endY)
|
||||
const maxY = Math.max(startY, endY)
|
||||
|
||||
const width = maxX - minX
|
||||
const height = maxY - minY
|
||||
|
||||
return {
|
||||
left: minX - offset.x + 'px',
|
||||
top: minY - offset.y + 'px',
|
||||
width: width + 'px',
|
||||
height: height + 'px',
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
selectionRef,
|
||||
start,
|
||||
end,
|
||||
elementType,
|
||||
createSelection,
|
||||
lineData,
|
||||
position,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.element-create-selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.selection {
|
||||
position: absolute;
|
||||
|
||||
&:not(.line) {
|
||||
border: 1px solid #888;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -7,6 +7,10 @@
|
|||
v-contextmenu="contextmenus"
|
||||
v-click-outside="removeEditorAreaFocus"
|
||||
>
|
||||
<ElementCreateSelection
|
||||
v-if="creatingElementType"
|
||||
@created="data => createElement(data)"
|
||||
/>
|
||||
<div
|
||||
class="viewport-wrapper"
|
||||
:style="{
|
||||
|
@ -77,7 +81,7 @@ import throttle from 'lodash/throttle'
|
|||
import { State, MutationTypes } from '@/store'
|
||||
import { ContextmenuItem } from '@/components/Contextmenu/types'
|
||||
import { PPTElement, Slide } from '@/types/slides'
|
||||
import { AlignmentLineProps } from '@/types/edit'
|
||||
import { AlignmentLineProps, CreateElementSelectionData } from '@/types/edit'
|
||||
|
||||
import useViewportSize from './hooks/useViewportSize'
|
||||
import useMouseSelection from './hooks/useMouseSelection'
|
||||
|
@ -97,6 +101,7 @@ import EditableElement from './EditableElement.vue'
|
|||
import MouseSelection from './MouseSelection.vue'
|
||||
import SlideBackground from './SlideBackground.vue'
|
||||
import AlignmentLine from './AlignmentLine.vue'
|
||||
import ElementCreateSelection from './ElementCreateSelection.vue'
|
||||
import MultiSelectOperate from './Operate/MultiSelectOperate.vue'
|
||||
import Operate from './Operate/index.vue'
|
||||
|
||||
|
@ -107,6 +112,7 @@ export default defineComponent({
|
|||
MouseSelection,
|
||||
SlideBackground,
|
||||
AlignmentLine,
|
||||
ElementCreateSelection,
|
||||
MultiSelectOperate,
|
||||
Operate,
|
||||
},
|
||||
|
@ -177,6 +183,11 @@ export default defineComponent({
|
|||
store.commit(MutationTypes.SET_GRID_LINES_STATE, !showGridLines.value)
|
||||
}
|
||||
|
||||
const creatingElementType = computed(() => store.state.creatingElementType)
|
||||
const createElement = (data: CreateElementSelectionData) => {
|
||||
console.log(data)
|
||||
}
|
||||
|
||||
const contextmenus = (): ContextmenuItem[] => {
|
||||
return [
|
||||
{
|
||||
|
@ -217,6 +228,8 @@ export default defineComponent({
|
|||
removeEditorAreaFocus,
|
||||
currentSlide,
|
||||
isShowGridLines,
|
||||
creatingElementType,
|
||||
createElement,
|
||||
alignmentLines,
|
||||
selectElement,
|
||||
rotateElement,
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
</div>
|
||||
|
||||
<div class="add-element-handler">
|
||||
<IconFont class="handler-item" type="icon-font-size" />
|
||||
<IconFont class="handler-item" type="icon-font-size" @click="createElement('text')" />
|
||||
<UploadInput @change="files => insertImageElement(files)">
|
||||
<IconFont class="handler-item" type="icon-image" />
|
||||
</UploadInput>
|
||||
<IconFont class="handler-item" type="icon-star" />
|
||||
<IconFont class="handler-item" type="icon-line" />
|
||||
<IconFont class="handler-item" type="icon-star" @click="createElement('shape')" />
|
||||
<IconFont class="handler-item" type="icon-line" @click="createElement('line')" />
|
||||
<IconFont class="handler-item" type="icon-table" />
|
||||
<IconFont class="handler-item" type="icon-piechart" />
|
||||
</div>
|
||||
|
@ -27,7 +27,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { State } from '@/store'
|
||||
import { MutationTypes, State } from '@/store'
|
||||
import { getImageDataURL } from '@/utils/image'
|
||||
import useScaleCanvas from '@/hooks/useScaleCanvas'
|
||||
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
|
||||
|
@ -59,6 +59,10 @@ export default defineComponent({
|
|||
getImageDataURL(imageFile).then(dataURL => createImageElement(dataURL))
|
||||
}
|
||||
|
||||
const createElement = (type: string) => {
|
||||
store.commit(MutationTypes.SET_CREATING_ELEMENT_TYPE, type)
|
||||
}
|
||||
|
||||
return {
|
||||
scaleCanvas,
|
||||
canvasScalePercentage,
|
||||
|
@ -67,6 +71,7 @@ export default defineComponent({
|
|||
redo,
|
||||
undo,
|
||||
insertImageElement,
|
||||
createElement,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -97,14 +97,15 @@ export default () => {
|
|||
const { ctrlKey, shiftKey } = e
|
||||
const key = e.key.toUpperCase()
|
||||
|
||||
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
||||
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
||||
|
||||
if(ctrlKey && key === KEYS.F) {
|
||||
e.preventDefault()
|
||||
enterScreening()
|
||||
store.commit(MutationTypes.SET_CTRL_KEY_STATE, false)
|
||||
}
|
||||
|
||||
if(ctrlKey && !ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, true)
|
||||
if(shiftKey && !shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, true)
|
||||
|
||||
if(!editorAreaFocus.value && !thumbnailsFocus.value) return
|
||||
|
||||
if(ctrlKey && key === KEYS.C) {
|
||||
|
|
Loading…
Reference in New Issue