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

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