관리자 블록 에디터 키보드 흐름 보정
This commit is contained in:
@@ -13,6 +13,7 @@ const blockRefs = ref([])
|
|||||||
const activeBlockId = ref('')
|
const activeBlockId = ref('')
|
||||||
const slashQuery = ref('')
|
const slashQuery = ref('')
|
||||||
const slashMenuDirection = ref('down')
|
const slashMenuDirection = ref('down')
|
||||||
|
const highlightedCommandIndex = ref(0)
|
||||||
const isApplyingExternalValue = ref(false)
|
const isApplyingExternalValue = ref(false)
|
||||||
let blockIdSeed = 0
|
let blockIdSeed = 0
|
||||||
|
|
||||||
@@ -368,6 +369,8 @@ const updateSlashQuery = (block) => {
|
|||||||
slashQuery.value = block.text.startsWith('/')
|
slashQuery.value = block.text.startsWith('/')
|
||||||
? block.text.slice(1).trim().toLowerCase()
|
? block.text.slice(1).trim().toLowerCase()
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
|
highlightedCommandIndex.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeBlockIndex = computed(() => editorBlocks.value.findIndex((block) => block.id === activeBlockId.value))
|
const activeBlockIndex = computed(() => editorBlocks.value.findIndex((block) => block.id === activeBlockId.value))
|
||||||
@@ -390,6 +393,8 @@ const visibleCommands = computed(() => {
|
|||||||
].some((keyword) => keyword.toLowerCase().includes(slashQuery.value)))
|
].some((keyword) => keyword.toLowerCase().includes(slashQuery.value)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const highlightedCommand = computed(() => visibleCommands.value[highlightedCommandIndex.value])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 슬래시 메뉴 명령 적용
|
* 슬래시 메뉴 명령 적용
|
||||||
* @param {Object} command - 블록 명령
|
* @param {Object} command - 블록 명령
|
||||||
@@ -425,6 +430,36 @@ const applyCommand = (command) => {
|
|||||||
focusBlock(index)
|
focusBlock(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 슬래시 메뉴 선택을 아래로 이동
|
||||||
|
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const highlightNextCommand = (event) => {
|
||||||
|
if (!visibleCommands.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
highlightedCommandIndex.value = (highlightedCommandIndex.value + 1) % visibleCommands.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 슬래시 메뉴 선택을 위로 이동
|
||||||
|
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const highlightPreviousCommand = (event) => {
|
||||||
|
if (!visibleCommands.value.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
highlightedCommandIndex.value = highlightedCommandIndex.value === 0
|
||||||
|
? visibleCommands.value.length - 1
|
||||||
|
: highlightedCommandIndex.value - 1
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 엔터 키로 다음 블록 생성
|
* 엔터 키로 다음 블록 생성
|
||||||
* @param {KeyboardEvent} event - 키보드 이벤트
|
* @param {KeyboardEvent} event - 키보드 이벤트
|
||||||
@@ -434,16 +469,18 @@ const applyCommand = (command) => {
|
|||||||
const handleEnter = (event, index) => {
|
const handleEnter = (event, index) => {
|
||||||
const currentBlock = editorBlocks.value[index]
|
const currentBlock = editorBlocks.value[index]
|
||||||
|
|
||||||
|
if (visibleCommands.value.length && currentBlock.text.startsWith('/')) {
|
||||||
|
event.preventDefault()
|
||||||
|
applyCommand(highlightedCommand.value || visibleCommands.value[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (currentBlock.type === 'code' && !event.shiftKey) {
|
if (currentBlock.type === 'code' && !event.shiftKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (!currentBlock.text.trim() && currentBlock.type === 'paragraph') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBlock.type === 'divider') {
|
if (currentBlock.type === 'divider') {
|
||||||
editorBlocks.value.splice(index + 1, 0, createEditorBlock())
|
editorBlocks.value.splice(index + 1, 0, createEditorBlock())
|
||||||
emitContent()
|
emitContent()
|
||||||
@@ -550,6 +587,8 @@ watch(editorBlocks, () => {
|
|||||||
@focus="activateBlock(block)"
|
@focus="activateBlock(block)"
|
||||||
@input="updateBlockText($event, index)"
|
@input="updateBlockText($event, index)"
|
||||||
@keydown.enter="handleEnter($event, index)"
|
@keydown.enter="handleEnter($event, index)"
|
||||||
|
@keydown.down="highlightNextCommand"
|
||||||
|
@keydown.up="highlightPreviousCommand"
|
||||||
@keydown.backspace="handleBackspace($event, index)"
|
@keydown.backspace="handleBackspace($event, index)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -569,9 +608,10 @@ watch(editorBlocks, () => {
|
|||||||
:class="slashMenuDirection === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'"
|
:class="slashMenuDirection === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-for="command in visibleCommands"
|
v-for="(command, commandIndex) in visibleCommands"
|
||||||
:key="`${command.type}-${command.level || 'default'}`"
|
:key="`${command.type}-${command.level || 'default'}`"
|
||||||
class="admin-block-editor__slash-item grid w-full gap-0.5 px-4 py-3 text-left hover:bg-surface"
|
class="admin-block-editor__slash-item grid w-full gap-0.5 px-4 py-3 text-left hover:bg-surface"
|
||||||
|
:class="commandIndex === highlightedCommandIndex ? 'bg-surface' : ''"
|
||||||
type="button"
|
type="button"
|
||||||
@mousedown.prevent="applyCommand(command)"
|
@mousedown.prevent="applyCommand(command)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# 의사결정 이력
|
# 의사결정 이력
|
||||||
|
|
||||||
|
## 2026-05-01 v0.0.11
|
||||||
|
|
||||||
|
### 블록 에디터 키보드 흐름 보정
|
||||||
|
|
||||||
|
빈 문단에서 Enter를 누를 때도 다음 빈 문단 블록을 생성하도록 유지한다. 작성 중 의도적으로 여백을 두거나 다음 입력 위치로 이동하는 행동이 자연스러운 글쓰기 흐름이기 때문이다. 저장 시에는 기존처럼 비어 있는 블록을 마크다운 문자열에 포함하지 않는다.
|
||||||
|
|
||||||
|
슬래시 메뉴는 입력 포커스를 본문 블록에 둔 채 키보드로 선택한다. `/제목 3`처럼 필터링한 뒤 Enter를 누르면 현재 강조된 항목을 적용하고, 위/아래 방향키로 강조 항목을 이동한다. 이렇게 하면 메뉴 항목으로 실제 DOM 포커스를 옮기지 않아도 Ghost류 에디터처럼 연속 입력 흐름을 유지할 수 있다.
|
||||||
|
|
||||||
## 2026-05-01 v0.0.10
|
## 2026-05-01 v0.0.10
|
||||||
|
|
||||||
### 블록 에디터 입력 안정화 결정
|
### 블록 에디터 입력 안정화 결정
|
||||||
|
|||||||
@@ -205,8 +205,11 @@ components/content/
|
|||||||
- 저장 데이터는 기존 `content` 필드의 마크다운 문자열을 유지한다.
|
- 저장 데이터는 기존 `content` 필드의 마크다운 문자열을 유지한다.
|
||||||
- `/` 입력 시 블록 선택 메뉴를 표시한다.
|
- `/` 입력 시 블록 선택 메뉴를 표시한다.
|
||||||
- `/` 명령 메뉴는 화면 하단 공간이 부족하면 현재 블록 위쪽으로 표시한다.
|
- `/` 명령 메뉴는 화면 하단 공간이 부족하면 현재 블록 위쪽으로 표시한다.
|
||||||
|
- `/` 명령 메뉴가 열린 상태에서 Enter를 누르면 현재 강조된 메뉴 항목을 적용한다.
|
||||||
|
- `/` 명령 메뉴가 열린 상태에서 위/아래 방향키로 강조 항목을 이동한다.
|
||||||
- 블록 메뉴는 문단, 제목 2, 제목 3, 인용, 목록, 코드, 구분선을 제공한다.
|
- 블록 메뉴는 문단, 제목 2, 제목 3, 인용, 목록, 코드, 구분선을 제공한다.
|
||||||
- `#`, `##`, `###`, `>`, `-` 입력 후 공백을 누르면 현재 블록 타입을 즉시 변환한다.
|
- `#`, `##`, `###`, `>`, `-` 입력 후 공백을 누르면 현재 블록 타입을 즉시 변환한다.
|
||||||
|
- 빈 문단에서 Enter를 누르면 다음 빈 문단 블록을 생성한다.
|
||||||
- 빈 블록 placeholder는 현재 활성 블록 또는 첫 빈 블록에만 표시한다.
|
- 빈 블록 placeholder는 현재 활성 블록 또는 첫 빈 블록에만 표시한다.
|
||||||
- 제목은 별도 라벨 영역이 아니라 에디터 상단의 큰 제목 입력으로 표시한다.
|
- 제목은 별도 라벨 영역이 아니라 에디터 상단의 큰 제목 입력으로 표시한다.
|
||||||
- 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다.
|
- 관리자 작성 화면과 공개 본문은 같은 마크다운 렌더링 기준을 사용한다.
|
||||||
@@ -288,6 +291,6 @@ APP_PORT=43118
|
|||||||
|
|
||||||
## 버전 관리
|
## 버전 관리
|
||||||
|
|
||||||
- 현재 버전: v0.0.10
|
- 현재 버전: v0.0.11
|
||||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
## 1차 관리자 개발
|
## 1차 관리자 개발
|
||||||
|
|
||||||
- [ ] 이미지 업로드
|
- [ ] 블록 에디터 브라우저 수동 QA: 빈 줄 Enter, `/` 메뉴 필터, 방향키, Enter 선택, 한글 조합 입력 확인
|
||||||
|
- [ ] 블록 에디터 저장/수정 왕복 QA: 기존 글 수정 시 블록 파싱, 저장 후 다시 열기 확인
|
||||||
|
- [ ] 이미지 업로드 블록 구현
|
||||||
|
- [ ] 콜아웃, 토글, 임베드 블록 추가
|
||||||
|
- [ ] 글 작성 중 자동 저장
|
||||||
|
|
||||||
## 2차 관리자 개발
|
## 2차 관리자 개발
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v0.0.11
|
||||||
|
|
||||||
|
- 관리자 블록 에디터에서 빈 문단 Enter 입력 시 새 빈 블록이 생성되도록 수정.
|
||||||
|
- 관리자 블록 에디터의 `/` 명령 메뉴에서 Enter로 선택 항목을 적용하도록 수정.
|
||||||
|
- 관리자 블록 에디터의 `/` 명령 메뉴에 위/아래 방향키 선택 이동 추가.
|
||||||
|
- 관리자 글 에디터 후속 작업 순서 정리.
|
||||||
|
- 패키지 버전을 0.0.11로 갱신.
|
||||||
|
|
||||||
## v0.0.10
|
## v0.0.10
|
||||||
|
|
||||||
- 관리자 블록 에디터의 `contenteditable` 입력 중복 문제 수정.
|
- 관리자 블록 에디터의 `contenteditable` 입력 중복 문제 수정.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.10",
|
"version": "0.0.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.10",
|
"version": "0.0.11",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "0.0.10",
|
"version": "0.0.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user