This commit is contained in:
pipipi-pikachu 2020-12-18 23:22:57 +08:00
parent 6c65908a07
commit ce9069d941
11 changed files with 351 additions and 30 deletions

View File

@ -1,12 +1,12 @@
const DEFAULT_COLOR = '#41464b'
export enum ELEMENTS {
text = '文本',
image = '图片',
shape = '形状',
line = '线条',
chart = '图表',
table = '表格',
export enum ElementTypes {
TEXT = '文本',
IMAGE = '图片',
SHAPE = '形状',
LINE = '线条',
CHART = '图表',
TABLE = '表格',
}
export const DEFAULT_TEXT = {

View File

@ -3,7 +3,7 @@ import { Slide } from '@/types/slides'
export const slides: Slide[] = [
{
id: 'xxx1',
background: ['solid', '#323f4f'],
background: ['solid', '#fff'],
elements: [
{
elId: 'xxx1',

View File

@ -24,4 +24,8 @@ export enum MutationTypes {
UNDO = 'undo',
REDO = 'redo',
SET_HISTORY_RECORD_LENGTH = 'setHistoryRecordLength',
// keyboard
SET_CTRL_KEY_STATE = 'setCtrlKeyState',
SET_SHIFT_KEY_STATE = 'setShiftKeyState',
}

View File

@ -8,6 +8,7 @@ export type Getters = {
handleElement(state: State): PPTElement | null;
canUndo(state: State): boolean;
canRedo(state: State): boolean;
ctrlOrShiftKeyActive(state: State): boolean;
}
export const getters: Getters = {
@ -45,4 +46,8 @@ export const getters: Getters = {
canRedo(state) {
return state.cursor < state.historyRecordLength - 1
},
ctrlOrShiftKeyActive(state) {
return state.ctrlKeyState || state.shiftKeyState
},
}

View File

@ -36,6 +36,9 @@ export type Mutations = {
[MutationTypes.UNDO](state: State): void;
[MutationTypes.REDO](state: State): void;
[MutationTypes.SET_HISTORY_RECORD_LENGTH](state: State, length: number): void;
[MutationTypes.SET_CTRL_KEY_STATE](state: State, isActive: boolean): void;
[MutationTypes.SET_SHIFT_KEY_STATE](state: State, isActive: boolean): void;
}
export const mutations: Mutations = {
@ -143,4 +146,13 @@ export const mutations: Mutations = {
[MutationTypes.SET_HISTORY_RECORD_LENGTH](state, length) {
state.historyRecordLength = length
},
// keyBoard
[MutationTypes.SET_CTRL_KEY_STATE](state, isActive) {
state.ctrlKeyState = isActive
},
[MutationTypes.SET_SHIFT_KEY_STATE](state, isActive) {
state.shiftKeyState = isActive
},
}

View File

@ -15,6 +15,8 @@ export type State = {
slideIndex: number;
cursor: number;
historyRecordLength: number;
ctrlKeyState: boolean;
shiftKeyState: boolean;
}
export const state: State = {
@ -30,4 +32,6 @@ export const state: State = {
slideIndex: 0,
cursor: -1,
historyRecordLength: 0,
ctrlKeyState: false,
shiftKeyState: false,
}

View File

@ -1,5 +1,14 @@
export type ElementType = 'text' | 'image' | 'shape' | 'line' | 'chart' | 'table'
export enum ElementTypes {
TEXT = 'text',
IMAGE = 'image',
SHAPE = 'shape',
LINE = 'line',
CHART = 'chart',
TABLE = 'table',
}
export interface PPTElementBaseProps {
elId: string;
isLock: boolean;
@ -41,7 +50,7 @@ export interface PPTImageElement extends PPTElementBaseProps, PPTElementSizeProp
range: [[number, number], [number, number]];
shape: 'rect' | 'roundRect' | 'ellipse' | 'triangle' | 'pentagon' | 'rhombus' | 'star';
};
flip?: { x?: number, y?: number };
flip?: { x?: number; y?: number };
shadow?: string;
}
@ -70,7 +79,7 @@ export interface PPTChartElement extends PPTElementBaseProps, PPTElementSizeProp
type: 'chart';
chartType: string;
theme: string;
data: Object;
data: string;
}
export interface TableElementCell {

View File

@ -0,0 +1,132 @@
<template>
<div
class="multi-select-operate"
:style="{
left: minX + 'px',
top: minY + 'px',
transform: `scale(${1 / canvasScale})`,
}"
>
<BorderLine v-for="line in borderLines" :key="line.type" :type="line.type" :style="line.style" />
<template v-if="!disableResizablePoint">
<ResizablePoint
v-for="point in resizablePoints"
:key="point.type"
:type="point.type"
:style="point.style"
@mousedown.stop="scaleMultiElement($event, { minX, maxX, minY, maxY }, point.direction)"
/>
</template>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, PropType, watch, toRefs, onMounted } from 'vue'
import { OPERATE_KEYS } from '@/configs/element'
import { PPTElement, ElementTypes } from '@/types/slides'
import { getElementListRange } from './utils/elementRange'
import { ElementScaleHandler, OperateResizablePointTypes, OperateBorderLineTypes } from '@/types/edit'
import ResizablePoint from '@/views/_common/_operate/ResizablePoint.vue'
import BorderLine from '@/views/_common/_operate/BorderLine.vue'
interface Range {
minX: number;
maxX: number;
minY: number;
maxY: number;
}
export default defineComponent({
name: 'multi-select-operate',
components: {
ResizablePoint,
BorderLine,
},
props: {
canvasScale: {
type: Number,
required: true,
},
activeElementList: {
type: Array as PropType<PPTElement[]>,
required: true,
},
scaleMultiElement: {
type: Function as PropType<(e: MouseEvent, range: Range, command: ElementScaleHandler) => void>,
required: true,
},
},
setup(props) {
const range = reactive({
minX: 0,
maxX: 0,
minY: 0,
maxY: 0,
})
const width = computed(() => (range.maxX - range.minX) * props.canvasScale)
const height = computed(() => (range.maxY - range.minY) * props.canvasScale)
const resizablePoints = computed(() => {
return [
{ type: OperateResizablePointTypes.TL, direction: OPERATE_KEYS.LEFT_TOP, style: {} },
{ type: OperateResizablePointTypes.TC, direction: OPERATE_KEYS.TOP, style: {left: width.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.TR, direction: OPERATE_KEYS.RIGHT_TOP, style: {left: width.value + 'px'} },
{ type: OperateResizablePointTypes.ML, direction: OPERATE_KEYS.LEFT, style: {top: height.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.MR, direction: OPERATE_KEYS.RIGHT, style: {left: width.value + 'px', top: height.value / 2 + 'px'} },
{ type: OperateResizablePointTypes.BL, direction: OPERATE_KEYS.LEFT_BOTTOM, style: {top: height.value + 'px'} },
{ type: OperateResizablePointTypes.BC, direction: OPERATE_KEYS.BOTTOM, style: {left: width.value / 2 + 'px', top: height.value + 'px'} },
{ type: OperateResizablePointTypes.BR, direction: OPERATE_KEYS.RIGHT_BOTTOM, style: {left: width.value + 'px', top: height.value + 'px'} },
]
})
const borderLines = computed(() => {
return [
{ type: OperateBorderLineTypes.T, style: {width: width.value + 'px'} },
{ type: OperateBorderLineTypes.B, style: {top: height.value + 'px', width: width.value + 'px'} },
{ type: OperateBorderLineTypes.L, style: {height: height.value + 'px'} },
{ type: OperateBorderLineTypes.R, style: {left: width.value + 'px', height: height.value + 'px'} },
]
})
const disableResizablePoint = computed(() => {
return props.activeElementList.some(item => {
if(
(item.type === ElementTypes.IMAGE || item.type === ElementTypes.SHAPE) &&
!item.rotate
) return false
return true
})
})
const setRange = () => {
const { minX, maxX, minY, maxY } = getElementListRange(props.activeElementList)
range.minX = minX
range.maxX = maxX
range.minY = minY
range.maxY = maxY
}
onMounted(setRange)
watch(props.activeElementList, setRange)
return {
...toRefs(range),
borderLines,
disableResizablePoint,
resizablePoints,
}
},
})
</script>
<style lang="scss" scoped>
.multi-select-operate {
position: absolute;
top: 0;
left: 0;
z-index: 100;
}
</style>

View File

@ -36,6 +36,13 @@
:type="line.type" :axis="line.axis" :length="line.length"
/>
<MultiSelectOperate
v-if="activeElementIdList.length > 1"
:activeElementList="activeElementList"
:canvasScale="canvasScale"
:scaleMultiElement="scaleMultiElement"
/>
<EditableElement
v-for="(element, index) in elementList"
:key="element.elId"
@ -63,13 +70,17 @@
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, watch } from 'vue'
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
import { useStore } from 'vuex'
import uniq from 'lodash/uniq'
import { State } from '@/store/state'
import { MutationTypes } from '@/store/constants'
import { ContextmenuItem } from '@/components/Contextmenu/types'
import { VIEWPORT_SIZE, VIEWPORT_ASPECT_RATIO } from '@/configs/canvas'
import { getImageDataURL } from '@/utils/image'
import { getElementRange } from './utils/elementRange'
import { PPTElement } from '@/types/slides'
import useDropImage from '@/hooks/useDropImage'
import useSetViewportSize from './hooks/useSetViewportSize'
@ -77,6 +88,7 @@ import useSetViewportSize from './hooks/useSetViewportSize'
import EditableElement from '@/views/_common/_element/EditableElement.vue'
import MouseSelection from './MouseSelection.vue'
import SlideBackground from './SlideBackground.vue'
import MultiSelectOperate from './MultiSelectOperate.vue'
import AlignmentLine, { AlignmentLineProps } from './AlignmentLine.vue'
export default defineComponent({
@ -85,22 +97,29 @@ export default defineComponent({
EditableElement,
MouseSelection,
SlideBackground,
MultiSelectOperate,
AlignmentLine,
},
setup() {
const store = useStore<State>()
const elementList = computed(() => {
const currentSlide = store.getters.currentSlide
return currentSlide ? JSON.parse(JSON.stringify(currentSlide.elements)) : []
})
const activeElementIdList = computed(() => store.state.activeElementIdList)
const handleElementId = computed(() => store.state.handleElementId)
const activeGroupElementId = ref('')
const activeElementIdList = computed(() => store.state.activeElementIdList)
const activeElementList = computed(() => store.getters.activeElementList)
const handleElementId = computed(() => store.state.handleElementId)
const ctrlOrShiftKeyActive = computed(() => store.getters.ctrlOrShiftKeyActive)
const activeGroupElementId = ref('')
const viewportRef = ref<HTMLElement | null>(null)
const isShowGridLines = ref(false)
const alignmentLines = ref<AlignmentLineProps[]>([])
const currentSlide = computed(() => store.getters.currentSlide)
const elementList = ref<PPTElement[]>([])
const setLocalElementList = () => {
elementList.value = currentSlide.value ? JSON.parse(JSON.stringify(currentSlide.value.elements)) : []
}
onMounted(setLocalElementList)
watch(currentSlide, setLocalElementList)
const dropImageFile = useDropImage(viewportRef)
watch(dropImageFile, () => {
@ -180,6 +199,67 @@ export default defineComponent({
document.onmouseup = null
isMouseDown = false
//
//
let inRangeElementList: PPTElement[] = []
for(let i = 0; i < elementList.value.length; i++) {
const element = elementList.value[i]
const mouseSelectionLeft = mouseSelectionState.left
const mouseSelectionTop = mouseSelectionState.top
const mouseSelectionWidth = mouseSelectionState.width
const mouseSelectionHeight = mouseSelectionState.height
const quadrant = mouseSelectionState.quadrant
const { minX, maxX, minY, maxY } = getElementRange(element)
let isInclude = false
if(quadrant === 4) {
isInclude = minX > mouseSelectionLeft &&
maxX < mouseSelectionLeft + mouseSelectionWidth &&
minY > mouseSelectionTop &&
maxY < mouseSelectionTop + mouseSelectionHeight
}
else if(quadrant === 1) {
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
minY > (mouseSelectionTop - mouseSelectionHeight) &&
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
}
else if(quadrant === 2) {
isInclude = minX > mouseSelectionLeft &&
maxX < mouseSelectionLeft + mouseSelectionWidth &&
minY > (mouseSelectionTop - mouseSelectionHeight) &&
maxY < (mouseSelectionTop - mouseSelectionHeight) + mouseSelectionHeight
}
else if(quadrant === 3) {
isInclude = minX > (mouseSelectionLeft - mouseSelectionWidth) &&
maxX < (mouseSelectionLeft - mouseSelectionWidth) + mouseSelectionWidth &&
minY > mouseSelectionTop &&
maxY < mouseSelectionTop + mouseSelectionHeight
}
//
if(isInclude && !element.isLock) inRangeElementList.push(element)
}
//
inRangeElementList = inRangeElementList.filter(inRangeElement => {
if(inRangeElement.groupId) {
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
const groupElementList = elementList.value.filter(element => element.groupId === inRangeElement.groupId)
return groupElementList.every(groupElement => inRangeElementIdList.includes(groupElement.elId))
}
return true
})
const inRangeElementIdList = inRangeElementList.map(inRangeElement => inRangeElement.elId)
//
// ->
if(activeElementIdList.value.length > 0 || inRangeElementIdList.length) {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, inRangeElementIdList)
}
mouseSelectionState.isShow = false
}
}
@ -187,7 +267,7 @@ export default defineComponent({
const editorAreaFocus = computed(() => store.state.editorAreaFocus)
const handleClickBlankArea = (e: MouseEvent) => {
updateMouseSelection(e)
if(!ctrlOrShiftKeyActive.value) updateMouseSelection(e)
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
}
@ -195,8 +275,76 @@ export default defineComponent({
if(editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, false)
}
const selectElement = () => {
console.log('selectElement')
const moveElement = (e: MouseEvent, element: PPTElement) => {
console.log(e, element)
}
const selectElement = (e: MouseEvent, element: PPTElement, canMove = true) => {
if(!editorAreaFocus.value) store.commit(MutationTypes.SET_EDITORAREA_FOCUS, true)
//
if(!activeElementIdList.value.includes(element.elId)) {
let newActiveIdList: string[] = []
if(ctrlOrShiftKeyActive.value) {
newActiveIdList = [...activeElementIdList.value, element.elId]
}
else newActiveIdList = [element.elId]
//
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
})
newActiveIdList = [...newActiveIdList, ...groupMembersId]
}
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, uniq(newActiveIdList))
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
}
//
else if(ctrlOrShiftKeyActive.value) {
let newActiveIdList: string[] = []
//
if(element.groupId) {
const groupMembersId: string[] = []
elementList.value.forEach((el: PPTElement) => {
if(el.groupId === element.groupId) groupMembersId.push(el.elId)
})
newActiveIdList = activeElementIdList.value.filter(elId => !groupMembersId.includes(elId))
}
else {
newActiveIdList = activeElementIdList.value.filter(elId => elId !== element.elId)
}
if(newActiveIdList.length > 0) {
store.commit(MutationTypes.SET_ACTIVE_ELEMENT_ID_LIST, newActiveIdList)
}
}
//
else if(handleElementId.value !== element.elId) {
store.commit(MutationTypes.SET_HANDLE_ELEMENT_ID, element.elId)
}
else if(activeGroupElementId.value !== element.elId && element.groupId) {
const startPageX = e.pageX
const startPageY = e.pageY
;(e.target as HTMLElement).onmouseup = (e: MouseEvent) => {
const currentPageX = e.pageX
const currentPageY = e.pageY
if(startPageX === currentPageX && startPageY === currentPageY) {
activeGroupElementId.value = element.elId
;(e.target as HTMLElement).onmouseup = null
}
}
}
if(canMove) moveElement(e, element)
}
const rotateElement = () => {
console.log('rotateElement')
@ -204,6 +352,9 @@ export default defineComponent({
const scaleElement = () => {
console.log('scaleElement')
}
const scaleMultiElement = () => {
console.log('scaleMultiElement')
}
const orderElement = () => {
console.log('orderElement')
}
@ -248,6 +399,7 @@ export default defineComponent({
return {
elementList,
activeElementIdList,
activeElementList,
handleElementId,
activeGroupElementId,
canvasRef,
@ -263,6 +415,7 @@ export default defineComponent({
selectElement,
rotateElement,
scaleElement,
scaleMultiElement,
orderElement,
combineElements,
uncombineElements,

View File

@ -13,7 +13,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref } from 'vue'
import { computed, defineComponent, onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex'
import { State } from '@/store/state'
import { KEYCODE } from '@/configs/keyCode'
@ -27,6 +27,7 @@ import Canvas from './Canvas/index.vue'
import CanvasTool from './CanvasTool/index.vue'
import Thumbnails from './Thumbnails/index.vue'
import Toolbar from './Toolbar/index.vue'
import { MutationTypes } from '@/store/constants'
export default defineComponent({
name: 'editor',
@ -38,10 +39,11 @@ export default defineComponent({
Toolbar,
},
setup() {
const ctrlKeyDown = ref(false)
const shiftKeyDown = ref(false)
const store = useStore<State>()
const ctrlKeyActive = computed(() => store.state.ctrlKeyState)
const shiftKeyActive = computed(() => store.state.shiftKeyState)
const editorAreaFocus = computed(() => store.state.editorAreaFocus)
const thumbnailsFocus = computed(() => store.state.thumbnailsFocus)
const disableHotkeys = computed(() => store.state.disableHotkeys)
@ -83,8 +85,8 @@ export default defineComponent({
const keydownListener = (e: KeyboardEvent) => {
const { keyCode, ctrlKey, shiftKey } = e
if(ctrlKey && !ctrlKeyDown.value) ctrlKeyDown.value = true
if(shiftKey && !shiftKeyDown.value) shiftKeyDown.value = true
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
@ -147,8 +149,8 @@ export default defineComponent({
}
const keyupListener = () => {
if(ctrlKeyDown.value) ctrlKeyDown.value = false
if(shiftKeyDown.value) shiftKeyDown.value = false
if(ctrlKeyActive.value) store.commit(MutationTypes.SET_CTRL_KEY_STATE, false)
if(shiftKeyActive.value) store.commit(MutationTypes.SET_SHIFT_KEY_STATE, false)
}
const pasteImageFile = (imageFile: File) => {

View File

@ -36,7 +36,7 @@
<div
class="operate"
:class="{
'show': isActive,
'active': isActive,
'multi-select': isMultiSelect && isActive,
'selected': isHandleEl
}"