Skip to content

Commit 6bcf1e1

Browse files
authored
improve RTL (#143)
- add `textDirection` property - add `options` controls in App - amend CSS for RTL - add RTL plugin to fix arrow keys - fix text alignment - refactor to view classes at root (rather than para and chapter) - also fix toolbar for implied paragraphs - also fix defaulting of `viewOptions` - also fix Show Comment button hover color matches in toolbar
1 parent 97219e5 commit 6bcf1e1

35 files changed

+1915
-2141
lines changed

packages/perf-react/src/App.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383

8484
.editor-oce {
8585
position: relative;
86-
text-align: left;
86+
text-align: start;
8787
}
8888

8989
.contentEditable {

packages/perf-vanilla/src/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ button:focus-visible {
6666

6767
.editor-oce {
6868
position: relative;
69-
text-align: left;
69+
text-align: start;
7070
}
7171

7272
.contentEditable {

packages/platform/src/App.css

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,49 @@ body {
2121
.controls {
2222
display: flex;
2323
justify-content: space-between;
24-
margin-bottom: 30px;
24+
margin-bottom: 20px;
25+
}
26+
27+
.controls button {
28+
cursor: pointer;
29+
border: 0;
30+
border-radius: 10px;
31+
background-color: inherit;
32+
}
33+
.controls button:hover {
34+
background-color: #ddd;
35+
}
36+
37+
.defined-options {
38+
display: flex;
39+
justify-content: space-between;
40+
margin-bottom: 20px;
41+
}
42+
43+
div.checkbox {
44+
border: 0;
45+
display: flex;
46+
background: none;
47+
border-radius: 10px;
48+
padding: 8px;
49+
vertical-align: middle;
50+
flex-shrink: 0;
51+
align-items: center;
52+
justify-content: space-between;
53+
}
54+
55+
input[type="checkbox"] {
56+
cursor: pointer;
57+
}
58+
59+
.defined-options label {
60+
cursor: pointer;
61+
color: black;
62+
font-size: 13.3333px;
63+
}
64+
65+
.defined-options .toolbar-item:hover:not([disabled]) {
66+
background-color: #ddd;
2567
}
2668

2769
#root {

packages/platform/src/App.tsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import { WEB_PSA_USX as usx } from "shared/data/WEB-PSA.usx";
66
import { WEB_PSA_COMMENTS as comments } from "shared/data/WEB_PSA.comments";
77
import { immutableNoteCallerNodeName } from "shared-react/nodes/scripture/usj/ImmutableNoteCallerNode";
88
import { UsjNodeOptions } from "shared-react/nodes/scripture/usj/usj-node-options.model";
9+
import { TextDirection } from "shared-react/plugins/text-direction.model";
910
import { getViewOptions, DEFAULT_VIEW_MODE } from "./editor/adaptors/view-options.utils";
10-
import ViewModeDropDown from "./editor/toolbar/ViewModeDropDown";
1111
import { EditorOptions } from "./editor/Editor";
1212
import { Comments } from "./marginal/comments/commenting";
1313
import Marginal, { MarginalRef } from "./marginal/Marginal";
14+
import TextDirectionDropDown from "./TextDirectionDropDown";
15+
import ViewModeDropDown from "./ViewModeDropDown";
16+
1417
import "./App.css";
1518

1619
const defaultUsj = usxStringToUsj('<usx version="3.0" />');
@@ -33,20 +36,37 @@ const annotationRange3 = {
3336
};
3437
const cursorLocation = { start: { jsonPath: "$.content[10].content[2]", offset: 15 } };
3538

39+
function getOptions(
40+
definedOptions: boolean,
41+
hasSpellCheck: boolean,
42+
textDirection: TextDirection,
43+
viewMode: string | undefined,
44+
): EditorOptions {
45+
return definedOptions
46+
? { hasSpellCheck, textDirection, view: getViewOptions(viewMode), nodes: nodeOptions }
47+
: {};
48+
}
49+
3650
export default function App() {
3751
const marginalRef = useRef<MarginalRef | null>(null);
52+
const [definedOptions, setDefinedOptions] = useState(true);
53+
const [hasSpellCheck, setHasSpellCheck] = useState(false);
54+
const [textDirection, setTextDirection] = useState<TextDirection>("ltr");
3855
const [viewMode, setViewMode] = useState(DEFAULT_VIEW_MODE);
3956
const [scrRef, setScrRef] = useState(defaultScrRef);
57+
4058
const options = useMemo<EditorOptions>(
41-
() => ({ view: getViewOptions(viewMode), nodes: nodeOptions }),
42-
[viewMode],
59+
() => getOptions(definedOptions, hasSpellCheck, textDirection, viewMode),
60+
[definedOptions, hasSpellCheck, textDirection, viewMode],
4361
);
4462

4563
const handleChange = useCallback((usj: Usj, comments: Comments | undefined) => {
4664
console.log({ usj, comments });
4765
marginalRef.current?.setUsj(usj);
4866
}, []);
4967

68+
const toggleDefineOptions = useCallback(() => setDefinedOptions((value) => !value), []);
69+
5070
// Simulate USJ updating after the editor is loaded.
5171
useEffect(() => {
5272
const timeoutId = setTimeout(() => {
@@ -84,8 +104,25 @@ export default function App() {
84104
<>
85105
<div className="controls">
86106
<BookChapterControl scrRef={scrRef} handleSubmit={setScrRef} />
87-
<ViewModeDropDown viewMode={viewMode} handleSelect={setViewMode} />
107+
<button onClick={toggleDefineOptions}>
108+
{definedOptions ? "Undefine" : "Define"} Options
109+
</button>
88110
</div>
111+
{definedOptions && (
112+
<div className="defined-options">
113+
<div className="checkbox">
114+
<input
115+
type="checkbox"
116+
id="hasSpellCheckBox"
117+
checked={hasSpellCheck}
118+
onChange={(e) => setHasSpellCheck(e.target.checked)}
119+
/>
120+
<label htmlFor="hasSpellCheckBox">Has Spell Check</label>
121+
</div>
122+
<TextDirectionDropDown textDirection={textDirection} handleSelect={setTextDirection} />
123+
<ViewModeDropDown viewMode={viewMode} handleSelect={setViewMode} />
124+
</div>
125+
)}
89126
<Marginal
90127
ref={marginalRef}
91128
defaultUsj={defaultUsj}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { JSX } from "react";
2+
import { directionToNames, type TextDirection } from "shared-react/plugins/text-direction.model";
3+
import DropDown, { DropDownItem } from "./editor/toolbar/DropDown";
4+
5+
function directionLabel(textDirection: TextDirection): string {
6+
return textDirection in directionToNames ? directionToNames[textDirection] : "select...";
7+
}
8+
9+
function dropDownActiveClass(active: boolean): string {
10+
return active ? "active dropdown-item-active" : "";
11+
}
12+
13+
export default function TextDirectionDropDown({
14+
textDirection,
15+
handleSelect,
16+
disabled = false,
17+
}: {
18+
textDirection: TextDirection;
19+
handleSelect: (textDirection: TextDirection) => void;
20+
disabled?: boolean;
21+
}): JSX.Element {
22+
return (
23+
<DropDown
24+
disabled={disabled}
25+
buttonClassName="toolbar-item"
26+
buttonIconClassName={"icon view-mode " + textDirection}
27+
buttonLabel={directionLabel(textDirection)}
28+
buttonAriaLabel="Selection options for text direction"
29+
>
30+
{Object.keys(directionToNames).map((item) => (
31+
<DropDownItem
32+
key={item}
33+
className={"item view-mode " + dropDownActiveClass(textDirection === item)}
34+
onClick={() => handleSelect(item as TextDirection)}
35+
>
36+
<i className={"icon view-mode " + item} />
37+
{directionToNames[item as TextDirection]}
38+
</DropDownItem>
39+
))}
40+
</DropDown>
41+
);
42+
}

packages/platform/src/editor/toolbar/ViewModeDropDown.tsx renamed to packages/platform/src/ViewModeDropDown.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { JSX } from "react";
2-
import DropDown, { DropDownItem } from "./DropDown";
3-
import { viewModeToViewNames, ViewNameKey } from "./view-mode.model";
2+
import DropDown, { DropDownItem } from "./editor/toolbar/DropDown";
3+
import { viewModeToViewNames, ViewNameKey } from "./editor/adaptors/view-mode.model";
44

55
function viewModeToClassName(viewMode: string): string {
66
return viewMode in viewModeToViewNames ? viewMode : "";
@@ -33,14 +33,14 @@ export default function ViewModeDropDown({
3333
buttonLabel={viewModeLabel(viewMode)}
3434
buttonAriaLabel="Selection options for view mode"
3535
>
36-
{Object.keys(viewModeToViewNames).map((itemViewMode) => (
36+
{Object.keys(viewModeToViewNames).map((item) => (
3737
<DropDownItem
38-
key={itemViewMode}
39-
className={"item view-mode " + dropDownActiveClass(viewMode === itemViewMode)}
40-
onClick={() => handleSelect(itemViewMode)}
38+
key={item}
39+
className={"item view-mode " + dropDownActiveClass(viewMode === item)}
40+
onClick={() => handleSelect(item)}
4141
>
42-
<i className={"icon view-mode " + viewModeToClassName(itemViewMode)} />
43-
{viewModeToViewNames[itemViewMode as ViewNameKey]}
42+
<i className={"icon view-mode " + viewModeToClassName(item)} />
43+
{viewModeToViewNames[item as ViewNameKey]}
4444
</DropDownItem>
4545
))}
4646
</DropDown>

packages/platform/src/editor/Editor.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ import { UsjNodeOptions } from "shared-react/nodes/scripture/usj/usj-node-option
3030
import { HistoryPlugin } from "shared-react/plugins/HistoryPlugin";
3131
import ClipboardPlugin from "shared-react/plugins/ClipboardPlugin";
3232
import ContextMenuPlugin from "shared-react/plugins/ContextMenuPlugin";
33-
import NoteNodePlugin from "shared-react/plugins/NoteNodePlugin";
3433
import { LoggerBasic } from "shared-react/plugins/logger-basic.model";
34+
import NoteNodePlugin from "shared-react/plugins/NoteNodePlugin";
35+
import TextDirectionPlugin from "shared-react/plugins/TextDirectionPlugin";
36+
import { TextDirection } from "shared-react/plugins/text-direction.model";
3537
import UpdateStatePlugin from "shared-react/plugins/UpdateStatePlugin";
3638
import editorUsjAdaptor from "./adaptors/editor-usj.adaptor";
3739
import usjEditorAdaptor from "./adaptors/usj-editor.adaptor";
38-
import { ViewOptions } from "./adaptors/view-options.utils";
40+
import { getViewClassList, getViewOptions, ViewOptions } from "./adaptors/view-options.utils";
3941
import editorTheme from "./editor.theme";
4042
import ScriptureReferencePlugin from "./ScriptureReferencePlugin";
4143
import ToolbarPlugin from "./toolbar/ToolbarPlugin";
@@ -76,6 +78,8 @@ export type EditorOptions = {
7678
isReadonly?: boolean;
7779
/** Is the editor enabled for spell checking. */
7880
hasSpellCheck?: boolean;
81+
/** Text direction. */
82+
textDirection?: TextDirection;
7983
/** View options. */
8084
view?: ViewOptions;
8185
/** Options for each editor node:
@@ -116,6 +120,8 @@ const editorConfig: Mutable<InitialConfigType> = {
116120
nodes: [TypedMarkNode, ImmutableNoteCallerNode, ...scriptureUsjNodes],
117121
};
118122

123+
const defaultViewOptions = getViewOptions(undefined);
124+
119125
function Placeholder(): JSX.Element {
120126
return <div className="editor-placeholder">Enter some Scripture...</div>;
121127
}
@@ -154,7 +160,8 @@ const Editor = forwardRef(function Editor<TLogger extends LoggerBasic>(
154160
const {
155161
isReadonly = false,
156162
hasSpellCheck = false,
157-
view: viewOptions,
163+
textDirection = "auto",
164+
view: viewOptions = defaultViewOptions,
158165
nodes: nodeOptions = {},
159166
} = options ?? {};
160167
useDefaultNodeOptions(nodeOptions);
@@ -209,7 +216,10 @@ const Editor = forwardRef(function Editor<TLogger extends LoggerBasic>(
209216
<EditorRefPlugin editorRef={editorRef} />
210217
<RichTextPlugin
211218
contentEditable={
212-
<ContentEditable className="editor-input" spellCheck={hasSpellCheck} />
219+
<ContentEditable
220+
className={`editor-input ${getViewClassList(viewOptions).join(" ")}`}
221+
spellCheck={hasSpellCheck}
222+
/>
213223
}
214224
placeholder={<Placeholder />}
215225
ErrorBoundary={LexicalErrorBoundary}
@@ -238,6 +248,7 @@ const Editor = forwardRef(function Editor<TLogger extends LoggerBasic>(
238248
<NoteNodePlugin nodeOptions={nodeOptions} logger={logger} />
239249
<ContextMenuPlugin />
240250
<ClipboardPlugin />
251+
<TextDirectionPlugin textDirection={textDirection} />
241252
{children}
242253
</div>
243254
</div>

packages/platform/src/editor/adaptors/usj-editor-adaptor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ import {
2727
usjMarks,
2828
usjWithUnknownItems,
2929
} from "../../../../utilities/src/converters/usj/converter-test.data";
30-
import { UNFORMATTED_VIEW_MODE } from "../toolbar/view-mode.model";
3130
import { serializeEditorState, reset, initialize } from "./usj-editor.adaptor";
31+
import { UNFORMATTED_VIEW_MODE } from "./view-mode.model";
3232
import { getViewOptions } from "./view-options.utils";
3333

3434
/**

packages/platform/src/editor/adaptors/usj-editor.adaptor.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ import {
105105
UsjNodeOptions,
106106
} from "shared-react/nodes/scripture/usj/usj-node-options.model";
107107
import { LoggerBasic } from "shared-react/plugins/logger-basic.model";
108-
import { ViewOptions, getClassList, getVerseNodeClass, getViewOptions } from "./view-options.utils";
108+
import { ViewOptions, getVerseNodeClass, getViewOptions } from "./view-options.utils";
109109

110110
interface UsjEditorAdaptor extends EditorAdaptor {
111111
initialize: typeof initialize;
@@ -252,7 +252,6 @@ function createChapter(
252252
if (marker !== CHAPTER_MARKER) {
253253
_logger?.warn(`Unexpected chapter marker '${marker}'!`);
254254
}
255-
const classList = getClassList(_viewOptions);
256255
const unknownAttributes = getUnknownAttributes(markerObject);
257256
let showMarker: boolean | undefined;
258257
if (_viewOptions?.markerMode === "visible") showMarker = true;
@@ -262,7 +261,6 @@ function createChapter(
262261
type: ChapterNode.getType(),
263262
marker: marker as ChapterMarker,
264263
number: number ?? "",
265-
classList,
266264
sid,
267265
altnumber,
268266
pubnumber,
@@ -277,7 +275,6 @@ function createChapter(
277275
type: ImmutableChapterNode.getType(),
278276
marker: marker as ChapterMarker,
279277
number: number ?? "",
280-
classList,
281278
showMarker,
282279
sid,
283280
altnumber,
@@ -361,7 +358,6 @@ function createPara(
361358
if (!ParaNode.isValidMarker(marker)) {
362359
_logger?.warn(`Unexpected para marker '${marker}'!`);
363360
}
364-
const classList = getClassList(_viewOptions);
365361
const children: SerializedLexicalNode[] = [];
366362
if (_viewOptions?.markerMode === "editable")
367363
children.push(createMarker(marker), createText(NBSP));
@@ -371,7 +367,6 @@ function createPara(
371367
return {
372368
type: ParaNode.getType(),
373369
marker,
374-
classList,
375370
unknownAttributes,
376371
children,
377372
direction: null,
@@ -664,7 +659,9 @@ function recurseNodes(markers: MarkerContent[] | undefined): SerializedLexicalNo
664659
function insertImpliedParasRecurse(nodes: SerializedLexicalNode[]): SerializedLexicalNode[] {
665660
const bookNodeIndex = nodes.findIndex((node) => node.type === BookNode.getType());
666661
const isBookNodeFound = bookNodeIndex >= 0;
667-
const chapterNodeIndex = nodes.findIndex((node) => node.type === ChapterNode.getType());
662+
const chapterNodeIndex = nodes.findIndex(
663+
(node) => node.type === ChapterNode.getType() || node.type === ImmutableChapterNode.getType(),
664+
);
668665
const isChapterNodeFound = chapterNodeIndex >= 0;
669666
if (isBookNodeFound && (!isChapterNodeFound || bookNodeIndex < chapterNodeIndex)) {
670667
const nodesBefore = insertImpliedParasRecurse(nodes.slice(0, bookNodeIndex));

packages/platform/src/editor/toolbar/view-mode.model.ts renamed to packages/platform/src/editor/adaptors/view-mode.model.ts

File renamed without changes.

0 commit comments

Comments
 (0)