Skip to content

Commit 601b487

Browse files
authored
Manually insert chapter and verse number and text into footnote and xref (#250)
Manually insert chapter and verse number and text into footnote and xref.
1 parent bb9a2dc commit 601b487

32 files changed

+1434
-57
lines changed

packages/scribe/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@biblionexus-foundation/scribe-editor",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "Scripture editor used in Scribe",
55
"license": "MIT",
66
"homepage": "https://github.com/BiblioNexus-Foundation/scripture-editors/tree/main/packages/scribe#readme",
@@ -30,10 +30,12 @@
3030
},
3131
"dependencies": {
3232
"@biblionexus-foundation/scripture-utilities": "workspace:~",
33+
"@floating-ui/dom": "^1.6.13",
3334
"@lexical/mark": "^0.24.0",
3435
"@lexical/react": "^0.24.0",
3536
"@lexical/selection": "^0.24.0",
3637
"@lexical/utils": "^0.24.0",
38+
"autoprefixer": "^10.4.20",
3739
"fast-equals": "^5.2.2",
3840
"lexical": "^0.24.0"
3941
},

packages/scribe/src/App.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { formattedViewMode as defaultViewMode } from "./plugins/view-mode.model"
77
import { ScriptureReference } from "shared/utils/get-marker-action.model";
88
import { UsjNodeOptions } from "shared-react/nodes/scripture/usj/usj-node-options.model";
99
import { immutableNoteCallerNodeName } from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
10-
import { Usj2Usfm } from "./hooks/usj2Usfm";
10+
// import { Usj2Usfm } from "./hooks/usj2Usfm";
11+
import "shared/styles/nodes-menu.css";
1112

