mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
8c716b173a
* chore: Update editor generics * fix: Address PR review on editor generics - Restore null-guard on Link click handler so anchors aren't inert when no onClickLink is provided - Mark onClickLink optional in LinkOptions and openLink command to match runtime - Remove dead `collapsed` option from HeadingOptions - Make ToggleBlock dictionary optional and restore optional-chained access for server-side schema instantiation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
137 lines
3.9 KiB
TypeScript
137 lines
3.9 KiB
TypeScript
import { action } from "mobx";
|
|
import { PlusIcon } from "outline-icons";
|
|
import { Plugin } from "prosemirror-state";
|
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
import ReactDOM from "react-dom";
|
|
import type { WidgetProps } from "@shared/editor/lib/Extension";
|
|
import { PlaceholderPlugin } from "@shared/editor/plugins/PlaceholderPlugin";
|
|
import { findParentNode } from "@shared/editor/queries/findParentNode";
|
|
import type { Dictionary } from "~/hooks/useDictionary";
|
|
import type { SuggestionOptions } from "~/editor/extensions/Suggestion";
|
|
import Suggestion from "~/editor/extensions/Suggestion";
|
|
import BlockMenu from "../components/BlockMenu";
|
|
|
|
/**
|
|
* Options for the BlockMenu extension.
|
|
*/
|
|
type BlockMenuOptions = SuggestionOptions & {
|
|
/** A dictionary of translated strings used in the editor. */
|
|
dictionary: Dictionary;
|
|
};
|
|
|
|
export default class BlockMenuExtension extends Suggestion<BlockMenuOptions> {
|
|
get defaultOptions() {
|
|
return {
|
|
trigger: "/",
|
|
allowSpaces: false,
|
|
requireSearchTerm: false,
|
|
enabledInCode: false,
|
|
};
|
|
}
|
|
|
|
get name() {
|
|
return "block-menu";
|
|
}
|
|
|
|
get plugins() {
|
|
const button = document.createElement("button");
|
|
button.className = "block-menu-trigger";
|
|
button.type = "button";
|
|
ReactDOM.render(<PlusIcon />, button);
|
|
|
|
return [
|
|
...super.plugins,
|
|
new Plugin({
|
|
props: {
|
|
decorations: (state) => {
|
|
const parent = findParentNode(
|
|
(node) => node.type.name === "paragraph"
|
|
)(state.selection);
|
|
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
const isTopLevel = state.selection.$from.depth === 1;
|
|
if (!isTopLevel) {
|
|
return;
|
|
}
|
|
|
|
const decorations: Decoration[] = [];
|
|
const isEmptyNode = parent && parent.node.content.size === 0;
|
|
|
|
if (isEmptyNode) {
|
|
decorations.push(
|
|
Decoration.widget(
|
|
parent.pos,
|
|
() => {
|
|
button.onclick = action(() => {
|
|
this.state.query = "";
|
|
this.state.open = true;
|
|
});
|
|
return button;
|
|
},
|
|
{
|
|
key: "block-trigger",
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
return DecorationSet.create(state.doc, decorations);
|
|
},
|
|
},
|
|
}),
|
|
new PlaceholderPlugin([
|
|
{
|
|
condition: ({ node, $start, textContent, state }) =>
|
|
$start.depth === 1 &&
|
|
state.selection.$from.pos === $start.pos + node.content.size &&
|
|
!!textContent &&
|
|
node.childCount === 0 &&
|
|
node.textContent === "",
|
|
text: this.options.dictionary.newLineEmpty,
|
|
},
|
|
{
|
|
condition: ({ node, $start, state }) =>
|
|
$start.depth === 1 &&
|
|
state.selection.$from.pos === $start.pos + node.content.size &&
|
|
node.textContent === "/",
|
|
text: ` ${this.options.dictionary.newLineWithSlash}`,
|
|
},
|
|
]),
|
|
];
|
|
}
|
|
|
|
private handleClose = action((insertNewLine: boolean) => {
|
|
const { view } = this.editor;
|
|
|
|
if (insertNewLine) {
|
|
const transaction = view.state.tr.split(view.state.selection.to);
|
|
view.dispatch(transaction);
|
|
view.focus();
|
|
}
|
|
|
|
this.state.open = false;
|
|
});
|
|
|
|
widget = ({ rtl }: WidgetProps) => {
|
|
const { props } = this.editor;
|
|
|
|
return (
|
|
<BlockMenu
|
|
rtl={rtl}
|
|
trigger={this.options.trigger}
|
|
isActive={this.state.open}
|
|
search={this.state.query}
|
|
onClose={this.handleClose}
|
|
uploadFile={props.uploadFile}
|
|
onFileUploadStart={props.onFileUploadStart}
|
|
onFileUploadStop={props.onFileUploadStop}
|
|
onFileUploadProgress={props.onFileUploadProgress}
|
|
embeds={props.embeds}
|
|
/>
|
|
);
|
|
};
|
|
}
|