Skip to content

Commit ade1e3f

Browse files
committed
feat(json-crdt-peritext-ui): 🎸 improve link behavior registration
1 parent bba3703 commit ade1e3f

File tree

9 files changed

+72
-46
lines changed

9 files changed

+72
-46
lines changed

src/json-crdt-peritext-ui/__demos__/components/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {DebugState} from '../../plugins/debug/state';
1111

1212
const markdown =
1313
'The German __automotive sector__ is in the process of *cutting ' +
14-
'thousands of jobs* as it grapples with a global shift toward electric vehicles ' +
14+
'thousands of jobs* as, [Google Docs](https://developers.google.com/workspace/docs), it grapples with a global shift toward electric vehicles ' +
1515
'— a transformation Musk himself has been at the forefront of.' +
1616
'\n\n' +
1717
'> To be, or not to be: that is the question.' +

src/json-crdt-peritext-ui/components/ContextPaneHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const ContextPaneHeader: React.FC<ContextPaneHeaderProps> = ({short, righ
4141
}
4242

4343
return (
44-
<ContextHeader style={{padding: short ? '12px 16px' : '16px'}}>
44+
<ContextHeader style={{padding: short ? '12px 16px' : '16px', borderRadius: '8px 8px 0 0'}}>
4545
{element}
4646
</ContextHeader>
4747
);
Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,27 @@
11
import * as React from 'react';
2-
import {Button} from 'nice-ui/lib/2-inline-block/Button';
32
import {ContextTitle} from 'nice-ui/lib/4-card/ContextMenu/ContextTitle';
43
import {EmptyState} from 'nice-ui/lib/4-card/EmptyState';
5-
import {ContextPaneHeader} from '../../../../../components/ContextPaneHeader';
6-
import {useToolbarPlugin} from '../../../context';
74
import {CollaborativeInput} from '../../../../../components/CollaborativeInput';
85
import {Input} from '../../../../../components/Input';
96
import {useSyncStoreOpt} from '../../../../../web/react/hooks';
107
import {ContextSep} from 'nice-ui/lib/4-card/ContextMenu';
118
import {BasicButtonClose} from 'nice-ui/lib/2-inline-block/BasicButton/BasicButtonClose';
129
import {UrlDisplayCard} from '../../../cards/UrlDisplayCard';
13-
import {rule} from 'nano-theme';
14-
import {parseUrl} from '../../../../../web/util';
15-
import {ContextPaneHeaderSep} from '../../../../../components/ContextPaneHeaderSep';
16-
import {FormattingTitle} from '../../FormattingTitle';
1710
import {NewProps} from '../../../types';
1811
import type {CollaborativeStr} from 'collaborative-editor';
1912

20-
const blockClass = rule({
21-
maxW: '600px',
22-
});
23-
2413
export const New: React.FC<NewProps> = ({formatting}) => {
25-
const {toolbar} = useToolbarPlugin();
2614
const inpRef = React.useRef<HTMLInputElement | null>(null);
2715
const href = React.useMemo(() => () => formatting.conf()?.str(['href']), [formatting]);
2816
const hrefView = useSyncStoreOpt(href()?.events) || '';
29-
const parsed = React.useMemo(() => parseUrl(hrefView), [hrefView]);
3017

3118
if (!href()) return null;
3219

3320
const str = href as (() => CollaborativeStr);
3421

3522
return (
36-
<form className={blockClass} onSubmit={(e) => {
37-
e.preventDefault();
38-
formatting.save();
39-
}}>
40-
<ContextPaneHeader short onCloseClick={() => toolbar.newSlice.next(void 0)}>
41-
<FormattingTitle formatting={formatting} />
42-
</ContextPaneHeader>
43-
<ContextPaneHeaderSep />
44-
45-
<div style={{padding: '16px'}}>
23+
<div style={{margin: -16}}>
24+
<div style={{padding: 16}}>
4625
<CollaborativeInput str={str} input={(ref) => (
4726
<Input focus
4827
inp={(el) => {
@@ -80,16 +59,10 @@ export const New: React.FC<NewProps> = ({formatting}) => {
8059
<UrlDisplayCard url={hrefView} />
8160
</div>
8261
) : (
83-
<div style={{margin: '-22px 0 -8px'}}>
62+
<div style={{margin: '-32px 0 -26px'}}>
8463
<EmptyState emoji=' ' title=' ' />
8564
</div>
8665
)}
87-
88-
<ContextSep line />
89-
90-
<div style={{padding: '16px'}}>
91-
<Button small lite={!hrefView} positive={!!parsed} block disabled={!hrefView} submit>Save</Button>
92-
</div>
93-
</form>
66+
</div>
9467
);
9568
};

src/json-crdt-peritext-ui/plugins/toolbar/formatting/tags/a/behavior.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ import {renderIcon} from './renderIcon';
55
import {New} from './New';
66
import {View} from './View';
77
import {ToolbarSliceBehaviorData} from '../../../types';
8+
import {getDomain} from '../../../../../web/util';
89

910
export const behavior = {
11+
validate: (formatting) => {
12+
const obj = formatting.conf()?.view() as {href: string};
13+
if (!obj || typeof obj !== 'object') return [{code: 'INVALID_CONFIG'}];
14+
const href = obj.href || '';
15+
if (typeof href !== 'string') return [{code: 'INVALID_URL'}];
16+
if (href.length < 4) return 'empty';
17+
const domain = getDomain(href);
18+
return domain ? 'good' : 'fine';
19+
},
1020
menu: {
1121
name: 'Link',
1222
icon: () => <Iconista width={15} height={15} set="lucide" icon="link" />,

src/json-crdt-peritext-ui/plugins/toolbar/formatting/views/new/FormattingNewCard.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,29 @@ import * as React from 'react';
22
import {ContextPane} from 'nice-ui/lib/4-card/ContextMenu/ContextPane';
33
import {useToolbarPlugin} from '../../../context';
44
import {FormattingNew} from './FormattingNew';
5+
import {ContextPaneHeader} from '../../../../../components/ContextPaneHeader';
6+
import {FormattingTitle} from '../../FormattingTitle';
7+
import {ContextPaneHeaderSep} from '../../../../../components/ContextPaneHeaderSep';
8+
import {ContextSep} from 'nice-ui/lib/4-card/ContextMenu';
9+
import {Button} from 'nice-ui/lib/2-inline-block/Button';
10+
import {rule} from 'nano-theme';
11+
import {useSyncStoreOpt} from '../../../../../web/react/hooks';
512
import type {NewFormatting} from '../../../state/formattings';
613

14+
const blockClass = rule({
15+
maxW: '600px',
16+
});
17+
718
export interface FormattingNewCardProps {
819
formatting: NewFormatting;
920
}
1021

1122
export const FormattingNewCard: React.FC<FormattingNewCardProps> = ({formatting}) => {
1223
const {toolbar} = useToolbarPlugin();
24+
useSyncStoreOpt(formatting.conf()?.api);
25+
const validation = formatting.validate();
26+
27+
const valid = validation === 'good' || validation === 'fine';
1328

1429
return (
1530
<div onKeyDown={(e) => {
@@ -20,7 +35,25 @@ export const FormattingNewCard: React.FC<FormattingNewCardProps> = ({formatting}
2035
}
2136
}}>
2237
<ContextPane style={{display: 'block', minWidth: 'calc(min(600px, max(50vw, 260px)))'}}>
23-
<FormattingNew formatting={formatting} />
38+
<form className={blockClass} onSubmit={(e) => {
39+
e.preventDefault();
40+
formatting.save();
41+
}}>
42+
<ContextPaneHeader short onCloseClick={() => toolbar.newSlice.next(void 0)}>
43+
<FormattingTitle formatting={formatting} />
44+
</ContextPaneHeader>
45+
<ContextPaneHeaderSep />
46+
47+
<div style={{padding: '16px'}}>
48+
<FormattingNew formatting={formatting} />
49+
</div>
50+
51+
<ContextSep line />
52+
53+
<div style={{padding: '16px'}}>
54+
<Button small lite={!valid} positive={validation === 'good'} block disabled={!valid} submit>Save</Button>
55+
</div>
56+
</form>
2457
</ContextPane>
2558
</div>
2659
);

src/json-crdt-peritext-ui/plugins/toolbar/inline/Link.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import * as React from 'react';
33
import {rule} from 'nano-theme';
44

55
const blockClass = rule({
6-
col: 'blue',
6+
col: '#05f',
77
td: 'underline',
8-
textDecorationColor: 'blue',
8+
textDecorationColor: '#05f',
99
textDecorationThickness: '1px',
1010
textDecorationStyle: 'wavy',
1111
textUnderlineOffset: '.25em',
1212
// textDecorationSkipInk: 'all',
1313

1414
'&:hover': {
15-
col: 'blue',
15+
col: '#05f',
1616
},
1717
pd: 0,
1818
mr: 0,
@@ -29,11 +29,14 @@ const blockClass = rule({
2929

3030
export interface LinkProps {
3131
children: React.ReactNode;
32+
layers?: number;
3233
}
3334

3435
export const Link: React.FC<LinkProps> = (props) => {
35-
const {children} = props;
36+
const {children, layers = 1} = props;
37+
const style: React.CSSProperties | undefined = layers < 2 ? void 0 : {
38+
textDecorationThickness: Math.max(Math.min(.5 + layers * .5, 3), 1) + 'px',
39+
};
3640

37-
// return <span className={blockClass}>{children}</span>;
38-
return <span className={blockClass}>{children}</span>;
41+
return <span className={blockClass} style={style}>{children}</span>;
3942
};

src/json-crdt-peritext-ui/plugins/toolbar/inline/RenderInline.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const RenderInline: React.FC<RenderInlineProps> = (props) => {
1717
const {inline, children} = props;
1818
const attrs = inline.attr();
1919
let element = children;
20-
if (attrs[SliceTypeCon.a]) element = <Link>{element}</Link>;
20+
const a = attrs[SliceTypeCon.a];
21+
if (a) element = <Link layers={a.length}>{element}</Link>;
2122
if (attrs[SliceTypeCon.mark]) element = <mark>{element}</mark>;
2223
if (attrs[SliceTypeCon.sup]) element = <sup>{element}</sup>;
2324
if (attrs[SliceTypeCon.sub]) element = <sub>{element}</sub>;

src/json-crdt-peritext-ui/plugins/toolbar/state/formattings.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {s} from '../../../../json-crdt-patch';
22
import {Model, ObjApi} from '../../../../json-crdt/model';
33
import type {Slice} from "../../../../json-crdt-extensions";
44
import type {Range} from "../../../../json-crdt-extensions/peritext/rga/Range";
5-
import type {ToolbarSliceBehavior} from "../types";
5+
import type {ToolbarSliceBehavior, ValidationResult} from "../types";
66
import type {SliceBehavior} from '../../../../json-crdt-extensions/peritext/registry/SliceBehavior';
77
import type {ObjNode} from '../../../../json-crdt/nodes';
88
import type {ToolbarState} from '.';
@@ -28,6 +28,10 @@ export class RangeFormatting<R extends Range<string> = Range<string>, Node exten
2828
public conf(): ObjApi<Node> | undefined {
2929
return;
3030
}
31+
32+
public validate(): ValidationResult {
33+
return this.behavior.data()?.validate?.(this) ?? 'fine';
34+
}
3135
}
3236

3337
/**

src/json-crdt-peritext-ui/plugins/toolbar/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface ToolbarSliceBehaviorData extends Record<string, unknown> {
1717
* or 'fine'. If the formatting is invalid, return an array of validation
1818
* errors.
1919
*/
20-
validate?: (formatting: ToolbarFormatting) => ValidationResult;
20+
validate?: (formatting: ToolbarFormatting<any, any>) => ValidationResult;
2121

2222
/**
2323
* Returns a short description of the formatting, for the user to easily
@@ -82,12 +82,14 @@ export interface EditProps {
8282
/**
8383
* Represents the result of a validation. The `good` and `fine` values
8484
* represent a successful validation, while the `ValidationErrorResult[]` is
85-
* a list of errors that occurred during validation.
85+
* a list of errors that occurred during validation. The `empty` value means
86+
* that not enough data was provided to validate the formatting.
8687
*/
87-
export type ValidationResult = 'good' | 'fine' | ValidationErrorResult[];
88+
export type ValidationResult = 'good' | 'fine' | 'empty' | ValidationErrorResult[];
8889

8990
export interface ValidationErrorResult {
90-
message: string;
91+
code: string;
92+
message?: string;
9193
field?: string;
9294
}
9395

0 commit comments

Comments
 (0)