init: the first public commit for AFFiNE

This commit is contained in:
DarkSky
2022-07-22 15:49:21 +08:00
commit e3e3741393
1451 changed files with 108124 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
import {
MuiClickAwayListener as ClickAwayListener,
styled,
} from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service';
import type {
AsyncBlock,
PluginHooks,
Virgo,
} from '@toeverything/framework/virgo';
import {
createContext,
useContext,
useEffect,
useState,
type CSSProperties,
} from 'react';
export type Store =
| {
editor: Virgo;
hooks: PluginHooks;
}
| Record<string, never>;
export const StoreContext = createContext<Store>({});
export const MenuApp = () => {
const { editor } = useContext(StoreContext);
const [show, setShow] = useState<boolean>(false);
const [style, setStyle] = useState<CSSProperties>();
const [selectedNodes, setSelectedNodes] = useState<AsyncBlock[]>([]);
useEffect(() => {
if (!editor) {
console.warn('Failed to found editor! ');
return undefined;
}
const unsubscribe = editor.selection.onSelectEnd(
async ({ type, selectedNodesIds }) => {
if (type !== 'Block' || selectedNodesIds.length <= 1) {
// Not show menu if only one node is selected.
// The user can still create a group with only one block.
return;
}
const selectedNodes = (
await Promise.all(
selectedNodesIds.map(id => editor.getBlockById(id))
)
).filter(Boolean) as AsyncBlock[];
if (
!selectedNodes.every(
node => node.type === Protocol.Block.Type.group
)
) {
return;
}
// Assume the first block is the topmost block
const topmostBlock = selectedNodes[0];
if (!topmostBlock) {
throw new Error(
'Failed to get block, id: ' + selectedNodesIds[0]
);
}
const dom = topmostBlock.dom;
if (!dom) {
return;
}
const rect = dom.getBoundingClientRect();
setSelectedNodes(selectedNodes);
setStyle({
position: 'fixed',
left: rect.right,
top: rect.top,
transform: 'translate(-100%, -100%)',
});
setShow(true);
}
);
return unsubscribe;
}, [editor]);
const handleGroup = () => {
editor.commands.blockCommands.mergeGroup(...selectedNodes);
setShow(false);
};
const handleClickAway = () => {
if (show) {
setShow(false);
}
};
return show ? (
<ClickAwayListener onClickAway={handleClickAway}>
<IconButton style={style} onClick={handleGroup}>
Merge Group
</IconButton>
</ClickAwayListener>
) : null;
};
const IconButton = styled('span')({
padding: '4px 8px',
borderRadius: '4px',
color: '#3E6FDB',
cursor: 'pointer',
'&:hover': {
backgroundColor: 'rgba(62, 111, 219, 0.1)',
},
'&.active': {
backgroundColor: 'rgba(62, 111, 219, 0.1)',
},
});

View File

@@ -0,0 +1,36 @@
import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils';
import { MenuApp, StoreContext } from './MenuApp';
const PLUGIN_NAME = 'selection-group';
export class SelectionGroupPlugin extends BasePlugin {
public static override get pluginName(): string {
return PLUGIN_NAME;
}
private root: PluginRenderRoot | undefined;
protected override on_render() {
this.root = new PluginRenderRoot({
name: SelectionGroupPlugin.pluginName,
render: this.editor.reactRenderRoot?.render,
});
this.root.mount();
this.root.render(
<StrictMode>
<StoreContext.Provider
value={{ editor: this.editor, hooks: this.hooks }}
>
<MenuApp />
</StoreContext.Provider>
</StrictMode>
);
}
public override dispose() {
this.root?.unmount();
super.dispose();
}
}

View File

@@ -0,0 +1 @@
export { SelectionGroupPlugin } from './SelectionGroupPlugin';