@growgroup/visual-editor 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/NOTICE +52 -0
- package/README.md +143 -0
- package/dist/editor.css +2 -0
- package/package.json +94 -0
- package/src/components/DeckSlideRender.tsx +21 -0
- package/src/components/SaveNote.tsx +21 -0
- package/src/components/auth/AuthProvider.tsx +44 -0
- package/src/components/ui/accordion.tsx +57 -0
- package/src/components/ui/alert-dialog.tsx +141 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +56 -0
- package/src/components/ui/card.tsx +76 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +11 -0
- package/src/components/ui/context-menu.tsx +200 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +201 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/scrubbable-label.tsx +95 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/components/viewer/useDeck.ts +58 -0
- package/src/editor/EditorContext.tsx +632 -0
- package/src/editor/EditorToolbar.tsx +446 -0
- package/src/editor/FrontendVisualEditor.tsx +2595 -0
- package/src/editor/autosave.ts +34 -0
- package/src/editor/components/AiPromptPopover.tsx +378 -0
- package/src/editor/components/BreakpointGuides.tsx +323 -0
- package/src/editor/components/BreakpointSelector.tsx +180 -0
- package/src/editor/components/ComponentPropertyEditor.tsx +426 -0
- package/src/editor/components/CssEditorDialog.tsx +168 -0
- package/src/editor/components/EditorCanvas.tsx +987 -0
- package/src/editor/components/EditorContextMenu.tsx +515 -0
- package/src/editor/components/EditorFooter.tsx +81 -0
- package/src/editor/components/EditorHeader.tsx +524 -0
- package/src/editor/components/EditorLayerPanel.tsx +1197 -0
- package/src/editor/components/EditorPropertyPanel.tsx +2996 -0
- package/src/editor/components/EditorSidebar.tsx +226 -0
- package/src/editor/components/FigmaColorPicker.tsx +1577 -0
- package/src/editor/components/HtmlEditorDialog.tsx +71 -0
- package/src/editor/components/HtmlImportDialog.tsx +346 -0
- package/src/editor/components/JsEditorDialog.tsx +175 -0
- package/src/editor/components/LayerTreeItem.tsx +494 -0
- package/src/editor/components/MasterComponentEditor.tsx +846 -0
- package/src/editor/components/MediaLibraryDialog.tsx +447 -0
- package/src/editor/components/PageSettingsDialog.tsx +776 -0
- package/src/editor/components/VariablePicker.tsx +355 -0
- package/src/editor/components/VariablesDialog.tsx +564 -0
- package/src/editor/components/VariantEditor.tsx +687 -0
- package/src/editor/components/VariantMatrix.tsx +507 -0
- package/src/editor/components/component-panel/CategoryAccordion.tsx +139 -0
- package/src/editor/components/component-panel/ComponentGrid.tsx +51 -0
- package/src/editor/components/component-panel/ComponentItem.tsx +368 -0
- package/src/editor/components/component-panel/ComponentPanel.tsx +275 -0
- package/src/editor/components/component-panel/ComponentSearch.tsx +82 -0
- package/src/editor/components/component-panel/VariantList.tsx +260 -0
- package/src/editor/components/component-panel/index.ts +12 -0
- package/src/editor/components/index.ts +48 -0
- package/src/editor/components/multi-page/CanvasZoomControls.tsx +71 -0
- package/src/editor/components/multi-page/InfiniteCanvas.tsx +95 -0
- package/src/editor/components/multi-page/MultiPageCanvasView.tsx +1404 -0
- package/src/editor/components/multi-page/PageFrameOverlay.tsx +76 -0
- package/src/editor/components/multi-page/PageLabel.tsx +34 -0
- package/src/editor/components/multi-page/PageLivePreview.tsx +242 -0
- package/src/editor/components/multi-page/PageThumbnail.tsx +59 -0
- package/src/editor/components/multi-page/index.ts +7 -0
- package/src/editor/components/ppt/PptChrome.tsx +2508 -0
- package/src/editor/components/ppt/PptComments.tsx +589 -0
- package/src/editor/components/ppt/PptDesignProposals.tsx +275 -0
- package/src/editor/components/ppt/PptFormatPane.tsx +320 -0
- package/src/editor/components/ppt/PptNotes.tsx +246 -0
- package/src/editor/components/property-panel/AlignmentPanel.tsx +110 -0
- package/src/editor/components/property-panel/AutoLayoutPanel.tsx +615 -0
- package/src/editor/components/property-panel/BorderSection.tsx +104 -0
- package/src/editor/components/property-panel/CompactNumberInput.tsx +237 -0
- package/src/editor/components/property-panel/CompactSizeInput.tsx +148 -0
- package/src/editor/components/property-panel/EffectSection.tsx +157 -0
- package/src/editor/components/property-panel/FillSection.tsx +69 -0
- package/src/editor/components/property-panel/GoogleFontPicker.tsx +192 -0
- package/src/editor/components/property-panel/ImageSection.tsx +206 -0
- package/src/editor/components/property-panel/ImgSrcSection.tsx +125 -0
- package/src/editor/components/property-panel/InstanceOverrideSection.tsx +413 -0
- package/src/editor/components/property-panel/LayoutSection.tsx +105 -0
- package/src/editor/components/property-panel/LinkSection.tsx +103 -0
- package/src/editor/components/property-panel/PropertyVariablePicker.tsx +311 -0
- package/src/editor/components/property-panel/ScalePanel.tsx +212 -0
- package/src/editor/components/property-panel/TypographySection.tsx +230 -0
- package/src/editor/components/property-panel/UnitAwareNumberInput.tsx +238 -0
- package/src/editor/components/property-panel/VariableAwareColorInput.tsx +477 -0
- package/src/editor/components/property-panel/VariableAwareInput.tsx +606 -0
- package/src/editor/components/property-panel/VariableAwareSizeInput.tsx +588 -0
- package/src/editor/components/property-panel/VariableAwareUnitInput.tsx +546 -0
- package/src/editor/components/property-panel/index.ts +24 -0
- package/src/editor/components/property-panel/unit-utils.ts +261 -0
- package/src/editor/components/shell/EditorTopBar.tsx +392 -0
- package/src/editor/components/shell/LeftPanel.tsx +47 -0
- package/src/editor/components/shell/PagesPanel.tsx +118 -0
- package/src/editor/components/variables-panel/ColorValueCell.tsx +165 -0
- package/src/editor/components/variables-panel/TextValueCell.tsx +116 -0
- package/src/editor/components/variables-panel/VariablesPanel.tsx +518 -0
- package/src/editor/components/variables-panel/index.ts +7 -0
- package/src/editor/constants.ts +468 -0
- package/src/editor/contexts/EditorArtboardContext.tsx +229 -0
- package/src/editor/contexts/EditorComponentsContext.tsx +1320 -0
- package/src/editor/contexts/EditorDocumentContext.tsx +113 -0
- package/src/editor/contexts/EditorHistoryContext.tsx +516 -0
- package/src/editor/contexts/EditorRefsContext.tsx +204 -0
- package/src/editor/contexts/EditorSelectionContext.tsx +54 -0
- package/src/editor/contexts/EditorToolContext.tsx +58 -0
- package/src/editor/contexts/EditorUIStateContext.tsx +122 -0
- package/src/editor/contexts/EditorVariablesContext.tsx +397 -0
- package/src/editor/contexts/EditorViewContext.tsx +101 -0
- package/src/editor/contexts/MultiPageCanvasContext.tsx +498 -0
- package/src/editor/contexts/index.ts +17 -0
- package/src/editor/editor-skin.css +122 -0
- package/src/editor/hooks/index.ts +38 -0
- package/src/editor/hooks/useAiReplace.ts +397 -0
- package/src/editor/hooks/useAltMeasure.ts +67 -0
- package/src/editor/hooks/useBrowserZoomPrevention.ts +121 -0
- package/src/editor/hooks/useCanvasControls.ts +1222 -0
- package/src/editor/hooks/useComponentEditMode.ts +658 -0
- package/src/editor/hooks/useComponentInstances.ts +515 -0
- package/src/editor/hooks/useContextMenuHandler.ts +157 -0
- package/src/editor/hooks/useCoordinateTransform.ts +207 -0
- package/src/editor/hooks/useDragResize.ts +1317 -0
- package/src/editor/hooks/useDrawingMode.ts +472 -0
- package/src/editor/hooks/useEditorColors.ts +144 -0
- package/src/editor/hooks/useEditorMessages.ts +108 -0
- package/src/editor/hooks/useElementActions.ts +1754 -0
- package/src/editor/hooks/useElementSelection.ts +1573 -0
- package/src/editor/hooks/useFocusManagement.ts +249 -0
- package/src/editor/hooks/useGoogleFonts.ts +305 -0
- package/src/editor/hooks/useIframeInitializer.ts +584 -0
- package/src/editor/hooks/useIframeSetup.ts +360 -0
- package/src/editor/hooks/useImageUpload.ts +270 -0
- package/src/editor/hooks/useInfiniteCanvas.ts +301 -0
- package/src/editor/hooks/useKeyboardShortcuts.ts +205 -0
- package/src/editor/hooks/useMarqueeSelection.ts +388 -0
- package/src/editor/hooks/useMediaLibrary.ts +129 -0
- package/src/editor/hooks/usePageSettingsManager.ts +739 -0
- package/src/editor/hooks/useResizablePanel.ts +163 -0
- package/src/editor/hooks/useRichPaste.ts +255 -0
- package/src/editor/hooks/useTouchGestures.ts +197 -0
- package/src/editor/index.ts +27 -0
- package/src/editor/types/page-settings.ts +513 -0
- package/src/editor/types.ts +382 -0
- package/src/editor/utils/align-elements.ts +169 -0
- package/src/editor/utils/api-json.ts +21 -0
- package/src/editor/utils/aspect-lock.ts +50 -0
- package/src/editor/utils/component-renderer.ts +702 -0
- package/src/editor/utils/component-sync.ts +845 -0
- package/src/editor/utils/crop-mode.ts +265 -0
- package/src/editor/utils/dom-utils.ts +2238 -0
- package/src/editor/utils/element-effects.ts +218 -0
- package/src/editor/utils/eyedropper.ts +35 -0
- package/src/editor/utils/figma-export.ts +560 -0
- package/src/editor/utils/figma-kiwi-decoder.ts +722 -0
- package/src/editor/utils/figma-kiwi-encoder.ts +583 -0
- package/src/editor/utils/figma-paste.ts +2262 -0
- package/src/editor/utils/flex-reorder.ts +231 -0
- package/src/editor/utils/flex-utils.ts +1230 -0
- package/src/editor/utils/geometry.ts +232 -0
- package/src/editor/utils/grid-layout.ts +210 -0
- package/src/editor/utils/html-utils.ts +470 -0
- package/src/editor/utils/index.ts +11 -0
- package/src/editor/utils/ink-style.ts +57 -0
- package/src/editor/utils/inline-format.ts +236 -0
- package/src/editor/utils/measure-distance.ts +120 -0
- package/src/editor/utils/override-apply.ts +526 -0
- package/src/editor/utils/override-detection.ts +613 -0
- package/src/editor/utils/paste-processors.ts +609 -0
- package/src/editor/utils/paste-sanitizer.ts +244 -0
- package/src/editor/utils/restore-flow.ts +188 -0
- package/src/editor/utils/shape-library.ts +171 -0
- package/src/editor/utils/slide-root.ts +29 -0
- package/src/editor/utils/smart-guides.ts +218 -0
- package/src/editor/utils/style-utils.ts +685 -0
- package/src/editor/utils/table-edit.ts +151 -0
- package/src/editor/utils/tailwind-mappings.ts +778 -0
- package/src/editor/utils/tailwind-utils.ts +804 -0
- package/src/editor/utils/text-highlight.ts +216 -0
- package/src/editor/utils/theme-to-variables.ts +341 -0
- package/src/editor/utils/variant-resolver.ts +456 -0
- package/src/editor/utils/viewport-utils.ts +295 -0
- package/src/hooks/useEditorHistory.ts +195 -0
- package/src/hooks/useEditorShortcuts.ts +260 -0
- package/src/index.ts +40 -0
- package/src/io.ts +134 -0
- package/src/lib/agent/slide-agent/types.ts +547 -0
- package/src/lib/agent/website-agent/types.ts +805 -0
- package/src/lib/api/auth-fetch.ts +58 -0
- package/src/lib/deck.ts +69 -0
- package/src/lib/export.ts +47 -0
- package/src/lib/firebase/config.ts +29 -0
- package/src/lib/firebase/css-variables.ts +378 -0
- package/src/lib/firebase/editor-components.ts +677 -0
- package/src/lib/firebase/storage.ts +469 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/editor.css +52 -0
- package/src/styles/skin.css +122 -0
- package/src/types/css-variables.ts +645 -0
- package/src/types/editor-components.ts +944 -0
- package/src/types/editor.ts +420 -0
- package/src/types/page-master.ts +197 -0
- package/src/types/page.ts +419 -0
- package/src/types/slide.ts +402 -0
- package/src/types/website-theme.ts +788 -0
- package/src/types/website.ts +374 -0
- package/src/vendor/firebase-firestore.ts +13 -0
- package/src/vendor/firebase-functions.ts +17 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { ShortcutCallbacks, ShortcutAction, KEYBOARD_SHORTCUTS } from '../types/editor';
|
|
5
|
+
|
|
6
|
+
interface UseEditorShortcutsOptions {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* キャンバスの iframe。読み込み(srcdoc の差し替え)のたびに
|
|
10
|
+
* 最新の contentDocument へ同じハンドラを張り直す。
|
|
11
|
+
*/
|
|
12
|
+
iframeRef?: React.RefObject<HTMLIFrameElement | null>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* KeyboardEvent からキーマップ検索用の文字列を作る
|
|
17
|
+
*/
|
|
18
|
+
function buildShortcutKey(event: KeyboardEvent, keyOverride?: string): string {
|
|
19
|
+
const parts: string[] = [];
|
|
20
|
+
|
|
21
|
+
// Add modifiers
|
|
22
|
+
if (event.metaKey) parts.push('meta');
|
|
23
|
+
if (event.ctrlKey) parts.push('ctrl');
|
|
24
|
+
if (event.shiftKey) parts.push('shift');
|
|
25
|
+
if (event.altKey) parts.push('alt');
|
|
26
|
+
|
|
27
|
+
const raw = keyOverride ?? event.key;
|
|
28
|
+
if (!raw) return parts.join('+');
|
|
29
|
+
const key = raw.toLowerCase();
|
|
30
|
+
|
|
31
|
+
// Handle special keys
|
|
32
|
+
if (key !== 'meta' && key !== 'control' && key !== 'shift' && key !== 'alt') {
|
|
33
|
+
parts.push(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return parts.join('+');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* event.code から「素の入力文字」を推定する。
|
|
41
|
+
*
|
|
42
|
+
* macOS で Option(Alt) を併用すると event.key が特殊文字になる(⌥C → "ç")ため、
|
|
43
|
+
* e.key だけでは Cmd+Alt+C(スタイルコピー)などが引けない。
|
|
44
|
+
* レイアウト差を壊さないように *まず e.key で引き、外れたときだけ* この値で引き直す。
|
|
45
|
+
*/
|
|
46
|
+
function keyFromCode(code: string | undefined): string | undefined {
|
|
47
|
+
if (!code) return undefined;
|
|
48
|
+
const letter = /^Key([A-Z])$/.exec(code);
|
|
49
|
+
if (letter) return letter[1].toLowerCase();
|
|
50
|
+
const digit = /^Digit([0-9])$/.exec(code);
|
|
51
|
+
if (digit) return digit[1];
|
|
52
|
+
const bracket = code === 'BracketLeft' ? '[' : code === 'BracketRight' ? ']' : undefined;
|
|
53
|
+
if (bracket) return bracket;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* このイベントに割り当てられたアクションを引く
|
|
59
|
+
*/
|
|
60
|
+
function resolveAction(event: KeyboardEvent): ShortcutAction | undefined {
|
|
61
|
+
const direct = KEYBOARD_SHORTCUTS[buildShortcutKey(event)];
|
|
62
|
+
if (direct) return direct;
|
|
63
|
+
|
|
64
|
+
const fallbackKey = keyFromCode(event.code);
|
|
65
|
+
if (!fallbackKey) return undefined;
|
|
66
|
+
return KEYBOARD_SHORTCUTS[buildShortcutKey(event, fallbackKey)];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* イベントの発生元がテキスト編集中かどうか
|
|
71
|
+
*
|
|
72
|
+
* [移植時の修正] `target instanceof HTMLElement` で判定してはいけない。
|
|
73
|
+
* iframe 内の要素は *別realm* の HTMLElement なので、親フレームの
|
|
74
|
+
* HTMLElement とは instanceof が一致せず、常に false になっていた。
|
|
75
|
+
* (実測: キャンバスでテキスト編集中に "r" を押すとツールが矩形に切り替わり、
|
|
76
|
+
* Cmd+A が要素の全選択に化けていた)
|
|
77
|
+
* realm をまたいでも壊れないプロパティ/メソッドだけで判定する。
|
|
78
|
+
*/
|
|
79
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
80
|
+
const el = target as (HTMLElement & { closest?: (s: string) => Element | null }) | null;
|
|
81
|
+
if (!el || typeof el !== 'object' || typeof el.tagName !== 'string') return false;
|
|
82
|
+
|
|
83
|
+
const tag = el.tagName.toUpperCase();
|
|
84
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
85
|
+
|
|
86
|
+
if (el.isContentEditable === true) return true;
|
|
87
|
+
if (typeof el.getAttribute === 'function' && el.getAttribute('contenteditable') === 'true') {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (typeof el.closest === 'function') {
|
|
92
|
+
return !!el.closest('[contenteditable="true"], input, textarea, select');
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* テキスト編集中でも横取りしてよいアクション。
|
|
99
|
+
*
|
|
100
|
+
* [移植時の修正] 以前は copy / cut / paste / selectAll もここに入っていたため、
|
|
101
|
+
* テキスト編集中の Cmd+C が「要素のコピー」に化け、Cmd+A が兄弟要素の全選択になっていた。
|
|
102
|
+
* 編集中は Undo/Redo と Escape(編集終了)以外はブラウザ既定に通す。
|
|
103
|
+
*/
|
|
104
|
+
const ALLOWED_IN_EDITABLE: ShortcutAction[] = ['undo', 'redo', 'deselect'];
|
|
105
|
+
|
|
106
|
+
function isAllowedInEditableContext(action: ShortcutAction | undefined): boolean {
|
|
107
|
+
return action ? ALLOWED_IN_EDITABLE.includes(action) : false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* エディタのキーボードショートカット・ディスパッチャ(唯一の経路)
|
|
112
|
+
*
|
|
113
|
+
* [設計]
|
|
114
|
+
* - キーマップは KEYBOARD_SHORTCUTS のみ。ここ以外でキー判定をしない。
|
|
115
|
+
* - **同一のハンドラ関数**を親 window と iframe の document の両方に張る。
|
|
116
|
+
* addEventListener は (関数, capture) が同じなら重複登録を無視するので、
|
|
117
|
+
* 二重登録による二重発火(Cmd+D で複製が2個)が構造的に起こらない。
|
|
118
|
+
* - iframe は srcdoc 差し替えのたびに document が作り直されるため、
|
|
119
|
+
* load とポーリングで最新の document に張り直す。これでフォーカスが
|
|
120
|
+
* iframe 内か外かによって効くキーが入れ替わらない。
|
|
121
|
+
* - バブリングフェーズで捕まえる。キャプチャにすると、コンテキストメニューや
|
|
122
|
+
* AIポップオーバーが document に張っている Escape ハンドラより先に
|
|
123
|
+
* stopPropagation してしまい、オーバーレイが閉じられなくなるため。
|
|
124
|
+
*/
|
|
125
|
+
export function useEditorShortcuts(
|
|
126
|
+
callbacks: ShortcutCallbacks,
|
|
127
|
+
options: UseEditorShortcutsOptions = {}
|
|
128
|
+
) {
|
|
129
|
+
const { enabled = true, iframeRef } = options;
|
|
130
|
+
|
|
131
|
+
// 毎レンダー最新化(ハンドラの識別子は変えない)
|
|
132
|
+
const callbacksRef = useRef(callbacks);
|
|
133
|
+
callbacksRef.current = callbacks;
|
|
134
|
+
const enabledRef = useRef(enabled);
|
|
135
|
+
enabledRef.current = enabled;
|
|
136
|
+
|
|
137
|
+
// ハンドラは初回に一度だけ生成し、以降ずっと同じ関数オブジェクトを使う
|
|
138
|
+
const handlerRef = useRef<((event: Event) => void) | null>(null);
|
|
139
|
+
if (!handlerRef.current) {
|
|
140
|
+
handlerRef.current = (event: Event) => {
|
|
141
|
+
if (!enabledRef.current) return;
|
|
142
|
+
const keyboardEvent = event as KeyboardEvent;
|
|
143
|
+
|
|
144
|
+
const action = resolveAction(keyboardEvent);
|
|
145
|
+
if (!action) return;
|
|
146
|
+
|
|
147
|
+
// イベント発生元 or そのドキュメントのフォーカスがテキスト編集中か
|
|
148
|
+
const doc = (keyboardEvent.target as HTMLElement | null)?.ownerDocument ?? null;
|
|
149
|
+
const inEditableContext =
|
|
150
|
+
isEditableTarget(keyboardEvent.target) || isEditableTarget(doc?.activeElement ?? null);
|
|
151
|
+
|
|
152
|
+
if (inEditableContext && !isAllowedInEditableContext(action)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const callback = callbacksRef.current[action];
|
|
157
|
+
if (!callback) return;
|
|
158
|
+
|
|
159
|
+
keyboardEvent.preventDefault();
|
|
160
|
+
keyboardEvent.stopPropagation();
|
|
161
|
+
callback();
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!enabled) return;
|
|
167
|
+
const handler = handlerRef.current;
|
|
168
|
+
if (!handler || typeof window === 'undefined') return;
|
|
169
|
+
|
|
170
|
+
// 親ウィンドウ(バブリング)
|
|
171
|
+
window.addEventListener('keydown', handler);
|
|
172
|
+
|
|
173
|
+
const attachedDocs = new Set<Document>();
|
|
174
|
+
let watchedIframe: HTMLIFrameElement | null = null;
|
|
175
|
+
|
|
176
|
+
// iframe の最新 document へ張り直す。
|
|
177
|
+
// load イベントだけだと「effect 実行時点でまだ iframe が生成されていない」
|
|
178
|
+
// ケースを取りこぼすので、軽いポーリングも併用する。
|
|
179
|
+
const syncIframeListener = () => {
|
|
180
|
+
const el = iframeRef?.current ?? null;
|
|
181
|
+
if (el !== watchedIframe) {
|
|
182
|
+
watchedIframe?.removeEventListener('load', syncIframeListener);
|
|
183
|
+
watchedIframe = el;
|
|
184
|
+
watchedIframe?.addEventListener('load', syncIframeListener);
|
|
185
|
+
}
|
|
186
|
+
let doc: Document | null = null;
|
|
187
|
+
try {
|
|
188
|
+
doc = el?.contentDocument ?? null;
|
|
189
|
+
} catch {
|
|
190
|
+
doc = null;
|
|
191
|
+
}
|
|
192
|
+
if (doc && !attachedDocs.has(doc)) {
|
|
193
|
+
doc.addEventListener('keydown', handler);
|
|
194
|
+
attachedDocs.add(doc);
|
|
195
|
+
}
|
|
196
|
+
// 破棄済み(defaultView が無い)document は保持しない。
|
|
197
|
+
// スライドを切り替えるたびに古い document が溜まるのを防ぐ
|
|
198
|
+
attachedDocs.forEach((d) => {
|
|
199
|
+
if (!d.defaultView) attachedDocs.delete(d);
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
syncIframeListener();
|
|
204
|
+
const timer = window.setInterval(syncIframeListener, 400);
|
|
205
|
+
|
|
206
|
+
return () => {
|
|
207
|
+
window.clearInterval(timer);
|
|
208
|
+
window.removeEventListener('keydown', handler);
|
|
209
|
+
watchedIframe?.removeEventListener('load', syncIframeListener);
|
|
210
|
+
attachedDocs.forEach((doc) => {
|
|
211
|
+
try {
|
|
212
|
+
doc.removeEventListener('keydown', handler);
|
|
213
|
+
} catch {
|
|
214
|
+
/* すでに破棄された document は無視 */
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
attachedDocs.clear();
|
|
218
|
+
};
|
|
219
|
+
}, [enabled, iframeRef]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get display string for a shortcut
|
|
224
|
+
*/
|
|
225
|
+
export function getShortcutDisplay(action: ShortcutAction): string {
|
|
226
|
+
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
227
|
+
|
|
228
|
+
// Find the shortcut key for this action
|
|
229
|
+
for (const [key, mappedAction] of Object.entries(KEYBOARD_SHORTCUTS)) {
|
|
230
|
+
if (mappedAction === action) {
|
|
231
|
+
// Transform key to display format
|
|
232
|
+
let display = key
|
|
233
|
+
.replace('meta+', isMac ? '⌘' : 'Ctrl+')
|
|
234
|
+
.replace('ctrl+', 'Ctrl+')
|
|
235
|
+
.replace('shift+', isMac ? '⇧' : 'Shift+')
|
|
236
|
+
.replace('alt+', isMac ? '⌥' : 'Alt+');
|
|
237
|
+
|
|
238
|
+
// Capitalize single letter keys
|
|
239
|
+
if (display.length === 1) {
|
|
240
|
+
display = display.toUpperCase();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Format arrow keys
|
|
244
|
+
display = display
|
|
245
|
+
.replace('arrowup', '↑')
|
|
246
|
+
.replace('arrowdown', '↓')
|
|
247
|
+
.replace('arrowleft', '←')
|
|
248
|
+
.replace('arrowright', '→')
|
|
249
|
+
.replace('delete', '⌫')
|
|
250
|
+
.replace('backspace', '⌫')
|
|
251
|
+
.replace('escape', 'Esc');
|
|
252
|
+
|
|
253
|
+
return display;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default useEditorShortcuts;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @growgroup/visual-editor
|
|
3
|
+
*
|
|
4
|
+
* HTMLをブラウザ上で直接編集するビジュアルエディタ。
|
|
5
|
+
* スライド(16:9固定)とWebページ(可変高さ)の両方に使える。
|
|
6
|
+
*
|
|
7
|
+
* 使い方:
|
|
8
|
+
*
|
|
9
|
+
* import { setEditorIO, VisualEditor } from '@growgroup/visual-editor'
|
|
10
|
+
* import '@growgroup/visual-editor/style.css'
|
|
11
|
+
*
|
|
12
|
+
* setEditorIO({ loadDeck, commentAction, uploadImage })
|
|
13
|
+
*
|
|
14
|
+
* <VisualEditor html={html} editorMode="webpage" onSave={save} onClose={close} />
|
|
15
|
+
*
|
|
16
|
+
* スタイルは dist/editor.css をビルド済みで同梱している。
|
|
17
|
+
* 利用側の Tailwind のバージョン(v3/v4)に依存しない。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { FrontendVisualEditor, FrontendVisualEditor as VisualEditor } from "./editor/FrontendVisualEditor";
|
|
21
|
+
export type { FrontendVisualEditorProps } from "./editor/FrontendVisualEditor";
|
|
22
|
+
|
|
23
|
+
export { EditorProvider, useEditorContext } from "./editor/EditorContext";
|
|
24
|
+
export type { ContentListItem, EditorMode } from "./editor/EditorContext";
|
|
25
|
+
|
|
26
|
+
export { WEBPAGE_WIDTH, WEBPAGE_MIN_HEIGHT, SLIDE_WIDTH, SLIDE_HEIGHT, BREAKPOINT_PRESETS } from "./editor/constants";
|
|
27
|
+
|
|
28
|
+
export { registerAutoSaveFlush, flushAutoSave } from "./editor/autosave";
|
|
29
|
+
|
|
30
|
+
/** 置き場との境界。利用側が起動時に1回渡す */
|
|
31
|
+
export { setEditorIO, io, can } from "./io";
|
|
32
|
+
export type {
|
|
33
|
+
EditorIO,
|
|
34
|
+
EditorDeck,
|
|
35
|
+
EditorContent,
|
|
36
|
+
EditorComment,
|
|
37
|
+
CommentAction,
|
|
38
|
+
ExportFormat,
|
|
39
|
+
UploadResult,
|
|
40
|
+
} from "./io";
|
package/src/io.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* エディタと「置き場」のあいだの唯一の境界。
|
|
5
|
+
*
|
|
6
|
+
* エディタ本体はHTMLを編集するだけで、それをどこから読みどこへ保存するかを知らない。
|
|
7
|
+
* 利用側(スライドのデッキ / Webページの構成ラフ など)がこの io を渡す。
|
|
8
|
+
*
|
|
9
|
+
* 【capability ベース】
|
|
10
|
+
* すべて任意。渡されていない機能は、エディタ側がUIごと出さない。
|
|
11
|
+
* 例: `exportDeck` が無ければ書き出しボタンを出さない。
|
|
12
|
+
* こうしないと「押すと404で失敗するボタン」が残る(実際に起きた)。
|
|
13
|
+
*
|
|
14
|
+
* 使い方(利用側のアプリ起動時に1回):
|
|
15
|
+
*
|
|
16
|
+
* import { setEditorIO } from '@growgroup/visual-editor'
|
|
17
|
+
* setEditorIO({ loadDeck, commentAction, uploadImage })
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** エディタが扱う1件(スライド1枚 / ページ1枚) */
|
|
21
|
+
export type EditorContent = {
|
|
22
|
+
/** 一意なID。並び順は配列の順で決まる */
|
|
23
|
+
id: string;
|
|
24
|
+
/** 元テンプレートのキー。文字列であること(接頭辞を見る処理がある) */
|
|
25
|
+
template: string;
|
|
26
|
+
/** 編集済みか */
|
|
27
|
+
edited: boolean;
|
|
28
|
+
/** 一覧表示用のタイトル */
|
|
29
|
+
title: string;
|
|
30
|
+
/** 一覧・書き出しから除外 */
|
|
31
|
+
hidden?: boolean;
|
|
32
|
+
/** 表示モードでの画面切り替え効果 */
|
|
33
|
+
transition?: "fade" | "push" | "zoom";
|
|
34
|
+
/** レビューコメント */
|
|
35
|
+
comments?: EditorComment[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type EditorComment = {
|
|
39
|
+
id: string;
|
|
40
|
+
author: string;
|
|
41
|
+
text: string;
|
|
42
|
+
createdAt: string;
|
|
43
|
+
resolved?: boolean;
|
|
44
|
+
/** 要素アンカー。無ければ全体へのコメント */
|
|
45
|
+
anchorSrc?: string;
|
|
46
|
+
anchorLabel?: string;
|
|
47
|
+
replies?: { id: string; author: string; text: string; createdAt: string }[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type EditorDeck = {
|
|
51
|
+
version: number;
|
|
52
|
+
title: string;
|
|
53
|
+
slides: EditorContent[];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type CommentAction =
|
|
57
|
+
| { action: "add"; author: string; text: string; anchorSrc?: string; anchorLabel?: string }
|
|
58
|
+
| { action: "reply"; commentId: string; author: string; text: string }
|
|
59
|
+
| { action: "resolve"; commentId: string; resolved: boolean }
|
|
60
|
+
| { action: "delete"; commentId: string };
|
|
61
|
+
|
|
62
|
+
export type ExportFormat = "pdf" | "pdf-lite" | "pptx" | "pptx-edit";
|
|
63
|
+
|
|
64
|
+
export type UploadResult = { url: string; width?: number; height?: number };
|
|
65
|
+
|
|
66
|
+
export type EditorIO = {
|
|
67
|
+
/** 一覧とコメントを取る。無ければ空の一覧として振る舞う */
|
|
68
|
+
loadDeck?: () => Promise<EditorDeck>;
|
|
69
|
+
|
|
70
|
+
/** コメントの追加・返信・解決・削除。無ければコメントUIを出さない */
|
|
71
|
+
commentAction?: (page: number, action: CommentAction) => Promise<EditorDeck>;
|
|
72
|
+
|
|
73
|
+
/** 並び替え・複製・削除など、一覧を編集する操作。無ければその操作を出さない */
|
|
74
|
+
deckOps?: {
|
|
75
|
+
move?: (from: number, to: number) => Promise<EditorDeck>;
|
|
76
|
+
duplicate?: (page: number) => Promise<EditorDeck>;
|
|
77
|
+
remove?: (page: number) => Promise<EditorDeck>;
|
|
78
|
+
updateMeta?: (
|
|
79
|
+
page: number,
|
|
80
|
+
patch: { hidden?: boolean; transition?: "fade" | "push" | "zoom" | null },
|
|
81
|
+
) => Promise<EditorDeck>;
|
|
82
|
+
insert?: (template: string, at: number) => Promise<EditorDeck>;
|
|
83
|
+
reset?: (page: number) => Promise<EditorDeck>;
|
|
84
|
+
templates?: () => Promise<{ id: string; title: string }[]>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 任意のAPI呼び出し。ページ設定・AI機能などが使う。
|
|
89
|
+
* 無ければそれらの機能は「未提供」として静かに既定値へ落ちる。
|
|
90
|
+
*/
|
|
91
|
+
apiFetch?: (path: string, init?: RequestInit) => Promise<unknown>;
|
|
92
|
+
|
|
93
|
+
/** 画像の保存。無ければ data URL のまま本文に埋め込む */
|
|
94
|
+
uploadImage?: (
|
|
95
|
+
data: File | Blob | string,
|
|
96
|
+
opts?: { fileName?: string; parentId?: string; contentId?: string },
|
|
97
|
+
) => Promise<UploadResult>;
|
|
98
|
+
|
|
99
|
+
/** PDF/PPTXの書き出し。無ければ書き出しUIを出さない */
|
|
100
|
+
exportDeck?: (format: ExportFormat, pages?: number[]) => Promise<void>;
|
|
101
|
+
|
|
102
|
+
/** 1件を描く(一覧のサムネイル等)。無ければ描かない */
|
|
103
|
+
renderContent?: (page: number) => ReactNode;
|
|
104
|
+
|
|
105
|
+
/** 保存結果の通知。無ければ何もしない */
|
|
106
|
+
notifySave?: (info: { page: number }) => void;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let current: EditorIO = {};
|
|
110
|
+
|
|
111
|
+
/** 利用側が起動時に1回呼ぶ */
|
|
112
|
+
export function setEditorIO(io: EditorIO): void {
|
|
113
|
+
current = io ?? {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** エディタ内部から参照する */
|
|
117
|
+
export function io(): EditorIO {
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 機能が使えるか。UIの出し分けに使う */
|
|
122
|
+
export function can<K extends keyof EditorIO>(key: K): boolean {
|
|
123
|
+
return current[key] != null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 未提供の機能が呼ばれたときのエラー。握り潰さず理由を出す */
|
|
127
|
+
export function notProvided(what: string): Error {
|
|
128
|
+
return new Error(
|
|
129
|
+
`この機能(${what})は、利用側から io として渡されていないため使えません。` +
|
|
130
|
+
`setEditorIO で ${what} を渡すか、この機能のUIを出さないようにしてください。`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const EMPTY_DECK: EditorDeck = { version: 0, title: "", slides: [] };
|