@storkspear/post-editor-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/ColorPicker.d.ts +11 -0
- package/dist/ColorPicker.d.ts.map +1 -0
- package/dist/ColorPicker.js +30 -0
- package/dist/ColorPicker.js.map +1 -0
- package/dist/DrawioModal.d.ts +9 -0
- package/dist/DrawioModal.d.ts.map +1 -0
- package/dist/DrawioModal.js +97 -0
- package/dist/DrawioModal.js.map +1 -0
- package/dist/FontMenu.d.ts +14 -0
- package/dist/FontMenu.d.ts.map +1 -0
- package/dist/FontMenu.js +14 -0
- package/dist/FontMenu.js.map +1 -0
- package/dist/Icon.d.ts +69 -0
- package/dist/Icon.d.ts.map +1 -0
- package/dist/Icon.js +47 -0
- package/dist/Icon.js.map +1 -0
- package/dist/InsertHandle.d.ts +14 -0
- package/dist/InsertHandle.d.ts.map +1 -0
- package/dist/InsertHandle.js +60 -0
- package/dist/InsertHandle.js.map +1 -0
- package/dist/PickMenu.d.ts +61 -0
- package/dist/PickMenu.d.ts.map +1 -0
- package/dist/PickMenu.js +57 -0
- package/dist/PickMenu.js.map +1 -0
- package/dist/Popover.d.ts +20 -0
- package/dist/Popover.d.ts.map +1 -0
- package/dist/Popover.js +22 -0
- package/dist/Popover.js.map +1 -0
- package/dist/PostTitle.d.ts +33 -0
- package/dist/PostTitle.d.ts.map +1 -0
- package/dist/PostTitle.js +171 -0
- package/dist/PostTitle.js.map +1 -0
- package/dist/SelectionBubble.d.ts +6 -0
- package/dist/SelectionBubble.d.ts.map +1 -0
- package/dist/SelectionBubble.js +71 -0
- package/dist/SelectionBubble.js.map +1 -0
- package/dist/TableTools.d.ts +10 -0
- package/dist/TableTools.d.ts.map +1 -0
- package/dist/TableTools.js +200 -0
- package/dist/TableTools.js.map +1 -0
- package/dist/Toolbar.d.ts +17 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +333 -0
- package/dist/Toolbar.js.map +1 -0
- package/dist/UrlPrompt.d.ts +24 -0
- package/dist/UrlPrompt.d.ts.map +1 -0
- package/dist/UrlPrompt.js +53 -0
- package/dist/UrlPrompt.js.map +1 -0
- package/dist/VariantMenu.d.ts +47 -0
- package/dist/VariantMenu.d.ts.map +1 -0
- package/dist/VariantMenu.js +21 -0
- package/dist/VariantMenu.js.map +1 -0
- package/dist/WidthToggle.d.ts +19 -0
- package/dist/WidthToggle.d.ts.map +1 -0
- package/dist/WidthToggle.js +17 -0
- package/dist/WidthToggle.js.map +1 -0
- package/dist/bubbleShows.d.ts +10 -0
- package/dist/bubbleShows.d.ts.map +1 -0
- package/dist/bubbleShows.js +29 -0
- package/dist/bubbleShows.js.map +1 -0
- package/dist/content-baked.d.ts +22 -0
- package/dist/content-baked.d.ts.map +1 -0
- package/dist/content-baked.js +21 -0
- package/dist/content-baked.js.map +1 -0
- package/dist/content.d.ts +29 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +35 -0
- package/dist/content.js.map +1 -0
- package/dist/extensions/DiagramView.d.ts +26 -0
- package/dist/extensions/DiagramView.d.ts.map +1 -0
- package/dist/extensions/DiagramView.js +146 -0
- package/dist/extensions/DiagramView.js.map +1 -0
- package/dist/extensions/FigureView.d.ts +3 -0
- package/dist/extensions/FigureView.d.ts.map +1 -0
- package/dist/extensions/FigureView.js +108 -0
- package/dist/extensions/FigureView.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +166 -0
- package/dist/index.js.map +1 -0
- package/dist/objects/ObjectFloat.d.ts +11 -0
- package/dist/objects/ObjectFloat.d.ts.map +1 -0
- package/dist/objects/ObjectFloat.js +42 -0
- package/dist/objects/ObjectFloat.js.map +1 -0
- package/dist/objects/ObjectLayer.d.ts +10 -0
- package/dist/objects/ObjectLayer.d.ts.map +1 -0
- package/dist/objects/ObjectLayer.js +20 -0
- package/dist/objects/ObjectLayer.js.map +1 -0
- package/dist/objects/blockSize.d.ts +30 -0
- package/dist/objects/blockSize.d.ts.map +1 -0
- package/dist/objects/blockSize.js +56 -0
- package/dist/objects/blockSize.js.map +1 -0
- package/dist/objects/code.d.ts +3 -0
- package/dist/objects/code.d.ts.map +1 -0
- package/dist/objects/code.js +55 -0
- package/dist/objects/code.js.map +1 -0
- package/dist/objects/diagram.d.ts +21 -0
- package/dist/objects/diagram.d.ts.map +1 -0
- package/dist/objects/diagram.js +109 -0
- package/dist/objects/diagram.js.map +1 -0
- package/dist/objects/diagramGlyphs.d.ts +12 -0
- package/dist/objects/diagramGlyphs.d.ts.map +1 -0
- package/dist/objects/diagramGlyphs.js +49 -0
- package/dist/objects/diagramGlyphs.js.map +1 -0
- package/dist/objects/divider.d.ts +22 -0
- package/dist/objects/divider.d.ts.map +1 -0
- package/dist/objects/divider.js +60 -0
- package/dist/objects/divider.js.map +1 -0
- package/dist/objects/figure.d.ts +3 -0
- package/dist/objects/figure.d.ts.map +1 -0
- package/dist/objects/figure.js +62 -0
- package/dist/objects/figure.js.map +1 -0
- package/dist/objects/imageRow.d.ts +3 -0
- package/dist/objects/imageRow.d.ts.map +1 -0
- package/dist/objects/imageRow.js +29 -0
- package/dist/objects/imageRow.js.map +1 -0
- package/dist/objects/quote.d.ts +6 -0
- package/dist/objects/quote.d.ts.map +1 -0
- package/dist/objects/quote.js +13 -0
- package/dist/objects/quote.js.map +1 -0
- package/dist/objects/quoteGlyphs.d.ts +6 -0
- package/dist/objects/quoteGlyphs.d.ts.map +1 -0
- package/dist/objects/quoteGlyphs.js +28 -0
- package/dist/objects/quoteGlyphs.js.map +1 -0
- package/dist/objects/quoteRow.d.ts +3 -0
- package/dist/objects/quoteRow.d.ts.map +1 -0
- package/dist/objects/quoteRow.js +61 -0
- package/dist/objects/quoteRow.js.map +1 -0
- package/dist/objects/registry.d.ts +60 -0
- package/dist/objects/registry.d.ts.map +1 -0
- package/dist/objects/registry.js +45 -0
- package/dist/objects/registry.js.map +1 -0
- package/dist/objects/table.d.ts +11 -0
- package/dist/objects/table.d.ts.map +1 -0
- package/dist/objects/table.js +136 -0
- package/dist/objects/table.js.map +1 -0
- package/dist/objects/useDomRect.d.ts +14 -0
- package/dist/objects/useDomRect.d.ts.map +1 -0
- package/dist/objects/useDomRect.js +32 -0
- package/dist/objects/useDomRect.js.map +1 -0
- package/dist/objects/useEngaged.d.ts +14 -0
- package/dist/objects/useEngaged.d.ts.map +1 -0
- package/dist/objects/useEngaged.js +41 -0
- package/dist/objects/useEngaged.js.map +1 -0
- package/dist/pickItems.d.ts +36 -0
- package/dist/pickItems.d.ts.map +1 -0
- package/dist/pickItems.js +40 -0
- package/dist/pickItems.js.map +1 -0
- package/dist/recentColors.d.ts +7 -0
- package/dist/recentColors.d.ts.map +1 -0
- package/dist/recentColors.js +30 -0
- package/dist/recentColors.js.map +1 -0
- package/dist/seeds/drawio/aws.d.ts +15 -0
- package/dist/seeds/drawio/aws.d.ts.map +1 -0
- package/dist/seeds/drawio/aws.js +15 -0
- package/dist/seeds/drawio/aws.js.map +1 -0
- package/dist/seeds/drawio/azure.d.ts +14 -0
- package/dist/seeds/drawio/azure.d.ts.map +1 -0
- package/dist/seeds/drawio/azure.js +14 -0
- package/dist/seeds/drawio/azure.js.map +1 -0
- package/dist/seeds/drawio/ec2docker.d.ts +15 -0
- package/dist/seeds/drawio/ec2docker.d.ts.map +1 -0
- package/dist/seeds/drawio/ec2docker.js +15 -0
- package/dist/seeds/drawio/ec2docker.js.map +1 -0
- package/dist/seeds/drawio/userflow.d.ts +21 -0
- package/dist/seeds/drawio/userflow.d.ts.map +1 -0
- package/dist/seeds/drawio/userflow.js +21 -0
- package/dist/seeds/drawio/userflow.js.map +1 -0
- package/dist/seeds/mermaid.d.ts +19 -0
- package/dist/seeds/mermaid.d.ts.map +1 -0
- package/dist/seeds/mermaid.js +82 -0
- package/dist/seeds/mermaid.js.map +1 -0
- package/dist/useAnchoredPos.d.ts +22 -0
- package/dist/useAnchoredPos.d.ts.map +1 -0
- package/dist/useAnchoredPos.js +57 -0
- package/dist/useAnchoredPos.js.map +1 -0
- package/dist/useDismiss.d.ts +17 -0
- package/dist/useDismiss.d.ts.map +1 -0
- package/dist/useDismiss.js +36 -0
- package/dist/useDismiss.js.map +1 -0
- package/dist/useLastVariant.d.ts +9 -0
- package/dist/useLastVariant.d.ts.map +1 -0
- package/dist/useLastVariant.js +33 -0
- package/dist/useLastVariant.js.map +1 -0
- package/dist/useUploadCount.d.ts +3 -0
- package/dist/useUploadCount.d.ts.map +1 -0
- package/dist/useUploadCount.js +9 -0
- package/dist/useUploadCount.js.map +1 -0
- package/package.json +57 -0
- package/src/editor-ui.css +737 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 떠 있는 것을 **문서 맨 끝**으로 옮겨 그린다.
|
|
4
|
+
*
|
|
5
|
+
* 왜 필요한가: 툴바(`.toolbar2`)가 `position: sticky; z-index: 20` 이라 **쌓임 맥락**을 만든다.
|
|
6
|
+
* 그 안에서 아무리 z-index 를 올려도 밖의 z-index 20 위로는 못 올라간다.
|
|
7
|
+
* 그래서 표 오버레이(`.tt-layer`, z-index 40)가 툴바에서 연 드롭다운을 덮었다
|
|
8
|
+
*.
|
|
9
|
+
*
|
|
10
|
+
* 좌표를 이미 `position: fixed` 로 계산해 두는 것들만 옮긴다(`useAnchoredPos`).
|
|
11
|
+
* 부모 기준 absolute 로 뜨는 것은 옮기면 자리를 잃으므로 제자리에 둔다.
|
|
12
|
+
*
|
|
13
|
+
* React 이벤트는 포털을 넘어 **부모 컴포넌트로 그대로 전파**되므로 호출부는 안 바뀐다.
|
|
14
|
+
* 다만 DOM 조상이 달라지니 `data-editor-ui` 는 뜨는 것 **자신**이 달아야 한다(useEngaged).
|
|
15
|
+
*/
|
|
16
|
+
export declare function Portal({ active, children }: {
|
|
17
|
+
active: boolean;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=Popover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Popover.d.ts","sourceRoot":"","sources":["../src/Popover.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAGpF"}
|
package/dist/Popover.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
/**
|
|
4
|
+
* 떠 있는 것을 **문서 맨 끝**으로 옮겨 그린다.
|
|
5
|
+
*
|
|
6
|
+
* 왜 필요한가: 툴바(`.toolbar2`)가 `position: sticky; z-index: 20` 이라 **쌓임 맥락**을 만든다.
|
|
7
|
+
* 그 안에서 아무리 z-index 를 올려도 밖의 z-index 20 위로는 못 올라간다.
|
|
8
|
+
* 그래서 표 오버레이(`.tt-layer`, z-index 40)가 툴바에서 연 드롭다운을 덮었다
|
|
9
|
+
*.
|
|
10
|
+
*
|
|
11
|
+
* 좌표를 이미 `position: fixed` 로 계산해 두는 것들만 옮긴다(`useAnchoredPos`).
|
|
12
|
+
* 부모 기준 absolute 로 뜨는 것은 옮기면 자리를 잃으므로 제자리에 둔다.
|
|
13
|
+
*
|
|
14
|
+
* React 이벤트는 포털을 넘어 **부모 컴포넌트로 그대로 전파**되므로 호출부는 안 바뀐다.
|
|
15
|
+
* 다만 DOM 조상이 달라지니 `data-editor-ui` 는 뜨는 것 **자신**이 달아야 한다(useEngaged).
|
|
16
|
+
*/
|
|
17
|
+
export function Portal({ active, children }) {
|
|
18
|
+
if (!active || typeof document === 'undefined')
|
|
19
|
+
return _jsx(_Fragment, { children: children });
|
|
20
|
+
return createPortal(children, document.body);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=Popover.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Popover.js","sourceRoot":"","sources":["../src/Popover.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAGxC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,EAA4C;IACnF,IAAI,CAAC,MAAM,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO,4BAAG,QAAQ,GAAI,CAAA;IACtE,OAAO,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC9C,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type BannerBleed, type TitleValue } from '@storkspear/post-editor-core/title';
|
|
2
|
+
import { type ProseWidth } from '@storkspear/post-editor-core/proseWidth';
|
|
3
|
+
export { EMPTY_TITLE } from '@storkspear/post-editor-core/title';
|
|
4
|
+
export type { TitleBanner, TitleValue } from '@storkspear/post-editor-core/title';
|
|
5
|
+
/**
|
|
6
|
+
* 글 제목 — 본문과 **다른 물건**이다.
|
|
7
|
+
*
|
|
8
|
+
* 본문(ProseMirror)에 넣지 않은 이유: 제목은 DB 에서도 따로 사는 값이고(목록·permalink·`<title>`),
|
|
9
|
+
* 무엇보다 **한 덩어리로만 꾸민다.** 굵게·밑줄 같은 부분 서식이 붙을 자리가 아니다.
|
|
10
|
+
*
|
|
11
|
+
* `<input>` 이 아니라 `<h1 contenteditable>` 인 이유: input 은 **접히지 않는다.**
|
|
12
|
+
* 긴 제목이 편집 화면에서는 한 줄로 흘러가고 발행 페이지에서는 두 줄이 되어,
|
|
13
|
+
* 두 화면이 애초에 어긋나 있었다. `plaintext-only` 라 부분 서식은 여전히 불가능하다.
|
|
14
|
+
*/
|
|
15
|
+
export declare function PostTitle({ value, onChange, onFocus, onBlur, placeholder, width, bleed }: {
|
|
16
|
+
value: TitleValue;
|
|
17
|
+
onChange: (v: TitleValue) => void;
|
|
18
|
+
onFocus?: () => void;
|
|
19
|
+
onBlur?: () => void;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/**
|
|
22
|
+
* 본문 폭 — **본문과 같은 값을 받아야 한다.**
|
|
23
|
+
* 제목은 `.prose` 밖에 그려지므로 `.prose` 에 건 폭이 여기까지 내려오지 않는다.
|
|
24
|
+
* 안 넘기면 본문은 화면 끝까지 가는데 제목 밑줄만 38rem 에서 끊긴다.
|
|
25
|
+
*/
|
|
26
|
+
width?: ProseWidth;
|
|
27
|
+
/**
|
|
28
|
+
* 배너를 어디까지 펴는가 — `container`(기본) 또는 `viewport`.
|
|
29
|
+
* 발행 쪽 `PostHead` 에 **같은 값**을 줘야 두 화면이 같아진다.
|
|
30
|
+
*/
|
|
31
|
+
bleed?: BannerBleed;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
//# sourceMappingURL=PostTitle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostTitle.d.ts","sourceRoot":"","sources":["../src/PostTitle.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoC,KAAK,WAAW,EAAoB,KAAK,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAC1I,OAAO,EAAmB,KAAK,UAAU,EAAE,MAAM,yCAAyC,CAAA;AAM1F,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAA;AAIjF;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,WAAkB,EAAE,KAAK,EAAE,KAAmB,EAAE,EAAE;IAC9G,KAAK,EAAE,UAAU,CAAA;IACjB,QAAQ,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAA;IACjC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB,+BAqMA"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { bannerPosition, titleInlineStyle } from '@storkspear/post-editor-core/title';
|
|
4
|
+
import { proseWidthStyle } from '@storkspear/post-editor-core/proseWidth';
|
|
5
|
+
import { FOCUS_MAX, FOCUS_MIN } from '@storkspear/post-editor-core/vocab';
|
|
6
|
+
import { ref, resolveSrc, upload } from '@storkspear/post-editor-core/attachments';
|
|
7
|
+
import { pickImageFiles, prepareFailMessage, prepareImage } from '@storkspear/post-editor-core/image';
|
|
8
|
+
import { Icon } from './Icon.js';
|
|
9
|
+
export { EMPTY_TITLE } from '@storkspear/post-editor-core/title';
|
|
10
|
+
const clampFocus = (n) => Math.min(FOCUS_MAX, Math.max(FOCUS_MIN, n));
|
|
11
|
+
/**
|
|
12
|
+
* 글 제목 — 본문과 **다른 물건**이다.
|
|
13
|
+
*
|
|
14
|
+
* 본문(ProseMirror)에 넣지 않은 이유: 제목은 DB 에서도 따로 사는 값이고(목록·permalink·`<title>`),
|
|
15
|
+
* 무엇보다 **한 덩어리로만 꾸민다.** 굵게·밑줄 같은 부분 서식이 붙을 자리가 아니다.
|
|
16
|
+
*
|
|
17
|
+
* `<input>` 이 아니라 `<h1 contenteditable>` 인 이유: input 은 **접히지 않는다.**
|
|
18
|
+
* 긴 제목이 편집 화면에서는 한 줄로 흘러가고 발행 페이지에서는 두 줄이 되어,
|
|
19
|
+
* 두 화면이 애초에 어긋나 있었다. `plaintext-only` 라 부분 서식은 여전히 불가능하다.
|
|
20
|
+
*/
|
|
21
|
+
export function PostTitle({ value, onChange, onFocus, onBlur, placeholder = '제목', width, bleed = 'container' }) {
|
|
22
|
+
const h1 = useRef(null);
|
|
23
|
+
const coverRef = useRef(null);
|
|
24
|
+
const [busy, setBusy] = useState(false);
|
|
25
|
+
/** 위치이동 중일 때의 임시 좌표. null 이면 평소 모드 — **저장값은 확인을 눌러야 바뀐다** */
|
|
26
|
+
const [moving, setMoving] = useState(null);
|
|
27
|
+
const banner = value.banner;
|
|
28
|
+
/**
|
|
29
|
+
* 밖에서 들어온 글자를 DOM 에 밀어 넣는다 — **손이 올라가 있을 때는 안 한다.**
|
|
30
|
+
* 조합 중(한글)에 DOM 을 다시 쓰면 조합이 끊겨 「ㅇㅎㅇㄴㅁㅎ」 가 된다.
|
|
31
|
+
*/
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
const el = h1.current;
|
|
34
|
+
if (!el || document.activeElement === el)
|
|
35
|
+
return;
|
|
36
|
+
if (el.textContent !== value.text)
|
|
37
|
+
el.textContent = value.text;
|
|
38
|
+
}, [value.text]);
|
|
39
|
+
const onInput = useCallback(() => {
|
|
40
|
+
const el = h1.current;
|
|
41
|
+
if (!el)
|
|
42
|
+
return;
|
|
43
|
+
// 줄바꿈은 공백으로 — 제목은 한 줄이다(접히는 것은 화면에서만)
|
|
44
|
+
const text = (el.textContent ?? '').replace(/[\r\n\t]+/g, ' ');
|
|
45
|
+
onChange({ ...value, text });
|
|
46
|
+
}, [onChange, value]);
|
|
47
|
+
const putBanner = useCallback(async () => {
|
|
48
|
+
setBusy(true);
|
|
49
|
+
try {
|
|
50
|
+
const files = await pickImageFiles();
|
|
51
|
+
const file = files[0];
|
|
52
|
+
if (!file)
|
|
53
|
+
return;
|
|
54
|
+
const prepared = await prepareImage(file);
|
|
55
|
+
// **조용히 끝내지 않는다.** 아무 일도 안 일어나면 왜 안 되는지 알 길이 없다
|
|
56
|
+
if (!prepared) {
|
|
57
|
+
window.alert(prepareFailMessage(file.type.startsWith('image/') ? 'undecodable' : 'not-image', file.name));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// **먼저 올리고 끝난 뒤에 넣는다** — 본문 사진과 같은 계약이다.
|
|
61
|
+
// 올리는 중에 저장하면 브라우저에만 있는 임시 주소가 저장되기 때문.
|
|
62
|
+
const up = await upload(prepared.blob, prepared.name);
|
|
63
|
+
if (!up) {
|
|
64
|
+
window.alert(`사진을 올리지 못했습니다 — ${file.name}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
onChange({ ...value, banner: { src: ref(up.id), focusX: 50, focusY: 50, cover: true } });
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
setBusy(false);
|
|
71
|
+
}
|
|
72
|
+
}, [onChange, value]);
|
|
73
|
+
const patch = (b) => banner && onChange({ ...value, banner: { ...banner, ...b } });
|
|
74
|
+
/**
|
|
75
|
+
* 사진을 끌어 프레임 안에서 옮긴다.
|
|
76
|
+
*
|
|
77
|
+
* `object-fit: cover` 에서 `object-position: X%` 는 넘치는 양(ox)만큼을 X% 지점에 맞춘다.
|
|
78
|
+
* 오른쪽으로 dx 끌면 사진의 왼쪽 가장자리가 +dx 로 가야 하므로 `X' = X − dx·100/ox` 다.
|
|
79
|
+
* **0~100 클램프가 곧 「빈 곳이 안 생긴다」는 보장이다** — cover 에서 % 가 그 범위면
|
|
80
|
+
* 프레임은 언제나 사진으로 덮인다.
|
|
81
|
+
*/
|
|
82
|
+
const onGrab = useCallback((e) => {
|
|
83
|
+
if (!moving)
|
|
84
|
+
return;
|
|
85
|
+
const host = coverRef.current;
|
|
86
|
+
const img = host?.querySelector('img');
|
|
87
|
+
if (!host || !img)
|
|
88
|
+
return;
|
|
89
|
+
const { width: W, height: H } = host.getBoundingClientRect();
|
|
90
|
+
const iw = img.naturalWidth, ih = img.naturalHeight;
|
|
91
|
+
if (!iw || !ih)
|
|
92
|
+
return; // 아직 안 실렸다 — 산식이 NaN 이 된다
|
|
93
|
+
const s = Math.max(W / iw, H / ih);
|
|
94
|
+
const ox = iw * s - W, oy = ih * s - H; // 넘치는 양. cover 라 한 축은 0 이다
|
|
95
|
+
const startX = e.clientX, startY = e.clientY;
|
|
96
|
+
const from = moving;
|
|
97
|
+
const target = e.currentTarget;
|
|
98
|
+
target.setPointerCapture(e.pointerId);
|
|
99
|
+
let latest = from;
|
|
100
|
+
const move = (ev) => {
|
|
101
|
+
latest = {
|
|
102
|
+
x: ox > 0.5 ? clampFocus(Math.round(from.x - ((ev.clientX - startX) / ox) * 100)) : from.x,
|
|
103
|
+
y: oy > 0.5 ? clampFocus(Math.round(from.y - ((ev.clientY - startY) / oy) * 100)) : from.y,
|
|
104
|
+
};
|
|
105
|
+
setMoving(latest);
|
|
106
|
+
};
|
|
107
|
+
const up = () => {
|
|
108
|
+
try {
|
|
109
|
+
target.releasePointerCapture(e.pointerId);
|
|
110
|
+
}
|
|
111
|
+
catch { /* 이미 풀렸으면 그만 */ }
|
|
112
|
+
target.removeEventListener('pointermove', move);
|
|
113
|
+
target.removeEventListener('pointerup', up);
|
|
114
|
+
// 브라우저가 포인터를 회수하면 pointerup 이 안 온다 — 리스너가 남아
|
|
115
|
+
// 버튼을 뗀 뒤에도 마우스만 움직이면 사진이 계속 움직인다
|
|
116
|
+
target.removeEventListener('pointercancel', up);
|
|
117
|
+
target.removeEventListener('lostpointercapture', up);
|
|
118
|
+
};
|
|
119
|
+
target.addEventListener('pointermove', move);
|
|
120
|
+
target.addEventListener('pointerup', up);
|
|
121
|
+
target.addEventListener('pointercancel', up);
|
|
122
|
+
target.addEventListener('lostpointercapture', up);
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
}, [moving]);
|
|
125
|
+
// 위치이동 중 방향키 — 손이 떨리는 마지막 1% 는 키로 맞추는 것이 빠르다
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!moving)
|
|
128
|
+
return;
|
|
129
|
+
const onKey = (ev) => {
|
|
130
|
+
if (ev.key === 'Escape') {
|
|
131
|
+
setMoving(null);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const step = ev.shiftKey ? 5 : 1;
|
|
135
|
+
const d = {
|
|
136
|
+
ArrowLeft: [step, 0], ArrowRight: [-step, 0], ArrowUp: [0, step], ArrowDown: [0, -step],
|
|
137
|
+
};
|
|
138
|
+
const v = d[ev.key];
|
|
139
|
+
if (!v)
|
|
140
|
+
return;
|
|
141
|
+
ev.preventDefault();
|
|
142
|
+
setMoving((m) => (m ? { x: clampFocus(m.x + v[0]), y: clampFocus(m.y + v[1]) } : m));
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener('keydown', onKey);
|
|
145
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
146
|
+
}, [moving]);
|
|
147
|
+
const shown = moving ?? (banner ? { x: banner.focusX, y: banner.focusY } : null);
|
|
148
|
+
return (_jsxs("header", { className: `post-head is-editing${moving ? ' is-moving' : ''}`, "data-banner": banner ? '1' : '0', "data-bleed": bleed, style: proseWidthStyle(width), children: [banner && (_jsxs("div", { className: "post-cover", ref: coverRef, onPointerDown: onGrab, children: [_jsx("img", { src: resolveSrc(banner.src), alt: "", style: { objectPosition: shown ? `${shown.x}% ${shown.y}%` : bannerPosition(banner) } }), moving && _jsx("p", { className: "ph-hint", "data-ui": "", children: "\uC774\uBBF8\uC9C0\uB97C \uC6C0\uC9C1\uC5EC \uC704\uCE58\uB97C \uC870\uC815\uD558\uC138\uC694." })] })), _jsx("h1", { ref: h1, className: "post-title",
|
|
149
|
+
/* plaintext-only 면 부분 서식이 **구조로** 불가능하다 — UI 가 막는 게 아니다.
|
|
150
|
+
미지원 브라우저를 위해 붙여넣기에서 text/plain 만 받는다. */
|
|
151
|
+
contentEditable: "plaintext-only", suppressContentEditableWarning: true, role: "textbox", "aria-multiline": "false", "aria-label": "\uAE00 \uC81C\uBAA9", "data-placeholder": placeholder, onInput: onInput, onFocus: onFocus, onBlur: onBlur, onKeyDown: (e) => { if (e.key === 'Enter')
|
|
152
|
+
e.preventDefault(); }, onPaste: (e) => {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
const t = e.clipboardData.getData('text/plain').replace(/[\r\n\t]+/g, ' ');
|
|
155
|
+
document.execCommand('insertText', false, t);
|
|
156
|
+
}, style: { ...styleOf(value) } }), !banner && (_jsx("div", { className: "ph-tools", "data-ui": "", children: _jsx("button", { type: "button", title: "\uC81C\uBAA9\uC5D0 \uBC30\uACBD \uC0AC\uC9C4 \uB123\uAE30", "aria-label": "\uBC30\uACBD \uC0AC\uC9C4", disabled: busy, onMouseDown: (e) => e.preventDefault(), onClick: putBanner, children: _jsx(Icon, { name: "image", size: 15 }) }) })), banner && !moving && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "ph-badge", "data-ui": "", "aria-pressed": banner.cover, title: banner.cover ? '이 사진이 글 목록의 대표 이미지입니다' : '끄면 본문의 첫 사진이 대표가 됩니다', onMouseDown: (e) => e.preventDefault(), onClick: () => patch({ cover: !banner.cover }), children: banner.cover ? '✓ 대표' : '대표' }), _jsxs("div", { className: "ph-actions", "data-ui": "", children: [_jsxs("button", { type: "button", title: "\uC0AC\uC9C4 \uC704\uCE58 \uC870\uC815", onMouseDown: (e) => e.preventDefault(), onClick: () => setMoving({ x: banner.focusX, y: banner.focusY }), children: [_jsx(Icon, { name: "move", size: 14 }), "\uC704\uCE58\uC774\uB3D9"] }), _jsx("button", { type: "button", title: "\uBC30\uACBD \uC0AC\uC9C4 \uC9C0\uC6B0\uAE30", "aria-label": "\uBC30\uACBD \uC0AC\uC9C4 \uC9C0\uC6B0\uAE30", onMouseDown: (e) => e.preventDefault(), onClick: () => onChange({ ...value, banner: null }), children: _jsx(Icon, { name: "trash", size: 15 }) })] })] })), banner && moving && (_jsx(_Fragment, { children: _jsxs("div", { className: "ph-actions", "data-ui": "", children: [_jsx("button", { type: "button", onMouseDown: (e) => e.preventDefault(), onClick: () => setMoving(null), children: "\uCDE8\uC18C" }), _jsx("button", { type: "button", className: "is-go", onMouseDown: (e) => e.preventDefault(), onClick: () => { patch({ focusX: moving.x, focusY: moving.y }); setMoving(null); }, children: "\uD655\uC778" })] }) }))] }));
|
|
157
|
+
}
|
|
158
|
+
/** 발행 쪽과 **같은 문자열**을 쓴다 — `titleInlineStyle` 이 그 한 곳이다 */
|
|
159
|
+
function styleOf(t) {
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const decl of titleInlineStyle(t).split(';')) {
|
|
162
|
+
const i = decl.indexOf(':');
|
|
163
|
+
if (i < 0)
|
|
164
|
+
continue;
|
|
165
|
+
// font-family → fontFamily
|
|
166
|
+
const k = decl.slice(0, i).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
167
|
+
out[k] = decl.slice(i + 1);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=PostTitle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostTitle.js","sourceRoot":"","sources":["../src/PostTitle.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectionBubble.d.ts","sourceRoot":"","sources":["../src/SelectionBubble.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,MAAM,EAAE,MAAM,eAAe,CAAA;AAY3D,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,+BA2I7D"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { BubbleMenu } from '@tiptap/react/menus';
|
|
4
|
+
import { useEditorState } from '@tiptap/react';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
import { ColorPicker } from './ColorPicker.js';
|
|
7
|
+
import { useRecentColors } from './recentColors.js';
|
|
8
|
+
import { FontMenu } from './FontMenu.js';
|
|
9
|
+
import { FONTS, FONT_SIZE_DEFAULT } from '@storkspear/post-editor-core/vocab';
|
|
10
|
+
import { Picker } from './PickMenu.js';
|
|
11
|
+
import { SIZE_ITEMS } from './pickItems.js';
|
|
12
|
+
import { bubbleShows } from './bubbleShows.js';
|
|
13
|
+
import { UrlPrompt } from './UrlPrompt.js';
|
|
14
|
+
import { OBJECTS, findActiveObject } from './objects/registry.js';
|
|
15
|
+
/** 본문에서 글자를 잡으면 그 자리에 뜨는 작은 툴바. 툴바까지 올라갈 필요를 없앤다. */
|
|
16
|
+
export function SelectionBubble({ editor }) {
|
|
17
|
+
const [pick, setPick] = useState(null);
|
|
18
|
+
const linkAnchor = useRef(null);
|
|
19
|
+
const colorAnchor = useRef(null);
|
|
20
|
+
const hlAnchor = useRef(null);
|
|
21
|
+
const fontAnchor = useRef(null);
|
|
22
|
+
const { recent, remember } = useRecentColors();
|
|
23
|
+
const s = useEditorState({
|
|
24
|
+
editor,
|
|
25
|
+
selector: ({ editor: e }) => ({
|
|
26
|
+
bold: e.isActive('bold'), italic: e.isActive('italic'), underline: e.isActive('underline'),
|
|
27
|
+
strike: e.isActive('strike'), link: e.isActive('link'),
|
|
28
|
+
href: e.getAttributes('link').href ?? '',
|
|
29
|
+
size: e.getAttributes('textStyle').fontSize ?? '',
|
|
30
|
+
font: e.getAttributes('textStyle').fontFamily ?? '',
|
|
31
|
+
color: e.getAttributes('textStyle').color ?? '',
|
|
32
|
+
hlColor: e.getAttributes('highlight').color ?? '',
|
|
33
|
+
inTable: e.isActive('table'),
|
|
34
|
+
// 링크는 걸 글자가 있어야 건다 — 인용구는 커서만 있어도 버블이 뜨므로 여기서 가른다
|
|
35
|
+
hasRange: e.state.selection.from !== e.state.selection.to,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
// 지금 잡은 글자가 어떤 객체 안인가 — 그 객체가 버블 줄을 내놓으면 앞에 붙인다
|
|
39
|
+
const obj = useEditorState({ editor, selector: () => findActiveObject(editor) });
|
|
40
|
+
const kind = obj ? OBJECTS[obj.kind] : null;
|
|
41
|
+
const objBubble = obj && kind?.Bubble ? { C: kind.Bubble, m: obj } : null;
|
|
42
|
+
/**
|
|
43
|
+
* 바를 어디에 붙일까 — 인용구 안이면 **인용구 상자 위**에 고정한다.
|
|
44
|
+
*
|
|
45
|
+
* 기본은 「잡은 글자」가 기준이라, 같은 인용구를 눌러도 누른 자리마다 바가 옮겨 다녔다
|
|
46
|
+
*. 인용구는 블록 하나를 통째로 다루는 물건이라 기준도 블록이어야 한다.
|
|
47
|
+
* null 을 돌려주면 기본(선택 영역 기준)으로 돌아간다 — 인용 밖에서는 그게 맞다.
|
|
48
|
+
*
|
|
49
|
+
* 화면에서 직접 읽어 오는 콜백이다. 렌더 때 값을 담아 두면 플러그인이 처음 받은 것을
|
|
50
|
+
* 계속 쓰는 탓에 「늘 null」이 되어 조용히 아무 일도 안 한다.
|
|
51
|
+
*/
|
|
52
|
+
const anchorRect = useCallback(() => {
|
|
53
|
+
const m = findActiveObject(editor);
|
|
54
|
+
const k = m ? OBJECTS[m.kind] : null;
|
|
55
|
+
if (!m || !k?.Bubble)
|
|
56
|
+
return null;
|
|
57
|
+
const el = (k.anchor ? k.anchor(m.dom) : m.dom);
|
|
58
|
+
if (!el)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
getBoundingClientRect: () => el.getBoundingClientRect(),
|
|
62
|
+
getClientRects: () => [el.getBoundingClientRect()],
|
|
63
|
+
contextElement: el,
|
|
64
|
+
};
|
|
65
|
+
}, [editor]);
|
|
66
|
+
const c = () => editor.chain().focus();
|
|
67
|
+
const applyColor = (hex) => { remember(hex); c().setColor(hex).run(); };
|
|
68
|
+
const applyHl = (hex) => { remember(hex); c().setHighlight({ color: hex }).run(); };
|
|
69
|
+
return (_jsx(BubbleMenu, { editor: editor, options: { placement: 'top', offset: 10 }, getReferencedVirtualElement: anchorRect, shouldShow: ({ editor: e }) => bubbleShows(e), children: _jsxs("div", { className: "bub", "data-editor-ui": "", children: [objBubble && (_jsxs(_Fragment, { children: [_jsx(objBubble.C, { editor: editor, m: objBubble.m }), _jsx("span", { className: "bub-div" })] })), _jsxs("div", { className: "bub-anchor", ref: fontAnchor, children: [_jsxs("button", { className: `bub-font${pick === 'font' ? ' on' : ''}`, title: "\uAE00\uAF34", style: { fontFamily: s.font || 'var(--font-body)' }, onMouseDown: (e) => e.preventDefault(), onClick: () => setPick(pick === 'font' ? null : 'font'), children: [FONTS.find((f) => f.value === s.font)?.label ?? '기본서체', _jsx("i", { className: "tb-caret" })] }), pick === 'font' && (_jsx(FontMenu, { value: s.font, anchorRef: fontAnchor, onClose: () => setPick(null), onPick: (v) => (v ? c().setFontFamily(v).run() : c().unsetFontFamily().run()) }))] }), _jsx(Picker, { title: "\uAE00\uC790 \uD06C\uAE30 (px)", className: "bub-size", width: 78, align: "center", items: SIZE_ITEMS, value: String(s.size ? parseInt(s.size, 10) : FONT_SIZE_DEFAULT), onPick: (v) => c().setFontSize(`${v}px`).run() }), _jsx("span", { className: "bub-div" }), _jsx("button", { className: s.bold ? 'on' : '', title: "\uAD75\uAC8C", onMouseDown: (e) => e.preventDefault(), onClick: () => c().toggleBold().run(), children: _jsx(Icon, { name: "bold", size: 16 }) }), _jsx("button", { className: s.italic ? 'on' : '', title: "\uAE30\uC6B8\uC784", onMouseDown: (e) => e.preventDefault(), onClick: () => c().toggleItalic().run(), children: _jsx(Icon, { name: "italic", size: 16 }) }), _jsx("button", { className: s.underline ? 'on' : '', title: "\uBC11\uC904", onMouseDown: (e) => e.preventDefault(), onClick: () => c().toggleUnderline().run(), children: _jsx(Icon, { name: "underline", size: 16 }) }), _jsx("button", { className: s.strike ? 'on' : '', title: "\uCDE8\uC18C\uC120", onMouseDown: (e) => e.preventDefault(), onClick: () => c().toggleStrike().run(), children: _jsx(Icon, { name: "strike", size: 16 }) }), _jsx("span", { className: "bub-div" }), _jsxs("div", { className: "bub-anchor", ref: colorAnchor, children: [_jsxs("button", { title: "\uAE00\uC790\uC0C9", onMouseDown: (e) => e.preventDefault(), onClick: () => setPick(pick === 'color' ? null : 'color'), children: [_jsx("span", { className: "bub-swatch", style: { background: s.color || '#1a1a1a' } }), _jsx("b", { className: "bub-letter", children: "\uAC00" })] }), pick === 'color' && (_jsx(ColorPicker, { label: "\uAE00\uC790\uC0C9", value: s.color, recent: recent, anchorRef: colorAnchor, onPick: applyColor, onClear: () => c().unsetColor().run(), onClose: () => setPick(null) }))] }), _jsxs("div", { className: "bub-anchor", ref: hlAnchor, children: [_jsx("button", { title: "\uAE00\uC790 \uBC30\uACBD\uC0C9", onMouseDown: (e) => e.preventDefault(), onClick: () => setPick(pick === 'highlight' ? null : 'highlight'), children: _jsx("b", { className: "bub-letter bub-hl-letter", style: { background: s.hlColor || '#fff2a8' }, children: "\uAC00" }) }), pick === 'highlight' && (_jsx(ColorPicker, { label: "\uAE00\uC790 \uBC30\uACBD\uC0C9", value: s.hlColor, recent: recent, anchorRef: hlAnchor, onPick: applyHl, onClear: () => c().unsetHighlight().run(), onClose: () => setPick(null) }))] }), s.hasRange && _jsx("span", { className: "bub-div" }), s.hasRange && (_jsxs("div", { className: "bub-anchor", ref: linkAnchor, children: [_jsx("button", { className: s.link || pick === 'link' ? 'on' : '', title: "\uB9C1\uD06C", onMouseDown: (e) => e.preventDefault(), onClick: () => setPick(pick === 'link' ? null : 'link'), children: _jsx(Icon, { name: "link", size: 16 }) }), pick === 'link' && (_jsx(UrlPrompt, { label: "\uB9C1\uD06C \uC8FC\uC18C", placeholder: "https://", initial: s.href, anchorRef: linkAnchor, hint: s.link ? '비우고 「걸기」를 누르면 링크가 풀립니다' : undefined, submitLabel: "\uAC78\uAE30", onClose: () => setPick(null), onSubmit: (url) => (url === '' ? c().unsetLink().run() : c().setLink({ href: url }).run()) }))] }))] }) }));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=SelectionBubble.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectionBubble.js","sourceRoot":"","sources":["../src/SelectionBubble.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* 표 조작 오버레이 — 셀 경계의 +로 그 자리에 삽입, 머리 띠로 행/열 선택,
|
|
4
|
+
* 선택하면 문맥 메뉴(병합·분할·너비맞춤·삭제), 표 전체 선택이면 휴지통.
|
|
5
|
+
* 표 NodeView 를 만들지 않고 화면 좌표로 덧그린다 — ProseMirror 표 구현을 안 건드리려고.
|
|
6
|
+
*/
|
|
7
|
+
export declare function TableTools({ editor }: {
|
|
8
|
+
editor: Editor;
|
|
9
|
+
}): import("react").JSX.Element | null;
|
|
10
|
+
//# sourceMappingURL=TableTools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableTools.d.ts","sourceRoot":"","sources":["../src/TableTools.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAkB3C;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,sCAsOxD"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { Icon } from './Icon.js';
|
|
4
|
+
import { ROW_MAX, ROW_MIN } from '@storkspear/post-editor-core/extensions/TableRowHeight';
|
|
5
|
+
const EMPTY = [];
|
|
6
|
+
/**
|
|
7
|
+
* 표 조작 오버레이 — 셀 경계의 +로 그 자리에 삽입, 머리 띠로 행/열 선택,
|
|
8
|
+
* 선택하면 문맥 메뉴(병합·분할·너비맞춤·삭제), 표 전체 선택이면 휴지통.
|
|
9
|
+
* 표 NodeView 를 만들지 않고 화면 좌표로 덧그린다 — ProseMirror 표 구현을 안 건드리려고.
|
|
10
|
+
*/
|
|
11
|
+
export function TableTools({ editor }) {
|
|
12
|
+
const [geo, setGeo] = useState(null);
|
|
13
|
+
const [sel, setSel] = useState({ cells: 0, wholeTable: false, anchorRect: null });
|
|
14
|
+
const [menu, setMenu] = useState(false);
|
|
15
|
+
const measure = useCallback(() => {
|
|
16
|
+
if (!editor?.isEditable)
|
|
17
|
+
return setGeo(null);
|
|
18
|
+
const { state, view } = editor;
|
|
19
|
+
const { from } = state.selection;
|
|
20
|
+
let dom = null;
|
|
21
|
+
try {
|
|
22
|
+
dom = view.domAtPos(from).node;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return setGeo(null);
|
|
26
|
+
}
|
|
27
|
+
const el = (dom?.nodeType === 3 ? dom.parentElement : dom);
|
|
28
|
+
const table = el?.closest('table');
|
|
29
|
+
if (!table) {
|
|
30
|
+
setGeo(null);
|
|
31
|
+
setMenu(false);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const tr = [...table.rows];
|
|
35
|
+
if (!tr.length)
|
|
36
|
+
return setGeo(null);
|
|
37
|
+
const tRect = table.getBoundingClientRect();
|
|
38
|
+
const cells0 = [...tr[0].cells];
|
|
39
|
+
const cols = cells0.map((c) => {
|
|
40
|
+
const r = c.getBoundingClientRect();
|
|
41
|
+
return { left: r.left, top: tRect.top - 14, width: r.width, height: 12 };
|
|
42
|
+
});
|
|
43
|
+
const rows = tr.map((row) => {
|
|
44
|
+
const r = row.getBoundingClientRect();
|
|
45
|
+
return { left: tRect.left - 14, top: r.top, width: 12, height: r.height };
|
|
46
|
+
});
|
|
47
|
+
const colEdges = cells0.map((c) => c.getBoundingClientRect().right);
|
|
48
|
+
const rowEdges = tr.map((r) => r.getBoundingClientRect().bottom);
|
|
49
|
+
const rowPos = tr.map((row) => { try {
|
|
50
|
+
return view.posAtDOM(row, 0) - 1;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return -1;
|
|
54
|
+
} });
|
|
55
|
+
const rowHeights = tr.map((row) => row.getBoundingClientRect().height);
|
|
56
|
+
const cellPos = tr.map((row) => [...row.cells].map((c) => {
|
|
57
|
+
try {
|
|
58
|
+
return view.posAtDOM(c, 0) - 1;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
// 표 너비는 감싸개에 건다 — updateColumns() 가 table 의 인라인 width 를 지우기 때문.
|
|
65
|
+
const wrap = table.closest('.tableWrapper');
|
|
66
|
+
if (wrap) {
|
|
67
|
+
const st = editor.getAttributes('table').style ?? '';
|
|
68
|
+
const m = /width\s*:\s*([\d.]+)%/.exec(st);
|
|
69
|
+
wrap.style.width = m ? `${m[1]}%` : '';
|
|
70
|
+
wrap.style.marginInline = m ? 'auto' : '';
|
|
71
|
+
}
|
|
72
|
+
setGeo({
|
|
73
|
+
table: { left: tRect.left, top: tRect.top, width: tRect.width, height: tRect.height },
|
|
74
|
+
cols, rows, colEdges, rowEdges, cellPos, rowPos, rowHeights,
|
|
75
|
+
});
|
|
76
|
+
const selected = table.querySelectorAll('.selectedCell');
|
|
77
|
+
const total = table.querySelectorAll('td,th').length;
|
|
78
|
+
const first = selected[0]?.getBoundingClientRect();
|
|
79
|
+
const last = selected[selected.length - 1]?.getBoundingClientRect();
|
|
80
|
+
setSel({
|
|
81
|
+
cells: selected.length,
|
|
82
|
+
wholeTable: selected.length > 0 && selected.length === total,
|
|
83
|
+
anchorRect: first && last
|
|
84
|
+
? { left: Math.min(first.left, last.left), top: Math.min(first.top, last.top),
|
|
85
|
+
width: Math.max(first.right, last.right) - Math.min(first.left, last.left),
|
|
86
|
+
height: Math.max(first.bottom, last.bottom) - Math.min(first.top, last.top) }
|
|
87
|
+
: null,
|
|
88
|
+
});
|
|
89
|
+
}, [editor]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
measure();
|
|
92
|
+
editor.on('selectionUpdate', measure);
|
|
93
|
+
editor.on('transaction', measure);
|
|
94
|
+
window.addEventListener('scroll', measure, true);
|
|
95
|
+
window.addEventListener('resize', measure);
|
|
96
|
+
return () => {
|
|
97
|
+
editor.off('selectionUpdate', measure);
|
|
98
|
+
editor.off('transaction', measure);
|
|
99
|
+
window.removeEventListener('scroll', measure, true);
|
|
100
|
+
window.removeEventListener('resize', measure);
|
|
101
|
+
};
|
|
102
|
+
}, [editor, measure]);
|
|
103
|
+
if (!geo)
|
|
104
|
+
return null;
|
|
105
|
+
const c = () => editor.chain().focus();
|
|
106
|
+
const pos = geo.cellPos;
|
|
107
|
+
const insertColAfter = (i) => {
|
|
108
|
+
const a = pos[0]?.[i], h = pos[pos.length - 1]?.[i];
|
|
109
|
+
if (a >= 0 && h >= 0)
|
|
110
|
+
editor.chain().focus().setCellSelection({ anchorCell: a, headCell: h }).addColumnAfter().run();
|
|
111
|
+
};
|
|
112
|
+
const insertRowAfter = (i) => {
|
|
113
|
+
const row = pos[i] ?? EMPTY;
|
|
114
|
+
if (row.length)
|
|
115
|
+
editor.chain().focus().setCellSelection({ anchorCell: row[0], headCell: row[row.length - 1] }).addRowAfter().run();
|
|
116
|
+
};
|
|
117
|
+
const selectCol = (i) => {
|
|
118
|
+
const anchor = pos[0]?.[i], head = pos[pos.length - 1]?.[i];
|
|
119
|
+
if (anchor >= 0 && head >= 0)
|
|
120
|
+
editor.chain().focus().setCellSelection({ anchorCell: anchor, headCell: head }).run();
|
|
121
|
+
};
|
|
122
|
+
const selectRow = (i) => {
|
|
123
|
+
const row = pos[i] ?? EMPTY;
|
|
124
|
+
if (row.length)
|
|
125
|
+
editor.chain().focus().setCellSelection({ anchorCell: row[0], headCell: row[row.length - 1] }).run();
|
|
126
|
+
};
|
|
127
|
+
const selectAll = () => {
|
|
128
|
+
const a = pos[0]?.[0], h = pos[pos.length - 1]?.[(pos[pos.length - 1]?.length ?? 1) - 1];
|
|
129
|
+
if (a >= 0 && h >= 0)
|
|
130
|
+
editor.chain().focus().setCellSelection({ anchorCell: a, headCell: h }).run();
|
|
131
|
+
};
|
|
132
|
+
/** 행 높이를 심는다. null 이면 지운다(원래대로) */
|
|
133
|
+
const setRowHeight = (i, px) => {
|
|
134
|
+
const at = geo.rowPos[i];
|
|
135
|
+
if (at == null || at < 0)
|
|
136
|
+
return;
|
|
137
|
+
const node = editor.state.doc.nodeAt(at);
|
|
138
|
+
if (!node)
|
|
139
|
+
return;
|
|
140
|
+
const h = px == null ? null : Math.min(ROW_MAX, Math.max(ROW_MIN, Math.round(px)));
|
|
141
|
+
editor.view.dispatch(editor.state.tr.setNodeMarkup(at, undefined, { ...node.attrs, height: h }));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* 행 경계 끌기.
|
|
145
|
+
*
|
|
146
|
+
* 포인터 이벤트로 받는다 — 마우스 전용으로 만들면 폰에서 못 쓴다.
|
|
147
|
+
* (라이브러리의 열 너비 조절이 마우스 전용이라 폰에서 안 되는 것과 같은 함정)
|
|
148
|
+
*/
|
|
149
|
+
const startRowDrag = (ev, i) => {
|
|
150
|
+
ev.preventDefault();
|
|
151
|
+
ev.stopPropagation();
|
|
152
|
+
const startY = ev.clientY;
|
|
153
|
+
const startH = geo.rowHeights[i] ?? ROW_MIN;
|
|
154
|
+
const target = ev.currentTarget;
|
|
155
|
+
target.setPointerCapture(ev.pointerId);
|
|
156
|
+
const move = (e) => setRowHeight(i, startH + (e.clientY - startY));
|
|
157
|
+
const up = () => {
|
|
158
|
+
try {
|
|
159
|
+
target.releasePointerCapture(ev.pointerId);
|
|
160
|
+
}
|
|
161
|
+
catch { /* 이미 풀렸으면 그만 */ }
|
|
162
|
+
target.removeEventListener('pointermove', move);
|
|
163
|
+
target.removeEventListener('pointerup', up);
|
|
164
|
+
target.removeEventListener('pointercancel', up);
|
|
165
|
+
};
|
|
166
|
+
target.addEventListener('pointermove', move);
|
|
167
|
+
target.addEventListener('pointerup', up);
|
|
168
|
+
target.addEventListener('pointercancel', up);
|
|
169
|
+
};
|
|
170
|
+
/** 열 너비를 균등하게 — colwidth 를 지우면 표가 균등 배분으로 돌아간다 */
|
|
171
|
+
const evenWidths = () => {
|
|
172
|
+
const { state, view } = editor;
|
|
173
|
+
const tr = state.tr;
|
|
174
|
+
state.doc.descendants((node, p) => {
|
|
175
|
+
if (node.type.name === 'tableCell' || node.type.name === 'tableHeader') {
|
|
176
|
+
if (node.attrs.colwidth)
|
|
177
|
+
tr.setNodeMarkup(p, undefined, { ...node.attrs, colwidth: null });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
view.dispatch(tr);
|
|
181
|
+
setMenu(false);
|
|
182
|
+
};
|
|
183
|
+
return (_jsxs("div", { className: "tt-layer", "data-editor-ui": "", children: [_jsx("button", { className: "tt-corner", style: { left: geo.table.left - 14, top: geo.table.top - 14 }, title: "\uD45C \uC804\uCCB4 \uC120\uD0DD", onMouseDown: (e) => e.preventDefault(), onClick: selectAll }), geo.cols.map((r, i) => (_jsx("button", { className: "tt-band", style: { left: r.left, top: r.top, width: r.width, height: r.height }, title: `${i + 1}번째 열 선택`, onMouseDown: (e) => e.preventDefault(), onClick: () => selectCol(i) }, 'c' + i))), geo.rows.map((r, i) => (_jsx("button", { className: "tt-band", style: { left: r.left, top: r.top, width: r.width, height: r.height }, title: `${i + 1}번째 행 선택`, onMouseDown: (e) => e.preventDefault(), onClick: () => selectRow(i) }, 'r' + i))), geo.colEdges.map((x, i) => (_jsx("button", { className: "tt-plus", style: { left: x - 9, top: geo.table.top - 23 }, title: `${i + 1}번째 열 뒤에 열 넣기`, onMouseDown: (e) => e.preventDefault(), onClick: () => insertColAfter(i), children: _jsx(PlusGlyph, {}) }, 'ce' + i))), geo.rowEdges.map((y, i) => (_jsx("button", { className: "tt-plus", style: { left: geo.table.left - 23, top: y - 9 }, title: `${i + 1}번째 행 아래에 행 넣기`, onMouseDown: (e) => e.preventDefault(), onClick: () => insertRowAfter(i), children: _jsx(PlusGlyph, {}) }, 're' + i))), sel.wholeTable && (_jsx("button", { className: "tt-trash", style: { left: geo.table.left + geo.table.width / 2 - 19, top: geo.table.top - 46 }, title: "\uD45C \uC0AD\uC81C", onMouseDown: (e) => e.preventDefault(), onClick: () => c().deleteTable().run(), children: _jsx(TrashGlyph, {}) })), geo.rowEdges.map((y, i) => (_jsx("div", { className: "tt-rowsize", style: { left: geo.table.left, top: y - 3, width: geo.table.width }, title: "\uB04C\uC5B4\uC11C \uD589 \uB192\uC774 \uC870\uC808 \u00B7 \uB450 \uBC88 \uB204\uB974\uBA74 \uC6D0\uB798\uB300\uB85C", onDoubleClick: () => setRowHeight(i, null), onPointerDown: (ev) => startRowDrag(ev, i) }, 'rs' + i))), sel.cells > 1 && !sel.wholeTable && sel.anchorRect && (_jsxs("div", { className: "tt-anchor", style: moreSpot(geo, sel.anchorRect), children: [_jsx("button", { className: "tt-more", title: "\uC140 \uC791\uC5C5", onMouseDown: (e) => e.preventDefault(), onClick: () => setMenu((v) => !v), children: "\u22EF" }), menu && (_jsxs("div", { className: "tt-menu", role: "menu", children: [_jsxs("button", { onClick: () => { c().mergeCells().run(); setMenu(false); }, children: [_jsx(Icon, { name: "rowPlus" }), " \uC140 \uBCD1\uD569"] }), _jsxs("button", { onClick: () => { c().splitCell().run(); setMenu(false); }, children: [_jsx(Icon, { name: "colPlus" }), " \uC140 \uBD84\uD560"] }), _jsxs("button", { onClick: evenWidths, children: [_jsx(Icon, { name: "table" }), " \uB108\uBE44 \uB9DE\uCDA4"] }), _jsx("span", { className: "tt-menu-div" }), _jsxs("button", { onClick: () => { c().deleteRow().run(); setMenu(false); }, children: [_jsx(TrashGlyph, {}), " \uD589 \uC0AD\uC81C"] }), _jsxs("button", { onClick: () => { c().deleteColumn().run(); setMenu(false); }, children: [_jsx(TrashGlyph, {}), " \uC5F4 \uC0AD\uC81C"] }), _jsxs("button", { className: "danger", onClick: () => { c().deleteTable().run(); setMenu(false); }, children: [_jsx(TrashGlyph, {}), " \uD45C \uC0AD\uC81C"] })] }))] }))] }));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* `⋯` 버튼 자리 — **항상 표 위 바깥**, 선택 영역의 가로 한가운데.
|
|
187
|
+
*
|
|
188
|
+
* 규칙을 하나로 둔다. 행이면 왼쪽에 두는 식으로 갈랐더니 본문이 전체 폭일 때
|
|
189
|
+
* 표 왼쪽에 공간이 없어 결국 표와 겹쳤다. 위쪽은 어떤 경우에도 비어 있다.
|
|
190
|
+
* 경계 `+`(-23) 보다 더 바깥(-46)이라 서로 안 겹친다.
|
|
191
|
+
*/
|
|
192
|
+
function moreSpot(geo, r) {
|
|
193
|
+
return {
|
|
194
|
+
left: Math.max(8, Math.min(r.left + r.width / 2 - 12, window.innerWidth - 210)),
|
|
195
|
+
top: Math.max(8, geo.table.top - 46),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const PlusGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", children: _jsx("path", { d: "M12 5v14M5 12h14" }) }));
|
|
199
|
+
const TrashGlyph = () => (_jsx("svg", { viewBox: "0 0 24 24", width: "15", height: "15", fill: "none", stroke: "currentColor", strokeWidth: "1.9", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M4 7h16M9.5 7V5h5v2M6.5 7l.8 12.2A1.8 1.8 0 0 0 9.1 21h5.8a1.8 1.8 0 0 0 1.8-1.8L17.5 7" }) }));
|
|
200
|
+
//# sourceMappingURL=TableTools.js.map
|