mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9706ba2e01 | |||
| de6e1cc943 | |||
| 6bde4342b3 | |||
| d22fbe9f85 | |||
| 3094bf88ad |
@@ -90,12 +90,19 @@ function usePosition({
|
||||
} as DOMRect);
|
||||
|
||||
// position at the top right of code blocks
|
||||
const codeBlock = findParentNode(isCode)(view.state.selection);
|
||||
const isCodeNodeSelection =
|
||||
selection instanceof NodeSelection && isCode(selection.node);
|
||||
const codeBlock = isCodeNodeSelection
|
||||
? { pos: selection.from, node: selection.node }
|
||||
: findParentNode(isCode)(view.state.selection);
|
||||
const noticeBlock = findParentNode(
|
||||
(node) => node.type.name === "container_notice"
|
||||
)(view.state.selection);
|
||||
|
||||
if ((codeBlock || noticeBlock) && view.state.selection.empty) {
|
||||
if (
|
||||
(codeBlock || noticeBlock) &&
|
||||
(view.state.selection.empty || isCodeNodeSelection)
|
||||
) {
|
||||
const position = codeBlock
|
||||
? codeBlock.pos
|
||||
: noticeBlock
|
||||
|
||||
@@ -240,7 +240,10 @@ export function SelectionToolbar(props: Props) {
|
||||
let items: MenuItem[] = [];
|
||||
let align: "center" | "start" | "end" = "center";
|
||||
|
||||
if (isCodeSelection && selection.empty) {
|
||||
if (
|
||||
isCodeSelection &&
|
||||
(selection.empty || selection instanceof NodeSelection)
|
||||
) {
|
||||
items = getCodeMenuItems(state, readOnly, dictionary);
|
||||
align = "end";
|
||||
} else if (isTableSelected(state)) {
|
||||
|
||||
@@ -45,9 +45,8 @@ export default class SelectionToolbarExtension extends Extension {
|
||||
}
|
||||
|
||||
if (
|
||||
(isNodeActive(schema.nodes.code_block)(state) ||
|
||||
isNodeActive(schema.nodes.code_fence)(state)) &&
|
||||
selection.from > 0
|
||||
isNodeActive(schema.nodes.code_block)(state) ||
|
||||
isNodeActive(schema.nodes.code_fence)(state)
|
||||
) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CopyIcon, EditIcon, ExpandedIcon } from "outline-icons";
|
||||
import type { Node as ProseMirrorNode } from "prosemirror-model";
|
||||
import { NodeSelection } from "prosemirror-state";
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import {
|
||||
pluginKey as mermaidPluginKey,
|
||||
@@ -19,7 +20,10 @@ export default function codeMenuItems(
|
||||
readOnly: boolean | undefined,
|
||||
dictionary: Dictionary
|
||||
): MenuItem[] {
|
||||
const node = state.selection.$from.node();
|
||||
const node =
|
||||
state.selection instanceof NodeSelection
|
||||
? state.selection.node
|
||||
: state.selection.$from.node();
|
||||
|
||||
const frequentLanguages = getFrequentCodeLanguages();
|
||||
|
||||
|
||||
@@ -1770,6 +1770,15 @@ mark {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
|
||||
& + .mermaid-diagram-wrapper:not(.empty) {
|
||||
cursor: zoom-in;
|
||||
outline: 2px solid ${props.theme.selected};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror[contenteditable="false"] .code-block[data-language=mermaid],
|
||||
@@ -1777,7 +1786,7 @@ mark {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
& + .mermaid-diagram-wrapper {
|
||||
& + .mermaid-diagram-wrapper:not(.empty) {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import type MermaidUnsafe from "mermaid";
|
||||
import type { Node } from "prosemirror-model";
|
||||
import type { Transaction } from "prosemirror-state";
|
||||
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
||||
import { NodeSelection, Plugin, PluginKey } from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { toast } from "sonner";
|
||||
import { isCode, isMermaid } from "../lib/isCode";
|
||||
@@ -160,12 +160,15 @@ function getNewState({
|
||||
doc,
|
||||
pluginState,
|
||||
editor,
|
||||
autoEditEmpty = false,
|
||||
}: {
|
||||
doc: Node;
|
||||
pluginState: MermaidState;
|
||||
editor: Editor;
|
||||
autoEditEmpty?: boolean;
|
||||
}): MermaidState {
|
||||
const decorations: Decoration[] = [];
|
||||
let newEditingId: string | undefined;
|
||||
|
||||
// Find all blocks that represent Mermaid diagrams (supports both "mermaid" and "mermaidjs")
|
||||
const blocks = findBlockNodes(doc).filter((item) => isMermaid(item.node));
|
||||
@@ -182,9 +185,19 @@ function getNewState({
|
||||
block
|
||||
);
|
||||
|
||||
const isNewBlock = !bestDecoration;
|
||||
const renderer: MermaidRenderer =
|
||||
bestDecoration?.spec?.renderer ?? new MermaidRenderer(editor);
|
||||
|
||||
// Auto-enter edit mode for newly created empty mermaid diagrams
|
||||
if (
|
||||
autoEditEmpty &&
|
||||
isNewBlock &&
|
||||
block.node.textContent.trim().length === 0
|
||||
) {
|
||||
newEditingId = renderer.diagramId;
|
||||
}
|
||||
|
||||
const diagramDecoration = Decoration.widget(
|
||||
block.pos + block.node.nodeSize,
|
||||
() => {
|
||||
@@ -214,6 +227,7 @@ function getNewState({
|
||||
|
||||
return {
|
||||
...pluginState,
|
||||
...(newEditingId !== undefined ? { editingId: newEditingId } : {}),
|
||||
decorationSet: DecorationSet.create(doc, decorations),
|
||||
};
|
||||
}
|
||||
@@ -307,12 +321,48 @@ export default function Mermaid({
|
||||
doc: transaction.doc,
|
||||
pluginState: nextPluginState,
|
||||
editor,
|
||||
autoEditEmpty:
|
||||
codeBlockChanged &&
|
||||
transaction.docChanged &&
|
||||
!isPaste &&
|
||||
!isRemoteTransaction(transaction),
|
||||
});
|
||||
}
|
||||
|
||||
return nextPluginState;
|
||||
},
|
||||
},
|
||||
appendTransaction(_transactions, _oldState, newState) {
|
||||
const { selection } = newState;
|
||||
if (selection instanceof NodeSelection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codeBlock = findParentNode(isCode)(selection);
|
||||
if (!codeBlock || !isMermaid(codeBlock.node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mermaidState = pluginKey.getState(newState) as MermaidState;
|
||||
const decorations = mermaidState?.decorationSet.find(
|
||||
codeBlock.pos,
|
||||
codeBlock.pos + codeBlock.node.nodeSize
|
||||
);
|
||||
const nodeDecoration = decorations?.find(
|
||||
(d) => d.spec.diagramId && d.from === codeBlock.pos
|
||||
);
|
||||
|
||||
if (
|
||||
nodeDecoration?.spec.diagramId &&
|
||||
mermaidState?.editingId === nodeDecoration.spec.diagramId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return newState.tr.setSelection(
|
||||
NodeSelection.create(newState.doc, codeBlock.pos)
|
||||
);
|
||||
},
|
||||
view: (view) => {
|
||||
view.dispatch(view.state.tr.setMeta(pluginKey, { loaded: true }));
|
||||
return {};
|
||||
@@ -334,12 +384,50 @@ export default function Mermaid({
|
||||
|
||||
return true;
|
||||
},
|
||||
mousedown(view, event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const diagram = target?.closest(".mermaid-diagram-wrapper");
|
||||
if (!diagram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const codeBlock = diagram.previousElementSibling;
|
||||
if (!codeBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pos = view.posAtDOM(codeBlock, 0);
|
||||
const $pos = view.state.doc.resolve(pos);
|
||||
const nodePos = $pos.before();
|
||||
const node = view.state.doc.nodeAt(nodePos);
|
||||
|
||||
const isSelected =
|
||||
view.state.selection instanceof NodeSelection &&
|
||||
view.state.selection.from === nodePos;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (isSelected || editor.props.readOnly) {
|
||||
// Already selected or read-only, open lightbox
|
||||
if (node && node.textContent.trim().length > 0) {
|
||||
editor.updateActiveLightboxImage(
|
||||
LightboxImageFactory.createLightboxImage(view, nodePos)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// First click, select the node
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(NodeSelection.create(view.state.doc, nodePos))
|
||||
.scrollIntoView()
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
mouseup(view, event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const diagram = target?.closest(".mermaid-diagram-wrapper");
|
||||
const codeBlock = diagram?.previousElementSibling;
|
||||
|
||||
if (!codeBlock) {
|
||||
if (!diagram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -356,85 +444,6 @@ export default function Mermaid({
|
||||
} catch (_err) {
|
||||
toast.error(dictionary.openLinkError);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const pos = view.posAtDOM(codeBlock, 0);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (diagram && event.detail === 1) {
|
||||
const { selection: textSelection } = view.state;
|
||||
const $pos = view.state.doc.resolve(pos);
|
||||
const selected =
|
||||
textSelection.from >= $pos.start() &&
|
||||
textSelection.to <= $pos.end();
|
||||
if (selected || editor.props.readOnly) {
|
||||
editor.updateActiveLightboxImage(
|
||||
LightboxImageFactory.createLightboxImage(view, $pos.before())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// select node
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(TextSelection.near(view.state.doc.resolve(pos)))
|
||||
.scrollIntoView()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
keydown: (view, event) => {
|
||||
switch (event.key) {
|
||||
case "ArrowDown": {
|
||||
const { selection } = view.state;
|
||||
const $pos = view.state.doc.resolve(
|
||||
Math.min(selection.from + 1, view.state.doc.nodeSize)
|
||||
);
|
||||
const nextBlock = $pos.nodeAfter;
|
||||
|
||||
if (nextBlock && isMermaid(nextBlock)) {
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(
|
||||
TextSelection.near(
|
||||
view.state.doc.resolve(selection.to + 1)
|
||||
)
|
||||
)
|
||||
.scrollIntoView()
|
||||
);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
const { selection } = view.state;
|
||||
const $pos = view.state.doc.resolve(
|
||||
Math.max(0, selection.from - 1)
|
||||
);
|
||||
const prevBlock = $pos.nodeBefore;
|
||||
|
||||
if (prevBlock && isMermaid(prevBlock)) {
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(
|
||||
TextSelection.near(
|
||||
view.state.doc.resolve(selection.from - 2)
|
||||
)
|
||||
)
|
||||
.scrollIntoView()
|
||||
);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -8,7 +8,12 @@ import type {
|
||||
Node as ProsemirrorNode,
|
||||
} from "prosemirror-model";
|
||||
import type { Command, EditorState } from "prosemirror-state";
|
||||
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
||||
import {
|
||||
NodeSelection,
|
||||
Plugin,
|
||||
PluginKey,
|
||||
TextSelection,
|
||||
} from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { toast } from "sonner";
|
||||
import type { Primitive } from "utility-types";
|
||||
@@ -124,7 +129,11 @@ export default class CodeFence extends Node {
|
||||
});
|
||||
},
|
||||
edit_mermaid: (): Command => (state, dispatch) => {
|
||||
const codeBlock = findParentNode(isCode)(state.selection);
|
||||
const codeBlock =
|
||||
state.selection instanceof NodeSelection &&
|
||||
isCode(state.selection.node)
|
||||
? { pos: state.selection.from, node: state.selection.node }
|
||||
: findParentNode(isCode)(state.selection);
|
||||
if (!codeBlock || !isMermaid(codeBlock.node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user