Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Block selection #39

Merged
merged 5 commits into from
Jan 4, 2024
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
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"printWidth": 150,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
Expand Down
61 changes: 61 additions & 0 deletions src/Kernel/BlockSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { EventManager } from './EventManager'
import { Block } from '@BlockHub/Block/Block'

export type BlkSlctnType = Block

export type SelectionEventType = 'select' | 'unselect' | 'update'

export class BlockSelection {
private _selectedBlocks: Array<BlkSlctnType> = []

get blocks(): BlkSlctnType[] {
return this._selectedBlocks
}

events = {
update: new EventManager<SelectionEventType>(),
}

isSelected(block: BlkSlctnType): boolean {
return this._selectedBlocks.findIndex((b) => b === block) !== -1
}

add(block: BlkSlctnType) {
this._selectedBlocks.push(block)
this.events.update.emit('update')
}

remove(block: BlkSlctnType) {
this._selectedBlocks.splice(this._selectedBlocks.indexOf(block), 1)
this.events.update.emit('update')
}

replace(blocks: BlkSlctnType[]) {
this._selectedBlocks = blocks
this.events.update.emit('update')
}

clear() {
this._selectedBlocks = []
this.events.update.emit('update')
}

toggle(block: BlkSlctnType) {
if (this.isSelected(block)) {
this.remove(block)
} else {
this.add(block)
}
}

batchToggle(blocks: BlkSlctnType[]) {
blocks.forEach((block) => {
this.toggle(block)
})
}

focus(block: BlkSlctnType) {
this.clear()
this.add(block)
}
}
11 changes: 10 additions & 1 deletion src/Kernel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HistoryManager } from './HistoryManager'
import { Slide } from './Slide'
import { TableBlock } from '@BlockHub/TableBlock/TableBlock'
import { PictureBlock } from '@BlockHub/PictureBlock/PictureBlock'
import { BlockSelection } from './BlockSelection'
import { EventManager } from './EventManager'
import { TextAtom } from './Store/TextStore'

Expand All @@ -16,7 +17,13 @@ interface RichTextStateChange {

export const kernel = {
currentIndex: 0,
slides: [new Slide([new TextBoxBlock(100, 50), new TableBlock(400, 300, 4, 3), new PictureBlock(100, 300)])],
slides: [
new Slide([
new TextBoxBlock(100, 50),
new TableBlock(400, 300, 4, 3),
new PictureBlock(100, 300),
]),
],

richTextObserver: new EventManager<RichTextStateChange>(),

Expand All @@ -26,3 +33,5 @@ export const kernel = {
}

export const history = new HistoryManager()

export const selectionBlk = new BlockSelection()
36 changes: 33 additions & 3 deletions src/UserInterface/Slide/Slide.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
<script setup lang="ts">
import { kernel } from '@Kernel/index'
import { kernel, selectionBlk } from '@Kernel/index'
import { BlockViews } from '@BlockHub/BlockHub'
import { type BlkSlctnType } from '@Kernel/BlockSelection'
import { ref } from 'vue'

const slide = kernel.currentSlide

const handleBlockClick = (evt: PointerEvent, block: BlkSlctnType) => {
if (evt.metaKey) {
return selectionBlk.toggle(block)
}
selectionBlk.focus(block)
}

const selected = ref(new Map())
selectionBlk.events.update.on((evtType) => {
if (evtType !== 'update') {
return
}
selected.value = new Map(selectionBlk.blocks.map((blk) => [blk.id, blk]))
})
</script>

<template>
<div class="relative w-[960px] h-[540px] bg-white shadow-lg">
<component v-for="block of slide.blocks" :is="BlockViews[block.type]" :block="block"></component>
<div
id="slide-wrapper"
class="relative w-[960px] h-[540px] bg-white shadow-lg"
>
<component
v-for="block of slide.blocks"
:is="BlockViews[block.type]"
:block="block"
:class="{
'border !border-solid border-secondary-border bg-gray-50': selected.has(
block.id
),
}"
@click.stop="handleBlockClick($event, block)"
></component>
</div>
</template>
107 changes: 105 additions & 2 deletions src/UserInterface/Slide/SlideContainer.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,112 @@
<script setup lang="ts">
import { ref } from 'vue'
import { kernel, selectionBlk } from '@Kernel/index'
import Slide from './Slide.vue'

const blocks = kernel.currentSlide.blocks

// drag selection
const showDrag = ref(false)
const initPos = { x: 0, y: 0 }
const initSize = { width: 0, height: 0 }
const dragPos = ref<{ x: number; y: number }>(initPos)
const dragSize = ref<{ width: number; height: number }>(initSize)

const handleMouseDown = (evt: MouseEvent) => {
showDrag.value = true
dragPos.value = { x: evt.clientX, y: evt.clientY }

const slideContainer = document.getElementById(
'slide-container'
) as HTMLElement
slideContainer.addEventListener('mousemove', handleMouseMove)
slideContainer.addEventListener('mouseup', handleMouseUp)
slideContainer.addEventListener('mouseleave', handleMouseLeave)
}

const handleMouseUp = (evt: MouseEvent) => {
const { clientX, clientY } = evt
const { x, y } = dragPos.value
const left = Math.min(x, clientX)
const top = Math.min(y, clientY)
const right = Math.max(x, clientX)
const bottom = Math.max(y, clientY)
const slideWrapper = document.getElementById('slide-wrapper')
const rect = slideWrapper?.getBoundingClientRect()
const leftMargin = rect?.left || 0
const topMargin = rect?.top || 0

selectionBlk.clear()
blocks.forEach((block) => {
const { x, y, width, height } = block
const blockLeft = x + leftMargin
const blockTop = y + topMargin
const blockRight = blockLeft + width
const blockBottom = blockTop + height
// 处理选区逻辑,判断是否包含
if (
left <= blockLeft &&
right >= blockRight &&
top <= blockTop &&
bottom >= blockBottom
) {
selectionBlk.add(block)
}
})
// const rect = { left, top, right, bottom, width, height }
// selectionBlk.setSelection(rect)
// 移除事件监听
handleMouseLeave()
}

const rotateX = ref(0)
const rotateY = ref(0)
const handleMouseMove = (evt: MouseEvent) => {
const { clientX, clientY } = evt
dragSize.value = {
width: Math.abs(clientX - dragPos.value.x),
height: Math.abs(clientY - dragPos.value.y),
}
// 判断拖动方向
rotateY.value = clientX < dragPos.value.x ? 180 : 0
rotateX.value = clientY < dragPos.value.y ? 180 : 0
}

const handleMouseLeave = () => {
showDrag.value = false
dragPos.value = initPos
dragSize.value = initSize
removeEvents()
}

const removeEvents = () => {
const slideContainer = document.getElementById(
'slide-container'
) as HTMLElement
slideContainer.removeEventListener('mousemove', handleMouseMove)
slideContainer.removeEventListener('mouseup', handleMouseUp)
slideContainer.removeEventListener('mouseleave', handleMouseLeave)
}
</script>

<template>
<div class="flex-auto flex items-center justify-center bg-secondary">
<Slide />
<div
id="slide-container"
class="relative flex-auto flex items-center justify-center bg-secondary"
@mousedown.self="handleMouseDown"
>
<Slide @mousedown.self="handleMouseDown" />
<div
v-if="showDrag"
class="fixed border border-secondary-border bg-gray-300/50"
:style="{
left: `${dragPos.x}px`,
top: `${dragPos.y}px`,
width: `${dragSize.width}px`,
height: `${dragSize.height}px`,
transformOrigin: 'top left',
transform: `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`,
}"
></div>
</div>
</template>
3 changes: 1 addition & 2 deletions src/UserInterface/ToolBar/ToolBar.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, type Component } from 'vue'
import ToolTabs from './components/ToolTabs.vue'
import TabContent from './components/TabContent.vue'
import Home from './components/Home/index.vue'
import Insert from './components/Insert/Index.vue'
import { TOOL_TABS } from './const'
import { type Component } from 'vue'

