mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57a2068d5c |
@@ -9,11 +9,8 @@ import { isMarkActive } from "@shared/editor/queries/isMarkActive";
|
||||
import { isNodeActive } from "@shared/editor/queries/isNodeActive";
|
||||
import { getColumnIndex, getRowIndex } from "@shared/editor/queries/table";
|
||||
import { MenuItem } from "@shared/editor/types";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useDictionary from "~/hooks/useDictionary";
|
||||
import useEventListener from "~/hooks/useEventListener";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import getAttachmentMenuItems from "../menus/attachment";
|
||||
import getCodeMenuItems from "../menus/code";
|
||||
import getDividerMenuItems from "../menus/divider";
|
||||
@@ -35,121 +32,30 @@ type Props = {
|
||||
readOnly?: boolean;
|
||||
canComment?: boolean;
|
||||
canUpdate?: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
active: boolean;
|
||||
onClickLink: (
|
||||
href: string,
|
||||
event: MouseEvent | React.MouseEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
};
|
||||
|
||||
function useIsActive(state: EditorState) {
|
||||
const { selection, doc } = state;
|
||||
|
||||
if (isMarkActive(state.schema.marks.link)(state)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(isNodeActive(state.schema.nodes.code_block)(state) ||
|
||||
isNodeActive(state.schema.nodes.code_fence)(state)) &&
|
||||
selection.from > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isInNotice(state) && selection.from > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!selection || selection.empty) {
|
||||
return false;
|
||||
}
|
||||
if (selection instanceof NodeSelection && selection.node.type.name === "hr") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
selection instanceof NodeSelection &&
|
||||
["image", "attachment"].includes(selection.node.type.name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (selection instanceof NodeSelection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionText = doc.cut(selection.from, selection.to).textContent;
|
||||
if (selection instanceof TextSelection && !selectionText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const slice = selection.content();
|
||||
const fragment = slice.content;
|
||||
const nodes = (fragment as any).content;
|
||||
|
||||
return some(nodes, (n) => n.content.size);
|
||||
}
|
||||
|
||||
function useIsDragging() {
|
||||
const [isDragging, setDragging, setNotDragging] = useBoolean();
|
||||
useEventListener("dragstart", setDragging);
|
||||
useEventListener("dragend", setNotDragging);
|
||||
useEventListener("drop", setNotDragging);
|
||||
return isDragging;
|
||||
}
|
||||
|
||||
export default function SelectionToolbar(props: Props) {
|
||||
const { onClose, readOnly, onOpen } = props;
|
||||
const {
|
||||
rtl,
|
||||
isTemplate,
|
||||
readOnly,
|
||||
canComment,
|
||||
canUpdate,
|
||||
active, // Added
|
||||
// onOpen, // Removed
|
||||
// onClose, // Removed
|
||||
onClickLink, // Stays
|
||||
...rest
|
||||
} = props;
|
||||
const { view, commands } = useEditor();
|
||||
const dictionary = useDictionary();
|
||||
const menuRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const isMobile = useMobile();
|
||||
const isActive = useIsActive(view.state) || isMobile;
|
||||
const isDragging = useIsDragging();
|
||||
const previousIsActive = usePrevious(isActive);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Trigger callbacks when the toolbar is opened or closed
|
||||
if (previousIsActive && !isActive) {
|
||||
onClose();
|
||||
}
|
||||
if (!previousIsActive && isActive) {
|
||||
onOpen();
|
||||
}
|
||||
}, [isActive, onClose, onOpen, previousIsActive]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (ev: MouseEvent): void => {
|
||||
if (
|
||||
ev.target instanceof HTMLElement &&
|
||||
menuRef.current &&
|
||||
menuRef.current.contains(ev.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (view.dom.contains(ev.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isActive || document.activeElement?.tagName === "INPUT") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.getSelection()?.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dispatch } = view;
|
||||
dispatch(
|
||||
view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0)))
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener("mouseup", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mouseup", handleClickOutside);
|
||||
};
|
||||
}, [isActive, previousIsActive, readOnly, view]);
|
||||
|
||||
const handleOnSelectLink = ({
|
||||
href,
|
||||
@@ -171,14 +77,9 @@ export default function SelectionToolbar(props: Props) {
|
||||
);
|
||||
};
|
||||
|
||||
const { isTemplate, rtl, canComment, canUpdate, ...rest } = props;
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
|
||||
if ((readOnly && !canComment) || isDragging) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDividerSelection = isNodeActive(state.schema.nodes.hr)(state);
|
||||
const colIndex = getColumnIndex(state);
|
||||
const rowIndex = getRowIndex(state);
|
||||
@@ -240,7 +141,7 @@ export default function SelectionToolbar(props: Props) {
|
||||
|
||||
return (
|
||||
<FloatingToolbar
|
||||
active={isActive}
|
||||
active={props.active}
|
||||
ref={menuRef}
|
||||
width={showLinkToolbar ? 336 : undefined}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { EditorState, TextSelection, NodeSelection, Plugin } from "prosemirror-state";
|
||||
import { Schema } from "prosemirror-model";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import SelectionToolbarExtension from "./SelectionToolbarExtension";
|
||||
import SelectionToolbar from "~/app/editor/components/SelectionToolbar"; // Path to the actual component
|
||||
|
||||
// Mock the actual toolbar component
|
||||
jest.mock("~/app/editor/components/SelectionToolbar", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => null), // Mock component returns null
|
||||
}));
|
||||
|
||||
// Define a basic schema for testing
|
||||
const schema = new Schema({
|
||||
nodes: {
|
||||
doc: { content: "block+" },
|
||||
paragraph: { content: "text*", group: "block", toDOM: () => ["p", 0] },
|
||||
text: { inline: true }, // text is inline
|
||||
image: { group: "block", toDOM: () => ["img"] },
|
||||
hr: { group: "block", toDOM: () => ["hr"] }, // For NodeSelection hr test
|
||||
code_block: { content: "text*", group: "block", code: true, defining: true, toDOM: () => ["pre", ["code", 0]] },
|
||||
code_fence: { content: "text*", group: "block", code: true, defining: true, toDOM: () => ["pre", ["code", 0]] },
|
||||
// Minimal 'attachment' node for testing NodeSelection
|
||||
attachment: { group: "block", toDOM: () => ["div", { class: "attachment" }] },
|
||||
},
|
||||
marks: {
|
||||
link: { toDOM: () => ["a", { href: "" }] },
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to create a mock editor environment
|
||||
const createMockEditor = (state: EditorState, editorProps: any = {}) => {
|
||||
const view = {
|
||||
state,
|
||||
props: {}, // ProseMirror view.props, not the React component props
|
||||
editable: !editorProps.readOnly,
|
||||
dispatch: jest.fn(),
|
||||
dom: document.createElement("div"),
|
||||
update: jest.fn(),
|
||||
// Add other EditorView properties if needed by the extension
|
||||
} as unknown as EditorView;
|
||||
|
||||
return {
|
||||
state, // Current EditorState
|
||||
view, // Mocked EditorView
|
||||
props: { // These are the props of the <Editor> React component
|
||||
dir: "ltr",
|
||||
isTemplate: false,
|
||||
readOnly: false,
|
||||
canComment: true,
|
||||
canUpdate: true,
|
||||
onClickLink: jest.fn(),
|
||||
...editorProps,
|
||||
},
|
||||
// Mock other editor instance methods/properties if the extension uses them
|
||||
// e.g., commands: {}
|
||||
};
|
||||
};
|
||||
|
||||
describe("SelectionToolbarExtension", () => {
|
||||
let extension: SelectionToolbarExtension;
|
||||
let initialEditorState: EditorState;
|
||||
let mockEditorView: EditorView;
|
||||
let mockEditor: ReturnType<typeof createMockEditor>;
|
||||
|
||||
beforeEach(() => {
|
||||
(SelectionToolbar as jest.Mock).mockClear(); // Clear mock calls before each test
|
||||
extension = new SelectionToolbarExtension();
|
||||
|
||||
initialEditorState = EditorState.create({ schema });
|
||||
mockEditor = createMockEditor(initialEditorState);
|
||||
mockEditorView = mockEditor.view;
|
||||
|
||||
// Simulate the extension being initialized with an editor instance
|
||||
// This is crucial as the extension accesses `this.editor`
|
||||
extension.editor = mockEditor as any;
|
||||
|
||||
// Initialize the plugin's view. This is what `EditorState.create` would do.
|
||||
// The plugin's `view` method is called when the plugin is first applied.
|
||||
const plugin = extension.plugins[0] as Plugin;
|
||||
if (plugin && plugin.spec && plugin.spec.view) {
|
||||
// Attach a mock state to the view before calling plugin.spec.view
|
||||
mockEditorView.state = initialEditorState;
|
||||
const pluginView = plugin.spec.view(mockEditorView);
|
||||
// If the plugin view has an update method, we might need to store it or re-evaluate
|
||||
// but for now, the view() call itself triggers the first updateSelectionToolbarVisibility
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to simulate plugin updates
|
||||
const updatePluginState = (newState: EditorState, oldState?: EditorState) => {
|
||||
const plugin = extension.plugins[0] as Plugin;
|
||||
if (plugin && plugin.spec && plugin.spec.view) {
|
||||
// The plugin view object is created once, its update method is called subsequently.
|
||||
// Prosemirror typically handles this. For tests, we directly call the update logic.
|
||||
// The extension's updateSelectionToolbarVisibility is called from plugin's update.
|
||||
|
||||
// Update the view's state reference
|
||||
mockEditorView.state = newState;
|
||||
extension['updateSelectionToolbarVisibility'](mockEditorView, oldState || initialEditorState);
|
||||
initialEditorState = newState; // Keep track of the current state for subsequent calls
|
||||
}
|
||||
};
|
||||
|
||||
it("should be inactive initially", () => {
|
||||
expect(extension.state.isActive).toBe(false);
|
||||
extension.widget(); // Call widget
|
||||
expect(SelectionToolbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should become active on text selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("paragraph", null, [schema.text("Hello World")])]);
|
||||
const selection = TextSelection.create(doc, 1, 6); // Select "Hello"
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
|
||||
updatePluginState(newState);
|
||||
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget(); // Call widget to render
|
||||
expect(SelectionToolbar).toHaveBeenCalledTimes(1);
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
});
|
||||
|
||||
it("should become inactive when selection is collapsed", () => {
|
||||
// First, make it active
|
||||
const docActive = schema.node("doc", null, [schema.node("paragraph", null, [schema.text("Hello")])]);
|
||||
const selectionActive = TextSelection.create(docActive, 1, 6); // Select "Hello"
|
||||
const stateActive = EditorState.create({ doc: docActive, schema, selection: selectionActive });
|
||||
updatePluginState(stateActive);
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget(); // Renders toolbar
|
||||
(SelectionToolbar as jest.Mock).mockClear(); // Clear calls for the next assertion
|
||||
|
||||
// Then, collapse selection
|
||||
const selectionCollapsed = TextSelection.create(docActive, 1, 1); // Cursor at start of "Hello"
|
||||
const stateCollapsed = stateActive.apply(stateActive.tr.setSelection(selectionCollapsed)).state;
|
||||
updatePluginState(stateCollapsed, stateActive);
|
||||
|
||||
expect(extension.state.isActive).toBe(false);
|
||||
extension.widget(); // Should return null, so SelectionToolbar not called again
|
||||
expect(SelectionToolbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should become active on image node selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("image")]);
|
||||
const selection = NodeSelection.create(doc, 0); // Position 0 for the image node
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
|
||||
updatePluginState(newState);
|
||||
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).toHaveBeenCalledTimes(1);
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
});
|
||||
|
||||
it("should become active on hr node selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("hr")]);
|
||||
const selection = NodeSelection.create(doc, 0);
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
updatePluginState(newState);
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
});
|
||||
|
||||
it("should become active on attachment node selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("attachment")]);
|
||||
const selection = NodeSelection.create(doc, 0);
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
updatePluginState(newState);
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
});
|
||||
|
||||
|
||||
it("should become inactive when selection is cleared (e.g., to an empty paragraph, collapsed)", () => {
|
||||
// Make it active first
|
||||
const docActive = schema.node("doc", null, [schema.node("paragraph", null, [schema.text("Selected Text")])]);
|
||||
const selectionActive = TextSelection.create(docActive, 1, 13);
|
||||
const stateActive = EditorState.create({ doc: docActive, schema, selection: selectionActive });
|
||||
updatePluginState(stateActive);
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget(); // Renders toolbar
|
||||
(SelectionToolbar as jest.Mock).mockClear();
|
||||
|
||||
// Clear selection / move to empty paragraph, collapsed
|
||||
const docEmpty = schema.node("doc", null, [schema.node("paragraph")]);
|
||||
const selectionEmpty = TextSelection.create(docEmpty, 1, 1); // Collapsed selection in an empty paragraph
|
||||
const stateEmpty = EditorState.create({ doc: docEmpty, schema, selection: selectionEmpty });
|
||||
updatePluginState(stateEmpty, stateActive);
|
||||
|
||||
expect(extension.state.isActive).toBe(false);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("Menu item contexts (conceptual)", () => {
|
||||
it("should allow SelectionToolbar to render formatting items for text selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("paragraph", null, [schema.text("Some text to select")])]);
|
||||
const selection = TextSelection.create(doc, 1, 5); // Select "Some"
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
|
||||
updatePluginState(newState);
|
||||
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
// Further assertions would depend on SelectionToolbar's own tests or if this extension passed item-specific props
|
||||
});
|
||||
|
||||
it("should allow SelectionToolbar to render image menu items for image selection", () => {
|
||||
const doc = schema.node("doc", null, [schema.node("image")]);
|
||||
const selection = NodeSelection.create(doc, 0);
|
||||
const newState = EditorState.create({ doc, schema, selection });
|
||||
|
||||
updatePluginState(newState);
|
||||
|
||||
expect(extension.state.isActive).toBe(true);
|
||||
extension.widget();
|
||||
expect(SelectionToolbar).toHaveBeenCalledWith(expect.objectContaining({ active: true }), {});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import { action, observable } from "mobx";
|
||||
import { Plugin, EditorState, TextSelection, NodeSelection } from "prosemirror-state";
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import Extension from "@shared/editor/lib/Extension";
|
||||
import SelectionToolbar from "~/app/editor/components/SelectionToolbar"; // Adjust path as needed
|
||||
import { isMarkActive } from "@shared/editor/queries/isMarkActive"; // If needed for isActive logic
|
||||
import { isNodeActive } from "@shared/editor/queries/isNodeActive"; // If needed for isActive logic
|
||||
|
||||
export default class SelectionToolbarExtension extends Extension {
|
||||
@observable
|
||||
state = {
|
||||
isActive: false,
|
||||
// Add other relevant state properties here if needed later
|
||||
};
|
||||
|
||||
get name() {
|
||||
return "selection-toolbar";
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
view: (editorView) => {
|
||||
// The plugin's view method can be used to store the editorView instance
|
||||
// if needed, or to set up initial event listeners if not using handleDOMEvents.
|
||||
this.updateSelectionToolbarVisibility(editorView);
|
||||
return {
|
||||
update: (view, prevState) => {
|
||||
this.updateSelectionToolbarVisibility(view, prevState);
|
||||
},
|
||||
destroy: () => {
|
||||
// Cleanup if necessary
|
||||
},
|
||||
};
|
||||
},
|
||||
// Alternatively, use props for more specific event handling if preferred later
|
||||
// props: {
|
||||
// handleDOMEvents: {
|
||||
// // mousedown, mouseup, etc.
|
||||
// }
|
||||
// }
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// Logic adapted from the existing useIsActive hook in SelectionToolbar.tsx
|
||||
// This will likely need further refinement.
|
||||
private isSelectionActive(state: EditorState): boolean {
|
||||
const { selection, doc, schema } = state;
|
||||
|
||||
if (isMarkActive(schema.marks.link)(state)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
(isNodeActive(schema.nodes.code_block)(state) ||
|
||||
isNodeActive(schema.nodes.code_fence)(state)) &&
|
||||
selection.from > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Add isInNotice check if that node type exists and is relevant
|
||||
// if (isInNotice(state) && selection.from > 0) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
if (!selection || selection.empty) {
|
||||
return false;
|
||||
}
|
||||
if (selection instanceof NodeSelection && selection.node.type.name === "hr") {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
selection instanceof NodeSelection &&
|
||||
["image", "attachment"].includes(selection.node.type.name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (selection instanceof NodeSelection) {
|
||||
// For other node selections, you might want to return false or true
|
||||
// depending on whether the toolbar should appear.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for non-empty text selection
|
||||
const selectionText = doc.cut(selection.from, selection.to).textContent;
|
||||
if (selection instanceof TextSelection && !selectionText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Original logic: const slice = selection.content(); const fragment = slice.content;
|
||||
// This part seems to check if there's actual content in the selection,
|
||||
// not just an empty range.
|
||||
const slice = selection.content();
|
||||
if (slice.content.childCount > 0) {
|
||||
let hasContent = false;
|
||||
slice.content.forEach(node => {
|
||||
if (node.content.size > 0 || node.isText || node.type.name !== 'paragraph' || (node.textContent && node.textContent.length > 0)) {
|
||||
hasContent = true;
|
||||
}
|
||||
});
|
||||
// Fallback for cases where a paragraph might seem empty but still has meaning for selection
|
||||
if (!hasContent && slice.content.childCount === 1 && slice.content.firstChild?.type.name === 'paragraph' && selectionText.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return hasContent;
|
||||
}
|
||||
|
||||
return false; // Default to false if no other condition met
|
||||
}
|
||||
|
||||
@action
|
||||
private updateSelectionToolbarVisibility(view: EditorView, prevState?: EditorState) {
|
||||
const { state } = view;
|
||||
if (prevState && prevState.doc.eq(state.doc) && prevState.selection.eq(state.selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isActive = this.isSelectionActive(state);
|
||||
if (this.state.isActive !== isActive) {
|
||||
this.state.isActive = isActive;
|
||||
}
|
||||
}
|
||||
|
||||
widget = () => {
|
||||
// The actual SelectionToolbar component will be passed props from this extension's state
|
||||
// and methods from the editor instance.
|
||||
// For now, just render it if active.
|
||||
// We'll need to pass editor properties and command functions later.
|
||||
if (!this.state.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure this.editor and this.editor.props are available.
|
||||
// Extensions are typically initialized with the editor instance.
|
||||
if (!this.editor || !this.editor.props) {
|
||||
// This case should ideally not happen if the extension is correctly initialized.
|
||||
// console.warn("SelectionToolbarExtension: editor or editor.props not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
dir,
|
||||
isTemplate,
|
||||
readOnly,
|
||||
canComment,
|
||||
canUpdate,
|
||||
onClickLink,
|
||||
} = this.editor.props as any; // Use 'as any' for now to bypass strict type checking on editor.props if its type isn't fully known here.
|
||||
// Or, ideally, ensure EditorProps is available and used.
|
||||
|
||||
return (
|
||||
<SelectionToolbar
|
||||
active={this.state.isActive}
|
||||
rtl={dir === "rtl"}
|
||||
isTemplate={isTemplate}
|
||||
readOnly={readOnly}
|
||||
canComment={canComment}
|
||||
canUpdate={canUpdate}
|
||||
onClickLink={onClickLink}
|
||||
// Props like dictionary, view, commands for ToolbarMenu will be handled by SelectionToolbar's useEditor()
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
+5
-43
@@ -53,7 +53,7 @@ import Logger from "~/utils/Logger";
|
||||
import ComponentView from "./components/ComponentView";
|
||||
import EditorContext from "./components/EditorContext";
|
||||
import { NodeViewRenderer } from "./components/NodeViewRenderer";
|
||||
import SelectionToolbar from "./components/SelectionToolbar";
|
||||
import SelectionToolbarExtension from "./extensions/SelectionToolbarExtension";
|
||||
import WithTheme from "./components/WithTheme";
|
||||
|
||||
export type Props = {
|
||||
@@ -144,8 +144,6 @@ type State = {
|
||||
isRTL: boolean;
|
||||
/** If the editor is currently focused */
|
||||
isEditorFocused: boolean;
|
||||
/** If the toolbar for a text selection is visible */
|
||||
selectionToolbarOpen: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -174,7 +172,6 @@ export class Editor extends React.PureComponent<
|
||||
state: State = {
|
||||
isRTL: false,
|
||||
isEditorFocused: false,
|
||||
selectionToolbarOpen: false,
|
||||
};
|
||||
|
||||
isInitialized = false;
|
||||
@@ -261,19 +258,12 @@ export class Editor extends React.PureComponent<
|
||||
this.calculateDir();
|
||||
}
|
||||
|
||||
if (
|
||||
!this.isBlurred &&
|
||||
!this.state.isEditorFocused &&
|
||||
!this.state.selectionToolbarOpen
|
||||
) {
|
||||
if (!this.isBlurred && !this.state.isEditorFocused) {
|
||||
this.isBlurred = true;
|
||||
this.props.onBlur?.();
|
||||
}
|
||||
|
||||
if (
|
||||
this.isBlurred &&
|
||||
(this.state.isEditorFocused || this.state.selectionToolbarOpen)
|
||||
) {
|
||||
if (this.isBlurred && this.state.isEditorFocused) {
|
||||
this.isBlurred = false;
|
||||
this.props.onFocus?.();
|
||||
}
|
||||
@@ -305,7 +295,8 @@ export class Editor extends React.PureComponent<
|
||||
}
|
||||
|
||||
private createExtensions() {
|
||||
return new ExtensionManager(this.props.extensions, this);
|
||||
const extensions = [...this.props.extensions, new SelectionToolbarExtension()];
|
||||
return new ExtensionManager(extensions, this);
|
||||
}
|
||||
|
||||
private createPlugins() {
|
||||
@@ -754,23 +745,6 @@ export class Editor extends React.PureComponent<
|
||||
return false;
|
||||
};
|
||||
|
||||
private handleOpenSelectionToolbar = () => {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectionToolbarOpen: true,
|
||||
}));
|
||||
};
|
||||
|
||||
private handleCloseSelectionToolbar = () => {
|
||||
if (!this.state.selectionToolbarOpen) {
|
||||
return;
|
||||
}
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectionToolbarOpen: false,
|
||||
}));
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { readOnly, canUpdate, grow, style, className, onKeyDown } =
|
||||
this.props;
|
||||
@@ -799,18 +773,6 @@ export class Editor extends React.PureComponent<
|
||||
ref={this.elementRef}
|
||||
lang=""
|
||||
/>
|
||||
{this.view && (
|
||||
<SelectionToolbar
|
||||
rtl={isRTL}
|
||||
readOnly={readOnly}
|
||||
canUpdate={this.props.canUpdate}
|
||||
canComment={this.props.canComment}
|
||||
isTemplate={this.props.template === true}
|
||||
onOpen={this.handleOpenSelectionToolbar}
|
||||
onClose={this.handleCloseSelectionToolbar}
|
||||
onClickLink={this.props.onClickLink}
|
||||
/>
|
||||
)}
|
||||
{this.widgets &&
|
||||
Object.values(this.widgets).map((Widget, index) => (
|
||||
<Widget key={String(index)} rtl={isRTL} readOnly={readOnly} />
|
||||
|
||||
Reference in New Issue
Block a user