+1
-4
actions/publishToPublication.ts
+1
-4
actions/publishToPublication.ts
···
32
32
import { scanIndexLocal } from "src/replicache/utils";
33
33
import type { Fact } from "src/replicache";
34
34
import type { Attribute } from "src/replicache/attributes";
35
-
import {
36
-
Delta,
37
-
YJSFragmentToString,
38
-
} from "components/Blocks/TextBlock/RenderYJSFragment";
35
+
import { Delta, YJSFragmentToString } from "src/utils/yjsFragmentToString";
39
36
import { ids } from "lexicons/api/lexicons";
40
37
import { BlobRef } from "@atproto/lexicon";
41
38
import { AtUri } from "@atproto/syntax";
+1
-1
actions/subscriptions/subscribeToMailboxWithEmail.ts
+1
-1
actions/subscriptions/subscribeToMailboxWithEmail.ts
···
11
11
import type { Attribute } from "src/replicache/attributes";
12
12
import { Database } from "supabase/database.types";
13
13
import * as Y from "yjs";
14
-
import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
14
+
import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
15
15
import { pool } from "supabase/pool";
16
16
17
17
let supabase = createServerClient<Database>(
+1
-1
app/[leaflet_id]/actions/PublishButton.tsx
+1
-1
app/[leaflet_id]/actions/PublishButton.tsx
···
35
35
} from "src/hooks/queries/useBlocks";
36
36
import * as Y from "yjs";
37
37
import * as base64 from "base64-js";
38
-
import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
38
+
import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
39
39
import { BlueskyLogin } from "app/login/LoginForm";
40
40
import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication";
41
41
import { AddTiny } from "components/Icons/AddTiny";
+1
-1
app/[leaflet_id]/page.tsx
+1
-1
app/[leaflet_id]/page.tsx
···
4
4
5
5
import type { Fact } from "src/replicache";
6
6
import type { Attribute } from "src/replicache/attributes";
7
-
import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
7
+
import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
8
8
import { Leaflet } from "./Leaflet";
9
9
import { scanIndexLocal } from "src/replicache/utils";
10
10
import { getRSVPData } from "actions/getRSVPData";
+1
-1
app/api/rpc/[command]/getFactsFromHomeLeaflets.ts
+1
-1
app/api/rpc/[command]/getFactsFromHomeLeaflets.ts
···
5
5
import type { Env } from "./route";
6
6
import { scanIndexLocal } from "src/replicache/utils";
7
7
import * as base64 from "base64-js";
8
-
import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
8
+
import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
9
9
import { applyUpdate, Doc } from "yjs";
10
10
11
11
export const getFactsFromHomeLeaflets = makeRoute({
+1
-1
components/ActionBar/ActionButton.tsx
+1
-1
components/ActionBar/ActionButton.tsx
···
3
3
import { useContext, useEffect } from "react";
4
4
import { SidebarContext } from "./Sidebar";
5
5
import React, { forwardRef, type JSX } from "react";
6
-
import { PopoverOpenContext } from "components/Popover";
6
+
import { PopoverOpenContext } from "components/Popover/PopoverContext";
7
7
8
8
type ButtonProps = Omit<JSX.IntrinsicElements["button"], "content">;
9
9
+3
-3
components/Blocks/BlockCommands.tsx
+3
-3
components/Blocks/BlockCommands.tsx
···
2
2
import { useUIState } from "src/useUIState";
3
3
4
4
import { generateKeyBetween } from "fractional-indexing";
5
-
import { focusPage } from "components/Pages";
5
+
import { focusPage } from "src/utils/focusPage";
6
6
import { v7 } from "uuid";
7
7
import { Replicache } from "replicache";
8
8
import { useEditorStates } from "src/state/useEditorState";
9
9
import { elementId } from "src/utils/elementId";
10
10
import { UndoManager } from "src/undoManager";
11
11
import { focusBlock } from "src/utils/focusBlock";
12
-
import { usePollBlockUIState } from "./PollBlock";
13
-
import { focusElement } from "components/Input";
12
+
import { usePollBlockUIState } from "./PollBlock/pollBlockState";
13
+
import { focusElement } from "src/utils/focusElement";
14
14
import { BlockBlueskySmall } from "components/Icons/BlockBlueskySmall";
15
15
import { BlockButtonSmall } from "components/Icons/BlockButtonSmall";
16
16
import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall";
+2
-120
components/Blocks/DeleteBlock.tsx
+2
-120
components/Blocks/DeleteBlock.tsx
···
1
-
import {
2
-
Fact,
3
-
ReplicacheMutators,
4
-
useEntity,
5
-
useReplicache,
6
-
} from "src/replicache";
7
-
import { Replicache } from "replicache";
8
-
import { useUIState } from "src/useUIState";
9
-
import { scanIndex } from "src/replicache/utils";
10
-
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
11
-
import { focusBlock } from "src/utils/focusBlock";
1
+
import { Fact, useReplicache } from "src/replicache";
12
2
import { ButtonPrimary } from "components/Buttons";
13
3
import { CloseTiny } from "components/Icons/CloseTiny";
4
+
import { deleteBlock } from "src/utils/deleteBlock";
14
5
15
6
export const AreYouSure = (props: {
16
7
entityID: string[] | string;
···
82
73
);
83
74
};
84
75
85
-
export async function deleteBlock(
86
-
entities: string[],
87
-
rep: Replicache<ReplicacheMutators>,
88
-
) {
89
-
// get what pagess we need to close as a result of deleting this block
90
-
let pagesToClose = [] as string[];
91
-
for (let entity of entities) {
92
-
let [type] = await rep.query((tx) =>
93
-
scanIndex(tx).eav(entity, "block/type"),
94
-
);
95
-
if (type.data.value === "card") {
96
-
let [childPages] = await rep?.query(
97
-
(tx) => scanIndex(tx).eav(entity, "block/card") || [],
98
-
);
99
-
pagesToClose = [childPages?.data.value];
100
-
}
101
-
if (type.data.value === "mailbox") {
102
-
let [archive] = await rep?.query(
103
-
(tx) => scanIndex(tx).eav(entity, "mailbox/archive") || [],
104
-
);
105
-
let [draft] = await rep?.query(
106
-
(tx) => scanIndex(tx).eav(entity, "mailbox/draft") || [],
107
-
);
108
-
pagesToClose = [archive?.data.value, draft?.data.value];
109
-
}
110
-
}
111
-
112
-
// the next and previous blocks in the block list
113
-
// if the focused thing is a page and not a block, return
114
-
let focusedBlock = useUIState.getState().focusedEntity;
115
-
let parent =
116
-
focusedBlock?.entityType === "page"
117
-
? focusedBlock.entityID
118
-
: focusedBlock?.parent;
119
-
120
-
if (parent) {
121
-
let parentType = await rep?.query((tx) =>
122
-
scanIndex(tx).eav(parent, "page/type"),
123
-
);
124
-
if (parentType[0]?.data.value === "canvas") {
125
-
useUIState
126
-
.getState()
127
-
.setFocusedBlock({ entityType: "page", entityID: parent });
128
-
useUIState.getState().setSelectedBlocks([]);
129
-
} else {
130
-
let siblings =
131
-
(await rep?.query((tx) => getBlocksWithType(tx, parent))) || [];
132
-
133
-
let selectedBlocks = useUIState.getState().selectedBlocks;
134
-
let firstSelected = selectedBlocks[0];
135
-
let lastSelected = selectedBlocks[entities.length - 1];
136
-
137
-
let prevBlock =
138
-
siblings?.[
139
-
siblings.findIndex((s) => s.value === firstSelected?.value) - 1
140
-
];
141
-
let prevBlockType = await rep?.query((tx) =>
142
-
scanIndex(tx).eav(prevBlock?.value, "block/type"),
143
-
);
144
-
145
-
let nextBlock =
146
-
siblings?.[
147
-
siblings.findIndex((s) => s.value === lastSelected.value) + 1
148
-
];
149
-
let nextBlockType = await rep?.query((tx) =>
150
-
scanIndex(tx).eav(nextBlock?.value, "block/type"),
151
-
);
152
-
153
-
if (prevBlock) {
154
-
useUIState.getState().setSelectedBlock({
155
-
value: prevBlock.value,
156
-
parent: prevBlock.parent,
157
-
});
158
-
159
-
focusBlock(
160
-
{
161
-
value: prevBlock.value,
162
-
type: prevBlockType?.[0].data.value,
163
-
parent: prevBlock.parent,
164
-
},
165
-
{ type: "end" },
166
-
);
167
-
} else {
168
-
useUIState.getState().setSelectedBlock({
169
-
value: nextBlock.value,
170
-
parent: nextBlock.parent,
171
-
});
172
-
173
-
focusBlock(
174
-
{
175
-
value: nextBlock.value,
176
-
type: nextBlockType?.[0]?.data.value,
177
-
parent: nextBlock.parent,
178
-
},
179
-
{ type: "start" },
180
-
);
181
-
}
182
-
}
183
-
}
184
-
185
-
pagesToClose.forEach((page) => page && useUIState.getState().closePage(page));
186
-
await Promise.all(
187
-
entities.map((entity) =>
188
-
rep?.mutate.removeBlock({
189
-
blockEntity: entity,
190
-
}),
191
-
),
192
-
);
193
-
}
+2
-1
components/Blocks/ExternalLinkBlock.tsx
+2
-1
components/Blocks/ExternalLinkBlock.tsx
···
8
8
import { v7 } from "uuid";
9
9
import { useSmoker } from "components/Toast";
10
10
import { Separator } from "components/Layout";
11
-
import { focusElement, Input } from "components/Input";
11
+
import { Input } from "components/Input";
12
+
import { focusElement } from "src/utils/focusElement";
12
13
import { isUrl } from "src/utils/isURL";
13
14
import { elementId } from "src/utils/elementId";
14
15
import { focusBlock } from "src/utils/focusBlock";
+1
-1
components/Blocks/MailboxBlock.tsx
+1
-1
components/Blocks/MailboxBlock.tsx
···
9
9
import { useEntitySetContext } from "components/EntitySetProvider";
10
10
import { subscribeToMailboxWithEmail } from "actions/subscriptions/subscribeToMailboxWithEmail";
11
11
import { confirmEmailSubscription } from "actions/subscriptions/confirmEmailSubscription";
12
-
import { focusPage } from "components/Pages";
12
+
import { focusPage } from "src/utils/focusPage";
13
13
import { v7 } from "uuid";
14
14
import { sendPostToSubscribers } from "actions/subscriptions/sendPostToSubscribers";
15
15
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
+1
-1
components/Blocks/PageLinkBlock.tsx
+1
-1
components/Blocks/PageLinkBlock.tsx
···
2
2
import { BlockProps, BaseBlock, ListMarker, Block } from "./Block";
3
3
import { focusBlock } from "src/utils/focusBlock";
4
4
5
-
import { focusPage } from "components/Pages";
5
+
import { focusPage } from "src/utils/focusPage";
6
6
import { useEntity, useReplicache } from "src/replicache";
7
7
import { useUIState } from "src/useUIState";
8
8
import { RenderedTextBlock } from "components/Blocks/TextBlock";
+5
-11
components/Blocks/PollBlock.tsx
components/Blocks/PollBlock/index.tsx
+5
-11
components/Blocks/PollBlock.tsx
components/Blocks/PollBlock/index.tsx
···
1
1
import { useUIState } from "src/useUIState";
2
-
import { BlockProps } from "./Block";
2
+
import { BlockProps } from "../Block";
3
3
import { ButtonPrimary, ButtonSecondary } from "components/Buttons";
4
4
import { useCallback, useEffect, useState } from "react";
5
-
import { focusElement, Input } from "components/Input";
5
+
import { Input } from "components/Input";
6
+
import { focusElement } from "src/utils/focusElement";
6
7
import { Separator } from "components/Layout";
7
8
import { useEntitySetContext } from "components/EntitySetProvider";
8
9
import { theme } from "tailwind.config";
···
13
14
usePollData,
14
15
} from "components/PageSWRDataProvider";
15
16
import { voteOnPoll } from "actions/pollActions";
16
-
import { create } from "zustand";
17
17
import { elementId } from "src/utils/elementId";
18
18
import { CheckTiny } from "components/Icons/CheckTiny";
19
19
import { CloseTiny } from "components/Icons/CloseTiny";
20
-
import { PublicationPollBlock } from "./PublicationPollBlock";
21
-
22
-
export let usePollBlockUIState = create(
23
-
() =>
24
-
({}) as {
25
-
[entity: string]: { state: "editing" | "voting" | "results" } | undefined;
26
-
},
27
-
);
20
+
import { PublicationPollBlock } from "../PublicationPollBlock";
21
+
import { usePollBlockUIState } from "./pollBlockState";
28
22
29
23
export const PollBlock = (props: BlockProps) => {
30
24
let { data: pub } = useLeafletPublicationData();
+8
components/Blocks/PollBlock/pollBlockState.ts
+8
components/Blocks/PollBlock/pollBlockState.ts
+2
-1
components/Blocks/PublicationPollBlock.tsx
+2
-1
components/Blocks/PublicationPollBlock.tsx
···
1
1
import { useUIState } from "src/useUIState";
2
2
import { BlockProps } from "./Block";
3
3
import { useMemo } from "react";
4
-
import { focusElement, AsyncValueInput } from "components/Input";
4
+
import { AsyncValueInput } from "components/Input";
5
+
import { focusElement } from "src/utils/focusElement";
5
6
import { useEntitySetContext } from "components/EntitySetProvider";
6
7
import { useEntity, useReplicache } from "src/replicache";
7
8
import { v7 } from "uuid";
+1
-39
components/Blocks/TextBlock/RenderYJSFragment.tsx
+1
-39
components/Blocks/TextBlock/RenderYJSFragment.tsx
···
5
5
import * as base64 from "base64-js";
6
6
import { didToBlueskyUrl } from "src/utils/mentionUtils";
7
7
import { AtMentionLink } from "components/AtMentionLink";
8
+
import { Delta } from "src/utils/yjsFragmentToString";
8
9
9
10
type BlockElements = "h1" | "h2" | "h3" | null | "blockquote" | "p";
10
11
export function RenderYJSFragment({
···
131
132
}
132
133
};
133
134
134
-
export type Delta = {
135
-
insert: string;
136
-
attributes?: {
137
-
strong?: {};
138
-
code?: {};
139
-
em?: {};
140
-
underline?: {};
141
-
strikethrough?: {};
142
-
highlight?: { color: string };
143
-
link?: { href: string };
144
-
};
145
-
};
146
-
147
135
function attributesToStyle(d: Delta) {
148
136
let props = {
149
137
style: {},
···
174
162
return props;
175
163
}
176
164
177
-
export function YJSFragmentToString(
178
-
node: XmlElement | XmlText | XmlHook,
179
-
): string {
180
-
if (node.constructor === XmlElement) {
181
-
// Handle hard_break nodes specially
182
-
if (node.nodeName === "hard_break") {
183
-
return "\n";
184
-
}
185
-
// Handle inline mention nodes
186
-
if (node.nodeName === "didMention" || node.nodeName === "atMention") {
187
-
return node.getAttribute("text") || "";
188
-
}
189
-
return node
190
-
.toArray()
191
-
.map((f) => YJSFragmentToString(f))
192
-
.join("");
193
-
}
194
-
if (node.constructor === XmlText) {
195
-
return (node.toDelta() as Delta[])
196
-
.map((d) => {
197
-
return d.insert;
198
-
})
199
-
.join("");
200
-
}
201
-
return "";
202
-
}
+1
-1
components/Blocks/TextBlock/keymap.ts
+1
-1
components/Blocks/TextBlock/keymap.ts
···
17
17
import { schema } from "./schema";
18
18
import { useUIState } from "src/useUIState";
19
19
import { setEditorState, useEditorStates } from "src/state/useEditorState";
20
-
import { focusPage } from "components/Pages";
20
+
import { focusPage } from "src/utils/focusPage";
21
21
import { v7 } from "uuid";
22
22
import { scanIndex } from "src/replicache/utils";
23
23
import { indent, outdent } from "src/utils/list-operations";
+1
-1
components/Blocks/useBlockKeyboardHandlers.ts
+1
-1
components/Blocks/useBlockKeyboardHandlers.ts
···
12
12
import { ReplicacheMutators, useEntity, useReplicache } from "src/replicache";
13
13
import { useEntitySetContext } from "components/EntitySetProvider";
14
14
import { Replicache } from "replicache";
15
-
import { deleteBlock } from "./DeleteBlock";
15
+
import { deleteBlock } from "src/utils/deleteBlock";
16
16
import { entities } from "drizzle/schema";
17
17
import { scanIndex } from "src/replicache/utils";
18
18
+1
-1
components/Blocks/useBlockMouseHandlers.ts
+1
-1
components/Blocks/useBlockMouseHandlers.ts
···
1
-
import { useSelectingMouse } from "components/SelectionManager";
1
+
import { useSelectingMouse } from "components/SelectionManager/selectionState";
2
2
import { MouseEvent, useCallback, useRef } from "react";
3
3
import { useUIState } from "src/useUIState";
4
4
import { Block } from "./Block";
+1
-36
components/Input.tsx
+1
-36
components/Input.tsx
···
2
2
import { useEffect, useRef, useState, type JSX } from "react";
3
3
import { onMouseDown } from "src/utils/iosInputMouseDown";
4
4
import { isIOS } from "src/utils/isDevice";
5
+
import { focusElement } from "src/utils/focusElement";
5
6
6
7
export const Input = (
7
8
props: {
···
56
57
}}
57
58
/>
58
59
);
59
-
};
60
-
61
-
export const focusElement = (
62
-
el?: HTMLInputElement | HTMLTextAreaElement | null,
63
-
) => {
64
-
if (!isIOS()) {
65
-
el?.focus();
66
-
return;
67
-
}
68
-
69
-
let fakeInput = document.createElement("input");
70
-
fakeInput.setAttribute("type", "text");
71
-
fakeInput.style.position = "fixed";
72
-
fakeInput.style.height = "0px";
73
-
fakeInput.style.width = "0px";
74
-
fakeInput.style.fontSize = "16px"; // disable auto zoom
75
-
document.body.appendChild(fakeInput);
76
-
fakeInput.focus();
77
-
setTimeout(() => {
78
-
if (!el) return;
79
-
el.style.transform = "translateY(-2000px)";
80
-
el?.focus();
81
-
fakeInput.remove();
82
-
el.value = " ";
83
-
el.setSelectionRange(1, 1);
84
-
requestAnimationFrame(() => {
85
-
if (el) {
86
-
el.style.transform = "";
87
-
}
88
-
});
89
-
setTimeout(() => {
90
-
if (!el) return;
91
-
el.value = "";
92
-
el.setSelectionRange(0, 0);
93
-
}, 50);
94
-
}, 20);
95
60
};
96
61
97
62
export const InputWithLabel = (
+1
-1
components/Layout.tsx
+1
-1
components/Layout.tsx
···
3
3
import { theme } from "tailwind.config";
4
4
import { NestedCardThemeProvider } from "./ThemeManager/ThemeProvider";
5
5
import { PopoverArrow } from "./Icons/PopoverArrow";
6
-
import { PopoverOpenContext } from "./Popover";
6
+
import { PopoverOpenContext } from "./Popover/PopoverContext";
7
7
import { useState } from "react";
8
8
9
9
export const Separator = (props: { classname?: string }) => {
+1
-1
components/Pages/Page.tsx
+1
-1
components/Pages/Page.tsx
···
12
12
import { Blocks } from "components/Blocks";
13
13
import { PublicationMetadata } from "./PublicationMetadata";
14
14
import { useCardBorderHidden } from "./useCardBorderHidden";
15
-
import { focusPage } from ".";
15
+
import { focusPage } from "src/utils/focusPage";
16
16
import { PageOptions } from "./PageOptions";
17
17
import { CardThemeProvider } from "components/ThemeManager/ThemeProvider";
18
18
import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer";
+2
-75
components/Pages/index.tsx
+2
-75
components/Pages/index.tsx
···
4
4
import { useUIState } from "src/useUIState";
5
5
import { useSearchParams } from "next/navigation";
6
6
7
-
import { focusBlock } from "src/utils/focusBlock";
8
-
import { elementId } from "src/utils/elementId";
7
+
import { useEntity } from "src/replicache";
9
8
10
-
import { Replicache } from "replicache";
11
-
import { Fact, ReplicacheMutators, useEntity } from "src/replicache";
12
-
13
-
import { scanIndex } from "src/replicache/utils";
14
-
import { CardThemeProvider } from "../ThemeManager/ThemeProvider";
15
-
import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded";
16
9
import { useCardBorderHidden } from "./useCardBorderHidden";
17
10
import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout";
18
11
import { LeafletSidebar } from "app/[leaflet_id]/Sidebar";
···
62
55
);
63
56
}
64
57
65
-
export async function focusPage(
66
-
pageID: string,
67
-
rep: Replicache<ReplicacheMutators>,
68
-
focusFirstBlock?: "focusFirstBlock",
69
-
) {
70
-
// if this page is already focused,
71
-
let focusedBlock = useUIState.getState().focusedEntity;
72
-
// else set this page as focused
73
-
useUIState.setState(() => ({
74
-
focusedEntity: {
75
-
entityType: "page",
76
-
entityID: pageID,
77
-
},
78
-
}));
79
-
80
-
setTimeout(async () => {
81
-
//scroll to page
82
-
83
-
scrollIntoViewIfNeeded(
84
-
document.getElementById(elementId.page(pageID).container),
85
-
false,
86
-
"smooth",
87
-
);
88
-
89
-
// if we asked that the function focus the first block, focus the first block
90
-
if (focusFirstBlock === "focusFirstBlock") {
91
-
let firstBlock = await rep.query(async (tx) => {
92
-
let type = await scanIndex(tx).eav(pageID, "page/type");
93
-
let blocks = await scanIndex(tx).eav(
94
-
pageID,
95
-
type[0]?.data.value === "canvas" ? "canvas/block" : "card/block",
96
-
);
97
-
98
-
let firstBlock = blocks[0];
99
-
100
-
if (!firstBlock) {
101
-
return null;
102
-
}
103
-
104
-
let blockType = (
105
-
await tx
106
-
.scan<
107
-
Fact<"block/type">
108
-
>({ indexName: "eav", prefix: `${firstBlock.data.value}-block/type` })
109
-
.toArray()
110
-
)[0];
111
-
112
-
if (!blockType) return null;
113
-
114
-
return {
115
-
value: firstBlock.data.value,
116
-
type: blockType.data.value,
117
-
parent: firstBlock.entity,
118
-
position: firstBlock.data.position,
119
-
};
120
-
});
121
-
122
-
if (firstBlock) {
123
-
setTimeout(() => {
124
-
focusBlock(firstBlock, { type: "start" });
125
-
}, 500);
126
-
}
127
-
}
128
-
}, 50);
129
-
}
130
-
131
-
export const blurPage = () => {
58
+
const blurPage = () => {
132
59
useUIState.setState(() => ({
133
60
focusedEntity: null,
134
61
selectedBlocks: [],
+4
-5
components/Popover.tsx
components/Popover/index.tsx
+4
-5
components/Popover.tsx
components/Popover/index.tsx
···
1
1
"use client";
2
2
import * as RadixPopover from "@radix-ui/react-popover";
3
3
import { theme } from "tailwind.config";
4
-
import { NestedCardThemeProvider } from "./ThemeManager/ThemeProvider";
5
-
import { createContext, useEffect, useState } from "react";
6
-
import { PopoverArrow } from "./Icons/PopoverArrow";
7
-
8
-
export const PopoverOpenContext = createContext(false);
4
+
import { NestedCardThemeProvider } from "../ThemeManager/ThemeProvider";
5
+
import { useEffect, useState } from "react";
6
+
import { PopoverArrow } from "../Icons/PopoverArrow";
7
+
import { PopoverOpenContext } from "./PopoverContext";
9
8
export const Popover = (props: {
10
9
trigger: React.ReactNode;
11
10
disabled?: boolean;
+3
components/Popover/PopoverContext.ts
+3
components/Popover/PopoverContext.ts
+7
-53
components/SelectionManager.tsx
components/SelectionManager/index.tsx
+7
-53
components/SelectionManager.tsx
components/SelectionManager/index.tsx
···
1
1
"use client";
2
2
import { useEffect, useRef, useState } from "react";
3
-
import { create } from "zustand";
4
-
import { ReplicacheMutators, useReplicache } from "src/replicache";
3
+
import { useReplicache } from "src/replicache";
5
4
import { useUIState } from "src/useUIState";
6
5
import { scanIndex } from "src/replicache/utils";
7
6
import { focusBlock } from "src/utils/focusBlock";
8
7
import { useEditorStates } from "src/state/useEditorState";
9
-
import { useEntitySetContext } from "./EntitySetProvider";
8
+
import { useEntitySetContext } from "../EntitySetProvider";
10
9
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
11
-
import { v7 } from "uuid";
12
10
import { indent, outdent, outdentFull } from "src/utils/list-operations";
13
11
import { addShortcut, Shortcut } from "src/shortcuts";
14
-
import { htmlToMarkdown } from "src/htmlMarkdownParsers";
15
12
import { elementId } from "src/utils/elementId";
16
13
import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded";
17
14
import { copySelection } from "src/utils/copySelection";
18
-
import { isTextBlock } from "src/utils/isTextBlock";
19
15
import { useIsMobile } from "src/hooks/isMobile";
20
-
import { deleteBlock } from "./Blocks/DeleteBlock";
21
-
import { Replicache } from "replicache";
22
-
import { schema } from "./Blocks/TextBlock/schema";
23
-
import { TextSelection } from "prosemirror-state";
16
+
import { deleteBlock } from "src/utils/deleteBlock";
17
+
import { schema } from "../Blocks/TextBlock/schema";
24
18
import { MarkType } from "prosemirror-model";
25
-
export const useSelectingMouse = create(() => ({
26
-
start: null as null | string,
27
-
}));
19
+
import { useSelectingMouse, getSortedSelection } from "./selectionState";
28
20
29
21
//How should I model selection? As ranges w/ a start and end? Store *blocks* so that I can just construct ranges?
30
22
// How does this relate to *when dragging* ?
···
632
624
endOffset: number;
633
625
direction: "forward" | "backward";
634
626
};
635
-
export function saveSelection() {
627
+
function saveSelection() {
636
628
let selection = window.getSelection();
637
629
if (selection && selection.rangeCount > 0) {
638
630
let ranges: SavedRange[] = [];
···
655
647
return [];
656
648
}
657
649
658
-
export function restoreSelection(savedRanges: SavedRange[]) {
650
+
function restoreSelection(savedRanges: SavedRange[]) {
659
651
if (savedRanges && savedRanges.length > 0) {
660
652
let selection = window.getSelection();
661
653
if (!selection) return;
···
693
685
return null;
694
686
}
695
687
696
-
export const getSortedSelection = async (
697
-
rep: Replicache<ReplicacheMutators>,
698
-
) => {
699
-
let selectedBlocks = useUIState.getState().selectedBlocks;
700
-
let foldedBlocks = useUIState.getState().foldedBlocks;
701
-
if (!selectedBlocks[0]) return [[], []];
702
-
let siblings =
703
-
(await rep?.query((tx) =>
704
-
getBlocksWithType(tx, selectedBlocks[0].parent),
705
-
)) || [];
706
-
let sortedBlocks = siblings.filter((s) => {
707
-
let selected = selectedBlocks.find((sb) => sb.value === s.value);
708
-
return selected;
709
-
});
710
-
let sortedBlocksWithChildren = siblings.filter((s) => {
711
-
let selected = selectedBlocks.find((sb) => sb.value === s.value);
712
-
if (s.listData && !selected) {
713
-
//Select the children of folded list blocks (in order to copy them)
714
-
return s.listData.path.find(
715
-
(p) =>
716
-
selectedBlocks.find((sb) => sb.value === p.entity) &&
717
-
foldedBlocks.includes(p.entity),
718
-
);
719
-
}
720
-
return selected;
721
-
});
722
-
return [
723
-
sortedBlocks,
724
-
siblings.filter(
725
-
(f) =>
726
-
!f.listData ||
727
-
!f.listData.path.find(
728
-
(p) => foldedBlocks.includes(p.entity) && p.entity !== f.value,
729
-
),
730
-
),
731
-
sortedBlocksWithChildren,
732
-
];
733
-
};
734
688
735
689
function toggleMarkInBlocks(blocks: string[], mark: MarkType, attrs?: any) {
736
690
let everyBlockHasMark = blocks.reduce((acc, block) => {
+48
components/SelectionManager/selectionState.ts
+48
components/SelectionManager/selectionState.ts
···
1
+
import { create } from "zustand";
2
+
import { Replicache } from "replicache";
3
+
import { ReplicacheMutators } from "src/replicache";
4
+
import { useUIState } from "src/useUIState";
5
+
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
6
+
7
+
export const useSelectingMouse = create(() => ({
8
+
start: null as null | string,
9
+
}));
10
+
11
+
export const getSortedSelection = async (
12
+
rep: Replicache<ReplicacheMutators>,
13
+
) => {
14
+
let selectedBlocks = useUIState.getState().selectedBlocks;
15
+
let foldedBlocks = useUIState.getState().foldedBlocks;
16
+
if (!selectedBlocks[0]) return [[], []];
17
+
let siblings =
18
+
(await rep?.query((tx) =>
19
+
getBlocksWithType(tx, selectedBlocks[0].parent),
20
+
)) || [];
21
+
let sortedBlocks = siblings.filter((s) => {
22
+
let selected = selectedBlocks.find((sb) => sb.value === s.value);
23
+
return selected;
24
+
});
25
+
let sortedBlocksWithChildren = siblings.filter((s) => {
26
+
let selected = selectedBlocks.find((sb) => sb.value === s.value);
27
+
if (s.listData && !selected) {
28
+
//Select the children of folded list blocks (in order to copy them)
29
+
return s.listData.path.find(
30
+
(p) =>
31
+
selectedBlocks.find((sb) => sb.value === p.entity) &&
32
+
foldedBlocks.includes(p.entity),
33
+
);
34
+
}
35
+
return selected;
36
+
});
37
+
return [
38
+
sortedBlocks,
39
+
siblings.filter(
40
+
(f) =>
41
+
!f.listData ||
42
+
!f.listData.path.find(
43
+
(p) => foldedBlocks.includes(p.entity) && p.entity !== f.value,
44
+
),
45
+
),
46
+
sortedBlocksWithChildren,
47
+
];
48
+
};
+1
-1
components/ThemeManager/PublicationThemeProvider.tsx
+1
-1
components/ThemeManager/PublicationThemeProvider.tsx
···
2
2
import { useMemo, useState } from "react";
3
3
import { parseColor } from "react-aria-components";
4
4
import { useEntity } from "src/replicache";
5
-
import { getColorContrast } from "./ThemeProvider";
5
+
import { getColorContrast } from "./themeUtils";
6
6
import { useColorAttribute, colorToString } from "./useColorAttribute";
7
7
import { BaseThemeProvider } from "./ThemeProvider";
8
8
import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api";
+1
-40
components/ThemeManager/ThemeProvider.tsx
+1
-40
components/ThemeManager/ThemeProvider.tsx
···
5
5
CSSProperties,
6
6
useContext,
7
7
useEffect,
8
-
useMemo,
9
-
useState,
10
8
} from "react";
11
9
import {
12
10
colorToString,
···
14
12
useColorAttributeNullable,
15
13
} from "./useColorAttribute";
16
14
import { Color as AriaColor, parseColor } from "react-aria-components";
17
-
import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn";
18
15
19
16
import { useEntity } from "src/replicache";
20
17
import { useLeafletPublicationData } from "components/PageSWRDataProvider";
···
23
20
PublicationThemeProvider,
24
21
} from "./PublicationThemeProvider";
25
22
import { PubLeafletPublication } from "lexicons/api";
26
-
27
-
type CSSVariables = {
28
-
"--bg-leaflet": string;
29
-
"--bg-page": string;
30
-
"--primary": string;
31
-
"--accent-1": string;
32
-
"--accent-2": string;
33
-
"--accent-contrast": string;
34
-
"--highlight-1": string;
35
-
"--highlight-2": string;
36
-
"--highlight-3": string;
37
-
};
38
-
39
-
// define the color defaults for everything
40
-
export const ThemeDefaults = {
41
-
"theme/page-background": "#FDFCFA",
42
-
"theme/card-background": "#FFFFFF",
43
-
"theme/primary": "#272727",
44
-
"theme/highlight-1": "#FFFFFF",
45
-
"theme/highlight-2": "#EDD280",
46
-
"theme/highlight-3": "#FFCDC3",
47
-
48
-
//everywhere else, accent-background = accent-1 and accent-text = accent-2.
49
-
// we just need to create a migration pipeline before we can change this
50
-
"theme/accent-text": "#FFFFFF",
51
-
"theme/accent-background": "#0000FF",
52
-
"theme/accent-contrast": "#0000FF",
53
-
};
23
+
import { getColorContrast } from "./themeUtils";
54
24
55
25
// define a function to set an Aria Color to a CSS Variable in RGB
56
26
function setCSSVariableToColor(
···
368
338
);
369
339
};
370
340
371
-
// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
372
-
export function getColorContrast(color1: string, color2: string) {
373
-
ColorSpace.register(sRGB);
374
-
375
-
let parsedColor1 = parse(`rgb(${color1})`);
376
-
let parsedColor2 = parse(`rgb(${color2})`);
377
-
378
-
return contrastLstar(parsedColor1, parsedColor2);
379
-
}
+27
components/ThemeManager/themeUtils.ts
+27
components/ThemeManager/themeUtils.ts
···
1
+
import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn";
2
+
3
+
// define the color defaults for everything
4
+
export const ThemeDefaults = {
5
+
"theme/page-background": "#FDFCFA",
6
+
"theme/card-background": "#FFFFFF",
7
+
"theme/primary": "#272727",
8
+
"theme/highlight-1": "#FFFFFF",
9
+
"theme/highlight-2": "#EDD280",
10
+
"theme/highlight-3": "#FFCDC3",
11
+
12
+
//everywhere else, accent-background = accent-1 and accent-text = accent-2.
13
+
// we just need to create a migration pipeline before we can change this
14
+
"theme/accent-text": "#FFFFFF",
15
+
"theme/accent-background": "#0000FF",
16
+
"theme/accent-contrast": "#0000FF",
17
+
};
18
+
19
+
// used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast
20
+
export function getColorContrast(color1: string, color2: string) {
21
+
ColorSpace.register(sRGB);
22
+
23
+
let parsedColor1 = parse(`rgb(${color1})`);
24
+
let parsedColor2 = parse(`rgb(${color2})`);
25
+
26
+
return contrastLstar(parsedColor1, parsedColor2);
27
+
}
+1
-1
components/ThemeManager/useColorAttribute.ts
+1
-1
components/ThemeManager/useColorAttribute.ts
···
2
2
import { Color, parseColor } from "react-aria-components";
3
3
import { useEntity, useReplicache } from "src/replicache";
4
4
import { FilterAttributes } from "src/replicache/attributes";
5
-
import { ThemeDefaults } from "./ThemeProvider";
5
+
import { ThemeDefaults } from "./themeUtils";
6
6
7
7
export function useColorAttribute(
8
8
entity: string | null,
+5
-14
components/Toolbar/BlockToolbar.tsx
+5
-14
components/Toolbar/BlockToolbar.tsx
···
2
2
import { ToolbarButton } from ".";
3
3
import { Separator, ShortcutKey } from "components/Layout";
4
4
import { metaKey } from "src/utils/metaKey";
5
-
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
6
5
import { useUIState } from "src/useUIState";
7
6
import { LockBlockButton } from "./LockBlockButton";
8
7
import { TextAlignmentButton } from "./TextAlignmentToolbar";
9
8
import { ImageFullBleedButton, ImageAltTextButton } from "./ImageToolbar";
10
9
import { DeleteSmall } from "components/Icons/DeleteSmall";
10
+
import { getSortedSelection } from "components/SelectionManager/selectionState";
11
11
12
12
export const BlockToolbar = (props: {
13
13
setToolbarState: (
···
66
66
67
67
const MoveBlockButtons = () => {
68
68
let { rep } = useReplicache();
69
-
const getSortedSelection = async () => {
70
-
let selectedBlocks = useUIState.getState().selectedBlocks;
71
-
let siblings =
72
-
(await rep?.query((tx) =>
73
-
getBlocksWithType(tx, selectedBlocks[0].parent),
74
-
)) || [];
75
-
let sortedBlocks = siblings.filter((s) =>
76
-
selectedBlocks.find((sb) => sb.value === s.value),
77
-
);
78
-
return [sortedBlocks, siblings];
79
-
};
80
69
return (
81
70
<>
82
71
<ToolbarButton
83
72
hiddenOnCanvas
84
73
onClick={async () => {
85
-
let [sortedBlocks, siblings] = await getSortedSelection();
74
+
if (!rep) return;
75
+
let [sortedBlocks, siblings] = await getSortedSelection(rep);
86
76
if (sortedBlocks.length > 1) return;
87
77
let block = sortedBlocks[0];
88
78
let previousBlock =
···
139
129
<ToolbarButton
140
130
hiddenOnCanvas
141
131
onClick={async () => {
142
-
let [sortedBlocks, siblings] = await getSortedSelection();
132
+
if (!rep) return;
133
+
let [sortedBlocks, siblings] = await getSortedSelection(rep);
143
134
if (sortedBlocks.length > 1) return;
144
135
let block = sortedBlocks[0];
145
136
let nextBlock = siblings
+1
-1
components/Toolbar/MultiSelectToolbar.tsx
+1
-1
components/Toolbar/MultiSelectToolbar.tsx
···
8
8
import { LockBlockButton } from "./LockBlockButton";
9
9
import { Props } from "components/Icons/Props";
10
10
import { TextAlignmentButton } from "./TextAlignmentToolbar";
11
-
import { getSortedSelection } from "components/SelectionManager";
11
+
import { getSortedSelection } from "components/SelectionManager/selectionState";
12
12
13
13
export const MultiselectToolbar = (props: {
14
14
setToolbarState: (
+2
-1
components/Toolbar/index.tsx
+2
-1
components/Toolbar/index.tsx
···
13
13
import { TextToolbar } from "./TextToolbar";
14
14
import { BlockToolbar } from "./BlockToolbar";
15
15
import { MultiselectToolbar } from "./MultiSelectToolbar";
16
-
import { AreYouSure, deleteBlock } from "components/Blocks/DeleteBlock";
16
+
import { AreYouSure } from "components/Blocks/DeleteBlock";
17
+
import { deleteBlock } from "src/utils/deleteBlock";
17
18
import { TooltipButton } from "components/Buttons";
18
19
import { TextAlignmentToolbar } from "./TextAlignmentToolbar";
19
20
import { useIsMobile } from "src/hooks/isMobile";
+1
-1
components/utils/UpdateLeafletTitle.tsx
+1
-1
components/utils/UpdateLeafletTitle.tsx
···
8
8
import { useEntity, useReplicache } from "src/replicache";
9
9
import * as Y from "yjs";
10
10
import * as base64 from "base64-js";
11
-
import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment";
11
+
import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
12
12
import { useParams, useRouter, useSearchParams } from "next/navigation";
13
13
import { focusBlock } from "src/utils/focusBlock";
14
14
import { useIsMobile } from "src/hooks/isMobile";
+116
src/utils/deleteBlock.ts
+116
src/utils/deleteBlock.ts
···
1
+
import { Replicache } from "replicache";
2
+
import { ReplicacheMutators } from "src/replicache";
3
+
import { useUIState } from "src/useUIState";
4
+
import { scanIndex } from "src/replicache/utils";
5
+
import { getBlocksWithType } from "src/hooks/queries/useBlocks";
6
+
import { focusBlock } from "src/utils/focusBlock";
7
+
8
+
export async function deleteBlock(
9
+
entities: string[],
10
+
rep: Replicache<ReplicacheMutators>,
11
+
) {
12
+
// get what pagess we need to close as a result of deleting this block
13
+
let pagesToClose = [] as string[];
14
+
for (let entity of entities) {
15
+
let [type] = await rep.query((tx) =>
16
+
scanIndex(tx).eav(entity, "block/type"),
17
+
);
18
+
if (type.data.value === "card") {
19
+
let [childPages] = await rep?.query(
20
+
(tx) => scanIndex(tx).eav(entity, "block/card") || [],
21
+
);
22
+
pagesToClose = [childPages?.data.value];
23
+
}
24
+
if (type.data.value === "mailbox") {
25
+
let [archive] = await rep?.query(
26
+
(tx) => scanIndex(tx).eav(entity, "mailbox/archive") || [],
27
+
);
28
+
let [draft] = await rep?.query(
29
+
(tx) => scanIndex(tx).eav(entity, "mailbox/draft") || [],
30
+
);
31
+
pagesToClose = [archive?.data.value, draft?.data.value];
32
+
}
33
+
}
34
+
35
+
// the next and previous blocks in the block list
36
+
// if the focused thing is a page and not a block, return
37
+
let focusedBlock = useUIState.getState().focusedEntity;
38
+
let parent =
39
+
focusedBlock?.entityType === "page"
40
+
? focusedBlock.entityID
41
+
: focusedBlock?.parent;
42
+
43
+
if (parent) {
44
+
let parentType = await rep?.query((tx) =>
45
+
scanIndex(tx).eav(parent, "page/type"),
46
+
);
47
+
if (parentType[0]?.data.value === "canvas") {
48
+
useUIState
49
+
.getState()
50
+
.setFocusedBlock({ entityType: "page", entityID: parent });
51
+
useUIState.getState().setSelectedBlocks([]);
52
+
} else {
53
+
let siblings =
54
+
(await rep?.query((tx) => getBlocksWithType(tx, parent))) || [];
55
+
56
+
let selectedBlocks = useUIState.getState().selectedBlocks;
57
+
let firstSelected = selectedBlocks[0];
58
+
let lastSelected = selectedBlocks[entities.length - 1];
59
+
60
+
let prevBlock =
61
+
siblings?.[
62
+
siblings.findIndex((s) => s.value === firstSelected?.value) - 1
63
+
];
64
+
let prevBlockType = await rep?.query((tx) =>
65
+
scanIndex(tx).eav(prevBlock?.value, "block/type"),
66
+
);
67
+
68
+
let nextBlock =
69
+
siblings?.[
70
+
siblings.findIndex((s) => s.value === lastSelected.value) + 1
71
+
];
72
+
let nextBlockType = await rep?.query((tx) =>
73
+
scanIndex(tx).eav(nextBlock?.value, "block/type"),
74
+
);
75
+
76
+
if (prevBlock) {
77
+
useUIState.getState().setSelectedBlock({
78
+
value: prevBlock.value,
79
+
parent: prevBlock.parent,
80
+
});
81
+
82
+
focusBlock(
83
+
{
84
+
value: prevBlock.value,
85
+
type: prevBlockType?.[0].data.value,
86
+
parent: prevBlock.parent,
87
+
},
88
+
{ type: "end" },
89
+
);
90
+
} else {
91
+
useUIState.getState().setSelectedBlock({
92
+
value: nextBlock.value,
93
+
parent: nextBlock.parent,
94
+
});
95
+
96
+
focusBlock(
97
+
{
98
+
value: nextBlock.value,
99
+
type: nextBlockType?.[0]?.data.value,
100
+
parent: nextBlock.parent,
101
+
},
102
+
{ type: "start" },
103
+
);
104
+
}
105
+
}
106
+
}
107
+
108
+
pagesToClose.forEach((page) => page && useUIState.getState().closePage(page));
109
+
await Promise.all(
110
+
entities.map((entity) =>
111
+
rep?.mutate.removeBlock({
112
+
blockEntity: entity,
113
+
}),
114
+
),
115
+
);
116
+
}
+37
src/utils/focusElement.ts
+37
src/utils/focusElement.ts
···
1
+
import { isIOS } from "src/utils/isDevice";
2
+
3
+
export const focusElement = (
4
+
el?: HTMLInputElement | HTMLTextAreaElement | null,
5
+
) => {
6
+
if (!isIOS()) {
7
+
el?.focus();
8
+
return;
9
+
}
10
+
11
+
let fakeInput = document.createElement("input");
12
+
fakeInput.setAttribute("type", "text");
13
+
fakeInput.style.position = "fixed";
14
+
fakeInput.style.height = "0px";
15
+
fakeInput.style.width = "0px";
16
+
fakeInput.style.fontSize = "16px"; // disable auto zoom
17
+
document.body.appendChild(fakeInput);
18
+
fakeInput.focus();
19
+
setTimeout(() => {
20
+
if (!el) return;
21
+
el.style.transform = "translateY(-2000px)";
22
+
el?.focus();
23
+
fakeInput.remove();
24
+
el.value = " ";
25
+
el.setSelectionRange(1, 1);
26
+
requestAnimationFrame(() => {
27
+
if (el) {
28
+
el.style.transform = "";
29
+
}
30
+
});
31
+
setTimeout(() => {
32
+
if (!el) return;
33
+
el.value = "";
34
+
el.setSelectionRange(0, 0);
35
+
}, 50);
36
+
}, 20);
37
+
};
+73
src/utils/focusPage.ts
+73
src/utils/focusPage.ts
···
1
+
import { Replicache } from "replicache";
2
+
import { Fact, ReplicacheMutators } from "src/replicache";
3
+
import { useUIState } from "src/useUIState";
4
+
import { scanIndex } from "src/replicache/utils";
5
+
import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded";
6
+
import { elementId } from "src/utils/elementId";
7
+
import { focusBlock } from "src/utils/focusBlock";
8
+
9
+
export async function focusPage(
10
+
pageID: string,
11
+
rep: Replicache<ReplicacheMutators>,
12
+
focusFirstBlock?: "focusFirstBlock",
13
+
) {
14
+
// if this page is already focused,
15
+
let focusedBlock = useUIState.getState().focusedEntity;
16
+
// else set this page as focused
17
+
useUIState.setState(() => ({
18
+
focusedEntity: {
19
+
entityType: "page",
20
+
entityID: pageID,
21
+
},
22
+
}));
23
+
24
+
setTimeout(async () => {
25
+
//scroll to page
26
+
27
+
scrollIntoViewIfNeeded(
28
+
document.getElementById(elementId.page(pageID).container),
29
+
false,
30
+
"smooth",
31
+
);
32
+
33
+
// if we asked that the function focus the first block, focus the first block
34
+
if (focusFirstBlock === "focusFirstBlock") {
35
+
let firstBlock = await rep.query(async (tx) => {
36
+
let type = await scanIndex(tx).eav(pageID, "page/type");
37
+
let blocks = await scanIndex(tx).eav(
38
+
pageID,
39
+
type[0]?.data.value === "canvas" ? "canvas/block" : "card/block",
40
+
);
41
+
42
+
let firstBlock = blocks[0];
43
+
44
+
if (!firstBlock) {
45
+
return null;
46
+
}
47
+
48
+
let blockType = (
49
+
await tx
50
+
.scan<
51
+
Fact<"block/type">
52
+
>({ indexName: "eav", prefix: `${firstBlock.data.value}-block/type` })
53
+
.toArray()
54
+
)[0];
55
+
56
+
if (!blockType) return null;
57
+
58
+
return {
59
+
value: firstBlock.data.value,
60
+
type: blockType.data.value,
61
+
parent: firstBlock.entity,
62
+
position: firstBlock.data.position,
63
+
};
64
+
});
65
+
66
+
if (firstBlock) {
67
+
setTimeout(() => {
68
+
focusBlock(firstBlock, { type: "start" });
69
+
}, 500);
70
+
}
71
+
}
72
+
}, 50);
73
+
}
+41
src/utils/yjsFragmentToString.ts
+41
src/utils/yjsFragmentToString.ts
···
1
+
import { XmlElement, XmlText, XmlHook } from "yjs";
2
+
3
+
export type Delta = {
4
+
insert: string;
5
+
attributes?: {
6
+
strong?: {};
7
+
code?: {};
8
+
em?: {};
9
+
underline?: {};
10
+
strikethrough?: {};
11
+
highlight?: { color: string };
12
+
link?: { href: string };
13
+
};
14
+
};
15
+
16
+
export function YJSFragmentToString(
17
+
node: XmlElement | XmlText | XmlHook,
18
+
): string {
19
+
if (node.constructor === XmlElement) {
20
+
// Handle hard_break nodes specially
21
+
if (node.nodeName === "hard_break") {
22
+
return "\n";
23
+
}
24
+
// Handle inline mention nodes
25
+
if (node.nodeName === "didMention" || node.nodeName === "atMention") {
26
+
return node.getAttribute("text") || "";
27
+
}
28
+
return node
29
+
.toArray()
30
+
.map((f) => YJSFragmentToString(f))
31
+
.join("");
32
+
}
33
+
if (node.constructor === XmlText) {
34
+
return (node.toDelta() as Delta[])
35
+
.map((d) => {
36
+
return d.insert;
37
+
})
38
+
.join("");
39
+
}
40
+
return "";
41
+
}