Skip to content

Commit

Permalink
Merge pull request #808 from streamich/peritext-floating-menus
Browse files Browse the repository at this point in the history
Peritext floating menus setup
  • Loading branch information
streamich authored Jan 18, 2025
2 parents 364d22b + 9c29f06 commit da37142
Show file tree
Hide file tree
Showing 49 changed files with 1,127 additions and 168 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
jobs:
gh-pages:
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
node-version: [20.x]
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"@jsonjoy.com/util": "^1.4.0",
"arg": "^5.0.2",
"hyperdyperid": "^1.2.0",
"nano-css": "^5.6.2",
"sonic-forest": "^1.0.3",
"thingies": "^2.1.1",
"tree-dump": "^1.0.2",
Expand All @@ -155,7 +156,7 @@
"json-crdt-traces": "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d",
"json-logic-js": "^2.0.2",
"nano-theme": "^1.4.3",
"nice-ui": "^1.18.0",
"nice-ui": "^1.25.0",
"quill-delta": "^5.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
12 changes: 7 additions & 5 deletions src/json-crdt-extensions/peritext/slice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@ export const enum SliceTypeCon {
bg = -18, // <span style="background: ...">
kbd = -19, // <kbd>
hidden = -20, // <span style="color: transparent; background: black">
footnote = -21, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
ref = -22, // <a> with href="#ref-..." and title="Reference ..." (Reference to some element in the document)
iaside = -23, // Inline <aside>
iembed = -24, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
bookmark = -25, // UI for creating a link to this slice
q = -21, // <q> (inline quote)
cite = -22, // <cite> (inline citation)
footnote = -23, // <sup> or <a> with href="#footnote-..." and title="Footnote ..."
ref = -24, // <a> with href="#ref-..." and title="Reference ..." (Reference to some element in the document)
iaside = -25, // Inline <aside>
iembed = -26, // inline embed (any media, dropdown, Google Docs-like chips: date, person, file, etc.)
bookmark = -27, // UI for creating a link to this slice
}

/**
Expand Down
13 changes: 13 additions & 0 deletions src/json-crdt-peritext-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,16 @@

CRDT-native UI for JSON CRDT `peritext` extension. Supports block-level and
inline-level collaborative editing, with the ability to nest blocks.


## Software architecture layers

Below is the software architecture of the Peritext rich text editor. At the top
is the most user-facing layer, and at the bottom is the most foundational layer.

- Rendering surface plugins
- React rendering surface `<PeritextFragment controller={DomController} />`
- DOM event handlers, `new DomController(defaults: PeritextEventDefaults)`
- Peritext events, `create(txt: Peritext): PeritextEventDefaults`
- Peritext JSON CRDT extension, `Peritext`
- JSON CRDT, `Model`
5 changes: 3 additions & 2 deletions src/json-crdt-peritext-ui/__demos__/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import {Provider, GlobalCss} from 'nano-theme';
import {NiceUiProvider} from 'nice-ui/lib/context';
import {ModelWithExt, ext} from '../../../json-crdt-extensions';
import {PeritextView} from '../../react';
import {CursorPlugin} from '../../plugins/cursor';
Expand All @@ -26,11 +27,11 @@ export const App: React.FC = () => {
}, []);

return (
<Provider theme={'light'}>
<NiceUiProvider>
<GlobalCss />
<div style={{maxWidth: '690px', fontSize: '21px', lineHeight: '1.7em', margin: '32px auto'}}>
<PeritextView peritext={peritext} plugins={plugins} />
</div>
</Provider>
</NiceUiProvider>
);
};
6 changes: 3 additions & 3 deletions src/json-crdt-peritext-ui/__demos__/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ document.body.appendChild(div);

const root = createRoot(div);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
// <React.StrictMode>
<App />,
// </React.StrictMode>,
);
14 changes: 6 additions & 8 deletions src/json-crdt-peritext-ui/dom/DomController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import {printTree, type Printable} from 'tree-dump';
import {InputController} from '../dom/InputController';
import {CursorController} from '../dom/CursorController';
import {RichTextController} from '../dom/RichTextController';
import {PeritextEventDefaults} from '../events/PeritextEventDefaults';
import {PeritextEventTarget} from '../events/PeritextEventTarget';
import {KeyController} from '../dom/KeyController';
import {CompositionController} from '../dom/CompositionController';
import type {PeritextEventDefaults} from '../events/PeritextEventDefaults';
import type {PeritextEventTarget} from '../events/PeritextEventTarget';
import type {UiLifeCycles} from '../dom/types';
import type {Peritext} from '../../json-crdt-extensions';

export interface DomControllerOpts {
source: HTMLElement;
txt: Peritext;
events: PeritextEventDefaults;
}

export class DomController implements UiLifeCycles, Printable {
Expand All @@ -23,10 +22,9 @@ export class DomController implements UiLifeCycles, Printable {
public readonly richText: RichTextController;

constructor(public readonly opts: DomControllerOpts) {
const {source, txt} = opts;
const et = (this.et = new PeritextEventTarget());
const defaults = new PeritextEventDefaults(txt, et);
et.defaults = defaults;
const {source, events} = opts;
const {txt} = events;
const et = (this.et = opts.events.et);
const keys = (this.keys = new KeyController({source}));
const comp = (this.comp = new CompositionController({et, source, txt}));
this.input = new InputController({et, source, txt, comp});
Expand Down
8 changes: 8 additions & 0 deletions src/json-crdt-peritext-ui/dom/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ export interface UiLifeCycles {
stop(): void;
}

export interface UiLifeCyclesRender {
/**
* Called when UI component is mounted. Returns a function to be called when
* the component is removed from the screen.
*/
start(): () => void;
}

