@dxos/react-ui-editor 0.6.4 → 0.6.5-staging.097cf0c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,9 @@ import React, { type PropsWithChildren, useEffect, useRef, useState } from 'reac
32
32
  import { useDropzone } from 'react-dropzone';
33
33
 
34
34
  import {
35
+ Button,
35
36
  DensityProvider,
37
+ DropdownMenu,
36
38
  ElevationProvider,
37
39
  Toolbar as NaturalToolbar,
38
40
  Tooltip,
@@ -40,25 +42,21 @@ import {
40
42
  type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
41
43
  type ToolbarButtonProps as NaturalToolbarButtonProps,
42
44
  useTranslation,
43
- DropdownMenu,
44
- Button,
45
45
  } from '@dxos/react-ui';
46
46
  import { getSize } from '@dxos/react-ui-theme';
47
47
 
48
48
  import { type Action, type ActionType, type Formatting } from '../../extensions';
49
49
  import { translationKey } from '../../translations';
50
50
 
51
+ const iconStyles = getSize(5);
52
+ const buttonStyles = 'min-bs-0 p-2';
51
53
  const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
52
54
 
53
- const HeadingIcons: { [key: string]: Icon } = {
54
- '0': Paragraph,
55
- '1': TextHOne,
56
- '2': TextHTwo,
57
- '3': TextHThree,
58
- '4': TextHFour,
59
- '5': TextHFive,
60
- '6': TextHSix,
61
- };
55
+ const ToolbarSeparator = () => <div role='separator' className='grow' />;
56
+
57
+ //
58
+ // Root
59
+ //
62
60
 
63
61
  export type ToolbarProps = ThemedClassName<
64
62
  PropsWithChildren<{
@@ -83,8 +81,9 @@ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) =>
83
81
  );
84
82
  };
85
83
 
86
- const buttonStyles = 'min-bs-0 p-2';
87
- const iconStyles = getSize(5);
84
+ //
85
+ // Button
86
+ //
88
87
 
89
88
  type ButtonProps = {
90
89
  type: ActionType;
@@ -135,7 +134,19 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
135
134
  );
136
135
  };
137
136
 
138
- const ToolbarSeparator = () => <div role='separator' className='grow' />;
137
+ //
138
+ // Heading
139
+ //
140
+
141
+ const HeadingIcons: { [key: string]: Icon } = {
142
+ '0': Paragraph,
143
+ '1': TextHOne,
144
+ '2': TextHTwo,
145
+ '3': TextHThree,
146
+ '4': TextHFour,
147
+ '5': TextHFive,
148
+ '6': TextHSix,
149
+ };
139
150
 
140
151
  const MarkdownHeading = () => {
141
152
  const { t } = useTranslation(translationKey);
@@ -215,6 +226,10 @@ const MarkdownHeading = () => {
215
226
  );
216
227
  };
217
228
 
229
+ //
230
+ // Markdown
231
+ //
232
+
218
233
  const markdownStyles: ButtonProps[] = [
219
234
  { type: 'strong', Icon: TextB, getState: (state) => state.strong },
220
235
  { type: 'emphasis', Icon: TextItalic, getState: (state) => state.emphasis },
@@ -321,6 +336,10 @@ const MarkdownStandard = () => (
321
336
  </>
322
337
  );
323
338
 
339
+ //
340
+ // Custom
341
+ //
342
+
324
343
  // TODO(burdon): Make extensible.
325
344
  export type MarkdownCustomOptions = {
326
345
  onUpload?: (file: File) => Promise<{ url?: string } | undefined>;
@@ -366,9 +385,13 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
366
385
  );
367
386
  };
368
387
 
388
+ //
389
+ // Actions
390
+ //
391
+
369
392
  // TODO(burdon): Make extensible.
