Compare commits

...

4 Commits

Author SHA1 Message Date
codegen-sh[bot] d85fb42622 Allow inline code to be bolded and italicized 2025-03-30 20:00:34 +00:00
Tom Moor 4af2b032dd fix: New comments are measured incorrectly (#8838)
* fix: New comments are measured incorrectly

* Remove defaultRect so we can always return a DOMRect
2025-03-30 11:48:51 -07:00
Tom Moor c52d9a850d fix: Paste partially over code prevents pasting PM nodes (#8836)
* fix: Paste over any inline code prevents pasting nodes
closes #8825

* Add inclusive logic for isNodeActive
2025-03-30 11:48:44 -07:00
Tom Moor 588e5bc17f fix: Reduce gap between at symbol and name in user mentions (#8839) 2025-03-30 17:26:35 +00:00
9 changed files with 87 additions and 45 deletions
+5 -1
View File
@@ -2,6 +2,11 @@ import React from "react";
import styled from "styled-components";
import { fadeIn } from "~/styles/animations";
/**
* Fade in animation for a component.
*
* @param timing - The duration of the fade in animation, default is 250ms.
*/
const Fade = styled.span<{ timing?: number | string }>`
animation: ${fadeIn} ${(props) => props.timing || "250ms"} ease-in-out;
`;
@@ -17,7 +22,6 @@ type Props = {
*/
export const ConditionalFade = ({ animate, children }: Props) => {
const [isAnimated] = React.useState(animate);
return isAnimated ? <Fade>{children}</Fade> : <>{children}</>;
};
+1 -1
View File
@@ -88,7 +88,7 @@ export default class PasteHandler extends Extension {
// If the users selection is currently in a code block then paste
// as plain text, ignore all formatting and HTML content.
if (isInCode(state)) {
if (isInCode(state, { inclusive: true })) {
event.preventDefault();
view.dispatch(state.tr.insertText(text));
return true;
@@ -16,6 +16,7 @@ import { useDocumentContext } from "~/components/DocumentContext";
import Facepile from "~/components/Facepile";
import Fade from "~/components/Fade";
import { ResizingHeightContainer } from "~/components/ResizingHeightContainer";
import useBoolean from "~/hooks/useBoolean";
import { useLocationSidebarContext } from "~/hooks/useLocationSidebarContext";
import useOnClickOutside from "~/hooks/useOnClickOutside";
import usePersistedState from "~/hooks/usePersistedState";
@@ -63,7 +64,7 @@ function CommentThread({
const history = useHistory();
const location = useLocation();
const sidebarContext = useLocationSidebarContext();
const [autoFocus, setAutoFocus] = React.useState(thread.isNew);
const [autoFocus, setAutoFocusOn, setAutoFocusOff] = useBoolean(thread.isNew);
const can = usePolicy(document);
@@ -156,9 +157,9 @@ function CommentThread({
React.useEffect(() => {
if (!focused && autoFocus) {
setAutoFocus(false);
setAutoFocusOff();
}
}, [focused, autoFocus]);
}, [focused, autoFocus, setAutoFocusOff]);
React.useEffect(() => {
if (focused) {
@@ -273,7 +274,7 @@ function CommentThread({
)}
</ResizingHeightContainer>
{!focused && !recessed && !draft && canReply && (
<Reply onClick={() => setAutoFocus(true)}>{t("Reply")}</Reply>
<Reply onClick={setAutoFocusOn}>{t("Reply")}</Reply>
)}
</Thread>
);
+4
View File
@@ -313,6 +313,10 @@ width: 100%;
background: ${props.theme.mentionHoverBackground};
}
&[data-type="user"] {
gap: 0;
}
&.mention-user::before {
content: "@";
}
+1 -1
View File
@@ -21,7 +21,7 @@ export default class Code extends Mark {
get schema(): MarkSpec {
return {
excludes: "mention placeholder highlight em strong",
excludes: "mention placeholder highlight",
parseDOM: [{ tag: "code", preserveWhitespace: true }],
toDOM: () => ["code", { class: "inline", spellCheck: "false" }],
};
+17 -3
View File
@@ -7,6 +7,8 @@ type Options = {
onlyBlock?: boolean;
/** Only check if the selection is inside a code mark. */
onlyMark?: boolean;
/** If true then code must contain entire selection */
inclusive?: boolean;
};
/**
@@ -20,17 +22,29 @@ export function isInCode(state: EditorState, options?: Options): boolean {
const { nodes, marks } = state.schema;
if (!options?.onlyMark) {
if (nodes.code_block && isNodeActive(nodes.code_block)(state)) {
if (
nodes.code_block &&
isNodeActive(nodes.code_block, undefined, {
inclusive: options?.inclusive,
})(state)
) {
return true;
}
if (nodes.code_fence && isNodeActive(nodes.code_fence)(state)) {
if (
nodes.code_fence &&
isNodeActive(nodes.code_fence, undefined, {
inclusive: options?.inclusive,
})(state)
) {
return true;
}
}
if (!options?.onlyBlock) {
if (marks.code_inline) {
return isMarkActive(marks.code_inline)(state);
return isMarkActive(marks.code_inline, undefined, {
inclusive: options?.inclusive,
})(state);
}
}
+4 -1
View File
@@ -6,6 +6,8 @@ import { getMarksBetween } from "./getMarksBetween";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then mark must contain entire selection */
inclusive?: boolean;
};
/**
@@ -40,7 +42,8 @@ export const isMarkActive =
Object.keys(attrs).every(
(key) => mark.attrs[key] === attrs[key]
)) &&
(!options?.exact || (start === from && end === to))
(!options?.exact || (start === from && end === to)) &&
(!options?.inclusive || (start <= from && end >= to))
);
}
+34 -10
View File
@@ -3,31 +3,55 @@ import { EditorState } from "prosemirror-state";
import { Primitive } from "utility-types";
import { findParentNode } from "./findParentNode";
type Options = {
/** Only return match if the range and attrs is exact */
exact?: boolean;
/** If true then node must contain entire selection */
inclusive?: boolean;
};
/**
* Checks if a node is active in the current selection or not.
*
* @param type The node type to check.
* @param attrs The attributes to check.
* @param options The options to use.
* @returns A function that checks if a node is active in the current selection or not.
*/
export const isNodeActive =
(type: NodeType, attrs: Record<string, Primitive> = {}) =>
(state: EditorState) => {
(type: NodeType, attrs?: Record<string, Primitive>, options?: Options) =>
(state: EditorState): boolean => {
if (!type) {
return false;
}
const nodeAfter = state.selection.$from.nodeAfter;
let node = nodeAfter?.type === type ? nodeAfter : undefined;
const { from, to } = state.selection;
const nodeWithPos = findParentNode(
(node) =>
node.type === type &&
(!attrs ||
Object.keys(attrs).every((key) => node.attrs[key] === attrs[key]))
)(state.selection);
if (!node) {
const parent = findParentNode((n) => n.type === type)(state.selection);
node = parent?.node;
if (!nodeWithPos) {
return false;
}
if (!Object.keys(attrs).length || !node) {
return !!node;
if (options?.inclusive) {
// Check if the node's position contains the entire selection
return (
nodeWithPos.pos <= from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize >= to
);
}
return node.hasMarkup(type, { ...node.attrs, ...attrs });
if (options?.exact) {
// Check if node's range exactly matches selection
return (
nodeWithPos.pos === from &&
nodeWithPos.pos + nodeWithPos.node.nodeSize === to
);
}
return true;
};
+16 -24
View File
@@ -1,16 +1,5 @@
import { useState, useLayoutEffect } from "react";
const defaultRect = {
top: 0,
left: 0,
bottom: 0,
right: 0,
x: 0,
y: 0,
width: 0,
height: 0,
};
/**
* A hook that returns the size of an element or ref.
*
@@ -19,19 +8,11 @@ const defaultRect = {
*/
export function useComponentSize(
input: HTMLElement | null | React.RefObject<HTMLElement | null>
): DOMRect | typeof defaultRect {
) {
const element = input instanceof HTMLElement ? input : input?.current;
const [size, setSize] = useState(() => element?.getBoundingClientRect());
useLayoutEffect(() => {
const sizeObserver = new ResizeObserver(() => {
element?.dispatchEvent(new CustomEvent("resize"));
});
if (element) {
sizeObserver.observe(element);
}
return () => sizeObserver.disconnect();
}, [element]);
const [size, setSize] = useState<DOMRect | undefined>(
() => element?.getBoundingClientRect() || new DOMRect()
);
useLayoutEffect(() => {
const handleResize = () => {
@@ -55,6 +36,7 @@ export function useComponentSize(
window.addEventListener("click", handleResize);
window.addEventListener("resize", handleResize);
element?.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("click", handleResize);
@@ -63,5 +45,15 @@ export function useComponentSize(
};
});
return size ?? defaultRect;
useLayoutEffect(() => {
const sizeObserver = new ResizeObserver(() => {
element?.dispatchEvent(new CustomEvent("resize"));
});
if (element) {
sizeObserver.observe(element);
}
return () => sizeObserver.disconnect();
}, [element]);
return size ?? new DOMRect();
}