export type Rect = Pick<DOMRect, 'x' | 'y' | 'width' | 'height'>;
6 changes: 3 additions & 3 deletions src/json-crdt-peritext-ui/events/PeritextEventDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {CursorAnchor} from '../../json-crdt-extensions/peritext/slice/constants';
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
import type {Peritext} from '../../json-crdt-extensions/peritext';
import type {EditorSlices} from '../../json-crdt-extensions/peritext/editor/EditorSlices';
import type {PeritextEventHandlerMap, PeritextEventTarget} from './PeritextEventTarget';
import type * as events from './types';

/**
Expand All @@ -13,8 +13,8 @@ import type * as events from './types';
*/
export class PeritextEventDefaults implements PeritextEventHandlerMap {
public constructor(
protected readonly txt: Peritext,
protected readonly et: PeritextEventTarget,
public readonly txt: Peritext,
public readonly et: PeritextEventTarget,
) {}

public readonly change = (event: CustomEvent<events.ChangeDetail>) => {};
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/events/PeritextEventTarget.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {TypedEventTarget} from '../../util/events/TypedEventTarget';
import {SubscriptionEventTarget} from '../../util/events/TypedEventTarget';
import type {PeritextEventDetailMap, CursorDetail, FormatDetail, DeleteDetail, MarkerDetail} from './types';

export type PeritextEventMap = {
Expand All @@ -11,7 +11,7 @@ export type PeritextEventHandlerMap = {

let __id = 0;

export class PeritextEventTarget extends TypedEventTarget<PeritextEventMap> {
export class PeritextEventTarget extends SubscriptionEventTarget<PeritextEventMap> {
public readonly id: number = __id++;

public defaults: Partial<PeritextEventHandlerMap> = {};
Expand Down
10 changes: 10 additions & 0 deletions src/json-crdt-peritext-ui/events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {PeritextEventDefaults} from './PeritextEventDefaults';
import {PeritextEventTarget} from './PeritextEventTarget';
import type {Peritext} from '../../json-crdt-extensions';

export const create = (txt: Peritext) => {
const et = new PeritextEventTarget();
const defaults = new PeritextEventDefaults(txt, et);
et.defaults = defaults;
return defaults;
};
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderAnchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import {rule, keyframes} from 'nano-theme';
import {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import type {AnchorViewProps} from '../../react/cursor/AnchorView';

export const fadeInAnimation = keyframes({
Expand Down Expand Up @@ -42,7 +42,7 @@ export interface RenderAnchorProps extends AnchorViewProps {

export const RenderAnchor: React.FC<RenderAnchorProps> = ({children}) => {
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const style = focus ? undefined : {background: DefaultRendererColors.InactiveCursor};

Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import useHarmonicIntervalFn from 'react-use/lib/useHarmonicIntervalFn';
import {keyframes, rule} from 'nano-theme';
import {usePeritext} from '../../react/context';
import {useSyncStore} from '../../react/hooks';
import {useSyncStore, useSyncStoreOpt} from '../../react/hooks';
import {DefaultRendererColors} from './constants';
import {CommonSliceType} from '../../../json-crdt-extensions';
import {useCursorPlugin} from './context';
Expand Down Expand Up @@ -54,7 +54,7 @@ export const RenderCaret: React.FC<RenderCaretProps> = ({italic, children}) => {
const [show, setShow] = React.useState(true);
useHarmonicIntervalFn(() => setShow(Date.now() % (ms + ms) > ms), ms);
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
const plugin = useCursorPlugin();

const score = plugin.score.value;
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import {rule, drule, keyframes} from 'nano-theme';
import {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import type {FocusViewProps} from '../../react/cursor/FocusView';

const width = 0.14;
Expand Down Expand Up @@ -43,7 +43,7 @@ export interface RenderFocusProps extends FocusViewProps {

export const RenderFocus: React.FC<RenderFocusProps> = ({left, italic, children}) => {
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const style: React.CSSProperties = focus ? {} : {background: DefaultRendererColors.InactiveCursor, animation: 'none'};

Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderInline.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import {DefaultRendererColors} from './constants';
import type {InlineViewProps} from '../../react/InlineView';

Expand All @@ -12,7 +12,7 @@ interface RenderInlineSelectionProps extends RenderInlineProps {
const RenderInlineSelection: React.FC<RenderInlineSelectionProps> = (props) => {
const {children, selection} = props;
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const [left, right] = selection;
const style: React.CSSProperties = {
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import * as React from 'react';
import {context, type CursorPluginContextValue} from './context';
import {ValueSyncStore} from '../../../util/events/sync-store';
import type {ChangeDetail} from '../../events/types';
import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
import type {PeritextSurfaceState, PeritextViewProps} from '../../react';
import type {CursorPlugin} from './CursorPlugin';

export interface RenderPeritextProps extends PeritextViewProps {
ctx?: PeritextSurfaceContextValue;
ctx?: PeritextSurfaceState;
plugin: CursorPlugin;
children?: React.ReactNode;
}
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import type {PeritextSurfaceContextValue} from '../../react';
import type {PeritextSurfaceState} from '../../react';
import type {ValueSyncStore} from '../../../util/events/sync-store';
import type {CursorPlugin} from './CursorPlugin';

export interface CursorPluginContextValue {
ctx?: PeritextSurfaceContextValue;
ctx?: PeritextSurfaceState;

plugin: CursorPlugin;

Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/debug/RenderPeritext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {drule, rule, useTheme} from 'nano-theme';
import {context} from './context';
import {Button} from '../../components/Button';
import {Console} from './Console';
import type {PeritextSurfaceContextValue, PeritextViewProps} from '../../react';
import {ValueSyncStore} from '../../../util/events/sync-store';
import type {PeritextSurfaceState, PeritextViewProps} from '../../react';

const blockClass = rule({
pos: 'relative',
Expand All @@ -29,7 +29,7 @@ const childrenDebugClass = rule({
export interface RenderPeritextProps extends PeritextViewProps {
enabled?: boolean;
children?: React.ReactNode;
ctx?: PeritextSurfaceContextValue;
ctx?: PeritextSurfaceState;
}

export const RenderPeritext: React.FC<RenderPeritextProps> = ({enabled: enabledProp = true, ctx, children}) => {
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/debug/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import type {ValueSyncStore} from '../../../util/events/sync-store';
import type {PeritextSurfaceContextValue} from '../../react';
import type {PeritextSurfaceState} from '../../react/state';

export interface DebugRenderersContextValue {
enabled: boolean;
Expand All @@ -10,7 +10,7 @@ export interface DebugRenderersContextValue {
peritext: ValueSyncStore<boolean>;
model: ValueSyncStore<boolean>;
};
ctx?: PeritextSurfaceContextValue;
ctx?: PeritextSurfaceState;
}

export const context = React.createContext<DebugRenderersContextValue>(null!);
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/minimal/RenderAnchor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {rule, keyframes} from 'nano-theme';
import {Char} from '../../constants';
import {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import type {AnchorViewProps} from '../../react/cursor/AnchorView';

export const fadeInAnimation = keyframes({
Expand Down Expand Up @@ -42,7 +42,7 @@ export interface RenderAnchorProps extends AnchorViewProps {}

export const RenderAnchor: React.FC<RenderAnchorProps> = () => {
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const style = focus ? undefined : {background: DefaultRendererColors.InactiveCursor};

Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/minimal/RenderCaret.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import useHarmonicIntervalFn from 'react-use/lib/useHarmonicIntervalFn';
import {keyframes, rule} from 'nano-theme';
import {usePeritext} from '../../react/context';
import {useSyncStore} from '../../react/hooks';
import {useSyncStore, useSyncStoreOpt} from '../../react/hooks';
import type {CaretViewProps} from '../../react/cursor/CaretView';
import {DefaultRendererColors} from './constants';
import {CommonSliceType} from '../../../json-crdt-extensions';
Expand Down Expand Up @@ -54,7 +54,7 @@ export const RenderCaret: React.FC<RenderCaretProps> = ({italic, children}) => {
const [show, setShow] = React.useState(true);
useHarmonicIntervalFn(() => setShow(Date.now() % (ms + ms) > ms), ms);
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;
const plugin = usePlugin();

const score = plugin.score.value;
Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/minimal/RenderFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import {rule, drule, keyframes} from 'nano-theme';
import {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import type {FocusViewProps} from '../../react/cursor/FocusView';

const width = 0.14;
Expand Down Expand Up @@ -44,7 +44,7 @@ export interface RenderFocusProps extends FocusViewProps {

export const RenderFocus: React.FC<RenderFocusProps> = ({left, italic, children}) => {
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const style: React.CSSProperties = focus ? {} : {background: DefaultRendererColors.InactiveCursor, animation: 'none'};

Expand Down
4 changes: 2 additions & 2 deletions src/json-crdt-peritext-ui/plugins/minimal/RenderInline.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import {useSyncStoreOpt} from '../../react/hooks';
import {DefaultRendererColors} from './constants';
import type {InlineViewProps} from '../../react/InlineView';
import {CommonSliceType} from '../../../json-crdt-extensions';
Expand All @@ -13,7 +13,7 @@ interface RenderInlineSelectionProps extends RenderInlineProps {
const RenderInlineSelection: React.FC<RenderInlineSelectionProps> = (props) => {
const {children, selection} = props;
const {dom} = usePeritext();
const focus = useSyncStore(dom.cursor.focus);
const focus = useSyncStoreOpt(dom?.cursor.focus) || false;

const [left, right] = selection;
const style: React.CSSProperties = {
Expand Down
Loading

0 comments on commit da37142

Please sign in to comment.