1213
const defaultUsj: Usj = {
1314
type: USJ_TYPE,
@@ -37,11 +38,8 @@ function App() {
3738
},
3839
};
3940
const viewOptions = useMemo(() => getViewOptions(viewMode), [viewMode]);
40-
// const noteViewOptions = useMemo(() => getViewOptions(noteViewMode), [noteViewMode]);
4141
const onChange = async (usj: Usj) => {
42-
// console.log({ usj });
43-
const usfm = await Usj2Usfm(usj);
44-
console.log(usfm);
42+
console.log(usj);
4543
};
4644
useEffect(() => {
4745
console.log({ scrRef });

packages/scribe/src/adaptors/usj-marker-action.utils.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,18 @@ const markerActions: {
2727
action?: (currentEditor: {
2828
reference: ScriptureReference;
2929
editor: LexicalEditor;
30+
autoNumbering?: boolean;
31+
newVerseRChapterNum?: number;
32+
noteText?: string;
3033
}) => MarkerContent[];
3134
};
3235
} = {
3336
c: {
3437
action: (currentEditor) => {
3538
const { book, chapterNum } = currentEditor.reference;
36-
const nextChapter = chapterNum + 1;
39+
const nextChapter = currentEditor.autoNumbering
40+
? chapterNum + 1
41+
: currentEditor.newVerseRChapterNum;
3742
const content: MarkerContent = {
3843
type: "chapter",
3944
marker: "c",
@@ -46,7 +51,9 @@ const markerActions: {
4651
v: {
4752
action: (currentEditor) => {
4853
const { book, chapterNum, verseNum, verse } = currentEditor.reference;
49-
const nextVerse = getNextVerse(verseNum, verse);
54+
const nextVerse = currentEditor.autoNumbering
55+
? getNextVerse(verseNum, verse)
56+
: currentEditor.newVerseRChapterNum;
5057
const content: MarkerContent = {
5158
type: "verse",
5259
marker: "v",
@@ -68,7 +75,7 @@ const markerActions: {
6875
{
6976
type: "char",
7077
marker: "ft",
71-
content: [" "],
78+
content: [currentEditor.noteText ?? " "],
7279
},
7380
],
7481
};
@@ -87,7 +94,7 @@ const markerActions: {
8794
{
8895
type: "char",
8996
marker: "xt",
90-
content: [" "],
97+
content: [currentEditor.noteText ?? " "],
9198
},
9299
],
93100
};
@@ -103,9 +110,17 @@ export function getUsjMarkerAction(
103110
viewOptions?: ViewOptions,
104111
): MarkerAction {
105112
const markerAction = getMarkerAction(marker);
106-
const action = (currentEditor: { reference: ScriptureReference; editor: LexicalEditor }) => {
113+
const action = (currentEditor: {
114+
reference: ScriptureReference;
115+
editor: LexicalEditor;
116+
autoNumbering?: boolean;
117+
newVerseRChapterNum?: number;
118+
noteText?: string;
119+
}) => {
107120
currentEditor.editor.update(() => {
108-
const content = markerAction?.action?.(currentEditor);
121+
const content = currentEditor.autoNumbering
122+
? markerAction?.action?.(currentEditor)
123+
: markerAction?.action?.(currentEditor);
109124
if (!content) return;
110125

111126
const serializedLexicalNode = createLexicalUsjNode(content, usjEditorAdaptor, viewOptions);

packages/scribe/src/components/Editor.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import UpdateStatePlugin from "shared-react/plugins/UpdateStatePlugin";
2222
import editorUsjAdaptor from "../adaptors/editor-usj.adaptor";
2323
import { getViewClassList, ViewOptions } from "../adaptors/view-options.utils";
2424
import usjEditorAdaptor from "../adaptors/usj-editor.adaptor";
25+
import UsjNodesMenuPlugin from "../plugins/UsjNodesMenuPlugin";
2526
import useDeferredState from "../hooks/use-deferred-state.hook";
2627
import { ScriptureReferencePlugin } from "../plugins/ScriptureReferencePlugin";
2728
import editorTheme from "../themes/editor-theme";
2829
import LoadingSpinner from "./LoadingSpinner";
2930
import { blackListedChangeTags } from "shared/nodes/scripture/usj/node-constants";
3031
import { deepEqual } from "fast-equals";
32+
import { getUsjMarkerAction } from "../adaptors/usj-marker-action.utils";
3133

3234
/** Forward reference for the editor. */
3335
export type EditorRef = {
@@ -73,7 +75,7 @@ const Editor = forwardRef(function Editor(
7375
const [usj, setUsj] = useState(usjInput);
7476
const [loadedUsj, , setEditedUsj] = useDeferredState(usj);
7577
useDefaultNodeOptions(nodeOptions);
76-
78+
const autoNumbering = false;
7779
const initialConfig = {
7880
namespace: "ScribeEditor",
7981
editable: true,
@@ -127,19 +129,21 @@ const Editor = forwardRef(function Editor(
127129
placeholder={<LoadingSpinner />}
128130
ErrorBoundary={LexicalErrorBoundary}
129131
/>
130-
{/* <UsjNodesMenuPlugin
131-
trigger={NODE_MENU_TRIGGER}
132+
{scrRef && (
133+
<UsjNodesMenuPlugin
134+
trigger={"\\"}
132135
scrRef={scrRef}
133136
getMarkerAction={(marker, markerData) =>
134137
getUsjMarkerAction(marker, markerData, viewOptions)
135138
}
136-
/> */}
139+
autoNumbering={autoNumbering}
140+
/>
141+
)}
137142
<UpdateStatePlugin
138143
scripture={loadedUsj}
139144
nodeOptions={nodeOptions}
140145
editorAdaptor={usjEditorAdaptor}
141146
viewOptions={viewOptions}
142-
// logger={logger}
143147
/>
144148
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
145149
<NoteNodePlugin nodeOptions={nodeOptions} />

packages/scribe/src/index.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2433,3 +2433,69 @@ span.read img {
24332433
background-repeat: no-repeat;
24342434
background-position: center;
24352435
}
2436+
/* NodesMenu.css */
2437+
2438+
.user-input-container {
2439+
border-radius: 0.5rem;
2440+
border: 1px solid rgba(229, 231, 235, 1);
2441+
background-color: rgba(252, 252, 252, 1);
2442+
padding: 1rem;
2443+
box-shadow:
2444+
0 10px 15px -3px rgba(0, 0, 0, 0.1),
2445+
0 4px 6px -2px rgba(0, 0, 0, 0.05);
2446+
}
2447+
2448+
.input-header {
2449+
margin-bottom: 0.5rem;
2450+
font-size: 1.125rem;
2451+
font-weight: 500;
2452+
color: rgba(25, 25, 25, 1);
2453+
}
2454+
2455+
.user-input-container input {
2456+
width: 100%;
2457+
border-radius: 0.25rem;
2458+
border: 1px solid rgba(209, 213, 219, 1);
2459+
padding: 0.5rem;
2460+
margin-bottom: 0.75rem;
2461+
background-color: white;
2462+
color: rgba(25, 25, 25, 1);
2463+
}
2464+
2465+
.user-input-container input:focus {
2466+
outline: none;
2467+
border-color: rgba(18, 82, 179, 1);
2468+
box-shadow: 0 0 0 3px rgba(18, 82, 179, 0.3);
2469+
}
2470+
2471+
.input-actions {
2472+
display: flex;
2473+
justify-content: flex-end;
2474+
gap: 0.5rem;
2475+
}
2476+
2477+
.cancel-button {
2478+
border-radius: 0.25rem;
2479+
background-color: rgba(229, 231, 235, 1);
2480+
padding: 0.25rem 0.75rem;
2481+
color: rgba(25, 25, 25, 1);
2482+
border: none;
2483+
cursor: pointer;
2484+
}
2485+
2486+
.cancel-button:hover {
2487+
background-color: rgba(209, 213, 219, 1);
2488+
}
2489+
2490+
.apply-button {
2491+
border-radius: 0.25rem;
2492+
background-color: rgba(18, 82, 179, 1);
2493+
color: white;
2494+
padding: 0.25rem 0.75rem;
2495+
border: none;
2496+
cursor: pointer;
2497+
}
2498+
2499+
.apply-button:hover {
2500+
background-color: rgba(15, 65, 143, 1);
2501+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { forwardRef, ReactNode } from "react";
2+
3+
export type FloatingBoxCoords = { x: number; y: number } | undefined;
4+
5+
type FloatingBoxProps = {
6+
coords: FloatingBoxCoords;
7+
children?: ReactNode;
8+
} & React.HTMLAttributes<HTMLDivElement>;
9+
10+
export const FloatingBox = forwardRef<HTMLDivElement, FloatingBoxProps>((props, ref) => {
11+
const { coords, children, style, ...extraProps } = props;
12+
const shouldShow = coords !== undefined;
13+
14+
return (
15+
<div
16+
ref={ref}
17+
className="floating-box"
18+
aria-hidden={!shouldShow}
19+
style={{
20+
...style,
21+
position: "absolute",
22+
zIndex: 1000,
23+
top: coords?.y,
24+
left: coords?.x,
25+
visibility: shouldShow ? "visible" : "hidden",
26+
opacity: shouldShow ? 1 : 0,
27+
}}
28+
{...extraProps}
29+
>
30+
{children}
31+
</div>
32+
);
33+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { memo, ReactNode, useMemo, useRef } from "react";
2+
import { createPortal } from "react-dom";
3+
import { FloatingBox } from "./FloatingBox";
4+
import useCursorCoords from "./useCursorCoords";
5+
import { Placement } from "@floating-ui/dom";
6+
7+
const DOM_ELEMENT = document.body;
8+
9+
const MemoizedFloatingBox = memo(FloatingBox);
10+
11+
export type FloatingMenuCoords = { x: number; y: number } | undefined;
12+
13+
type CursorFloatingBox = {
14+
isOpen?: boolean;
15+
children:
16+
| ReactNode
17+
| ((props: { isOpen: boolean | undefined; placement?: Placement }) => ReactNode);
18+
};
19+
20+
/**
21+
* FloatingBoxAtCursor component is responsible for rendering a floating menu
22+
* at the cursor position when the isOpen prop is true
23+
*/
24+
export default function FloatingBoxAtCursor({ isOpen = false, children }: CursorFloatingBox) {
25+
const floatingBoxRef = useRef<HTMLDivElement>(null);
26+
const { coords, placement } = useCursorCoords({ isOpen, floatingBoxRef });
27+
28+
const renderChildren = useMemo(
29+
() => (coords ? (typeof children === "function" ? children : () => children) : () => null),
30+
[children, coords],
31+
);
32+
33+
return createPortal(
34+
<MemoizedFloatingBox
35+
ref={floatingBoxRef}
36+
coords={coords}
37+
style={coords ? undefined : { display: "none" }}
38+
>
39+
{renderChildren({ isOpen, placement })}
40+
</MemoizedFloatingBox>,
41+
DOM_ELEMENT,
42+
);
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { useEffect } from "react";
2+
import { useFloatingPosition } from "./useFloatingPosition";
3+
4+
export default function useCursorCoords({
5+
isOpen,
6+
floatingBoxRef,
7+
}: {
8+
isOpen: boolean;
9+
floatingBoxRef: React.RefObject<HTMLDivElement>;
10+
}) {
11+
const { coords, updatePosition, cleanup, placement } = useFloatingPosition();
12+
13+
useEffect(() => {
14+
if (!isOpen || !floatingBoxRef.current) {
15+
cleanup();
16+
return;
17+
}
18+
19+
const domRange = window.getSelection()?.getRangeAt(0);
20+
if (domRange) {
21+
updatePosition(domRange, floatingBoxRef.current);
22+
return cleanup;
23+
}
24+
}, [isOpen, updatePosition, cleanup]);
25+
26+
return { coords, placement };
27+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useState, useCallback, useRef, useEffect } from "react";
2+
import { autoUpdate, computePosition, shift, flip, Placement } from "@floating-ui/dom";
3+
4+
export function useFloatingPosition() {
5+
const [coords, setCoords] = useState<{ x: number; y: number } | undefined>(undefined);
6+
const [placement, setPlacement] = useState<Placement>();
7+
const cleanupRef = useRef<(() => void) | null>(null);
8+
9+
const updatePosition = useCallback((domRange: Range, anchorElement: HTMLElement) => {
10+
if (cleanupRef.current) {
11+
cleanupRef.current();
12+
}
13+
const referenceElement =
14+
domRange.commonAncestorContainer.nodeType === domRange.commonAncestorContainer.TEXT_NODE
15+
? domRange
16+
: (domRange.commonAncestorContainer as HTMLElement);
17+
18+
cleanupRef.current = autoUpdate(referenceElement, anchorElement, () => {
19+
computePosition(referenceElement, anchorElement, {
20+
placement: "bottom-start",
21+
middleware: [shift(), flip()],
22+
})
23+
.then((pos) => {
24+
setPlacement(pos.placement);
25+
setCoords((prevCoords) =>
26+
prevCoords?.x === pos.x && prevCoords?.y === pos.y
27+
? prevCoords
28+
: { x: pos.x, y: pos.y },
29+
);
30+
})
31+
.catch(() => {
32+
setCoords(undefined);
33+
});
34+
});
35+
}, []);
36+
37+
const cleanup = useCallback(() => {
38+
if (cleanupRef.current) {
39+
setCoords(undefined);
40+
cleanupRef.current();
41+
cleanupRef.current = null;
42+
}
43+
}, []);
44+
45+
useEffect(() => {
46+
return cleanup;
47+
}, [cleanup]);
48+
49+
return { coords, placement, updatePosition, cleanup };
50+
}

0 commit comments

Comments
 (0)