Skip to content

Commit

Permalink
feat: Start line in code block
Browse files Browse the repository at this point in the history
  • Loading branch information
areknawo committed Jul 8, 2024
1 parent 2b54ff8 commit a94b7a2
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 53 deletions.
202 changes: 152 additions & 50 deletions apps/web/src/lib/editor/extensions/block-action-menu/options.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { mdiCubeOutline, mdiDotsVertical, mdiTrashCanOutline, mdiXml } from "@mdi/js";
import {
mdiCheck,
mdiChevronLeft,
mdiCubeOutline,
mdiDotsVertical,
mdiNumeric,
mdiTrashCanOutline,
mdiXml
} from "@mdi/js";
import { Component, For, Show, createEffect, createMemo, createSignal } from "solid-js";
import { SolidEditor } from "@vrite/tiptap-solid";
import { Node as PMNode } from "@tiptap/pm/model";
import { Range } from "@tiptap/core";
import { Dropdown, Tooltip, IconButton } from "#components/primitives";
import { Dynamic } from "solid-js/web";
import { Dropdown, Tooltip, IconButton, Input } from "#components/primitives";

interface OptionsDropdownProps {
editor: SolidEditor;
Expand All @@ -19,6 +28,17 @@ interface OptionProps {
pos: number;
}

type OptionMenu = Component<{ state: OptionProps; close(): void; goBack(): void }>;

interface NodeOption {
color?: "danger" | "success";
show?: (props: OptionProps) => boolean;
icon: string | ((props: OptionProps) => string);
label: string | ((props: OptionProps) => string);
menu?: OptionMenu;
onClick?(props: OptionProps): void;
}

const showCustomElementOption = (props: OptionProps): boolean => {
const element = props.editor.view.nodeDOM(props.pos);

Expand All @@ -30,16 +50,18 @@ const showCustomElementOption = (props: OptionProps): boolean => {

return false;
};
const options: Record<
string,
Array<{
color?: "danger" | "success";
show?: (props: OptionProps) => boolean;
icon: string | ((props: OptionProps) => string);
label: string | ((props: OptionProps) => string);
onClick(props: OptionProps): void;
}>
> = {
const removeBlockOption: NodeOption = {
color: "danger",
icon: mdiTrashCanOutline,
label: "Remove block",
onClick(props) {
props.editor.commands.deleteRange({
from: props.pos,
to: props.pos + props.node.nodeSize
});
}
};
const options: Record<string, NodeOption[]> = {
element: [
{
icon: (props) => {
Expand Down Expand Up @@ -84,21 +106,74 @@ const options: Record<
}
},
{
color: "danger",
icon: mdiTrashCanOutline,
label: "Remove element",
onClick(props) {
props.editor.commands.deleteRange({
from: props.pos,
to: props.pos + props.node.nodeSize
});
},
...removeBlockOption,
show: showCustomElementOption
}
],
codeBlock: [
{
icon: mdiNumeric,
label: "Set start line",
menu: (props) => {
const [startLine, setStartLine] = createSignal<number>(
props.state.node.attrs.startLine || 1
);

return (
<div class="flex flex-col items-start gap-0.5">
<div class="flex justify-center items-center">
<IconButton
path={mdiChevronLeft}
size="small"
class="m-0"
variant="text"
text="soft"
label="Start line"
onClick={props.goBack}
color="contrast"
/>
</div>
<div class="flex gap-1 text-base">
<Input
class="max-w-24 w-24 m-0"
wrapperClass="m-0"
type="number"
min={1}
max={9999}
value={`${startLine()}`}
setValue={(value) => {
let parsedValue = parseInt(`${value || 1}`);

if (!parsedValue || isNaN(parsedValue)) {
parsedValue = 1;
}

parsedValue = Math.min(Math.max(parsedValue, 1), 9999);
setStartLine(parsedValue);
}}
/>
<IconButton
path={mdiCheck}
class="m-0"
color="primary"
onClick={() => {
props.state.editor.commands.updateAttributes("codeBlock", {
startLine: startLine()
});
props.close();
}}
/>
</div>
</div>
);
}
},
removeBlockOption
]
};
const OptionsDropdown: Component<OptionsDropdownProps> = (props) => {
const [opened, setOpened] = createSignal(false);
const [activeMenu, setActiveMenu] = createSignal<OptionMenu | null>(null);
const availableOptions = createMemo(() => {
opened();

Expand All @@ -114,6 +189,11 @@ const OptionsDropdown: Component<OptionsDropdownProps> = (props) => {
setOpened(false);
}
});
createEffect(() => {
if (!opened()) {
setActiveMenu(() => null);
}
});

return (
<Show when={availableOptions().length}>
Expand All @@ -136,35 +216,57 @@ const OptionsDropdown: Component<OptionsDropdownProps> = (props) => {
}}
>
<div class="flex flex-col gap-1">
<For each={availableOptions()}>
{(option) => {
const path = (): string => {
if (!opened()) return "";

return typeof option.icon === "function" ? option.icon(props) : option.icon;
};
const label = (): string => {
if (!opened()) return "";

return typeof option.label === "function" ? option.label(props) : option.label;
};

return (
<IconButton
class="m-0 w-full justify-start"
text={option.color || "soft"}
color={option.color || "base"}
variant="text"
path={path()}
label={label()}
onClick={() => {
option.onClick(props);
setOpened(false);
}}
/>
);
}}
</For>
<Show
when={activeMenu()}
fallback={
<For each={availableOptions()}>
{(option) => {
const path = (): string => {
if (!opened()) return "";

return typeof option.icon === "function" ? option.icon(props) : option.icon;
};
const label = (): string => {
if (!opened()) return "";

return typeof option.label === "function" ? option.label(props) : option.label;
};

return (
<IconButton
class="m-0 w-full justify-start"
text={option.color || "soft"}
color={option.color || "base"}
variant="text"
path={path()}
label={label()}
onClick={() => {
if (option.menu) {
setActiveMenu(() => option.menu || null);

return;
}

option.onClick?.(props);
setOpened(false);
}}
/>
);
}}
</For>
}
>
<Dynamic
component={activeMenu()!}
state={props}
close={() => {
setOpened(false);
}}
goBack={() => {
setActiveMenu(() => null);
}}
/>
</Show>
</div>
</Dropdown>
</Show>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const repositionMenu = (editor: SolidEditor): void => {
let rangeFrom = selection.$from.pos;
let rangeTo = selection.$to.pos;

box.style.top = `${relativePos.top}px`;
box.style.top = `${relativePos.top - (childPos.height <= 32 ? (32 - childPos.height) / 2 : 0)}px`;
box.style.left = `${relativePos.left + parentPos.width}px`;
box.style.display = "block";

Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/lib/editor/extensions/code-block/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const CodeBlockView: Component<CodeBlockViewProps> = (props) => {
const selected = (): boolean => {
return state().selected;
};
const startLine = (): number => {
return parseInt(`${attrs().startLine || 1}`);
};
const repositionMenu = (): void => {
const referenceContainer = editorContainerRef();
const menuContainer = menuContainerRef();
Expand Down Expand Up @@ -169,6 +172,9 @@ const CodeBlockView: Component<CodeBlockViewProps> = (props) => {
fontSize: 13,
fontFamily: "JetBrainsMonoVariable",
tabSize: 2,
lineNumbers(lineNumber) {
return `${lineNumber + startLine() - 1}`;
},
insertSpaces: true,
readOnly: !state().editor.isEditable,
tabFocusMode: false,
Expand Down Expand Up @@ -357,6 +363,15 @@ const CodeBlockView: Component<CodeBlockViewProps> = (props) => {
}
}).observe(editorContainer);
});
createEffect(() => {
startLine();
codeEditor()?.updateOptions({
lineNumbers(lineNumber) {
return `${lineNumber + startLine() - 1}`;
}
});
codeEditor()?.render(true);
});
createEffect(
on(selected, (selected) => {
if (!selected) return;
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/primitives/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ const Dropdown: Component<DropdownProps> = (props) => {
createEffect(
on([() => props.overlay, opened], ([overlay, opened]) => {
const handleClick = (event: MouseEvent): void => {
if (!boxRef()?.contains(event.target as Node)) {
if (
!boxRef()?.contains(event.target as Node) &&
!["INPUT", "TEXTAREA"].includes(document.activeElement?.tagName || "")
) {
setOpened(false);
event.preventDefault();
event.stopPropagation();
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/primitives/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ const Overlay: Component<OverlayProps> = (props) => {
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
props.onOverlayClick?.();

if (!["INPUT", "TEXTAREA"].includes(document.activeElement?.tagName || "")) {
props.onOverlayClick?.();
}
}}
{...passedProps}
/>
Expand Down
14 changes: 14 additions & 0 deletions packages/editor/src/code-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface CodeBlockAttributes {
lang?: string;
title?: string;
meta?: string;
startLine?: number;
}
interface CodeBlockOptions {
inline: boolean;
Expand Down Expand Up @@ -63,6 +64,18 @@ const CodeBlock = Node.create<CodeBlockOptions>({
return element.getAttribute("data-title");
}
},
startLine: {
default: 1,
parseHTML: (element) => {
const value = parseInt(element.getAttribute("data-start-line") || "1");

if (!value || isNaN(value)) {
return 1;
}

return value;
}
},
meta: {
default: null,
parseHTML: (element) => {
Expand All @@ -87,6 +100,7 @@ const CodeBlock = Node.create<CodeBlockOptions>({
"code",
{
"class": node.attrs.lang ? `language-${node.attrs.lang}` : null,
"data-start-line": `${node.attrs.startLine}`,
"data-title": node.attrs.title,
"data-meta": node.attrs.meta
},
Expand Down

0 comments on commit a94b7a2

Please sign in to comment.