refactor: remove bookmark plugin (#4544)

This commit is contained in:
Alex Yang
2023-09-30 00:48:33 -05:00
committed by GitHub
parent b440c3a820
commit c911806062
23 changed files with 32 additions and 361 deletions

View File

@@ -1,5 +0,0 @@
# AFFiNE Bookmark
> Bookmark plugin
![preview](assets/preview.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,30 +0,0 @@
{
"name": "@affine/bookmark-plugin",
"type": "module",
"version": "0.9.0-canary.13",
"description": "Bookmark Plugin",
"affinePlugin": {
"release": true,
"entry": {
"core": "./src/index.ts",
"server": "./src/server.ts"
},
"serverCommand": [
"com.blocksuite.bookmark-block.get-bookmark-data-by-link"
]
},
"scripts": {
"dev": "af dev",
"build": "af build"
},
"dependencies": {
"@affine/component": "workspace:*",
"@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33",
"foxact": "^0.2.20",
"link-preview-js": "^3.0.5"
},
"devDependencies": {
"@affine/plugin-cli": "workspace:*"
}
}

View File

@@ -1,26 +0,0 @@
{
"name": "@affine/bookmark-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"namedInputs": {
"default": [
"{projectRoot}/**/*",
"{workspaceRoot}/packages/plugin-cli/**/*",
"sharedGlobals"
]
},
"targets": {
"build": {
"executor": "nx:run-script",
"options": {
"script": "build"
},
"dependsOn": ["^build"],
"inputs": ["default"],
"outputs": [
"{workspaceRoot}/apps/core/public/plugins/bookmark",
"{workspaceRoot}/apps/electron/dist/plugins/bookmark"
]
}
},
"tags": ["plugin"]
}

View File

@@ -1,258 +0,0 @@
import { MenuItem, PureMenu } from '@affine/component';
import { MuiClickAwayListener } from '@affine/component';
import type { SerializedBlock } from '@blocksuite/blocks';
import type { BaseBlockModel } from '@blocksuite/store';
import type { Page } from '@blocksuite/store';
import type { VEditor } from '@blocksuite/virgo';
import type { ReactElement } from 'react';
import { StrictMode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
export type BookMarkProps = {
page: Page;
};
type ShortcutMap = {
[key: string]: (e: KeyboardEvent, page: Page) => void;
};
const menuOptions = [
{
id: 'dismiss',
label: 'Dismiss',
},
{
id: 'bookmark',
label: 'Create bookmark',
},
];
function getCurrentNativeRange(selection = window.getSelection()) {
if (!selection) {
return null;
}
if (selection.rangeCount === 0) {
return null;
}
if (selection.rangeCount > 1) {
console.warn('getCurrentRange may be wrong, rangeCount > 1');
}
return selection.getRangeAt(0);
}
const handleEnter = ({
page,
selectedOption,
callback,
}: {
page: Page;
selectedOption: keyof ShortcutMap;
callback: () => void;
}) => {
if (selectedOption === 'dismiss') {
return callback();
}
const native = getCurrentNativeRange();
if (!native) {
return callback();
}
const container = native.startContainer;
const element =
container instanceof Element ? container : container?.parentElement;
const virgo = element?.closest<Element & { virgoEditor: VEditor }>(
'[data-virgo-root]'
)?.virgoEditor;
if (!virgo) {
return callback();
}
const linkInfo = virgo
?.getDeltasByVRange({
index: native.startOffset,
length: 0,
})
.find(delta => delta[0]?.attributes?.link);
if (!linkInfo) {
return;
}
const [, { index, length }] = linkInfo;
const link = linkInfo[0]?.attributes?.link as string;
const model = element?.closest<Element & { model: BaseBlockModel }>(
'[data-block-id]'
)?.model;
if (!model) {
return callback();
}
const parent = page.getParent(model);
if (!parent) {
return callback();
}
const currentBlockIndex = parent.children.indexOf(model);
page.addBlock(
'affine:bookmark',
{ url: link },
parent,
currentBlockIndex + 1
);
virgo?.deleteText({
index,
length,
});
if (model.isEmpty()) {
page.deleteBlock(model);
}
return callback();
};
const shouldShowBookmarkMenu = (pastedBlocks: Record<string, unknown>[]) => {
if (!pastedBlocks.length || pastedBlocks.length > 1) {
return;
}
const [firstBlock] = pastedBlocks as [SerializedBlock];
if (
!firstBlock.text ||
!firstBlock.text.length ||
firstBlock.text.length > 1
) {
return;
}
return !!firstBlock.text[0].attributes?.link;
};
const BookMarkUI = ({ page }: BookMarkProps) => {
const [anchor, setAnchor] = useState<Range | null>(null);
const [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id
);
const shortcutMap = useMemo<ShortcutMap>(
() => ({
ArrowUp: () => {
const curIndex = menuOptions.findIndex(
({ id }) => id === selectedOption
);
if (menuOptions[curIndex - 1]) {
setSelectedOption(menuOptions[curIndex - 1].id);
} else if (curIndex === -1) {
setSelectedOption(menuOptions[0].id);
} else {
setSelectedOption(menuOptions[menuOptions.length - 1].id);
}
},
ArrowDown: () => {
const curIndex = menuOptions.findIndex(
({ id }) => id === selectedOption
);
if (curIndex !== -1 && menuOptions[curIndex + 1]) {
setSelectedOption(menuOptions[curIndex + 1].id);
} else {
setSelectedOption(menuOptions[0].id);
}
},
Enter: () =>
handleEnter({
page,
selectedOption,
callback: () => {
setAnchor(null);
},
}),
Escape: () => {
setAnchor(null);
},
}),
[page, selectedOption]
);
const onKeydown = useCallback(
(e: KeyboardEvent) => {
const shortcut = shortcutMap[e.key];
if (shortcut) {
e.stopPropagation();
e.preventDefault();
shortcut(e, page);
} else {
setAnchor(null);
}
},
[page, shortcutMap]
);
useEffect(() => {
const disposer = page.slots.pasted.on(pastedBlocks => {
if (!shouldShowBookmarkMenu(pastedBlocks)) {
return;
}
window.setTimeout(() => {
setAnchor(getCurrentNativeRange());
}, 100);
});
return () => {
disposer.dispose();
};
}, [onKeydown, page, shortcutMap]);
useEffect(() => {
if (anchor) {
document.addEventListener('keydown', onKeydown, { capture: true });
} else {
// reset status and remove event
setSelectedOption(menuOptions[0].id);
document.removeEventListener('keydown', onKeydown, { capture: true });
}
return () => {
document.removeEventListener('keydown', onKeydown, { capture: true });
};
}, [anchor, onKeydown]);
return anchor ? (
<MuiClickAwayListener
onClickAway={() => {
setAnchor(null);
setSelectedOption('');
}}
>
<div>
<PureMenu open={!!anchor} anchorEl={anchor} placement="bottom-start">
{menuOptions.map(({ id, label }) => {
return (
<MenuItem
key={id}
active={selectedOption === id}
onClick={() => {
handleEnter({
page,
selectedOption: id,
callback: () => {
setAnchor(null);
},
});
}}
disableHover={true}
onMouseEnter={() => {
setSelectedOption(id);
}}
>
{label}
</MenuItem>
);
})}
</PureMenu>
</div>
</MuiClickAwayListener>
) : null;
};
type AppProps = {
page: Page;
};
export const App = (props: AppProps): ReactElement => {
return (
<StrictMode>
<BookMarkUI page={props.page} />
</StrictMode>
);
};

View File

@@ -1,21 +0,0 @@
import type { PluginContext } from '@affine/sdk/entry';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './app';
export const entry = (context: PluginContext) => {
console.log('register');
context.register('editor', (div, editor) => {
const root = createRoot(div);
root.render(createElement(App, { page: editor.page }));
return () => {
root.unmount();
};
});
return () => {
console.log('unregister');
};
};

View File

@@ -1,65 +0,0 @@
import type { ServerContext } from '@affine/sdk/server';
import { getLinkPreview } from 'link-preview-js';
type MetaData = {
title?: string;
description?: string;
icon?: string;
image?: string;
[x: string]: string | string[] | undefined;
};
export interface PreviewType {
url: string;
title: string;
siteName: string | undefined;
description: string | undefined;
mediaType: string;
contentType: string | undefined;
images: string[];
videos: {
url: string | undefined;
secureUrl: string | null | undefined;
type: string | null | undefined;
width: string | undefined;
height: string | undefined;
}[];
favicons: string[];
}
export const entry = (context: ServerContext) => {
context.registerCommand(
'com.blocksuite.bookmark-block.get-bookmark-data-by-link',
async (url: string): Promise<MetaData> => {
const previewData = (await getLinkPreview(url, {
timeout: 6000,
headers: {
'user-agent': 'googlebot',
},
followRedirects: 'follow',
}).catch(() => {
return {
title: '',
siteName: '',
description: '',
images: [],
videos: [],
contentType: `text/html`,
favicons: [],
};
})) as PreviewType;
return {
title: previewData.title,
description: previewData.description,
icon: previewData.favicons[0],
image: previewData.images[0],
};
}
);
return () => {
context.unregisterCommand(
'com.blocksuite.bookmark-block.get-bookmark-data-by-link'
);
};
};

View File

@@ -1,17 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
"noEmit": false,
"outDir": "lib",
"jsx": "preserve"
},
"references": [
{
"path": "../../packages/sdk"
},
{
"path": "../../packages/component"
}
]
}