Skip to content

Commit

Permalink
refactor: Element custom views, collaboration improvements (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
areknawo authored May 23, 2024
1 parent 54947ec commit 690a5f5
Show file tree
Hide file tree
Showing 29 changed files with 2,210 additions and 684 deletions.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"clsx": "^2.1.0",
"dayjs": "^1.11.10",
"dompurify": "^3.0.8",
"lib0": "^0.2.94",
"marked": "^12.0.0",
"minisearch": "^6.3.0",
"monaco-editor": "^0.45.0",
Expand Down
224 changes: 126 additions & 98 deletions apps/web/src/lib/editor/extensions/block-action-menu/component.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { OptionsDropdown } from "./options";
import { debounce } from "@solid-primitives/scheduled";
import { Range, createNodeFromContent, generateJSON } from "@tiptap/core";
import { Node as PMNode } from "@tiptap/pm/model";
Expand Down Expand Up @@ -26,6 +27,7 @@ interface BlockActionMenuProps {
editor: SolidEditor;
range: Range | null;
node: PMNode | null;
pos: number | null;
repositionMenu: () => void;
};
}
Expand Down Expand Up @@ -64,12 +66,12 @@ const BlockActionMenu: Component<BlockActionMenuProps> = (props) => {
const { notify } = useNotifications();
const { installedExtensions } = useExtensions();
const { storage } = useLocalStorage();
const [computeDropdownPosition, setComputeDropdownPosition] = createRef(() => {});
const [containerRef, setContainerRef] = createRef<HTMLDivElement | null>(null);
const [range, setRange] = createSignal<Range | null>(props.state.range);
const [node, setNode] = createSignal<PMNode | null>(props.state.node);
const [locked, setLocked] = createSignal(false);
const [opened, setOpened] = createSignal(false);
const replaceContentCallbacks: Array<() => void> = [];
const { repositionMenu } = props.state;
const unlock = debounce(() => {
setLocked(false);
Expand Down Expand Up @@ -99,6 +101,9 @@ const BlockActionMenu: Component<BlockActionMenuProps> = (props) => {

return blockActions;
});
const topLevelNode = (): boolean => {
return props.state.editor.state.doc.resolve(props.state.pos || 0).depth <= 1;
};
const usableEnvData = (): { content: JSONContent } => {
return { content: node()?.toJSON() || { type: "doc", content: [] } };
};
Expand Down Expand Up @@ -143,113 +148,136 @@ const BlockActionMenu: Component<BlockActionMenuProps> = (props) => {
)}
ref={setContainerRef}
>
<For each={blockActions()}>
{({ blockAction, extension }) => {
const [scrollableContainerRef, setScrollableContainerRef] = createRef<HTMLElement | null>(
null
);
<Show when={props.state.range && props.state.node && typeof props.state.pos === "number"}>
<OptionsDropdown
editor={props.state.editor}
range={props.state.range!}
node={props.state.node!}
pos={props.state.pos!}
onReplaceContent={(callback) => {
replaceContentCallbacks.push(callback);
onCleanup(() => {
replaceContentCallbacks.splice(replaceContentCallbacks.indexOf(callback), 1);
});
}}
/>
</Show>
<Show when={topLevelNode()}>
<For each={blockActions()}>
{({ blockAction, extension }) => {
const [scrollableContainerRef, setScrollableContainerRef] =
createRef<HTMLElement | null>(null);

return (
<Dropdown
placement="left-end"
class={clsx(
blockAction.blocks.length !== 0 &&
!blockAction.blocks.some((block) => {
return node()?.type.name === props.state.editor.schema.nodes[block].name;
}) &&
"hidden"
)}
cardProps={{ class: "p-0 m-0 -ml-1 pr-1.5 p-3" }}
overlayProps={{
onOverlayClick: () => {
if (!locked()) {
setOpened(false);
return (
<Dropdown
placement="left-end"
class={clsx(
blockAction.blocks.length !== 0 &&
!blockAction.blocks.some((block) => {
return node()?.type.name === props.state.editor.schema.nodes[block].name;
}) &&
"hidden"
)}
cardProps={{ class: "p-0 m-0 -ml-1 pr-1.5 p-3" }}
overlayProps={{
onOverlayClick: () => {
if (!locked()) {
setOpened(false);
}
}
}
}}
opened={opened()}
setOpened={setOpened}
activatorButton={(props) => {
setComputeDropdownPosition(props.computeDropdownPosition);
}}
opened={opened()}
setOpened={setOpened}
activatorButton={(props) => {
replaceContentCallbacks.push(props.computeDropdownPosition);
onCleanup(() => {
replaceContentCallbacks.splice(
replaceContentCallbacks.indexOf(props.computeDropdownPosition),
1
);
});

return (
<Tooltip text={blockAction.label} side="left" class="-ml-1">
<Button
class={clsx(
"h-8 w-8 p-0 m-0 border-2 flex justify-center items-center",
props.opened && "border-primary",
!props.opened &&
"border-gray-200 dark:border-gray-700 hover:border-gray-300 hover:dark:border-gray-700"
)}
variant="text"
>
<ExtensionIcon spec={extension.spec} class="h-8 w-8" />
</Button>
</Tooltip>
);
}}
>
<div
ref={setScrollableContainerRef}
class="text-base overflow-auto pr-1.5 not-prose scrollbar-sm"
return (
<Tooltip text={blockAction.label} side="left" class="-ml-1">
<Button
class={clsx(
"h-8 w-8 p-0 m-0 border-2 flex justify-center items-center",
props.opened && "border-primary",
!props.opened &&
"border-gray-200 dark:border-gray-700 hover:border-gray-300 hover:dark:border-gray-700"
)}
variant="text"
>
<ExtensionIcon spec={extension.spec} class="h-8 w-8" />
</Button>
</Tooltip>
);
}}
>
<ScrollShadow scrollableContainerRef={scrollableContainerRef} />
<ExtensionViewRenderer<ExtensionBlockActionViewContext>
extension={extension}
ctx={{
contextFunctions: ["notify", "replaceContent", "refreshContent"],
usableEnv: { readable: ["content"], writable: [] },
config: extension.config || {}
}}
func={{
notify,
refreshContent: () => {
setRange(props.state.range);
setNode(props.state.node);
},
replaceContent(content) {
unlock.clear();
setLocked(true);
<div
ref={setScrollableContainerRef}
class="text-base overflow-auto pr-1.5 not-prose scrollbar-sm"
>
<ScrollShadow scrollableContainerRef={scrollableContainerRef} />
<ExtensionViewRenderer<ExtensionBlockActionViewContext>
extension={extension}
ctx={{
contextFunctions: ["notify", "replaceContent", "refreshContent"],
usableEnv: { readable: ["content"], writable: [] },
config: extension.config || {}
}}
func={{
notify,
refreshContent: () => {
setRange(props.state.range);
setNode(props.state.node);
},
replaceContent(content) {
unlock.clear();
setLocked(true);

if (range()) {
let size = 0;
if (range()) {
let size = 0;

const nodeOrFragment = createNodeFromContent(
content,
props.state.editor.schema
);
const nodeOrFragment = createNodeFromContent(
content,
props.state.editor.schema
);

if (nodeOrFragment instanceof PMNode) {
size = nodeOrFragment.nodeSize;
} else {
size = nodeOrFragment.size || 0;
if (nodeOrFragment instanceof PMNode) {
size = nodeOrFragment.nodeSize;
} else {
size = nodeOrFragment.size || 0;
}

props.state.editor
.chain()
.focus()
.insertContentAt(
range()!,
generateJSON(content, props.state.editor.extensionManager.extensions)
)
.scrollIntoView()
.focus()
.run();
setRange({ from: range()!.from, to: range()!.from + size - 1 });
replaceContentCallbacks.forEach((callback) => {
callback();
});
}

props.state.editor
.chain()
.focus()
.insertContentAt(
range()!,
generateJSON(content, props.state.editor.extensionManager.extensions)
)
.scrollIntoView()
.focus()
.run();
setRange({ from: range()!.from, to: range()!.from + size - 1 });
computeDropdownPosition()();
unlock();
}

unlock();
}
}}
viewId={blockAction.view}
usableEnvData={usableEnvData()}
/>
</div>
</Dropdown>
);
}}
</For>
}}
viewId={blockAction.view}
usableEnvData={usableEnvData()}
/>
</div>
</Dropdown>
);
}}
</For>
</Show>
</div>
);
};
Expand Down
Loading

0 comments on commit 690a5f5

Please sign in to comment.