mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
Remove unused files and dependencies (#11850)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
export default function Arrow() {
|
||||
return (
|
||||
<svg
|
||||
width="13"
|
||||
height="30"
|
||||
viewBox="0 0 13 30"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.40242 1.48635C8.23085 0.0650039 10.0656 -0.421985 11.5005 0.39863C12.9354 1.21924 13.427 3.03671 12.5986 4.45806L5.59858 16.4681C4.77015 17.8894 2.93538 18.3764 1.5005 17.5558C0.065623 16.7352 -0.426002 14.9177 0.402425 13.4964L7.40242 1.48635Z" />
|
||||
<path d="M12.5986 25.5419C13.427 26.9633 12.9354 28.7808 11.5005 29.6014C10.0656 30.422 8.23087 29.935 7.40244 28.5136L0.402438 16.5036C-0.425989 15.0823 0.0656365 13.2648 1.50051 12.4442C2.93539 11.6236 4.77016 12.1106 5.59859 13.5319L12.5986 25.5419Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
|
||||
const Divider = styled.hr`
|
||||
border: 0;
|
||||
border-bottom: 1px solid ${s("divider")};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export default Divider;
|
||||
@@ -1,167 +0,0 @@
|
||||
import {
|
||||
useFocusEffect,
|
||||
useRovingTabIndex,
|
||||
} from "@getoutline/react-roving-tabindex";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import { s, hover, ellipsis } from "@shared/styles";
|
||||
import type Document from "~/models/Document";
|
||||
import Highlight, { Mark } from "~/components/Highlight";
|
||||
import { sharedModelPath } from "~/utils/routeHelpers";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
highlight: string;
|
||||
context: string | undefined;
|
||||
showParentDocuments?: boolean;
|
||||
showCollection?: boolean;
|
||||
showPublished?: boolean;
|
||||
shareId?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
};
|
||||
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
|
||||
|
||||
function replaceResultMarks(tag: string) {
|
||||
// don't use SEARCH_RESULT_REGEX here as it causes
|
||||
// an infinite loop to trigger a regex inside it's own callback
|
||||
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
|
||||
}
|
||||
|
||||
function DocumentListItem(
|
||||
props: Props,
|
||||
ref: React.RefObject<HTMLAnchorElement>
|
||||
) {
|
||||
const { document, highlight, context, shareId, ...rest } = props;
|
||||
|
||||
let itemRef: React.Ref<HTMLAnchorElement> =
|
||||
React.useRef<HTMLAnchorElement>(null);
|
||||
if (ref) {
|
||||
itemRef = ref;
|
||||
}
|
||||
|
||||
const { focused, ...rovingTabIndex } = useRovingTabIndex(itemRef, false);
|
||||
useFocusEffect(focused, itemRef);
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
ref={itemRef}
|
||||
dir={document.dir}
|
||||
to={{
|
||||
pathname: shareId
|
||||
? sharedModelPath(shareId, document.url)
|
||||
: document.url,
|
||||
search: highlight ? `?q=${encodeURIComponent(highlight)}` : undefined,
|
||||
state: {
|
||||
title: document.titleWithDefault,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
{...rovingTabIndex}
|
||||
onClick={(ev) => {
|
||||
if (rest.onClick) {
|
||||
rest.onClick(ev);
|
||||
}
|
||||
rovingTabIndex.onClick(ev);
|
||||
}}
|
||||
>
|
||||
<Content>
|
||||
<Heading dir={document.dir}>
|
||||
<Title
|
||||
text={document.titleWithDefault}
|
||||
highlight={highlight}
|
||||
dir={document.dir}
|
||||
/>
|
||||
</Heading>
|
||||
|
||||
{
|
||||
<ResultContext
|
||||
text={context}
|
||||
highlight={highlight ? SEARCH_RESULT_REGEX : undefined}
|
||||
processResult={replaceResultMarks}
|
||||
/>
|
||||
}
|
||||
</Content>
|
||||
</DocumentLink>
|
||||
);
|
||||
}
|
||||
|
||||
const Content = styled.div`
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)<{
|
||||
$isStarred?: boolean;
|
||||
$menuOpen?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
max-height: 50vh;
|
||||
cursor: var(--pointer);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
${breakpoint("tablet")`
|
||||
width: auto;
|
||||
`};
|
||||
|
||||
&:${hover},
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
background: ${s("listItemHoverBackground")};
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.$menuOpen &&
|
||||
css`
|
||||
background: ${s("listItemHoverBackground")};
|
||||
`}
|
||||
`;
|
||||
|
||||
const Heading = styled.h4<{ rtl?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.25em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: ${s("text")};
|
||||
`;
|
||||
|
||||
const Title = styled(Highlight)`
|
||||
max-width: 90%;
|
||||
${ellipsis()}
|
||||
|
||||
${Mark} {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ResultContext = styled(Highlight)`
|
||||
display: block;
|
||||
color: ${s("textTertiary")};
|
||||
font-size: 14px;
|
||||
margin-top: -0.25em;
|
||||
margin-bottom: 0;
|
||||
${ellipsis()}
|
||||
|
||||
${Mark} {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(React.forwardRef(DocumentListItem));
|
||||
@@ -1,30 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { TemplateForm } from "./TemplateForm";
|
||||
import type Template from "~/models/Template";
|
||||
|
||||
type Props = {
|
||||
template: Template;
|
||||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export const TemplateEdit = observer(function TemplateEdit_({
|
||||
template,
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
await template?.save();
|
||||
onSubmit?.();
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
}, [template, onSubmit]);
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TemplateForm template={template} handleSubmit={handleSubmit} />;
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Template from "~/models/Template";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { TemplateForm } from "./TemplateForm";
|
||||
|
||||
type Props = {
|
||||
collectionId?: string | null;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export const TemplateNew = observer(function TemplateNew_({
|
||||
collectionId,
|
||||
onSubmit,
|
||||
}: Props) {
|
||||
const { templates } = useStores();
|
||||
const [template] = useState(
|
||||
new Template({ title: "", collectionId }, templates)
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
try {
|
||||
await template.save();
|
||||
onSubmit?.();
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
}, [template, onSubmit]);
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TemplateForm template={template} handleSubmit={handleSubmit} />;
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import Flex from "~/components/Flex";
|
||||
|
||||
const Label = ({ icon, value }: { icon: React.ReactNode; value: string }) => (
|
||||
<Flex align="center" gap={4}>
|
||||
<IconWrapper>{icon}</IconWrapper>
|
||||
{value}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
export default Label;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Hook to get the current viewport height, accounting for mobile virtual keyboards.
|
||||
* Uses the VisualViewport API when available, falling back to window.innerHeight.
|
||||
*
|
||||
* @returns The current viewport height in pixels
|
||||
*/
|
||||
export default function useViewportHeight(): number | void {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport#browser_compatibility
|
||||
// Note: No support in Firefox at time of writing, however this mainly exists
|
||||
// for virtual keyboards on mobile devices, so that's okay.
|
||||
const [height, setHeight] = useState<number>(
|
||||
() => window.visualViewport?.height || window.innerHeight
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handleResize = () => {
|
||||
setHeight(() => window.visualViewport?.height || window.innerHeight);
|
||||
};
|
||||
|
||||
window.visualViewport?.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
window.visualViewport?.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return height;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { MenuSeparator } from "~/types";
|
||||
|
||||
export default function separator(): MenuSeparator {
|
||||
return {
|
||||
type: "separator",
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const runAllPromises = () => new Promise<void>(setImmediate);
|
||||
@@ -83,7 +83,6 @@
|
||||
"@node-oauth/oauth2-server": "^5.2.0",
|
||||
"@notionhq/client": "^2.3.0",
|
||||
"@octokit/auth-app": "^6.1.4",
|
||||
"@octokit/webhooks": "^13.9.1",
|
||||
"@outlinewiki/koa-passport": "^4.2.1",
|
||||
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -112,7 +111,6 @@
|
||||
"addressparser": "^1.0.1",
|
||||
"async-sema": "^3.1.1",
|
||||
"autotrack": "^2.4.1",
|
||||
"body-scroll-lock": "^4.0.0-beta.0",
|
||||
"bull": "^4.16.5",
|
||||
"class-validator": "^0.14.3",
|
||||
"command-score": "^0.1.2",
|
||||
@@ -287,7 +285,6 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@relative-ci/agent": "^4.3.1",
|
||||
"@types/addressparser": "^1.0.3",
|
||||
"@types/body-scroll-lock": "^3.1.2",
|
||||
"@types/cookie": "0.6.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/diff": "^5.0.9",
|
||||
@@ -382,7 +379,6 @@
|
||||
"@hocuspocus/server": "1.1.2",
|
||||
"fengari": "0.1.5",
|
||||
"prosemirror-transform": "1.10.0",
|
||||
"body-scroll-lock": "^4.0.0-beta.0",
|
||||
"d3": "^7.0.0",
|
||||
"debug": "4.3.4",
|
||||
"node-fetch": "^2.7.0",
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
export const Translations = () => (
|
||||
<>
|
||||
<Trans defaults={`New attribute`} />
|
||||
<Trans defaults={`Paper size`} />
|
||||
<Trans defaults={`Ask AI "{{question}}"`} />
|
||||
<Trans defaults={`Are you sure you want to delete?`} />
|
||||
<Trans
|
||||
defaults={`Deleting this version of the document will permanently and irrevocably remove it from the history.`}
|
||||
/>
|
||||
<Trans defaults={`Format`} />
|
||||
<Trans defaults={`Add option`} />
|
||||
<Trans defaults={`Optional`} />
|
||||
<Trans defaults={`Choose a size for your exported document`} />
|
||||
<Trans defaults={`Revision renamed`} />
|
||||
<Trans defaults={`Failed to save revision`} />
|
||||
<Trans defaults={`Invite to document`} />
|
||||
<Trans defaults={`Sorry, invalid embed link`} />
|
||||
<Trans defaults={`Data Attributes`} />
|
||||
<Trans defaults={`Edit attribute`} />
|
||||
<Trans defaults={`Property`} />
|
||||
<Trans defaults={`Yes`} />
|
||||
<Trans defaults={`No`} />
|
||||
<Trans defaults={`Search or ask a question`} />
|
||||
<Trans
|
||||
defaults={`Invited {{roleName}} will not receive access to any collections or documents unless explicitly shared.`}
|
||||
/>
|
||||
<Trans defaults={`Can view only what is explicitly shared`} />
|
||||
<Trans
|
||||
defaults={`SAML assertion was invalid or missing fields, please check your configuration`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`AI generated answer based on related documents in your workspace`}
|
||||
/>
|
||||
<Trans defaults={`References`} />
|
||||
<Trans
|
||||
defaults={`Enable AI answers to get direct answers to searched questions.`}
|
||||
/>
|
||||
<Trans defaults={`Go to settings`} />
|
||||
<Trans defaults={`Where do I find the file?`} />
|
||||
<Trans
|
||||
defaults={`In a Confluence space, navigate to <em>Space Settings -> Manage space -> Export space</em> and choose to export as HTML with the "Normal Export" option.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Drag and drop the zip file from Confluence's HTML export option, or click to upload`}
|
||||
/>
|
||||
<Trans defaults={`Guests`} />
|
||||
<Trans defaults={`New Attribute`} />
|
||||
<Trans
|
||||
defaults={`Attributes allow you to define data to be stored with your documents. They can be used to store custom properties, metadata, or any other structured information that is common across documents.`}
|
||||
/>
|
||||
<Trans defaults={`Custom domain`} />
|
||||
<Trans defaults={`AI answers`} />
|
||||
<Trans
|
||||
defaults={`Use AI to directly answer searched questions using content in your workspace.`}
|
||||
/>
|
||||
<Trans defaults={`API access`} />
|
||||
<Trans
|
||||
defaults={`Allow members to create API keys for programmatic access`}
|
||||
/>
|
||||
<Trans defaults={`Public document embedding`} />
|
||||
<Trans
|
||||
defaults={`When enabled, publicly shared documents can be embedded in third-party websites`}
|
||||
/>
|
||||
<Trans defaults={`Include previews in emails`} />
|
||||
<Trans
|
||||
defaults={`When enabled, email notifications will include content previews`}
|
||||
/>
|
||||
<Trans defaults={`Boolean`} />
|
||||
<Trans defaults={`Number`} />
|
||||
<Trans defaults={`Text`} />
|
||||
<Trans defaults={`List`} />
|
||||
<Trans defaults={`Could not load events`} />
|
||||
<Trans defaults={`Audit Log`} />
|
||||
<Trans
|
||||
defaults={`The audit log details the history of security related and other events across your knowledge base.`}
|
||||
/>
|
||||
<Trans defaults={`IP address`} />
|
||||
<Trans defaults={`Actor`} />
|
||||
<Trans defaults={`Event`} />
|
||||
<Trans defaults={`Timestamp`} />
|
||||
<Trans defaults={`IP`} />
|
||||
<Trans defaults={`a group`} />
|
||||
<Trans defaults={`All users`} />
|
||||
<Trans defaults={`Private`} />
|
||||
<Trans defaults={`View and edit`} />
|
||||
<Trans defaults={`Sharing enabled`} />
|
||||
<Trans defaults={`Date archived`} />
|
||||
<Trans defaults={`Could not load collections`} />
|
||||
<Trans
|
||||
defaults={`Manage the permissions and settings of all collections in the knowledge base. As a workspace admin you can also administer private collections.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Automatically index and search document content from {{appName}} inside <4>Glean</4> in realtime.`}
|
||||
/>
|
||||
<Trans defaults={`API Endpoint`} />
|
||||
<Trans defaults={`API Secret`} />
|
||||
<Trans defaults={`Datasource`} />
|
||||
<Trans
|
||||
defaults={`Details of the current {{appName}} license. To arrange contract renewal as expiry or seat limits approach or increase licensed seats please contact your account manager or email <4>priority@getoutline.com</4>.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Sorry, an answer could not be found in the collection, try widening your search.`}
|
||||
/>
|
||||
<Trans
|
||||
defaults={`Sorry, an answer could not be found in the workspace, try widening your search.`}
|
||||
/>
|
||||
<Trans defaults={`Looking for answers`} />
|
||||
<Trans defaults={`Answer to "{{ query }}"`} />
|
||||
</>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./comments";
|
||||
@@ -1,390 +0,0 @@
|
||||
import { Plugin, PluginKey, type EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { moveTableColumn, moveTableRow } from "prosemirror-tables";
|
||||
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
|
||||
import { getCellsInRow, getRowsInTable } from "../queries/table";
|
||||
|
||||
interface DragState {
|
||||
/** The type of drag operation. */
|
||||
type: "row" | "column";
|
||||
/** The index of the row/column being dragged. */
|
||||
fromIndex: number;
|
||||
/** The current target index for the drop. */
|
||||
toIndex: number;
|
||||
/** The grip element being dragged. */
|
||||
gripElement: HTMLElement;
|
||||
/** The table element containing the drag. */
|
||||
tableElement: HTMLElement;
|
||||
}
|
||||
|
||||
const pluginKey = new PluginKey<DragState | null>("table-drag-drop");
|
||||
|
||||
/**
|
||||
* Creates a drop indicator element for visual feedback during drag operations.
|
||||
*
|
||||
* @returns the drop indicator element.
|
||||
*/
|
||||
function createDropIndicator(): HTMLElement {
|
||||
const indicator = document.createElement("div");
|
||||
indicator.className = EditorStyleHelper.tableDragDropIndicator;
|
||||
return indicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the target index based on mouse position during drag.
|
||||
*
|
||||
* @param dragState Current drag state.
|
||||
* @param mouseX Current mouse X position.
|
||||
* @param mouseY Current mouse Y position.
|
||||
* @param state Editor state.
|
||||
* @returns The target index for the drop.
|
||||
*/
|
||||
function calculateTargetIndex(
|
||||
dragState: DragState,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
state: EditorState
|
||||
): number {
|
||||
if (dragState.type === "row") {
|
||||
return calculateRowTargetIndex(dragState, mouseY, state);
|
||||
} else {
|
||||
return calculateColumnTargetIndex(dragState, mouseX, state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the target row index based on mouse Y position.
|
||||
*
|
||||
* @param dragState Current drag state.
|
||||
* @param mouseY Current mouse Y position.
|
||||
* @param state Editor state.
|
||||
* @returns The target row index.
|
||||
*/
|
||||
function calculateRowTargetIndex(
|
||||
dragState: DragState,
|
||||
mouseY: number,
|
||||
state: EditorState
|
||||
): number {
|
||||
const rows = getRowsInTable(state);
|
||||
if (rows.length === 0) {
|
||||
return dragState.fromIndex;
|
||||
}
|
||||
|
||||
const table = dragState.tableElement.querySelector("table");
|
||||
if (!table) {
|
||||
return dragState.fromIndex;
|
||||
}
|
||||
|
||||
const tableRows = table.querySelectorAll("tr");
|
||||
let targetIndex = dragState.fromIndex;
|
||||
|
||||
tableRows.forEach((row, index) => {
|
||||
const rect = row.getBoundingClientRect();
|
||||
const midpoint = rect.top + rect.height / 2;
|
||||
|
||||
if (mouseY < midpoint && index <= dragState.fromIndex) {
|
||||
targetIndex = index;
|
||||
} else if (mouseY >= midpoint && index >= dragState.fromIndex) {
|
||||
targetIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(0, Math.min(targetIndex, rows.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the target column index based on mouse X position.
|
||||
*
|
||||
* @param dragState Current drag state.
|
||||
* @param mouseX Current mouse X position.
|
||||
* @param state Editor state.
|
||||
* @returns The target column index.
|
||||
*/
|
||||
function calculateColumnTargetIndex(
|
||||
dragState: DragState,
|
||||
mouseX: number,
|
||||
state: EditorState
|
||||
): number {
|
||||
const cols = getCellsInRow(0)(state);
|
||||
if (cols.length === 0) {
|
||||
return dragState.fromIndex;
|
||||
}
|
||||
|
||||
const table = dragState.tableElement.querySelector("table");
|
||||
if (!table) {
|
||||
return dragState.fromIndex;
|
||||
}
|
||||
|
||||
const headerRow = table.querySelector("tr");
|
||||
if (!headerRow) {
|
||||
return dragState.fromIndex;
|
||||
}
|
||||
|
||||
const cells = headerRow.querySelectorAll("th, td");
|
||||
let targetIndex = dragState.fromIndex;
|
||||
|
||||
cells.forEach((cell, index) => {
|
||||
const rect = cell.getBoundingClientRect();
|
||||
const midpoint = rect.left + rect.width / 2;
|
||||
|
||||
if (mouseX < midpoint && index <= dragState.fromIndex) {
|
||||
targetIndex = index;
|
||||
} else if (mouseX >= midpoint && index >= dragState.fromIndex) {
|
||||
targetIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(0, Math.min(targetIndex, cols.length - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions the drop indicator based on the current drag state.
|
||||
*
|
||||
* @param indicator Drop indicator element.
|
||||
* @param dragState Current drag state.
|
||||
*/
|
||||
function positionDropIndicator(
|
||||
indicator: HTMLElement,
|
||||
dragState: DragState
|
||||
): void {
|
||||
const table = dragState.tableElement.querySelector("table");
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tableRect = dragState.tableElement.getBoundingClientRect();
|
||||
|
||||
if (dragState.type === "row") {
|
||||
const tableRows = table.querySelectorAll("tr");
|
||||
const targetRow = tableRows[dragState.toIndex];
|
||||
|
||||
if (targetRow) {
|
||||
const rowRect = targetRow.getBoundingClientRect();
|
||||
const isMovingDown = dragState.toIndex > dragState.fromIndex;
|
||||
|
||||
indicator.style.left = `${rowRect.left - tableRect.left}px`;
|
||||
indicator.style.width = `${rowRect.width}px`;
|
||||
indicator.style.height = "2px";
|
||||
indicator.style.top = isMovingDown
|
||||
? `${rowRect.bottom - tableRect.top}px`
|
||||
: `${rowRect.top - tableRect.top}px`;
|
||||
}
|
||||
} else {
|
||||
const headerRow = table.querySelector("tr");
|
||||
if (!headerRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cells = headerRow.querySelectorAll("th, td");
|
||||
const targetCell = cells[dragState.toIndex];
|
||||
|
||||
if (targetCell) {
|
||||
const cellRect = targetCell.getBoundingClientRect();
|
||||
const isMovingRight = dragState.toIndex > dragState.fromIndex;
|
||||
|
||||
indicator.style.top = `${table.getBoundingClientRect().top - tableRect.top}px`;
|
||||
indicator.style.height = `${table.getBoundingClientRect().height}px`;
|
||||
indicator.style.width = "2px";
|
||||
indicator.style.left = isMovingRight
|
||||
? `${cellRect.right - tableRect.left}px`
|
||||
: `${cellRect.left - tableRect.left}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A ProseMirror plugin that enables drag and drop for table rows and columns
|
||||
* using the grip handles.
|
||||
*/
|
||||
export class TableDragDropPlugin extends Plugin<DragState | null> {
|
||||
private dropIndicator: HTMLElement | null = null;
|
||||
private dragState: DragState | null = null;
|
||||
private view: EditorView | null = null;
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
key: pluginKey,
|
||||
view: (view: EditorView) => {
|
||||
this.view = view;
|
||||
return {
|
||||
destroy: () => {
|
||||
this.cleanup();
|
||||
this.view = null;
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
handleDOMEvents: {
|
||||
mousedown: (view: EditorView, event: MouseEvent) =>
|
||||
this.handleMouseDown(view, event),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleMouseDown(view: EditorView, event: MouseEvent): boolean {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're clicking on a row grip
|
||||
const rowGrip = event.target.closest(`.${EditorStyleHelper.tableGripRow}`);
|
||||
if (rowGrip instanceof HTMLElement) {
|
||||
return this.startDrag(view, event, rowGrip, "row");
|
||||
}
|
||||
|
||||
// Check if we're clicking on a column grip
|
||||
const colGrip = event.target.closest(
|
||||
`.${EditorStyleHelper.tableGripColumn}`
|
||||
);
|
||||
if (colGrip instanceof HTMLElement) {
|
||||
return this.startDrag(view, event, colGrip, "column");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private startDrag(
|
||||
view: EditorView,
|
||||
event: MouseEvent,
|
||||
gripElement: HTMLElement,
|
||||
type: "row" | "column"
|
||||
): boolean {
|
||||
const tableElement = gripElement.closest(`.${EditorStyleHelper.table}`);
|
||||
if (!(tableElement instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const indexStr = gripElement.dataset.index;
|
||||
if (indexStr === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fromIndex = parseInt(indexStr, 10);
|
||||
if (isNaN(fromIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize drag state
|
||||
this.dragState = {
|
||||
type,
|
||||
fromIndex,
|
||||
toIndex: fromIndex,
|
||||
gripElement,
|
||||
tableElement,
|
||||
};
|
||||
|
||||
// Add dragging class
|
||||
gripElement.classList.add(EditorStyleHelper.tableDragging);
|
||||
document.body.style.cursor = "grabbing";
|
||||
|
||||
// Create and position drop indicator
|
||||
this.dropIndicator = createDropIndicator();
|
||||
this.dropIndicator.dataset.type = type;
|
||||
tableElement.appendChild(this.dropIndicator);
|
||||
positionDropIndicator(this.dropIndicator, this.dragState);
|
||||
|
||||
// Bind event listeners
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
this.handleMouseMove(view, e);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
this.handleMouseUp(view, e);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
// Prevent default to avoid text selection during drag
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
private handleMouseMove(view: EditorView, event: MouseEvent): void {
|
||||
if (!this.dragState || !this.dropIndicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIndex = calculateTargetIndex(
|
||||
this.dragState,
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
view.state
|
||||
);
|
||||
|
||||
if (targetIndex !== this.dragState.toIndex) {
|
||||
this.dragState.toIndex = targetIndex;
|
||||
positionDropIndicator(this.dropIndicator, this.dragState);
|
||||
}
|
||||
|
||||
// Show/hide indicator based on whether it's a valid drop target
|
||||
if (targetIndex !== this.dragState.fromIndex) {
|
||||
this.dropIndicator.classList.add("active");
|
||||
} else {
|
||||
this.dropIndicator.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseUp(view: EditorView, _event: MouseEvent): void {
|
||||
if (!this.dragState) {
|
||||
this.cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, fromIndex, toIndex } = this.dragState;
|
||||
|
||||
// Only perform move if indices are different
|
||||
if (fromIndex !== toIndex) {
|
||||
// Execute the appropriate move command
|
||||
if (type === "row") {
|
||||
this.executeMoveRow(view, fromIndex, toIndex);
|
||||
} else {
|
||||
this.executeMoveColumn(view, fromIndex, toIndex);
|
||||
}
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private executeMoveRow(
|
||||
view: EditorView,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
): void {
|
||||
// Use the moveTableRow command from prosemirror-tables
|
||||
const { state, dispatch } = view;
|
||||
moveTableRow({ from: fromIndex, to: toIndex })(state, dispatch);
|
||||
}
|
||||
|
||||
private executeMoveColumn(
|
||||
view: EditorView,
|
||||
fromIndex: number,
|
||||
toIndex: number
|
||||
): void {
|
||||
// Use the moveTableColumn command from prosemirror-tables
|
||||
const { state, dispatch } = view;
|
||||
moveTableColumn({ from: fromIndex, to: toIndex })(state, dispatch);
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
// Remove dragging class
|
||||
if (this.dragState?.gripElement) {
|
||||
this.dragState.gripElement.classList.remove("table-dragging");
|
||||
}
|
||||
|
||||
// Remove drop indicator
|
||||
if (this.dropIndicator) {
|
||||
this.dropIndicator.remove();
|
||||
this.dropIndicator = null;
|
||||
}
|
||||
|
||||
// Reset cursor
|
||||
document.body.style.cursor = "";
|
||||
|
||||
// Clear drag state
|
||||
this.dragState = null;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { Token } from "markdown-it";
|
||||
import type MarkdownIt from "markdown-it";
|
||||
import type { EmbedDescriptor } from "../embeds";
|
||||
|
||||
function isParagraph(token: Token) {
|
||||
return token?.type === "paragraph_open";
|
||||
}
|
||||
|
||||
function isLinkOpen(token: Token) {
|
||||
return token.type === "link_open";
|
||||
}
|
||||
|
||||
function isLinkClose(token: Token) {
|
||||
return token.type === "link_close";
|
||||
}
|
||||
|
||||
export default function linksToEmbeds(embeds: EmbedDescriptor[]) {
|
||||
function isEmbed(token: Token, link: Token) {
|
||||
const href = link.attrs ? link.attrs[0][1] : "";
|
||||
const simpleLink = href === token.content;
|
||||
|
||||
if (!simpleLink) {
|
||||
return false;
|
||||
}
|
||||
if (!embeds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const embed of embeds) {
|
||||
if (!embed.matchOnInput) {
|
||||
continue;
|
||||
}
|
||||
const matches = embed.matcher(href);
|
||||
if (matches) {
|
||||
return {
|
||||
...embed,
|
||||
matches,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return function markdownEmbeds(md: MarkdownIt) {
|
||||
md.core.ruler.after("inline", "embeds", (state) => {
|
||||
const tokens = state.tokens;
|
||||
let insideLink;
|
||||
|
||||
for (let i = 0; i < tokens.length - 1; i++) {
|
||||
// once we find a paragraph, look through it's children for links
|
||||
if (isParagraph(tokens[i - 1])) {
|
||||
const tokenChildren = tokens[i].children || [];
|
||||
|
||||
for (let j = 0; j < tokenChildren.length - 1; j++) {
|
||||
const current = tokenChildren[j];
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isLinkOpen(current)) {
|
||||
insideLink = current;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isLinkClose(current)) {
|
||||
insideLink = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// of hey, we found a link – lets check to see if it should be
|
||||
// converted to an embed
|
||||
if (insideLink) {
|
||||
const result = isEmbed(current, insideLink);
|
||||
if (result) {
|
||||
const { content } = current;
|
||||
|
||||
// convert to embed token
|
||||
const token = new state.Token("embed", "iframe", 0);
|
||||
token.attrSet("href", content);
|
||||
|
||||
// delete the inline link – this makes the assumption that the
|
||||
// embed is the only thing in the para.
|
||||
tokens.splice(i - 1, 3, token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -4842,20 +4842,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-types@npm:^25.1.0":
|
||||
version: 25.1.0
|
||||
resolution: "@octokit/openapi-types@npm:25.1.0"
|
||||
checksum: 10c0/b5b1293b11c6ec7112c7a2713f8507c2696d5db8902ce893b594080ab0329f5a6fcda1b5ac6fe6eed9425e897f4d03326c1bdf5c337e35d324e7b925e52a2661
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-webhooks-types@npm:11.0.0":
|
||||
version: 11.0.0
|
||||
resolution: "@octokit/openapi-webhooks-types@npm:11.0.0"
|
||||
checksum: 10c0/cea59ba6b976a242fa4e6bedfab7e6fc3437381557d2c1876ca3aea5f6d4231559a378f9bc35e09593cb2d466afb4a2415be66b960f3e0a38b65b8b9f22ff90e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/plugin-paginate-graphql@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "@octokit/plugin-paginate-graphql@npm:4.0.1"
|
||||
@@ -4934,15 +4920,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request-error@npm:^6.1.7":
|
||||
version: 6.1.8
|
||||
resolution: "@octokit/request-error@npm:6.1.8"
|
||||
dependencies:
|
||||
"@octokit/types": "npm:^14.0.0"
|
||||
checksum: 10c0/02aa5bfebb5b1b9e152558b4a6f4f7dcb149b41538778ffe0fce3395fd0da5c0862311a78e94723435667581b2a58a7cefa458cf7aa19ae2948ae419276f7ee1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/request@npm:^8.3.1, @octokit/request@npm:^8.4.1":
|
||||
version: 8.4.1
|
||||
resolution: "@octokit/request@npm:8.4.1"
|
||||
@@ -4973,15 +4950,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^14.0.0":
|
||||
version: 14.1.0
|
||||
resolution: "@octokit/types@npm:14.1.0"
|
||||
dependencies:
|
||||
"@octokit/openapi-types": "npm:^25.1.0"
|
||||
checksum: 10c0/4640a6c0a95386be4d015b96c3a906756ea657f7df3c6e706d19fea6bf3ac44fd2991c8c817afe1e670ff9042b85b0e06f7fd373f6bbd47da64208701bb46d5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/webhooks-methods@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "@octokit/webhooks-methods@npm:4.1.0"
|
||||
@@ -4989,13 +4957,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/webhooks-methods@npm:^5.1.1":
|
||||
version: 5.1.1
|
||||
resolution: "@octokit/webhooks-methods@npm:5.1.1"
|
||||
checksum: 10c0/837aa49dbc7f8bc01448f4eaea32cfb0c1cbfa0b4ac570f7bcda385c71f43e4be05e91a3fb63708448410b27e246570fde42056b6b87d755154d84eff5a6500c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/webhooks-types@npm:7.6.1":
|
||||
version: 7.6.1
|
||||
resolution: "@octokit/webhooks-types@npm:7.6.1"
|
||||
@@ -5015,17 +4976,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/webhooks@npm:^13.9.1":
|
||||
version: 13.9.1
|
||||
resolution: "@octokit/webhooks@npm:13.9.1"
|
||||
dependencies:
|
||||
"@octokit/openapi-webhooks-types": "npm:11.0.0"
|
||||
"@octokit/request-error": "npm:^6.1.7"
|
||||
"@octokit/webhooks-methods": "npm:^5.1.1"
|
||||
checksum: 10c0/feb4a595d5f160908f97be9b68c9c579b5cdf7abc28407998e652759b15f9082b026caa6b41bd4f56132e3c99f0a9e5ebfe4319177818d14bd65ec49cd2b93f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@one-ini/wasm@npm:0.1.1":
|
||||
version: 0.1.1
|
||||
resolution: "@one-ini/wasm@npm:0.1.1"
|
||||
@@ -7641,13 +7591,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/body-scroll-lock@npm:^3.1.2":
|
||||
version: 3.1.2
|
||||
resolution: "@types/body-scroll-lock@npm:3.1.2"
|
||||
checksum: 10c0/e766e44ef16e2b9a50488944eb8310247df7a8acfbbc4ed6d9f19ba83987404080aa02eae0256a57b5dd29bf5b9d39f6fa357abcabc3fbc47b8772dc681a1fac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/btoa-lite@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "@types/btoa-lite@npm:1.0.2"
|
||||
@@ -9952,13 +9895,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-scroll-lock@npm:^4.0.0-beta.0":
|
||||
version: 4.0.0-beta.0
|
||||
resolution: "body-scroll-lock@npm:4.0.0-beta.0"
|
||||
checksum: 10c0/32a9553b83424e69f3784d7bbcef2949c43159ea13b5084c96dc08b1e2585e4eeb1e915f14e37a4ac44110339dfa6fa2ddde4d3d812be88ecbea060aa724a791
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"boolbase@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "boolbase@npm:1.0.0"
|
||||
@@ -17633,7 +17569,6 @@ __metadata:
|
||||
"@node-oauth/oauth2-server": "npm:^5.2.0"
|
||||
"@notionhq/client": "npm:^2.3.0"
|
||||
"@octokit/auth-app": "npm:^6.1.4"
|
||||
"@octokit/webhooks": "npm:^13.9.1"
|
||||
"@outlinewiki/koa-passport": "npm:^4.2.1"
|
||||
"@outlinewiki/passport-azure-ad-oauth2": "npm:^0.1.0"
|
||||
"@radix-ui/react-collapsible": "npm:^1.1.12"
|
||||
@@ -17656,7 +17591,6 @@ __metadata:
|
||||
"@tanstack/react-table": "npm:^8.21.3"
|
||||
"@tanstack/react-virtual": "npm:^3.13.12"
|
||||
"@types/addressparser": "npm:^1.0.3"
|
||||
"@types/body-scroll-lock": "npm:^3.1.2"
|
||||
"@types/cookie": "npm:0.6.0"
|
||||
"@types/crypto-js": "npm:^4.2.2"
|
||||
"@types/diff": "npm:^5.0.9"
|
||||
@@ -17732,7 +17666,6 @@ __metadata:
|
||||
babel-plugin-transform-inline-environment-variables: "npm:^0.4.4"
|
||||
babel-plugin-transform-typescript-metadata: "npm:^0.4.0"
|
||||
babel-plugin-tsconfig-paths-module-resolver: "npm:^1.0.4"
|
||||
body-scroll-lock: "npm:^4.0.0-beta.0"
|
||||
browserslist-to-esbuild: "npm:^1.2.0"
|
||||
bull: "npm:^4.16.5"
|
||||
class-validator: "npm:^0.14.3"
|
||||
|
||||
Reference in New Issue
Block a user