라이브 한글 Enter 중복 보정
This commit is contained in:
@@ -70,6 +70,11 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
/** 마지막 줄 아래 방향키로 외부 줄을 만들지 여부 */
|
||||||
|
arrowExitCreatesLine: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
/** ESC 등으로 이 줄의 슬래시 메뉴를 닫은 상태(문자 `/`는 유지) */
|
/** ESC 등으로 이 줄의 슬래시 메뉴를 닫은 상태(문자 `/`는 유지) */
|
||||||
slashCommandSuppressed: {
|
slashCommandSuppressed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -103,8 +108,8 @@ const suppressBlurCommit = ref(false)
|
|||||||
const splitLock = ref(false)
|
const splitLock = ref(false)
|
||||||
/** 조합 중 Enter 후 compositionend에서 분리할지 */
|
/** 조합 중 Enter 후 compositionend에서 분리할지 */
|
||||||
const pendingSplitAfterComposition = ref(false)
|
const pendingSplitAfterComposition = ref(false)
|
||||||
/** 조합 종료 직후 중복 Enter를 무시할 기준 시각 */
|
/** 조합 종료 직후 중복 Enter를 1회 무시할지 */
|
||||||
const suppressComposedEnterUntil = ref(0)
|
const suppressNextEnterAfterComposition = ref(false)
|
||||||
const showingRaw = ref(false)
|
const showingRaw = ref(false)
|
||||||
|
|
||||||
/** @returns {string} Enter 동작 모드 */
|
/** @returns {string} Enter 동작 모드 */
|
||||||
@@ -528,7 +533,10 @@ const navigateToAdjacentBlock = (direction, column, caretMode = 'column') => {
|
|||||||
const target = elements[currentIndex + direction]
|
const target = elements[currentIndex + direction]
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
if (['insert-below', 'multiline'].includes(resolvedEnterMode.value) && direction === 1) {
|
if (
|
||||||
|
direction === 1
|
||||||
|
&& (resolvedEnterMode.value === 'multiline' || props.arrowExitCreatesLine)
|
||||||
|
) {
|
||||||
emit('insert-below', buildInsertBelowPayload())
|
emit('insert-below', buildInsertBelowPayload())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,11 +881,11 @@ const onKeydown = (event) => {
|
|||||||
if (
|
if (
|
||||||
event.key === 'Enter'
|
event.key === 'Enter'
|
||||||
&& !event.shiftKey
|
&& !event.shiftKey
|
||||||
&& suppressComposedEnterUntil.value
|
&& suppressNextEnterAfterComposition.value
|
||||||
&& Date.now() < suppressComposedEnterUntil.value
|
|
||||||
) {
|
) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
suppressNextEnterAfterComposition.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,7 +906,6 @@ const onKeydown = (event) => {
|
|||||||
|
|
||||||
if (event.isComposing || event.keyCode === 229) {
|
if (event.isComposing || event.keyCode === 229) {
|
||||||
pendingSplitAfterComposition.value = true
|
pendingSplitAfterComposition.value = true
|
||||||
suppressComposedEnterUntil.value = Date.now() + 240
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,7 +943,6 @@ const onCompositionEnd = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pendingSplitAfterComposition.value = false
|
pendingSplitAfterComposition.value = false
|
||||||
suppressComposedEnterUntil.value = Date.now() + 240
|
|
||||||
const enterMode = showingRaw.value ? 'insert-below' : resolvedEnterMode.value
|
const enterMode = showingRaw.value ? 'insert-below' : resolvedEnterMode.value
|
||||||
|
|
||||||
if (enterMode !== 'split-paragraph' && enterMode !== 'insert-below') {
|
if (enterMode !== 'split-paragraph' && enterMode !== 'insert-below') {
|
||||||
@@ -944,7 +950,11 @@ const onCompositionEnd = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
suppressNextEnterAfterComposition.value = true
|
||||||
scheduleEnterAction(enterMode === 'split-paragraph' ? 'split' : 'insert-below')
|
scheduleEnterAction(enterMode === 'split-paragraph' ? 'split' : 'insert-below')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
suppressNextEnterAfterComposition.value = false
|
||||||
|
}, 1200)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2469,6 +2469,7 @@ onBeforeUnmount(() => {
|
|||||||
:model-value="quoteLine.text"
|
:model-value="quoteLine.text"
|
||||||
enter-mode="insert-below"
|
enter-mode="insert-below"
|
||||||
allow-raw-toggle
|
allow-raw-toggle
|
||||||
|
arrow-exit-creates-line
|
||||||
:raw-line="getMarkdownLine(quoteLine.sourceLine)"
|
:raw-line="getMarkdownLine(quoteLine.sourceLine)"
|
||||||
:source-line="quoteLine.sourceLine"
|
:source-line="quoteLine.sourceLine"
|
||||||
@commit="onQuoteLineInlineCommit(block, quoteLine.sourceIndex, $event)"
|
@commit="onQuoteLineInlineCommit(block, quoteLine.sourceIndex, $event)"
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 요약
|
# 업데이트 요약
|
||||||
|
|
||||||
|
## v1.5.51
|
||||||
|
|
||||||
|
- 라이브 인용·콜아웃에서 한글 입력 후 Enter가 줄을 2개 만들던 문제를 다시 보정했다.
|
||||||
|
- 콜아웃 마지막 줄에서 아래 방향키를 눌러도 새 본문 줄이 생기지 않도록 했다.
|
||||||
|
- 인용은 마지막 줄 아래 방향키에서만 외부 문단을 만들도록 동작을 분리했다.
|
||||||
|
|
||||||
## v1.5.50
|
## v1.5.50
|
||||||
|
|
||||||
- 라이브 작성 모드에서 한글 인용문 Enter가 외부 문단으로 빠지지 않고 다음 인용 줄을 만들도록 보강했다.
|
- 라이브 작성 모드에서 한글 인용문 Enter가 외부 문단으로 빠지지 않고 다음 인용 줄을 만들도록 보강했다.
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ docker exec sori-studio-db pg_isready -U sori_studio -d sori_studio
|
|||||||
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
|
docker exec sori-studio-db psql -U sori_studio -d sori_studio -c 'SELECT count(*) AS posts_count FROM posts;'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### v1.5.51 참고
|
||||||
|
|
||||||
|
- 추가 DB 마이그레이션은 없다.
|
||||||
|
- 라이브 인용 안에서 한글 입력 후 Enter 시 인용 줄이 1줄만 추가되는지 확인한다.
|
||||||
|
- 라이브 콜아웃 안에서 한글 입력 후 Enter 시 콜아웃 본문 줄이 1줄만 추가되는지 확인한다.
|
||||||
|
- 라이브 콜아웃 마지막 줄에서 아래 방향키 입력 시 새 본문 줄이 생성되지 않는지 확인한다.
|
||||||
|
- 라이브 인용 마지막 줄에서 아래 방향키 입력 시 외부 빈 문단이 생성되고 커서가 이동하는지 확인한다.
|
||||||
|
|
||||||
### v1.5.50 참고
|
### v1.5.50 참고
|
||||||
|
|
||||||
- 추가 DB 마이그레이션은 없다.
|
- 추가 DB 마이그레이션은 없다.
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
| 파일 | 화면 위치 |
|
| 파일 | 화면 위치 |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| components/content/ContentRenderer.vue | 게시물/페이지 본문 |
|
| components/content/ContentRenderer.vue | 게시물/페이지 본문 |
|
||||||
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 확장 블록 파싱, 인용 배경 옵션(`> [!bg=...]`), 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용 Enter 줄 추가·마지막 줄 아래 방향키 이탈, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 |
|
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 확장 블록 파싱, 인용 배경 옵션(`> [!bg=...]`), 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용 Enter 줄 추가·마지막 줄 아래 방향키 이탈, 콜아웃 아래 방향키 새 줄 생성 차단, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 갤러리 비율 기반 행 레이아웃, 라이브 갤러리 개별 이미지 편집·삭제, 리스트 마커 파란 계열 통일 |
|
||||||
| components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 |
|
| components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 |
|
||||||
| components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder |
|
| components/content/ProseImage.vue | 본문 내 이미지, 로드 실패·빈 URL placeholder |
|
||||||
| components/content/ProseList.vue | 목록 |
|
| components/content/ProseList.vue | 목록 |
|
||||||
|
|||||||
@@ -628,6 +628,7 @@ components/content/
|
|||||||
- `ProseImage`는 URL이 비어 있거나 로드에 실패해도 최소 높이 placeholder와 「이미지를 불러올 수 없음」 안내를 표시해 라이브 모드에서 블록 선택·편집이 가능하다.
|
- `ProseImage`는 URL이 비어 있거나 로드에 실패해도 최소 높이 placeholder와 「이미지를 불러올 수 없음」 안내를 표시해 라이브 모드에서 블록 선택·편집이 가능하다.
|
||||||
- 인용(`>`) 블록은 첫 인용 줄에 `> [!bg=yellow]` 또는 `> {bg=yellow}` 옵션 줄을 두면 해당 줄은 숨기고 블록 배경을 바꾼다. 지원 배경 프리셋은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이며, 옵션이 없으면 회색 기본 인용 스타일을 쓴다.
|
- 인용(`>`) 블록은 첫 인용 줄에 `> [!bg=yellow]` 또는 `> {bg=yellow}` 옵션 줄을 두면 해당 줄은 숨기고 블록 배경을 바꾼다. 지원 배경 프리셋은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이며, 옵션이 없으면 회색 기본 인용 스타일을 쓴다.
|
||||||
- 관리자 **라이브 모드**(미리보기) 인라인 편집: 문단·빈 줄·제목·인용·목록·코드 블록·콜아웃·토글을 렌더 스타일 그대로 contenteditable로 수정한다. blur·문단 이동(방향키) 시 편집 영역의 `<strong>`·`<em>` 등을 `**`·`*` 마크다운으로 다시 직렬화해 저장한다. **Enter**·**Shift+Enter** 모두 다음 문단(블록) 분리. 문단 안 `/`로 슬래시 명령 메뉴(`/image`+Enter 이미지 삽입 등). **소스(작성) 모드** textarea에서도 동일한 `/` 슬래시 메뉴를 사용하며, 상단 마크다운 툴바는 두지 않는다. 슬래시 기본 제목은 **h2·h3·h4**만 표시하며, 본문 **h1**은 `/h1` 검색 시에만 삽입한다(게시물 **제목 필드**가 페이지의 유일한 h1). `Cmd+Shift+K`는 현재 줄을 삭제하며 코드·콜아웃·토글 블록 내부에서는 커서가 있는 본문 줄을 삭제하고, 남은 본문 줄이 1개뿐이면 fenced 블록 전체를 삭제한다. 콜아웃 옵션은 첫 줄 `:::callout emoji=💡 bg=blue`처럼 `emoji`·`bg`(gray|blue|green|yellow|red|purple)로 지정하며, 라이브 모드에서는 블록에 포커스가 들어오면 오른쪽 설정 패널에서 수정한다. 코드 블록은 ` ```언어`·`nolinenos`(줄 번호 숨김)를 지원한다. 라이브·공개 모두 `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)으로 동일하게 표시한다. 라이브 모드 호버·포커스 시 Language 입력·줄번호 토글이 보인다. 공개 화면에는 언어 라벨 옆 **복사** 버튼으로 본문을 클립보드에 넣는다. 본문 하단 클릭으로 새 문단을 추가한다.
|
- 관리자 **라이브 모드**(미리보기) 인라인 편집: 문단·빈 줄·제목·인용·목록·코드 블록·콜아웃·토글을 렌더 스타일 그대로 contenteditable로 수정한다. blur·문단 이동(방향키) 시 편집 영역의 `<strong>`·`<em>` 등을 `**`·`*` 마크다운으로 다시 직렬화해 저장한다. **Enter**·**Shift+Enter** 모두 다음 문단(블록) 분리. 문단 안 `/`로 슬래시 명령 메뉴(`/image`+Enter 이미지 삽입 등). **소스(작성) 모드** textarea에서도 동일한 `/` 슬래시 메뉴를 사용하며, 상단 마크다운 툴바는 두지 않는다. 슬래시 기본 제목은 **h2·h3·h4**만 표시하며, 본문 **h1**은 `/h1` 검색 시에만 삽입한다(게시물 **제목 필드**가 페이지의 유일한 h1). `Cmd+Shift+K`는 현재 줄을 삭제하며 코드·콜아웃·토글 블록 내부에서는 커서가 있는 본문 줄을 삭제하고, 남은 본문 줄이 1개뿐이면 fenced 블록 전체를 삭제한다. 콜아웃 옵션은 첫 줄 `:::callout emoji=💡 bg=blue`처럼 `emoji`·`bg`(gray|blue|green|yellow|red|purple)로 지정하며, 라이브 모드에서는 블록에 포커스가 들어오면 오른쪽 설정 패널에서 수정한다. 코드 블록은 ` ```언어`·`nolinenos`(줄 번호 숨김)를 지원한다. 라이브·공개 모두 `ProseCodeBlock`(`#15171a`, `px-4 py-3`, `text-sm leading-6`)으로 동일하게 표시한다. 라이브 모드 호버·포커스 시 Language 입력·줄번호 토글이 보인다. 공개 화면에는 언어 라벨 옆 **복사** 버튼으로 본문을 클립보드에 넣는다. 본문 하단 클릭으로 새 문단을 추가한다.
|
||||||
|
- 라이브 모드 인용·콜아웃 내부 Enter는 한글 IME 조합 확정 뒤에도 한 번만 줄을 추가한다. 인용 마지막 줄에서 아래 방향키를 누르면 외부 빈 문단을 만들 수 있지만, 콜아웃 아래 방향키는 본문 줄을 새로 만들지 않는다.
|
||||||
- 이미지 파일을 붙여넣거나 드롭하면 관리자 업로드 API로 저장한 뒤 현재 커서 위치에 이미지 또는 갤러리 마크다운을 삽입한다.
|
- 이미지 파일을 붙여넣거나 드롭하면 관리자 업로드 API로 저장한 뒤 현재 커서 위치에 이미지 또는 갤러리 마크다운을 삽입한다.
|
||||||
- 툴바 `이미지`·`갤러리`는 미디어 모달을 연다. 모달 기본 탭은 **미디어 라이브러리**이며 **업로드** 탭에서 드래그·파일 선택 후 즉시 삽입한다.
|
- 툴바 `이미지`·`갤러리`는 미디어 모달을 연다. 모달 기본 탭은 **미디어 라이브러리**이며 **업로드** 탭에서 드래그·파일 선택 후 즉시 삽입한다.
|
||||||
- 미디어 라이브러리에서 단일 이미지를 선택하면 `` 형식으로 삽입한다.
|
- 미디어 라이브러리에서 단일 이미지를 선택하면 `` 형식으로 삽입한다.
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.5.51
|
||||||
|
|
||||||
|
- 게시물 글쓰기: 라이브 모드 인용·콜아웃에서 한글 조합 Enter가 줄 추가를 중복 실행하지 않도록 보정.
|
||||||
|
- 게시물 글쓰기: 라이브 모드 콜아웃 마지막 줄 아래 방향키가 새 본문 줄을 만들지 않도록 수정.
|
||||||
|
- 게시물 글쓰기: 라이브 모드 인용 마지막 줄 아래 방향키만 외부 문단을 만들도록 분리.
|
||||||
|
|
||||||
## v1.5.50
|
## v1.5.50
|
||||||
|
|
||||||
- 게시물 글쓰기: 라이브 모드 인용 Enter가 한글 조합 입력 뒤에도 내부 다음 인용 줄을 만들도록 수정.
|
- 게시물 글쓰기: 라이브 모드 인용 Enter가 한글 조합 입력 뒤에도 내부 다음 인용 줄을 만들도록 수정.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.5.50",
|
"version": "1.5.51",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.5.50",
|
"version": "1.5.51",
|
||||||
"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": "1.5.50",
|
"version": "1.5.51",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
Reference in New Issue
Block a user