Rebuilding code block menus (#5569)
This commit is contained in:
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user