Rebuilding code block menus (#5569)
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { wrapIn, lift } from "prosemirror-commands";
|
||||
import { NodeType } from "prosemirror-model";
|
||||
import { Command } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import isNodeActive from "../queries/isNodeActive";
|
||||
|
||||
export default function toggleWrap(
|
||||
type: NodeType,
|
||||
attrs?: Record<string, any>
|
||||
attrs?: Record<string, Primitive>
|
||||
): Command {
|
||||
return (state, dispatch) => {
|
||||
const isActive = isNodeActive(type)(state);
|
||||
|
||||
@@ -302,8 +302,8 @@ const ResizeLeft = styled.div<{ $dragging: boolean }>`
|
||||
height: 15%;
|
||||
min-height: 20px;
|
||||
border-radius: 4px;
|
||||
background: ${s("toolbarBackground")};
|
||||
box-shadow: 0 0 0 1px ${s("toolbarItem")};
|
||||
background: ${s("menuBackground")};
|
||||
box-shadow: 0 0 0 1px ${s("textSecondary")};
|
||||
opacity: 0.75;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { darken, lighten, transparentize } from "polished";
|
||||
import { lighten, transparentize } from "polished";
|
||||
import styled, { DefaultTheme, css } from "styled-components";
|
||||
|
||||
export type Props = {
|
||||
@@ -1033,106 +1033,25 @@ mark {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.code-actions,
|
||||
.notice-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.notice-actions {
|
||||
${props.rtl ? "left" : "right"}: 8px;
|
||||
}
|
||||
|
||||
.code-block,
|
||||
.notice-block {
|
||||
.code-block {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
select,
|
||||
button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: ${props.theme.buttonNeutralBackground};
|
||||
color: ${props.theme.buttonNeutralText};
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
|
||||
props.theme.buttonNeutralBorder
|
||||
} 0 0 0 1px inset;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
cursor: var(--pointer);
|
||||
user-select: none;
|
||||
appearance: none !important;
|
||||
padding: 6px 8px;
|
||||
display: none;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${darken(0.05, props.theme.buttonNeutralBackground)};
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${
|
||||
props.theme.buttonNeutralBorder
|
||||
} 0 0 0 1px inset;
|
||||
}
|
||||
.code-block[data-language=mermaidjs] {
|
||||
pre {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin-bottom: -12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.03087 9C8.20119 9 7.73238 9.95209 8.23824 10.6097L11.2074 14.4696C11.6077 14.99 12.3923 14.99 12.7926 14.4696L15.7618 10.6097C16.2676 9.95209 15.7988 9 14.9691 9L9.03087 9Z" fill="currentColor"/> </svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:hover {
|
||||
select {
|
||||
display: ${props.readOnly ? "none" : "inline"};
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
select:focus,
|
||||
select:active,
|
||||
button:focus,
|
||||
button:active {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
button.show-source-button {
|
||||
&:not(.code-active) {
|
||||
display: none;
|
||||
}
|
||||
button.show-diagram-button {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
&.code-hidden {
|
||||
button,
|
||||
select,
|
||||
button.show-diagram-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button.show-source-button {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.ProseMirror[contenteditable="false"] .code-block[data-language=mermaidjs] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.code-block.with-line-numbers {
|
||||
@@ -1144,7 +1063,7 @@ mark {
|
||||
content: attr(data-line-numbers);
|
||||
position: absolute;
|
||||
padding-left: 1em;
|
||||
left: 0;
|
||||
left: 1px;
|
||||
top: calc(1px + 0.75em);
|
||||
width: calc(var(--line-number-gutter-width,0) * 1em + .25em);
|
||||
word-break: break-all;
|
||||
@@ -1175,8 +1094,9 @@ mark {
|
||||
font-family: ${props.theme.fontFamily};
|
||||
}
|
||||
|
||||
&.diagram-hidden {
|
||||
display: none;
|
||||
&.parse-error {
|
||||
font-size: 14px;
|
||||
color: ${props.theme.brand.red};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Primitive } from "utility-types";
|
||||
import { IntegrationType } from "../../types";
|
||||
import type { IntegrationSettings } from "../../types";
|
||||
import { urlRegex } from "../../utils/urls";
|
||||
@@ -76,7 +77,7 @@ export class EmbedDescriptor {
|
||||
keywords?: string;
|
||||
tooltip?: string;
|
||||
defaultHidden?: boolean;
|
||||
attrs?: Record<string, any>;
|
||||
attrs?: Record<string, Primitive>;
|
||||
visible?: boolean;
|
||||
active?: (state: EditorState) => boolean;
|
||||
component: typeof React.Component | React.FC<any>;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
import { Plugin, PluginKey, Transaction } from "prosemirror-state";
|
||||
import {
|
||||
Plugin,
|
||||
PluginKey,
|
||||
TextSelection,
|
||||
Transaction,
|
||||
} from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { isCode } from "../lib/isCode";
|
||||
import { findBlockNodes } from "../queries/findChildren";
|
||||
|
||||
type MermaidState = {
|
||||
decorationSet: DecorationSet;
|
||||
diagramVisibility: Record<number, boolean>;
|
||||
isDark: boolean;
|
||||
};
|
||||
|
||||
@@ -28,23 +33,14 @@ function getNewState({
|
||||
);
|
||||
|
||||
blocks.forEach((block) => {
|
||||
const diagramDecorationPos = block.pos + block.node.nodeSize;
|
||||
const existingDecorations = pluginState.decorationSet.find(
|
||||
block.pos,
|
||||
diagramDecorationPos
|
||||
block.pos + block.node.nodeSize
|
||||
);
|
||||
|
||||
// Attempt to find the existing diagramId from the decoration, or assign
|
||||
// a new one if none exists yet.
|
||||
let diagramId = existingDecorations[0]?.spec["diagramId"];
|
||||
if (diagramId === undefined) {
|
||||
diagramId = uuidv4();
|
||||
}
|
||||
|
||||
// Make the diagram visible by default if it contains source code
|
||||
if (pluginState.diagramVisibility[diagramId] === undefined) {
|
||||
pluginState.diagramVisibility[diagramId] = !!block.node.textContent;
|
||||
}
|
||||
const diagramId = existingDecorations[0]?.spec["diagramId"] ?? uuidv4();
|
||||
|
||||
const diagramDecoration = Decoration.widget(
|
||||
block.pos + block.node.nodeSize,
|
||||
@@ -55,13 +51,6 @@ function getNewState({
|
||||
element.id = elementId;
|
||||
element.classList.add("mermaid-diagram-wrapper");
|
||||
|
||||
if (pluginState.diagramVisibility[diagramId] === false) {
|
||||
element.classList.add("diagram-hidden");
|
||||
return element;
|
||||
} else {
|
||||
element.classList.remove("diagram-hidden");
|
||||
}
|
||||
|
||||
void import("mermaid").then((module) => {
|
||||
module.default.initialize({
|
||||
startOnLoad: true,
|
||||
@@ -78,16 +67,13 @@ function getNewState({
|
||||
"mermaid-diagram-" + diagramId,
|
||||
block.node.textContent,
|
||||
(svgCode) => {
|
||||
element.classList.remove("parse-error");
|
||||
element.innerHTML = svgCode;
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const errorNode = document.getElementById(
|
||||
"d" + "mermaid-diagram-" + diagramId
|
||||
);
|
||||
if (errorNode) {
|
||||
element.appendChild(errorNode);
|
||||
}
|
||||
element.innerText = "Error rendering diagram";
|
||||
element.classList.add("parse-error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,15 +84,10 @@ function getNewState({
|
||||
}
|
||||
);
|
||||
|
||||
const attributes = { "data-diagram-id": "" + diagramId };
|
||||
if (pluginState.diagramVisibility[diagramId] !== false) {
|
||||
attributes["class"] = "code-hidden";
|
||||
}
|
||||
|
||||
const diagramIdDecoration = Decoration.node(
|
||||
block.pos,
|
||||
block.pos + block.node.nodeSize,
|
||||
attributes,
|
||||
{},
|
||||
{
|
||||
diagramId,
|
||||
}
|
||||
@@ -118,7 +99,6 @@ function getNewState({
|
||||
|
||||
return {
|
||||
decorationSet: DecorationSet.create(doc, decorations),
|
||||
diagramVisibility: pluginState.diagramVisibility,
|
||||
isDark: pluginState.isDark,
|
||||
};
|
||||
}
|
||||
@@ -130,15 +110,12 @@ export default function Mermaid({
|
||||
name: string;
|
||||
isDark: boolean;
|
||||
}) {
|
||||
let diagramShown = false;
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey("mermaid"),
|
||||
state: {
|
||||
init: (_, { doc }) => {
|
||||
const pluginState: MermaidState = {
|
||||
decorationSet: DecorationSet.create(doc, []),
|
||||
diagramVisibility: {},
|
||||
isDark,
|
||||
};
|
||||
return pluginState;
|
||||
@@ -154,28 +131,15 @@ export default function Mermaid({
|
||||
const codeBlockChanged =
|
||||
transaction.docChanged && [nodeName, previousNodeName].includes(name);
|
||||
const ySyncEdit = !!transaction.getMeta("y-sync$");
|
||||
const mermaidMeta = transaction.getMeta("mermaid");
|
||||
const themeMeta = transaction.getMeta("theme");
|
||||
const diagramToggled = mermaidMeta?.toggleDiagram !== undefined;
|
||||
const mermaidMeta = transaction.getMeta("mermaid");
|
||||
const themeToggled = themeMeta?.isDark !== undefined;
|
||||
|
||||
if (themeToggled) {
|
||||
pluginState.isDark = themeMeta.isDark;
|
||||
}
|
||||
|
||||
if (diagramToggled) {
|
||||
pluginState.diagramVisibility[mermaidMeta.toggleDiagram] =
|
||||
!pluginState.diagramVisibility[mermaidMeta.toggleDiagram];
|
||||
}
|
||||
|
||||
if (
|
||||
!diagramShown ||
|
||||
themeToggled ||
|
||||
codeBlockChanged ||
|
||||
diagramToggled ||
|
||||
ySyncEdit
|
||||
) {
|
||||
diagramShown = true;
|
||||
if (mermaidMeta || themeToggled || codeBlockChanged || ySyncEdit) {
|
||||
return getNewState({
|
||||
doc: transaction.doc,
|
||||
name,
|
||||
@@ -188,28 +152,100 @@ export default function Mermaid({
|
||||
transaction.mapping,
|
||||
transaction.doc
|
||||
),
|
||||
diagramVisibility: pluginState.diagramVisibility,
|
||||
isDark: pluginState.isDark,
|
||||
};
|
||||
},
|
||||
},
|
||||
view: (view) => {
|
||||
if (!diagramShown) {
|
||||
// we don't draw diagrams on code blocks on the first render as part of mounting
|
||||
// as it's expensive (relative to the rest of the document). Instead let
|
||||
// it render without a diagram and then trigger a defered render of Mermaid
|
||||
// by updating the plugins metadata
|
||||
setTimeout(() => {
|
||||
view.dispatch(view.state.tr.setMeta("mermaid", { loaded: true }));
|
||||
}, 10);
|
||||
}
|
||||
|
||||
view.dispatch(view.state.tr.setMeta("mermaid", { loaded: true }));
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state)?.decorationSet;
|
||||
},
|
||||
handleDOMEvents: {
|
||||
mousedown(view, event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const diagram = target?.closest(".mermaid-diagram-wrapper");
|
||||
const codeBlock = diagram?.previousElementSibling;
|
||||
|
||||
if (!codeBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pos = view.posAtDOM(codeBlock, 0);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// select node
|
||||
if (diagram && event.detail === 1) {
|
||||
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(selection.from + 1);
|
||||
const nextBlock = $pos.nodeAfter;
|
||||
|
||||
if (
|
||||
nextBlock &&
|
||||
isCode(nextBlock) &&
|
||||
nextBlock.attrs.language === "mermaidjs"
|
||||
) {
|
||||
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(selection.from - 1);
|
||||
const prevBlock = $pos.nodeBefore;
|
||||
|
||||
if (
|
||||
prevBlock &&
|
||||
isCode(prevBlock) &&
|
||||
prevBlock.attrs.language === "mermaidjs"
|
||||
) {
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(
|
||||
TextSelection.near(
|
||||
view.state.doc.resolve(selection.from - 2)
|
||||
)
|
||||
)
|
||||
.scrollIntoView()
|
||||
);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export default class PasteHandler extends Extension {
|
||||
// was pasted.
|
||||
const vscodeMeta = vscode ? JSON.parse(vscode) : undefined;
|
||||
const pasteCodeLanguage = vscodeMeta?.mode;
|
||||
const supportsCodeBlock = !!view.state.schema.nodes.code_fence;
|
||||
const supportsCodeBlock = !!view.state.schema.nodes.code_block;
|
||||
|
||||
if (
|
||||
supportsCodeBlock &&
|
||||
@@ -140,7 +140,7 @@ export default class PasteHandler extends Extension {
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.replaceSelectionWith(
|
||||
view.state.schema.nodes.code_fence.create({
|
||||
view.state.schema.nodes.code_block.create({
|
||||
language: Object.keys(LANGUAGES).includes(vscodeMeta.mode)
|
||||
? vscodeMeta.mode
|
||||
: null,
|
||||
|
||||
@@ -6,7 +6,7 @@ import refractor from "refractor/core";
|
||||
import { findBlockNodes } from "../queries/findChildren";
|
||||
|
||||
export const LANGUAGES = {
|
||||
none: "None", // additional entry to disable highlighting
|
||||
none: "Plain text", // additional entry to disable highlighting
|
||||
bash: "Bash",
|
||||
css: "CSS",
|
||||
clike: "C",
|
||||
|
||||
@@ -2,9 +2,10 @@ import { PluginSimple } from "markdown-it";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { NodeType, MarkType, Schema } from "prosemirror-model";
|
||||
import { Command, Plugin } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import { Editor } from "../../../app/editor";
|
||||
|
||||
export type CommandFactory = (attrs?: Record<string, any>) => Command;
|
||||
export type CommandFactory = (attrs?: Record<string, Primitive>) => Command;
|
||||
|
||||
export default class Extension {
|
||||
options: any;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { keymap } from "prosemirror-keymap";
|
||||
import { MarkdownParser } from "prosemirror-markdown";
|
||||
import { Schema } from "prosemirror-model";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { Primitive } from "utility-types";
|
||||
import { Editor } from "~/editor";
|
||||
import Mark from "../marks/Mark";
|
||||
import Node from "../nodes/Node";
|
||||
@@ -203,7 +204,7 @@ export default class ExtensionManager {
|
||||
|
||||
const apply = (
|
||||
callback: CommandFactory,
|
||||
attrs: Record<string, any>
|
||||
attrs: Record<string, Primitive>
|
||||
) => {
|
||||
if (!view.editable && !extension.allowInReadOnly) {
|
||||
return false;
|
||||
@@ -214,10 +215,10 @@ export default class ExtensionManager {
|
||||
|
||||
const handle = (_name: string, _value: CommandFactory) => {
|
||||
if (Array.isArray(_value)) {
|
||||
commands[_name] = (attrs: Record<string, any>) =>
|
||||
commands[_name] = (attrs: Record<string, Primitive>) =>
|
||||
_value.forEach((callback) => apply(callback, attrs));
|
||||
} else if (typeof _value === "function") {
|
||||
commands[_name] = (attrs: Record<string, any>) =>
|
||||
commands[_name] = (attrs: Record<string, Primitive>) =>
|
||||
apply(_value, attrs);
|
||||
}
|
||||
};
|
||||
|
||||
5
shared/editor/lib/isCode.ts
Normal file
5
shared/editor/lib/isCode.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Node } from "prosemirror-model";
|
||||
|
||||
export function isCode(node: Node) {
|
||||
return node.type.name === "code_block" || node.type.name === "code_fence";
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { NodeSelection } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Primitive } from "utility-types";
|
||||
import { bytesToHumanReadable } from "../../utils/files";
|
||||
import { sanitizeUrl } from "../../utils/urls";
|
||||
import toggleWrap from "../commands/toggleWrap";
|
||||
@@ -102,7 +103,7 @@ export default class Attachment extends Node {
|
||||
};
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return (attrs: Record<string, any>) => toggleWrap(type, attrs);
|
||||
return (attrs: Record<string, Primitive>) => toggleWrap(type, attrs);
|
||||
}
|
||||
|
||||
toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) {
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Schema,
|
||||
Node as ProsemirrorNode,
|
||||
} from "prosemirror-model";
|
||||
import { Selection, Plugin, PluginKey } from "prosemirror-state";
|
||||
import { Command, Plugin, PluginKey } from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import refractor from "refractor/core";
|
||||
import bash from "refractor/lang/bash";
|
||||
import clike from "refractor/lang/clike";
|
||||
@@ -49,6 +50,7 @@ import visualbasic from "refractor/lang/visual-basic";
|
||||
import yaml from "refractor/lang/yaml";
|
||||
import zig from "refractor/lang/zig";
|
||||
|
||||
import { Primitive } from "utility-types";
|
||||
import { Dictionary } from "~/hooks/useDictionary";
|
||||
import { UserPreferences } from "../../types";
|
||||
import Storage from "../../utils/Storage";
|
||||
@@ -61,7 +63,9 @@ import {
|
||||
import toggleBlockType from "../commands/toggleBlockType";
|
||||
import Mermaid from "../extensions/Mermaid";
|
||||
import Prism, { LANGUAGES } from "../extensions/Prism";
|
||||
import { isCode } from "../lib/isCode";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { findParentNode } from "../queries/findParentNode";
|
||||
import isInCode from "../queries/isInCode";
|
||||
import Node from "./Node";
|
||||
|
||||
@@ -157,77 +161,38 @@ export default class CodeFence extends Node {
|
||||
}),
|
||||
},
|
||||
],
|
||||
toDOM: (node) => {
|
||||
let actions;
|
||||
if (typeof document !== "undefined") {
|
||||
const button = document.createElement("button");
|
||||
button.innerText = this.options.dictionary.copy;
|
||||
button.type = "button";
|
||||
button.addEventListener("click", this.handleCopyToClipboard);
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.addEventListener("change", this.handleLanguageChange);
|
||||
|
||||
actions = document.createElement("div");
|
||||
actions.className = "code-actions";
|
||||
actions.appendChild(select);
|
||||
actions.appendChild(button);
|
||||
|
||||
this.languageOptions.forEach(([key, label]) => {
|
||||
const option = document.createElement("option");
|
||||
const value = key === "none" ? "" : key;
|
||||
option.value = value;
|
||||
option.innerText = label;
|
||||
option.selected = node.attrs.language === value;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// For the Mermaid language we add an extra button to toggle between
|
||||
// source code and a rendered diagram view.
|
||||
if (node.attrs.language === "mermaidjs") {
|
||||
const showSourceButton = document.createElement("button");
|
||||
showSourceButton.innerText = this.options.dictionary.showSource;
|
||||
showSourceButton.type = "button";
|
||||
showSourceButton.classList.add("show-source-button");
|
||||
showSourceButton.addEventListener(
|
||||
"click",
|
||||
this.handleToggleDiagram
|
||||
);
|
||||
actions.prepend(showSourceButton);
|
||||
|
||||
const showDiagramButton = document.createElement("button");
|
||||
showDiagramButton.innerText = this.options.dictionary.showDiagram;
|
||||
showDiagramButton.type = "button";
|
||||
showDiagramButton.classList.add("show-digram-button");
|
||||
showDiagramButton.addEventListener(
|
||||
"click",
|
||||
this.handleToggleDiagram
|
||||
);
|
||||
actions.prepend(showDiagramButton);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"div",
|
||||
{
|
||||
class: `code-block ${
|
||||
this.showLineNumbers ? "with-line-numbers" : ""
|
||||
}`,
|
||||
"data-language": node.attrs.language,
|
||||
},
|
||||
...(actions ? [["div", { contentEditable: "false" }, actions]] : []),
|
||||
["pre", ["code", { spellCheck: "false" }, 0]],
|
||||
];
|
||||
},
|
||||
toDOM: (node) => [
|
||||
"div",
|
||||
{
|
||||
class: `code-block ${
|
||||
this.showLineNumbers ? "with-line-numbers" : ""
|
||||
}`,
|
||||
"data-language": node.attrs.language,
|
||||
},
|
||||
["pre", ["code", { spellCheck: "false" }, 0]],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
commands({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
return (attrs: Record<string, any>) =>
|
||||
toggleBlockType(type, schema.nodes.paragraph, {
|
||||
language: Storage.get(PERSISTENCE_KEY, DEFAULT_LANGUAGE),
|
||||
...attrs,
|
||||
});
|
||||
return {
|
||||
code_block: (attrs: Record<string, Primitive>) =>
|
||||
toggleBlockType(type, schema.nodes.paragraph, {
|
||||
language: Storage.get(PERSISTENCE_KEY, DEFAULT_LANGUAGE),
|
||||
...attrs,
|
||||
}),
|
||||
copyToClipboard: (): Command => (state) => {
|
||||
const codeBlock = findParentNode(isCode)(state.selection);
|
||||
|
||||
if (!codeBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
copy(codeBlock.node.textContent);
|
||||
this.options.onShowToast(this.options.dictionary.codeCopied);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
keys({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
@@ -241,75 +206,6 @@ export default class CodeFence extends Node {
|
||||
};
|
||||
}
|
||||
|
||||
handleCopyToClipboard = (event: MouseEvent) => {
|
||||
const { view } = this.editor;
|
||||
const element = event.target;
|
||||
if (!(element instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
const { top, left } = element.getBoundingClientRect();
|
||||
const result = view.posAtCoords({ top, left });
|
||||
|
||||
if (result) {
|
||||
const node = view.state.doc.nodeAt(result.pos);
|
||||
if (node) {
|
||||
copy(node.textContent);
|
||||
this.options.onShowToast(this.options.dictionary.codeCopied);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleLanguageChange = (event: InputEvent) => {
|
||||
const { view } = this.editor;
|
||||
const { tr } = view.state;
|
||||
const element = event.currentTarget;
|
||||
if (!(element instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { top, left } = element.getBoundingClientRect();
|
||||
const result = view.posAtCoords({ top, left });
|
||||
|
||||
if (result) {
|
||||
const language = element.value;
|
||||
const transaction = tr
|
||||
.setSelection(Selection.near(view.state.doc.resolve(result.inside)))
|
||||
.setNodeMarkup(result.inside, undefined, {
|
||||
language,
|
||||
});
|
||||
|
||||
view.dispatch(transaction);
|
||||
|
||||
Storage.set(PERSISTENCE_KEY, language);
|
||||
}
|
||||
};
|
||||
|
||||
handleToggleDiagram = (event: InputEvent) => {
|
||||
const { view } = this.editor;
|
||||
const { tr } = view.state;
|
||||
const element = event.currentTarget;
|
||||
if (!(element instanceof HTMLButtonElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { top, left } = element.getBoundingClientRect();
|
||||
const result = view.posAtCoords({ top, left });
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diagramId = element
|
||||
.closest(".code-block")
|
||||
?.getAttribute("data-diagram-id");
|
||||
if (!diagramId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transaction = tr.setMeta("mermaid", { toggleDiagram: diagramId });
|
||||
view.dispatch(transaction);
|
||||
};
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
Prism({
|
||||
@@ -336,6 +232,24 @@ export default class CodeFence extends Node {
|
||||
},
|
||||
},
|
||||
}),
|
||||
new Plugin({
|
||||
props: {
|
||||
decorations(state) {
|
||||
const codeBlock = findParentNode(isCode)(state.selection);
|
||||
|
||||
if (!codeBlock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decoration = Decoration.node(
|
||||
codeBlock.pos,
|
||||
codeBlock.pos + codeBlock.node.nodeSize,
|
||||
{ class: "code-active" }
|
||||
);
|
||||
return DecorationSet.create(state.doc, [decoration]);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Token from "markdown-it/lib/token";
|
||||
import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { Command } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import { Primitive } from "utility-types";
|
||||
import { sanitizeUrl } from "../../utils/urls";
|
||||
import DisabledEmbed from "../components/DisabledEmbed";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
@@ -114,7 +115,7 @@ export default class Embed extends Node {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return (attrs: Record<string, any>): Command =>
|
||||
return (attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
dispatch?.(
|
||||
state.tr.replaceSelectionWith(type.create(attrs)).scrollIntoView()
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
import { Command, TextSelection } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import Suggestion from "../extensions/Suggestion";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { SuggestionsMenuType } from "../plugins/Suggestions";
|
||||
@@ -77,7 +78,7 @@ export default class Emoji extends Suggestion {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType; schema: Schema }) {
|
||||
return (attrs: Record<string, string>): Command =>
|
||||
return (attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
const { selection } = state;
|
||||
const position =
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "prosemirror-model";
|
||||
import { Command, Plugin, Selection } from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import { Primitive } from "utility-types";
|
||||
import Storage from "../../utils/Storage";
|
||||
import backspaceToParagraph from "../commands/backspaceToParagraph";
|
||||
import splitHeading from "../commands/splitHeading";
|
||||
@@ -113,7 +114,7 @@ export default class Heading extends Node {
|
||||
}
|
||||
|
||||
commands({ type, schema }: { type: NodeType; schema: Schema }) {
|
||||
return (attrs: Record<string, any>) =>
|
||||
return (attrs: Record<string, Primitive>) =>
|
||||
toggleBlockType(type, schema.nodes.paragraph, attrs);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Token from "markdown-it/lib/token";
|
||||
import { InputRule } from "prosemirror-inputrules";
|
||||
import { NodeSpec, NodeType, Node as ProsemirrorNode } from "prosemirror-model";
|
||||
import { Command } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import Node from "./Node";
|
||||
|
||||
@@ -27,7 +28,7 @@ export default class HorizontalRule extends Node {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return (attrs: Record<string, any>): Command =>
|
||||
return (attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
dispatch?.(
|
||||
state.tr.replaceSelectionWith(type.create(attrs)).scrollIntoView()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Schema,
|
||||
} from "prosemirror-model";
|
||||
import { Command, TextSelection } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import Suggestion from "../extensions/Suggestion";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import { SuggestionsMenuType } from "../plugins/Suggestions";
|
||||
@@ -79,7 +80,7 @@ export default class Mention extends Suggestion {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType; schema: Schema }) {
|
||||
return (attrs: Record<string, string>): Command =>
|
||||
return (attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
const { selection } = state;
|
||||
const position =
|
||||
|
||||
@@ -4,21 +4,13 @@ import { wrappingInputRule } from "prosemirror-inputrules";
|
||||
import { NodeSpec, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Primitive } from "utility-types";
|
||||
import toggleWrap from "../commands/toggleWrap";
|
||||
import { MarkdownSerializerState } from "../lib/markdown/serializer";
|
||||
import noticesRule from "../rules/notices";
|
||||
import Node from "./Node";
|
||||
|
||||
export default class Notice extends Node {
|
||||
get styleOptions() {
|
||||
return Object.entries({
|
||||
info: this.options.dictionary.info,
|
||||
warning: this.options.dictionary.warning,
|
||||
success: this.options.dictionary.success,
|
||||
tip: this.options.dictionary.tip,
|
||||
});
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "container_notice";
|
||||
}
|
||||
@@ -90,23 +82,8 @@ export default class Notice extends Node {
|
||||
},
|
||||
],
|
||||
toDOM: (node) => {
|
||||
let icon, actions;
|
||||
let icon;
|
||||
if (typeof document !== "undefined") {
|
||||
const select = document.createElement("select");
|
||||
select.addEventListener("change", this.handleStyleChange);
|
||||
|
||||
this.styleOptions.forEach(([key, label]) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = key;
|
||||
option.innerText = label;
|
||||
option.selected = node.attrs.style === key;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
actions = document.createElement("div");
|
||||
actions.className = "notice-actions";
|
||||
actions.appendChild(select);
|
||||
|
||||
let component;
|
||||
|
||||
if (node.attrs.style === "tip") {
|
||||
@@ -128,7 +105,6 @@ export default class Notice extends Node {
|
||||
"div",
|
||||
{ class: `notice-block ${node.attrs.style}` },
|
||||
...(icon ? [icon] : []),
|
||||
["div", { contentEditable: "false" }, ...(actions ? [actions] : [])],
|
||||
["div", { class: "content" }, 0],
|
||||
];
|
||||
},
|
||||
@@ -136,7 +112,7 @@ export default class Notice extends Node {
|
||||
}
|
||||
|
||||
commands({ type }: { type: NodeType }) {
|
||||
return (attrs: Record<string, any>) => toggleWrap(type, attrs);
|
||||
return (attrs: Record<string, Primitive>) => toggleWrap(type, attrs);
|
||||
}
|
||||
|
||||
handleStyleChange = (event: InputEvent) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InputRule } from "prosemirror-inputrules";
|
||||
import { Node as ProsemirrorNode, NodeSpec, NodeType } from "prosemirror-model";
|
||||
import { TextSelection, NodeSelection, Command } from "prosemirror-state";
|
||||
import * as React from "react";
|
||||
import { Primitive } from "utility-types";
|
||||
import { getEventFiles } from "../../utils/files";
|
||||
import { sanitizeUrl } from "../../utils/urls";
|
||||
import { AttachmentValidation } from "../../validations";
|
||||
@@ -216,7 +217,7 @@ export default class SimpleImage extends Node {
|
||||
return true;
|
||||
},
|
||||
createImage:
|
||||
(attrs: Record<string, any>): Command =>
|
||||
(attrs: Record<string, Primitive>): Command =>
|
||||
(state, dispatch) => {
|
||||
const { selection } = state;
|
||||
const position =
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { NodeType } from "prosemirror-model";
|
||||
import { EditorState } from "prosemirror-state";
|
||||
import { Primitive } from "utility-types";
|
||||
import { findParentNode } from "./findParentNode";
|
||||
|
||||
const isNodeActive =
|
||||
(type: NodeType, attrs: Record<string, any> = {}) =>
|
||||
(type: NodeType, attrs: Record<string, Primitive> = {}) =>
|
||||
(state: EditorState) => {
|
||||
if (!type) {
|
||||
return false;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EditorState } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import * as React from "react";
|
||||
import { DefaultTheme } from "styled-components";
|
||||
import { Primitive } from "utility-types";
|
||||
|
||||
export type PlainTextSerializer = (node: ProsemirrorNode) => string;
|
||||
|
||||
@@ -20,8 +21,11 @@ export type MenuItem = {
|
||||
keywords?: string;
|
||||
tooltip?: string;
|
||||
label?: string;
|
||||
children?: MenuItem[];
|
||||
defaultHidden?: boolean;
|
||||
attrs?: Record<string, any>;
|
||||
attrs?:
|
||||
| Record<string, Primitive>
|
||||
| ((state: EditorState) => Record<string, Primitive>);
|
||||
visible?: boolean;
|
||||
active?: (state: EditorState) => boolean;
|
||||
appendSpace?: boolean;
|
||||
|
||||
@@ -143,10 +143,6 @@ export const buildLightTheme = (input: Partial<Colors>): DefaultTheme => {
|
||||
inputBorderFocused: colors.slate,
|
||||
listItemHoverBackground: colors.warmGrey,
|
||||
mentionBackground: colors.warmGrey,
|
||||
toolbarHoverBackground: colors.black,
|
||||
toolbarBackground: colors.almostBlack,
|
||||
toolbarInput: colors.white10,
|
||||
toolbarItem: colors.white,
|
||||
tableDivider: colors.smokeDark,
|
||||
tableSelected: colors.accent,
|
||||
buttonNeutralBackground: colors.white,
|
||||
@@ -210,10 +206,6 @@ export const buildDarkTheme = (input: Partial<Colors>): DefaultTheme => {
|
||||
inputBorderFocused: colors.slate,
|
||||
listItemHoverBackground: colors.white10,
|
||||
mentionBackground: colors.white10,
|
||||
toolbarHoverBackground: colors.slate,
|
||||
toolbarBackground: colors.white,
|
||||
toolbarInput: colors.black10,
|
||||
toolbarItem: colors.lightBlack,
|
||||
tableDivider: colors.lightBlack,
|
||||
tableSelected: colors.accent,
|
||||
buttonNeutralBackground: colors.almostBlack,
|
||||
|
||||
Reference in New Issue
Block a user