Rebuilding code block menus (#5569)

This commit is contained in:
Tom Moor
2023-07-17 21:25:22 -04:00
committed by GitHub
parent 60b456f35a
commit 2427f4747a
42 changed files with 474 additions and 469 deletions

View File

@@ -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);

View File

@@ -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;
}
`;

View File

@@ -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};
}
}

View File

@@ -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>;

View File

@@ -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;
},
},
},
});
}

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;

View File

@@ -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);
}
};

View 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";
}

View File

@@ -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) {

View File

@@ -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]);
},
},
}),
];
}

View File

@@ -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()

View File

@@ -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 =

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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 =

View File

@@ -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) => {

View File

@@ -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 =

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,