370
393
  const MarkdownActions = () => {
371
- const { onAction, state } = useToolbarContext('MarkdownStyles');
394
+ const { onAction, state } = useToolbarContext('MarkdownActions');
372
395
  const { t } = useTranslation(translationKey);
373
396
  return (
374
397
  <>
@@ -389,6 +412,10 @@ const MarkdownActions = () => {
389
412
  );
390
413
  };
391
414
 
415
+ //
416
+ // Toolbar
417
+ //
418
+
392
419
  export const Toolbar = {
393
420
  Root: ToolbarRoot,
394
421
  Button: ToolbarToggleButton,
@@ -5,7 +5,7 @@
5
5
  import '@dxosTheme';
6
6
 
7
7
  import '@preact/signals-react';
8
- import React, { useEffect, useMemo, useState } from 'react';
8
+ import React, { useEffect, useState } from 'react';
9
9
 
10
10
  import { TextType } from '@braneframe/types';
11
11
  import { Repo } from '@dxos/automerge/automerge-repo';
@@ -40,7 +40,7 @@ const Editor = ({ source, autoFocus }: EditorProps) => {
40
40
  doc: DocAccessor.getValue(source),
41
41
  extensions: [
42
42
  createBasicExtensions({ placeholder: 'Type here...' }),
43
- createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
43
+ createThemeExtensions({ themeMode, slots: { editor: { className: 'w-full p-2 bg-white dark:bg-black' } } }),
44
44
  automerge(source),
45
45
  ],
46
46
  autoFocus,
@@ -48,7 +48,7 @@ const Editor = ({ source, autoFocus }: EditorProps) => {
48
48
  [source, themeMode],
49
49
  );
50
50
 
51
- return <div ref={parentRef} />;
51
+ return <div ref={parentRef} className='flex w-full' />;
52
52
  };
53
53
 
54
54
  const Story = () => {
@@ -94,14 +94,18 @@ export default {
94
94
  };
95
95
 
96
96
  const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
97
- // TODO(burdon): Test identity.
98
- // const identity = useIdentity();
99
97
  const space = useSpace(spaceKey);
100
- const source = useMemo<DocAccessor | undefined>(() => {
101
- const { objects = [] } = space?.db.query<Expando>(Filter.from({ type: 'test' })) ?? {};
102
- if (objects.length) {
103
- return createDocAccessor(objects[0].content, ['content']);
104
- }
98
+ const [source, setSource] = useState<DocAccessor>();
99
+ useEffect(() => {
100
+ setTimeout(async () => {
101
+ if (space) {
102
+ const { objects = [] } = await space.db.query<Expando>(Filter.from({ type: 'test' })).run();
103
+ if (objects.length) {
104
+ const source = createDocAccessor(objects[0].content, ['content']);
105
+ setSource(source);
106
+ }
107
+ }
108
+ });
105
109
  }, [space]);
106
110
 
107
111
  if (!source) {
@@ -113,8 +117,6 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
113
117
 
114
118
  export const Default = {};
115
119
 
116
- // TODO(burdon): Error:
117
- // chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
118
120
  export const WithEcho = {
119
121
  decorators: [withTheme],
120
122
  render: () => {
@@ -37,6 +37,8 @@ import { defaultTheme } from '../themes';
37
37
  // Basic
38
38
  //
39
39
 
40
+ export const preventNewline = EditorState.transactionFilter.of((tr) => (tr.newDoc.lines > 1 ? [] : tr));
41
+
40
42
  /**
41
43
  * https://codemirror.net/docs/extensions
42
44
  * https://github.com/codemirror/basic-setup
@@ -159,15 +161,15 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
159
161
  // Data
160
162
  //
161
163
 
162
- export type DataExtensionsProps = {
164
+ export type DataExtensionsProps<T> = {
163
165
  id: string;
164
- text?: DocAccessor;
166
+ text?: DocAccessor<T>;
165
167
  space?: Space;
166
168
  identity?: Identity | null;
167
169
  };
168
170
 
169
171
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
170
- export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
172
+ export const createDataExtensions = <T>({ id, text, space, identity }: DataExtensionsProps<T>): Extension[] => {
171
173
  const extensions: Extension[] = text ? [automerge(text)] : [];
172
174
 
173
175
  if (space && identity) {
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { expect } from 'chai';
6
+
7
+ import { describe, test } from '@dxos/test';
8
+
9
+ import { formatUrlForDisplay } from './linkPaste';
10
+
11
+ const testCases = [
12
+ { input: 'https://www.example.com', expected: 'example.com' },
13
+ { input: 'http://example.com', expected: 'example.com' },
14
+ { input: 'https://example.com/', expected: 'example.com' },
15
+ { input: 'https://www.example.com/', expected: 'example.com' },
16
+ { input: 'https://example.com/test', expected: 'example.com/test' },
17
+ { input: 'https://www.example.com/test', expected: 'example.com/test' },
18
+ { input: 'http://example.com/test', expected: 'example.com/test' },
19
+ {
20
+ input: 'https://example.com/some/path?name=value&another_name=another_value',
21
+ expected: 'example.com/some/path?name=value&anot...',
22
+ },
23
+ {
24
+ input: 'https://www.example.com/some/path?name=value&another_name=another_value',
25
+ expected: 'example.com/some/path?name=value&anot...',
26
+ },
27
+ {
28
+ input: 'http://example.com/some/path?name=value&another_name=another_value',
29
+ expected: 'example.com/some/path?name=value&anot...',
30
+ },
31
+ {
32
+ input: 'https://www.example.com?name=value&another_name=another_value',
33
+ expected: 'example.com?name=value&anot...',
34
+ },
35
+ { input: 'http://example.com?name=value&another_name=another_value', expected: 'example.com?name=value&anot...' },
36
+ { input: 'https://example.com?name=value', expected: 'example.com?name=value' },
37
+ { input: 'http://example.com?name=value', expected: 'example.com?name=value' },
38
+ { input: 'https://www.example.com?name=value', expected: 'example.com?name=value' },
39
+ { input: 'ftp://example.com', expected: 'ftp://example.com' },
40
+ ];
41
+
42
+ describe('formatUrlForDisplay', () =>
43
+ testCases.forEach(({ input, expected }) =>
44
+ test(`formats ${input} into ${expected}`, () => expect(formatUrlForDisplay(input)).equals(expected)),
45
+ ));
@@ -15,9 +15,18 @@ const createUrlLink = (url: string): string => {
15
15
  return `[${displayUrl}](${url})`;
16
16
  };
17
17
 
18
- const formatUrlForDisplay = (url: string): string => {
19
- const withoutProtocol = url.replace(/^https?:\/\//, '');
20
- return truncateQueryParams(withoutProtocol);
18
+ export const formatUrlForDisplay = (url: string): string => {
19
+ // Remove protocol (http:// or https://)
20
+ let formattedUrl = url.replace(/^https?:\/\//, '');
21
+
22
+ // NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
23
+ // Remove 'www.' if at the beginning of the URL
24
+ formattedUrl = formattedUrl.replace(/^www\./, '');
25
+
26
+ // Remove trailing slash if the URL ends with `.com/`
27
+ formattedUrl = formattedUrl.replace(/\.com\/$/, '.com');
28
+
29
+ return truncateQueryParams(formattedUrl);
21
30
  };
22
31
 
23
32
  const truncateQueryParams = (url: string, maxQueryLength: number = 15): string => {