mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5384ee8aae | |||
| 97c3ea7da8 | |||
| 4af2b032dd | |||
| c52d9a850d | |||
| 588e5bc17f | |||
| a2bd0edd82 |
@@ -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}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -645,12 +645,11 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
"section" in item ? item.section?.({ t }) : undefined;
|
||||
|
||||
const response = (
|
||||
<>
|
||||
<React.Fragment key={`${index}-${item.name}`}>
|
||||
{currentHeading !== previousHeading && (
|
||||
<Header key={currentHeading}>{currentHeading}</Header>
|
||||
)}
|
||||
<ListItem
|
||||
key={index}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerDown={handlePointerDown}
|
||||
>
|
||||
@@ -659,7 +658,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
onClick: () => handleClickItem(item),
|
||||
})}
|
||||
</ListItem>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
previousHeading = currentHeading;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -313,6 +313,10 @@ width: 100%;
|
||||
background: ${props.theme.mentionHoverBackground};
|
||||
}
|
||||
|
||||
&[data-type="user"] {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&.mention-user::before {
|
||||
content: "@";
|
||||
}
|
||||
|
||||
@@ -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" }],
|
||||
};
|
||||
|
||||
@@ -98,6 +98,8 @@ export default class TableCell extends Node {
|
||||
}
|
||||
}
|
||||
|
||||
// For all other cases, preserve the slice as is to maintain table structure
|
||||
// including column widths and other attributes
|
||||
return slice;
|
||||
},
|
||||
handleDOMEvents: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user