const activeTab = ref('Home')
const handleClickTab = (tab: string) => {
Expand Down
29 changes: 25 additions & 4 deletions src/UserInterface/ToolBar/components/Home/FontStyle.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
<script setup lang="ts">
import { FontSizeTwo, AddText, ClearFormat, TextBold, TextItalic, TextUnderline, Strikethrough, BackgroundColor } from '@icon-park/vue-next'
import {
FontSizeTwo,
AddText,
ClearFormat,
TextBold,
TextItalic,
TextUnderline,
Strikethrough,
BackgroundColor,
} from '@icon-park/vue-next'
import MenuWrapper from '../MenuWrapper.vue'
import { kernel } from '@Kernel/index'
import { selectionBlk } from '@Kernel/index'

kernel.richTextObserver.on((newState) => {
// console.log('newState', newState)
console.log('newState', newState)
})

const handleBoldClick = () => {
selectionBlk.blocks.forEach((b) => {
b.formatBlock('bold', true)
})
}
</script>

<template>
Expand Down Expand Up @@ -35,9 +51,14 @@ kernel.richTextObserver.on((newState) => {
<add-text theme="outline" size="20" :strokeWidth="2" />
</button>
<button class="menu-btn">
<clear-format theme="two-tone" size="20" :fill="['#333', '#DE6C00']" :strokeWidth="2" />
<clear-format
theme="two-tone"
size="20"
:fill="['#333', '#DE6C00']"
:strokeWidth="2"
/>
</button>
<button class="menu-btn px-2">
<button class="menu-btn px-2" @click.stop="handleBoldClick">
<text-bold size="20" :strokeWidth="4" />
</button>
<button class="menu-btn px-2">
Expand Down
3 changes: 0 additions & 3 deletions src/UserInterface/ToolBar/components/Home/Paragraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ import MenuWrapper from '../MenuWrapper.vue'
<button class="menu-btn">
<align-text-both size="24" :strokeWidth="2" />
</button>
<button class="menu-btn w-7 h-7">
<font-size-two size="16" :strokeWidth="2" />
</button>
</div>
</MenuWrapper>
</template>
8 changes: 7 additions & 1 deletion src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@

:root {
/* font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; */
font-family: 'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
font-family: 'Segoe UI', 'Segoe UI Web (West European)', 'Segoe UI',
-apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif;
}

body {
margin: 0;
-moz-user-select: none; /*火狐*/
-webkit-user-select: none; /*webkit浏览器*/
-ms-user-select: none; /*IE10*/
-khtml-user-select: none; /*早期浏览器*/
user-select: none;
}

svg {
Expand Down