mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c0f375901 | |||
| 4894e92d6b | |||
| 5e6eebd0ec |
@@ -61,9 +61,6 @@ export default function codeMenuItems(
|
||||
: undefined,
|
||||
tooltip: dictionary.copy,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "edit_mermaid",
|
||||
icon: <EditIcon />,
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function useDictionary() {
|
||||
codeBlock: t("Code block"),
|
||||
codeCopied: t("Copied to clipboard"),
|
||||
codeInline: t("Code"),
|
||||
collapseCode: t("Collapse"),
|
||||
comment: t("Comment"),
|
||||
copy: t("Copy"),
|
||||
createLink: t("Create link"),
|
||||
@@ -54,6 +55,7 @@ export default function useDictionary() {
|
||||
replaceImage: t("Replace image"),
|
||||
em: t("Italic"),
|
||||
embedInvalidLink: t("Sorry, that link won’t work for this embed type"),
|
||||
expandCode: t("Expand"),
|
||||
file: t("File attachment"),
|
||||
pdf: t("Embed PDF"),
|
||||
enterLink: `${t("Enter a link")}…`,
|
||||
|
||||
@@ -1739,6 +1739,49 @@ mark {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.code-block.collapsed {
|
||||
max-height: 350px;
|
||||
clip-path: inset(0 -1px 0 0);
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
z-index: 1;
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
${
|
||||
props.theme.isDark
|
||||
? `
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(15, 17, 21, 0) 0%,
|
||||
rgba(15, 17, 21, 0.2) 40%,
|
||||
rgba(15, 17, 21, 0.4) 60%,
|
||||
rgba(15, 17, 21, 0.5) 85%,
|
||||
rgba(15, 17, 21, 0.6) 100%
|
||||
);
|
||||
`
|
||||
: `
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.2) 40%,
|
||||
rgba(255, 255, 255, 0.4) 60%,
|
||||
rgba(255, 255, 255, 0.5) 85%,
|
||||
rgba(255, 255, 255, 0.6) 100%
|
||||
);
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.code-block[data-language=none],
|
||||
.code-block[data-language=markdown] {
|
||||
pre code {
|
||||
@@ -1808,6 +1851,10 @@ mark {
|
||||
.mermaid-diagram-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-block-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block.with-line-wrap {
|
||||
@@ -1841,6 +1888,49 @@ mark {
|
||||
}
|
||||
}
|
||||
|
||||
.code-block-toggle {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 100px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: ${props.theme.fontFamily};
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
cursor: var(--pointer);
|
||||
z-index: 2;
|
||||
pointer-events: auto;
|
||||
transition: background 150ms ease-out;
|
||||
backdrop-filter: blur(4px);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-block.collapsed .code-block-toggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.code-block.tall:not(.collapsed):hover .code-block-toggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.mermaid-diagram-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -206,6 +206,7 @@ export function CodeHighlighting({
|
||||
state: {
|
||||
init: (_, { doc }) => DecorationSet.create(doc, []),
|
||||
apply: (transaction: Transaction, decorationSet, oldState, state) => {
|
||||
const collapsedChanged = transaction.getMeta("collapsed")?.changed;
|
||||
const nodeName = state.selection.$head.parent.type.name;
|
||||
const previousNodeName = oldState.selection.$head.parent.type.name;
|
||||
const codeBlockChanged =
|
||||
@@ -216,6 +217,7 @@ export function CodeHighlighting({
|
||||
const langLoaded = transaction.getMeta("codeHighlighting")?.langLoaded;
|
||||
|
||||
if (
|
||||
collapsedChanged ||
|
||||
!highlighted ||
|
||||
codeBlockChanged ||
|
||||
isPaste ||
|
||||
|
||||
@@ -7,13 +7,14 @@ import type {
|
||||
Schema,
|
||||
Node as ProsemirrorNode,
|
||||
} from "prosemirror-model";
|
||||
import type { Command, EditorState } from "prosemirror-state";
|
||||
import type { Command, EditorState, Transaction } from "prosemirror-state";
|
||||
import {
|
||||
NodeSelection,
|
||||
Plugin,
|
||||
PluginKey,
|
||||
TextSelection,
|
||||
} from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { toast } from "sonner";
|
||||
import type { Primitive } from "utility-types";
|
||||
@@ -44,12 +45,23 @@ import {
|
||||
import { isCode, isMermaid } from "../lib/isCode";
|
||||
import type { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { findNextNewline, findPreviousNewline } from "../queries/findNewlines";
|
||||
import { findParentNode } from "../queries/findParentNode";
|
||||
import {
|
||||
findParentNode,
|
||||
findParentNodeClosestToPos,
|
||||
} from "../queries/findParentNode";
|
||||
import { getMarkRange } from "../queries/getMarkRange";
|
||||
import { isInCode } from "../queries/isInCode";
|
||||
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
|
||||
import Node from "./Node";
|
||||
|
||||
const DEFAULT_LANGUAGE = "javascript";
|
||||
const TALL_HEIGHT = 350;
|
||||
|
||||
const CHEVRON_DOWN =
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4 7L8 11L12 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
const CHEVRON_UP =
|
||||
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M4 11L8 7L12 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
export default class CodeFence extends Node {
|
||||
constructor(options: {
|
||||
@@ -78,6 +90,11 @@ export default class CodeFence extends Node {
|
||||
default: false,
|
||||
validate: "boolean",
|
||||
},
|
||||
collapsed: {
|
||||
default: null,
|
||||
validate: (value: any) =>
|
||||
value === null || typeof value === "boolean",
|
||||
},
|
||||
},
|
||||
content: "text*",
|
||||
marks: "comment",
|
||||
@@ -109,20 +126,48 @@ export default class CodeFence extends Node {
|
||||
},
|
||||
},
|
||||
],
|
||||
toDOM: (node) => [
|
||||
"div",
|
||||
{
|
||||
class: `code-block ${
|
||||
node.attrs.wrap
|
||||
? "with-line-wrap"
|
||||
: this.showLineNumbers
|
||||
? "with-line-numbers"
|
||||
: ""
|
||||
toDOM: (node) => {
|
||||
const buttonIcon = document.createElement("i");
|
||||
buttonIcon.innerHTML = node.attrs.collapsed ? CHEVRON_DOWN : CHEVRON_UP;
|
||||
|
||||
return [
|
||||
"div",
|
||||
{
|
||||
class: `code-block ${
|
||||
node.attrs.wrap
|
||||
? "with-line-wrap"
|
||||
: this.showLineNumbers
|
||||
? "with-line-numbers"
|
||||
: ""
|
||||
} ${node.attrs.collapsed === null ? "" : node.attrs.collapsed ? "tall collapsed" : "tall"} `,
|
||||
"data-language": node.attrs.language,
|
||||
},
|
||||
["pre", ["code", { spellCheck: "false" }, 0]],
|
||||
[
|
||||
"button",
|
||||
{
|
||||
class: EditorStyleHelper.codeBlockToggle,
|
||||
contenteditable: "false",
|
||||
type: "button",
|
||||
"data-expand-label": this.options.dictionary.expandCode,
|
||||
"data-collapse-label": this.options.dictionary.collapseCode,
|
||||
},
|
||||
[
|
||||
"span",
|
||||
{
|
||||
class: "icon",
|
||||
},
|
||||
buttonIcon,
|
||||
],
|
||||
`
|
||||
${
|
||||
node.attrs.collapsed
|
||||
? this.options.dictionary.expandCode
|
||||
: this.options.dictionary.collapseCode
|
||||
}`,
|
||||
"data-language": node.attrs.language,
|
||||
},
|
||||
["pre", ["code", { spellCheck: "false" }, 0]],
|
||||
],
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -218,6 +263,18 @@ export default class CodeFence extends Node {
|
||||
|
||||
return false;
|
||||
},
|
||||
toggleCodeBlockCollapse: (): Command => (state, dispatch) => {
|
||||
const codeBlock = findParentNode(isCode)(state.selection);
|
||||
if (codeBlock && dispatch) {
|
||||
dispatch(
|
||||
state.tr.setNodeMarkup(codeBlock.pos, undefined, {
|
||||
...codeBlock.node.attrs,
|
||||
collapsed: !codeBlock.node.attrs.collapsed,
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,6 +420,127 @@ export default class CodeFence extends Node {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Plugin({
|
||||
key: new PluginKey("collapse-toggle"),
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousedown: (view, event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest(
|
||||
`.${EditorStyleHelper.codeBlockToggle}`
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (!button) {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const codeBlockEl = button.closest<HTMLElement>(".code-block");
|
||||
const codeEl = codeBlockEl?.querySelector("code");
|
||||
if (!codeEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const innerPos = view.posAtDOM(codeEl, 0);
|
||||
const $pos = view.state.doc.resolve(innerPos);
|
||||
const codeBlock = findParentNodeClosestToPos($pos, isCode);
|
||||
|
||||
if (!codeBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setNodeMarkup(codeBlock.pos, undefined, {
|
||||
...codeBlock.node.attrs,
|
||||
collapsed: !codeBlock.node.attrs.collapsed,
|
||||
})
|
||||
.setMeta("collapsed", { changed: true })
|
||||
);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Plugin({
|
||||
key: new PluginKey("auto-collapse-code-block"),
|
||||
view: () => {
|
||||
let initialized = false;
|
||||
let processing = false;
|
||||
|
||||
const processNode = (
|
||||
view: EditorView,
|
||||
tr: Transaction,
|
||||
node: ProsemirrorNode,
|
||||
pos: number,
|
||||
initialized: boolean
|
||||
) => {
|
||||
if (!isCode(node)) {return false;}
|
||||
|
||||
const dom = view.nodeDOM(pos) as HTMLElement | null;
|
||||
if (!dom) {return false;}
|
||||
|
||||
let modified = false;
|
||||
|
||||
if (node.attrs.collapsed === null) {
|
||||
const collapsedValue =
|
||||
dom.scrollHeight > TALL_HEIGHT ? false : null;
|
||||
if (collapsedValue !== node.attrs.collapsed) {
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
collapsed: collapsedValue,
|
||||
});
|
||||
modified = true;
|
||||
}
|
||||
} else if (dom.scrollHeight < TALL_HEIGHT) {
|
||||
if (node.attrs.collapsed !== null) {
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
collapsed: null,
|
||||
});
|
||||
modified = true;
|
||||
}
|
||||
} else if (!initialized && node.attrs.collapsed !== true) {
|
||||
tr.setNodeMarkup(pos, undefined, {
|
||||
...node.attrs,
|
||||
collapsed: true,
|
||||
});
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return modified;
|
||||
};
|
||||
|
||||
return {
|
||||
update: (view, prevState) => {
|
||||
if (processing) {return;}
|
||||
processing = true;
|
||||
|
||||
const { state } = view;
|
||||
const docChanged = !prevState || state.doc !== prevState.doc;
|
||||
|
||||
if (docChanged || !initialized) {
|
||||
const tr = state.tr;
|
||||
let modified = false;
|
||||
|
||||
state.doc.descendants((node, pos) => {
|
||||
modified =
|
||||
processNode(view, tr, node, pos, initialized) || modified;
|
||||
});
|
||||
|
||||
if (modified) {
|
||||
view.dispatch(tr);
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
processing = false;
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
].filter(Boolean) as Plugin[];
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export class EditorStyleHelper {
|
||||
|
||||
static readonly codeWord = "code-word";
|
||||
|
||||
static readonly codeBlockToggle = "code-block-toggle";
|
||||
|
||||
// Diffs
|
||||
|
||||
static readonly diffInsertion = "diff-insertion";
|
||||
|
||||
Reference in New Issue
Block a user