Skip to content

Commit

Permalink
Merge pull request #776 from streamich/peritext-rendering-surface-inline
Browse files Browse the repository at this point in the history
Peritext rendering surface inline element improvements
  • Loading branch information
streamich authored Nov 14, 2024
2 parents 837b23b + 7352122 commit 64dab2a
Show file tree
Hide file tree
Showing 38 changed files with 1,355 additions and 96 deletions.
3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"noStaticOnlyClass": "off",
"useOptionalChain": "off",
"noUselessLabel": "off"
},
"a11y": {
"noSvgWithoutTitle": "off"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@radix-ui/react-icons": "^1.3.1",
"@types/benchmark": "^2.1.5",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.11",
Expand All @@ -153,6 +154,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",
"quill-delta": "^5.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
24 changes: 20 additions & 4 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {PersistedSlice} from '../slice/PersistedSlice';
import {ValueSyncStore} from '../../../util/events/sync-store';
import {formatType} from '../slice/util';
import {CommonSliceType, type SliceType} from '../slice';
import {tick} from '../../../json-crdt-patch';
import type {ChunkSlice} from '../util/ChunkSlice';
import type {Peritext} from '../Peritext';
import type {Point} from '../rga/Point';
import type {Range} from '../rga/Range';
import type {CharIterator, CharPredicate, Position, TextRangeUnit} from './types';
import type {Printable} from 'tree-dump';
import {tick} from '../../../json-crdt-patch';

/**
* For inline boolean ("Overwrite") slices, both range endpoints should be
Expand Down Expand Up @@ -105,15 +105,31 @@ export class Editor<T = string> implements Printable {
for (let cursor: Cursor<T> | undefined, i = this.cursors0(); (cursor = i()); ) callback(cursor);
}

/**
* @returns Returns `true` if there is at least one cursor in the document.
*/
public hasCursor(): boolean {
return !!this.cursors0()();
}

/**
* @returns Returns the exact number of cursors in the document.
*/
public cursorCount(): number {
let cnt = 0;
for (const i = this.cursors0(); i(); ) cnt++;
return cnt;
}

/** Returns true if there is at least one cursor in the document. */
public hasCursor(): boolean {
return !!this.cursors0()();
/**
* Returns relative count of cursors (cardinality).
*
* @returns 0 if there are no cursors, 1 if there is exactly one cursor, 2 if
* there are more than one cursor.
*/
public cursorCard(): 0 | 1 | 2 {
const i = this.cursors0();
return !i() ? 0 : !i() ? 1 : 2;
}

public delCursor(cursor: Cursor<T>): void {
Expand Down
3 changes: 3 additions & 0 deletions src/json-crdt-extensions/peritext/slice/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export enum CommonSliceType {
aside = 19, // <aside>
embed = 20, // <embed>, <iframe>, <object>, <video>, <audio>, etc.
column = 21, // <div style="column-count: ..."> (represents 2 and 3 column layouts)
contents = 22, // Table of contents
row = 23, // Table row
cell = 24, // Table cell

// ------------------------------------------------ inline slices (-64 to -1)
Cursor = -1,
Expand Down
15 changes: 11 additions & 4 deletions src/json-crdt-peritext-ui/__demos__/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as React from 'react';
import {Provider, GlobalCss} from 'nano-theme';
import {ModelWithExt, ext} from '../../../json-crdt-extensions';
import {PeritextView} from '../../react';
import {cursorPlugin} from '../../plugins/cursor';
import {renderers} from '../../plugins/default';
import {renderers as debugRenderers} from '../../plugins/debug';
import {CursorPlugin} from '../../plugins/cursor';
import {ToolbarPlugin} from '../../plugins/toolbar';
import {DebugPlugin} from '../../plugins/debug';

export const App: React.FC = () => {
const [[model, peritext]] = React.useState(() => {
Expand All @@ -18,11 +18,18 @@ export const App: React.FC = () => {
return [model, peritext] as const;
});

const plugins = React.useMemo(() => {
const cursorPlugin = new CursorPlugin();
const toolbarPlugin = new ToolbarPlugin();
const debugPlugin = new DebugPlugin({enabled: false});
return [cursorPlugin, toolbarPlugin, debugPlugin];
}, []);

return (
<Provider theme={'light'}>
<GlobalCss />
<div style={{maxWidth: '690px', fontSize: '21px', lineHeight: '1.7em', margin: '32px auto'}}>
<PeritextView peritext={peritext} plugins={[cursorPlugin, renderers, debugRenderers({enabled: false})]} />
<PeritextView peritext={peritext} plugins={plugins} />
</div>
</Provider>
);
Expand Down
151 changes: 151 additions & 0 deletions src/json-crdt-peritext-ui/components/BasicButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// biome-ignore lint: React is used for JSX
import * as React from 'react';
import {rule, lightTheme as theme, useRule, useTheme} from 'nano-theme';
import {Link} from 'nice-ui/lib/1-inline/Link';
import {Ripple} from 'nice-ui/lib/misc/Ripple';

export const blockClass = rule({
...theme.font.ui1.mid,
fz: '14px',
cur: 'pointer',
us: 'none',
trs: 'background .12s ease-in 0s, opacity .12s ease-in 0s',
d: 'inline-flex',
ai: 'center',
jc: 'center',
fls: 0,
bdrad: '3px',
bg: 'transparent',
bd: 0,
mr: 0,
pd: 0,
out: 0,
ov: 'visible',
'&:hover': {
bxsh: 'none',
},
'&:active': {
bxsh: 'none',
},
});

const handleDragStart = (e: React.MouseEvent) => e.preventDefault();

export interface BasicButtonProps extends React.AllHTMLAttributes<any> {
to?: string;
border?: boolean;
fill?: boolean;
size?: number;
width?: 'auto' | number | string;
height?: number | string;
round?: boolean;
title?: string;
positive?: boolean;
noOutline?: boolean;
transparent?: boolean;
compact?: boolean;
spacious?: boolean;
radius?: number | string;
onClick?: React.MouseEventHandler;
}

export const BasicButton: React.FC<BasicButtonProps> = ({
to,
border,
fill,
size = 24,
width = size,
height = size,
round,
title,
positive,
noOutline,
transparent,
onClick,
children,
compact,
spacious,
radius,
className = '',
...rest
}) => {
const theme = useTheme();
const {isLight} = theme;
const bgFactor = isLight ? 1 : 1.1;

const dynamicBlockClass = useRule(({g}) => ({
// col: g(0.2),
// svg: {
// fill: g(0.5),
// col: g(0.5),
// },
'&:hover': {
// col: g(0.2),
bg: g(0, 0.08 * bgFactor),
},
'&:active': {
bg: g(0, 0.16 * bgFactor),
},
}));

const fillBlockClass = useRule(({g}) => ({
bg: transparent ? 'transparent' : g(0, 0.04 * bgFactor),
'&:hover': {
bg: g(0, 0.08 * bgFactor),
},
'&:active': {
bg: g(0, 0.16 * bgFactor),
},
}));

const borderClass = useRule(({g}) => ({
bd: `1px solid ${g(0, 0.16 * bgFactor)}`,
boxShadow: `0 0 2px ${g(0, 0.04 * bgFactor)}`,
}));

const style: React.CSSProperties = {
height,
};

style.width = width;
if (typeof width !== 'number') {
if (spacious) {
style.padding = '8px 16px';
} else {
style.paddingLeft = compact ? 8 : 16;
style.paddingRight = compact ? 8 : 16;
}
}

if (round) {
style.borderRadius = '50%';
}

if (noOutline) {
style.boxShadow = 'none';
}

if (radius !== undefined) {
style.borderRadius = radius;
}

return (
<Ripple ms={1_500} color={positive ? theme.color.sem.positive[0] : theme.g(0, 0.08)}>
<Link
{...rest}
a={!!to}
className={
className + blockClass + dynamicBlockClass + (border ? borderClass : '') + (fill ? fillBlockClass : '')
}
style={style}
title={title}
onClick={to ? undefined : onClick}
onDragStart={handleDragStart}
to={to}
data-testid="BasicButton"
>
{children}
</Link>
</Ripple>
);
};
22 changes: 22 additions & 0 deletions src/json-crdt-peritext-ui/plugins/cursor/CursorPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import {RenderCaret} from './RenderCaret';
import {RenderFocus} from './RenderFocus';
import {RenderAnchor} from './RenderAnchor';
import {RenderInline} from './RenderInline';
import {RenderPeritext} from './RenderPeritext';
import type {PeritextPlugin} from '../../react/types';

const h = React.createElement;

/**
* Plugin which renders the main cursor and all other current user local
* cursors.
*/
export class CursorPlugin implements PeritextPlugin {
public readonly caret: PeritextPlugin['caret'] = (props, children) => h(RenderCaret, <any>props, children);
public readonly focus: PeritextPlugin['focus'] = (props, children) => h(RenderFocus, <any>props, children);
public readonly anchor: PeritextPlugin['anchor'] = (props, children) => h(RenderAnchor, <any>props, children);
public readonly inline: PeritextPlugin['inline'] = (props, children) => h(RenderInline, props as any, children);
public readonly peritext: PeritextPlugin['peritext'] = (props, children, ctx) =>
h(RenderPeritext, {...props, children, ctx, plugin: this});
}
2 changes: 1 addition & 1 deletion src/json-crdt-peritext-ui/plugins/cursor/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 {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import type {AnchorViewProps} from '../../react/selection/AnchorView';
import type {AnchorViewProps} from '../../react/cursor/AnchorView';

export const fadeInAnimation = keyframes({
from: {
Expand Down
5 changes: 3 additions & 2 deletions src/json-crdt-peritext-ui/plugins/cursor/RenderCaret.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {DefaultRendererColors} from './constants';
import {CommonSliceType} from '../../../json-crdt-extensions';
import {useCursorPlugin} from './context';
import {CaretScore} from '../../components/CaretScore';
import type {CaretViewProps} from '../../react/selection/CaretView';
import type {CaretViewProps} from '../../react/cursor/CaretView';

const height = 1.5;
const ms = 350;

const moveAnimation = keyframes({
Expand All @@ -35,7 +36,7 @@ const innerClass = rule({
b: '-.18em',
l: '-.065em',
w: 'calc(max(.2em, 2px))',
h: '1.5em',
h: `${height}em`,
bg: DefaultRendererColors.ActiveCursor,
bdl: `1px dotted ${DefaultRendererColors.InactiveCursor}`,
bdrad: '0.0625em',
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-peritext-ui/plugins/cursor/RenderFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {rule, drule, keyframes} from 'nano-theme';
import {DefaultRendererColors} from './constants';
import {usePeritext} from '../../react';
import {useSyncStore} from '../../react/hooks';
import type {FocusViewProps} from '../../react/selection/FocusView';
import type {FocusViewProps} from '../../react/cursor/FocusView';

const width = 0.14;
const animationTime = '1s';
Expand Down
6 changes: 5 additions & 1 deletion src/json-crdt-peritext-ui/plugins/cursor/RenderPeritext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ 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 {CursorPlugin} from './CursorPlugin';

export interface RenderPeritextProps extends PeritextViewProps {
ctx?: PeritextSurfaceContextValue;
plugin: CursorPlugin;
children?: React.ReactNode;
}

export const RenderPeritext: React.FC<RenderPeritextProps> = ({ctx, children}) => {
export const RenderPeritext: React.FC<RenderPeritextProps> = ({ctx, plugin, children}) => {
// biome-ignore lint: explicit dependency handling
const value: CursorPluginContextValue = React.useMemo(
() => ({
ctx,
plugin,
score: new ValueSyncStore(0),
scoreDelta: new ValueSyncStore(0),
lastVisScore: new ValueSyncStore(0),
Expand Down
3 changes: 3 additions & 0 deletions src/json-crdt-peritext-ui/plugins/cursor/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from 'react';
import type {PeritextSurfaceContextValue} from '../../react';
import type {ValueSyncStore} from '../../../util/events/sync-store';
import type {CursorPlugin} from './CursorPlugin';

export interface CursorPluginContextValue {
ctx?: PeritextSurfaceContextValue;

plugin: CursorPlugin;

/** Current score. */
score: ValueSyncStore<number>;

Expand Down
22 changes: 1 addition & 21 deletions src/json-crdt-peritext-ui/plugins/cursor/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1 @@
import * as React from 'react';
import {RenderCaret} from './RenderCaret';
import {RenderFocus} from './RenderFocus';
import {RenderAnchor} from './RenderAnchor';
import {RenderInline} from './RenderInline';
import {RenderPeritext} from './RenderPeritext';
import type {PeritextPlugin} from '../../react/types';

const h = React.createElement;

/**
* Plugin which renders the main cursor and all other current user local
* cursors.
*/
export const cursorPlugin: PeritextPlugin = {
caret: (props, children) => h(RenderCaret, <any>props, children),
focus: (props, children) => h(RenderFocus, <any>props, children),
anchor: (props, children) => h(RenderAnchor, <any>props, children),
inline: (props, children) => h(RenderInline, props as any, children),
peritext: (props, children, ctx) => h(RenderPeritext, {...props, children, ctx}),
};
export {CursorPlugin} from './CursorPlugin';
Loading

0 comments on commit 64dab2a

Please sign in to comment.