mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
Merge branch 'develop' into fix/clipboard
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import type { Column } from '@toeverything/datasource/db-service';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
|
||||
export const RootContext = createContext<{
|
||||
const RootContext = createContext<{
|
||||
editor: BlockEditor;
|
||||
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
|
||||
editorElement: () => JSX.Element;
|
||||
@@ -14,6 +13,8 @@ export const RootContext = createContext<{
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
};
|
||||
@@ -22,16 +23,3 @@ export const useEditor = () => {
|
||||
* @deprecated
|
||||
*/
|
||||
export const BlockContext = createContext<AsyncBlock>(null as any);
|
||||
|
||||
/**
|
||||
* Context of column information
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
export const ColumnsContext = createContext<{
|
||||
fromId: string;
|
||||
columns: Column[];
|
||||
}>({
|
||||
fromId: '',
|
||||
columns: [],
|
||||
});
|
||||
@@ -2,14 +2,14 @@ import type { BlockEditor } from './editor';
|
||||
import { styled, usePatchNodes } from '@toeverything/components/ui';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { RootContext } from './contexts';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
import {
|
||||
Protocol,
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { addNewGroup } from './recast-block';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -151,7 +151,7 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<RootContext.Provider value={{ editor, editorElement }}>
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isWhiteboard={editor.isWhiteboard}
|
||||
ref={ref => {
|
||||
@@ -183,7 +183,7 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
{editor.isWhiteboard ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</RootContext.Provider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -199,24 +199,32 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
mouseMoved.current = false;
|
||||
return;
|
||||
}
|
||||
const lastBlock = await editor.getRootLastChildrenBlock();
|
||||
const rootBlock = await editor.getBlockById(
|
||||
editor.getRootBlockId()
|
||||
);
|
||||
if (!rootBlock) {
|
||||
throw new Error('root block is not found');
|
||||
}
|
||||
|
||||
const lastGroupBlock = await editor.getRootLastChildrenBlock();
|
||||
const lastRootChildren = await rootBlock.lastChild();
|
||||
// If last block is not a group
|
||||
// create a group with a empty text
|
||||
if (lastGroupBlock.type !== 'group') {
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
if (lastRootChildren == null) {
|
||||
appendNewGroup(editor, rootBlock, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastGroupBlock.childrenIds.length > 1) {
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
if (
|
||||
lastRootChildren.type !== Protocol.Block.Type.group ||
|
||||
lastRootChildren.childrenIds.length > 1
|
||||
) {
|
||||
addNewGroup(editor, lastRootChildren, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the **only** block in the group is text and is empty
|
||||
// active the text block
|
||||
const theGroupChildBlock = await lastGroupBlock.firstChild();
|
||||
const theGroupChildBlock = await lastRootChildren.firstChild();
|
||||
|
||||
if (
|
||||
theGroupChildBlock &&
|
||||
@@ -229,7 +237,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
return;
|
||||
}
|
||||
// else create a new group
|
||||
addNewGroup(editor, lastBlock, true);
|
||||
addNewGroup(editor, lastRootChildren, true);
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
@@ -187,7 +187,6 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const scrollDirections = getScrollDirections(
|
||||
endPointRef.current,
|
||||
scrollManager.verticalScrollTriggerDistance,
|
||||
@@ -204,6 +203,7 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
|
||||
mouseType.current = 'up';
|
||||
startPointBlock.current = null;
|
||||
setShow(false);
|
||||
setRect(Rect.fromLTRB(0, 0, 0, 0));
|
||||
scrollManager.stopAutoScroll();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { AsyncBlock, BlockEditor } from '../editor';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import { BlockPendantProvider } from '../block-pendant';
|
||||
import { DragDropWrapper } from '../drag-drop-wrapper';
|
||||
|
||||
type BlockContentWrapperProps = {
|
||||
block: AsyncBlock;
|
||||
editor: BlockEditor;
|
||||
children: ReactElement | null;
|
||||
};
|
||||
|
||||
// TODO: remove
|
||||
export const WrapperWithPendantAndDragDrop: FC<BlockContentWrapperProps> =
|
||||
function ({ block, children, editor }) {
|
||||
return (
|
||||
<DragDropWrapper block={block} editor={editor}>
|
||||
<BlockPendantProvider block={block}>
|
||||
{children}
|
||||
</BlockPendantProvider>
|
||||
</DragDropWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './BlockContentWrapper';
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { ReactNode, useRef, useEffect, useState } from 'react';
|
||||
import { getPendantHistory } from '../utils';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
RecastMetaProperty,
|
||||
@@ -30,22 +29,22 @@ export const PendantHistoryPanel = ({
|
||||
|
||||
const [history, setHistory] = useState<RecastBlockValue[]>([]);
|
||||
const popoverHandlerRef = useRef<{ [key: string]: PopperHandler }>({});
|
||||
const { getValueHistory } = getRecastItemValue(block);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
const currentBlockValues = getRecastItemValue(block).getAllValue();
|
||||
const allProperties = getProperties();
|
||||
const missProperties = allProperties.filter(
|
||||
const missValues = getProperties().filter(
|
||||
property => !currentBlockValues.find(v => v.id === property.id)
|
||||
);
|
||||
const pendantHistory = getPendantHistory({
|
||||
const valueHistory = getValueHistory({
|
||||
recastBlockId: recastBlock.id,
|
||||
});
|
||||
const historyMap = missProperties.reduce<{
|
||||
[key: RecastPropertyId]: string;
|
||||
const historyMap = missValues.reduce<{
|
||||
[key: RecastPropertyId]: string[];
|
||||
}>((history, property) => {
|
||||
if (pendantHistory[property.id]) {
|
||||
history[property.id] = pendantHistory[property.id];
|
||||
if (valueHistory[property.id]) {
|
||||
history[property.id] = valueHistory[property.id];
|
||||
}
|
||||
|
||||
return history;
|
||||
@@ -54,18 +53,30 @@ export const PendantHistoryPanel = ({
|
||||
const blockHistory = (
|
||||
await Promise.all(
|
||||
Object.entries(historyMap).map(
|
||||
async ([propertyId, blockId]) => {
|
||||
const latestValueBlock = (
|
||||
await groupBlock.children()
|
||||
).find((block: AsyncBlock) => block.id === blockId);
|
||||
async ([propertyId, blockIds]) => {
|
||||
const blocks = await groupBlock.children();
|
||||
const latestChangeBlock = blockIds
|
||||
.reverse()
|
||||
.reduce<AsyncBlock>((block, id) => {
|
||||
if (!block) {
|
||||
return blocks.find(
|
||||
block => block.id === id
|
||||
);
|
||||
}
|
||||
return block;
|
||||
}, null);
|
||||
|
||||
return getRecastItemValue(
|
||||
latestValueBlock
|
||||
).getValue(propertyId as RecastPropertyId);
|
||||
if (latestChangeBlock) {
|
||||
return getRecastItemValue(
|
||||
latestChangeBlock
|
||||
).getValue(propertyId as RecastPropertyId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)
|
||||
)
|
||||
).filter(v => v);
|
||||
|
||||
setHistory(blockHistory);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ModifyPanelContentProps } from './types';
|
||||
import { StyledDivider, StyledPopoverSubTitle } from '../StyledComponent';
|
||||
import { BasicSelect } from './Select';
|
||||
import { InformationProperty, InformationValue } from '../../recast-block';
|
||||
import { genInitialOptions, getPendantIconsConfigByName } from '../utils';
|
||||
import { generateInitialOptions, getPendantIconsConfigByName } from '../utils';
|
||||
|
||||
export default (props: ModifyPanelContentProps) => {
|
||||
const { onPropertyChange, onValueChange, initialValue, property } = props;
|
||||
@@ -38,7 +38,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.emailOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Email')
|
||||
)
|
||||
@@ -66,7 +66,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.phoneOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Phone')
|
||||
)
|
||||
@@ -94,7 +94,7 @@ export default (props: ModifyPanelContentProps) => {
|
||||
}}
|
||||
initialOptions={
|
||||
propProperty?.locationOptions ||
|
||||
genInitialOptions(
|
||||
generateInitialOptions(
|
||||
property?.type,
|
||||
getPendantIconsConfigByName('Location')
|
||||
)
|
||||
|
||||
@@ -18,7 +18,9 @@ export default ({
|
||||
user: { username, nickname, photo },
|
||||
} = useUserAndSpaces();
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState(initialValue?.value);
|
||||
const [selectedValue, setSelectedValue] = useState(
|
||||
initialValue?.value || ''
|
||||
);
|
||||
const [focus, setFocus] = useState(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@toeverything/components/ui';
|
||||
import { HighLightIconInput } from './IconInput';
|
||||
import { PendantConfig, IconNames, OptionIdType, OptionType } from '../types';
|
||||
import { genBasicOption } from '../utils';
|
||||
import { generateBasicOption } from '../utils';
|
||||
|
||||
type OptionItemType = {
|
||||
option: OptionType;
|
||||
@@ -66,7 +66,7 @@ export const BasicSelect = ({
|
||||
const [selectIds, setSelectIds] = useState<OptionIdType[]>(initialValue);
|
||||
|
||||
const insertOption = (insertId: OptionIdType) => {
|
||||
const newOption = genBasicOption({
|
||||
const newOption = generateBasicOption({
|
||||
index: options.length + 1,
|
||||
iconConfig,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Input, Option, Select, Tooltip } from '@toeverything/components/ui';
|
||||
import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { AsyncBlock } from '../../editor';
|
||||
@@ -15,13 +14,13 @@ import {
|
||||
StyledPopoverSubTitle,
|
||||
StyledPopoverWrapper,
|
||||
} from '../StyledComponent';
|
||||
import { genInitialOptions, getPendantConfigByType } from '../utils';
|
||||
import {
|
||||
generateRandomFieldName,
|
||||
generateInitialOptions,
|
||||
getPendantConfigByType,
|
||||
} from '../utils';
|
||||
import { useOnCreateSure } from './hooks';
|
||||
|
||||
const upperFirst = (str: string) => {
|
||||
return `${str[0].toUpperCase()}${str.slice(1)}`;
|
||||
};
|
||||
|
||||
export const CreatePendantPanel = ({
|
||||
block,
|
||||
onSure,
|
||||
@@ -35,7 +34,7 @@ export const CreatePendantPanel = ({
|
||||
|
||||
useEffect(() => {
|
||||
selectedOption &&
|
||||
setFieldName(upperFirst(`${selectedOption.type}#${nanoid(4)}`));
|
||||
setFieldName(generateRandomFieldName(selectedOption.type));
|
||||
}, [selectedOption]);
|
||||
|
||||
return (
|
||||
@@ -45,7 +44,7 @@ export const CreatePendantPanel = ({
|
||||
<Select
|
||||
width={284}
|
||||
placeholder="Search for a field type"
|
||||
value={selectedOption}
|
||||
value={selectedOption ?? null}
|
||||
onChange={(selectedValue: PendantOptions) => {
|
||||
setSelectedOption(selectedValue);
|
||||
}}
|
||||
@@ -93,7 +92,7 @@ export const CreatePendantPanel = ({
|
||||
<PendantModifyPanel
|
||||
type={selectedOption.type}
|
||||
// Select, MultiSelect, Status use this props as initial property
|
||||
initialOptions={genInitialOptions(
|
||||
initialOptions={generateInitialOptions(
|
||||
selectedOption.type,
|
||||
getPendantConfigByType(selectedOption.type)
|
||||
)}
|
||||
|
||||
@@ -4,11 +4,11 @@ import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { PendantModifyPanel } from '../pendant-modify-panel';
|
||||
import type { AsyncBlock } from '../../editor';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
type RecastBlockValue,
|
||||
type RecastMetaProperty,
|
||||
} from '../../recast-block';
|
||||
import { getPendantConfigByType } from '../utils';
|
||||
import { usePendant } from '../use-pendant';
|
||||
import {
|
||||
StyledPopoverWrapper,
|
||||
StyledOperationLabel,
|
||||
@@ -42,7 +42,8 @@ export const UpdatePendantPanel = ({
|
||||
}: Props) => {
|
||||
const pendantOption = pendantOptions.find(v => v.type === property.type);
|
||||
const iconConfig = getPendantConfigByType(property.type);
|
||||
const { removePendant } = usePendant(block);
|
||||
const { removeValue } = getRecastItemValue(block);
|
||||
|
||||
const Icon = IconMap[iconConfig.iconName];
|
||||
const [fieldName, setFieldName] = useState(property.name);
|
||||
const onUpdateSure = useOnUpdateSure({ block, property });
|
||||
@@ -108,7 +109,7 @@ export const UpdatePendantPanel = ({
|
||||
onDelete={
|
||||
hasDelete
|
||||
? async () => {
|
||||
await removePendant(property);
|
||||
await removeValue(property.id);
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import {
|
||||
genSelectOptionId,
|
||||
getRecastItemValue,
|
||||
type InformationProperty,
|
||||
type MultiSelectProperty,
|
||||
type RecastMetaProperty,
|
||||
type SelectOption,
|
||||
type SelectProperty,
|
||||
useRecastBlock,
|
||||
useRecastBlockMeta,
|
||||
useSelectProperty,
|
||||
SelectValue,
|
||||
MultiSelectValue,
|
||||
StatusValue,
|
||||
InformationValue,
|
||||
TextValue,
|
||||
DateValue,
|
||||
} from '../../recast-block';
|
||||
import { type AsyncBlock } from '../../editor';
|
||||
import { usePendant } from '../use-pendant';
|
||||
import {
|
||||
type OptionType,
|
||||
PendantTypes,
|
||||
@@ -41,8 +48,8 @@ const genOptionWithId = (options: OptionType[] = []) => {
|
||||
export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
const { addProperty } = useRecastBlockMeta();
|
||||
const { createSelect } = useSelectProperty();
|
||||
const { setPendant } = usePendant(block);
|
||||
|
||||
const recastBlock = useRecastBlock();
|
||||
const { setValue } = getRecastItemValue(block);
|
||||
return async ({
|
||||
type,
|
||||
fieldName,
|
||||
@@ -79,7 +86,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
tempSelectedId: newValue,
|
||||
});
|
||||
|
||||
await setPendant(newProperty, selectedId);
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: selectedId,
|
||||
} as SelectValue | MultiSelectValue | StatusValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else if (type === PendantTypes.Information) {
|
||||
const emailOptions = genOptionWithId(newPropertyItem.emailOptions);
|
||||
|
||||
@@ -97,26 +111,33 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
locationOptions,
|
||||
} as Omit<InformationProperty, 'id'>);
|
||||
|
||||
await setPendant(newProperty, {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions,
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions,
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions,
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
});
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions,
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions,
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions,
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
},
|
||||
} as InformationValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else {
|
||||
// TODO: Color and background should use pendant config, but ui is not design now
|
||||
const iconConfig = getPendantConfigByType(type);
|
||||
@@ -129,8 +150,14 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
color: iconConfig.color as CSSProperties['color'],
|
||||
iconName: iconConfig.iconName,
|
||||
});
|
||||
|
||||
await setPendant(newProperty, newValue);
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: newValue,
|
||||
} as TextValue | DateValue,
|
||||
recastBlock.id
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -144,8 +171,9 @@ export const useOnUpdateSure = ({
|
||||
property: RecastMetaProperty;
|
||||
}) => {
|
||||
const { updateSelect } = useSelectProperty();
|
||||
const { setPendant } = usePendant(block);
|
||||
const { updateProperty } = useRecastBlockMeta();
|
||||
const { setValue } = getRecastItemValue(block);
|
||||
const recastBlock = useRecastBlock();
|
||||
|
||||
return async ({
|
||||
type,
|
||||
@@ -199,7 +227,14 @@ export const useOnUpdateSure = ({
|
||||
tempSelectedId: newValue,
|
||||
});
|
||||
|
||||
await setPendant(selectProperty, selectedId);
|
||||
await setValue(
|
||||
{
|
||||
id: selectProperty.id,
|
||||
type: selectProperty.type,
|
||||
value: selectedId,
|
||||
} as SelectValue | MultiSelectValue | StatusValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else if (type === PendantTypes.Information) {
|
||||
// const { emailOptions, phoneOptions, locationOptions } =
|
||||
// property as InformationProperty;
|
||||
@@ -231,28 +266,42 @@ export const useOnUpdateSure = ({
|
||||
locationOptions,
|
||||
} as InformationProperty);
|
||||
|
||||
await setPendant(newProperty, {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
});
|
||||
await setValue(
|
||||
{
|
||||
id: newProperty.id,
|
||||
type: newProperty.type,
|
||||
value: {
|
||||
email: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: emailOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.emailOptions,
|
||||
tempSelectedId: newValue.email,
|
||||
}),
|
||||
phone: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: phoneOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.phoneOptions,
|
||||
tempSelectedId: newValue.phone,
|
||||
}),
|
||||
location: getOfficialSelected({
|
||||
isMulti: true,
|
||||
options: locationOptions as SelectOption[],
|
||||
tempOptions: newPropertyItem.locationOptions,
|
||||
tempSelectedId: newValue.location,
|
||||
}),
|
||||
},
|
||||
} as InformationValue,
|
||||
recastBlock.id
|
||||
);
|
||||
} else {
|
||||
await setPendant(property, newValue);
|
||||
await setValue(
|
||||
{
|
||||
id: property.id,
|
||||
type: property.type,
|
||||
value: newValue,
|
||||
} as TextValue | DateValue,
|
||||
recastBlock.id
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldName !== property.name) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
MuiZoom,
|
||||
MuiFade,
|
||||
Popover,
|
||||
PopperHandler,
|
||||
styled,
|
||||
@@ -100,16 +100,15 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
|
||||
);
|
||||
})}
|
||||
{hasAddBtn ? (
|
||||
<MuiZoom in={showAddBtn}>
|
||||
<MuiFade in={showAddBtn}>
|
||||
<div>
|
||||
<AddPendantPopover
|
||||
block={block}
|
||||
iconStyle={{ marginTop: 4 }}
|
||||
container={blockRenderContainerRef.current}
|
||||
trigger="click"
|
||||
/>
|
||||
</div>
|
||||
</MuiZoom>
|
||||
</MuiFade>
|
||||
) : null}
|
||||
</BlockPendantContainer>
|
||||
);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { removePropertyValueRecord, setPendantHistory } from './utils';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import {
|
||||
getRecastItemValue,
|
||||
RecastMetaProperty,
|
||||
useRecastBlock,
|
||||
} from '../recast-block';
|
||||
|
||||
export const usePendant = (block: AsyncBlock) => {
|
||||
// const { getProperties, removeProperty } = useRecastBlockMeta();
|
||||
const recastBlock = useRecastBlock();
|
||||
const { getValue, setValue, removeValue } = getRecastItemValue(block);
|
||||
// const { updateSelect } = useSelectProperty();
|
||||
|
||||
const setPendant = async (property: RecastMetaProperty, newValue: any) => {
|
||||
const nv = {
|
||||
id: property.id,
|
||||
type: property.type,
|
||||
value: newValue,
|
||||
};
|
||||
await setValue(nv);
|
||||
setPendantHistory({
|
||||
recastBlockId: recastBlock.id,
|
||||
blockId: block.id,
|
||||
propertyId: property.id,
|
||||
});
|
||||
};
|
||||
|
||||
const removePendant = async (property: RecastMetaProperty) => {
|
||||
await removeValue(property.id);
|
||||
removePropertyValueRecord({
|
||||
recastBlockId: block.id,
|
||||
propertyId: property.id,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
setPendant,
|
||||
removePendant,
|
||||
};
|
||||
};
|
||||
@@ -1,84 +1,7 @@
|
||||
import {
|
||||
PropertyType,
|
||||
RecastBlockValue,
|
||||
RecastPropertyId,
|
||||
SelectOption,
|
||||
} from '../recast-block';
|
||||
import { OptionIdType, OptionType } from './types';
|
||||
import { PropertyType, SelectOption } from '../recast-block';
|
||||
import { OptionIdType, OptionType, PendantConfig, PendantTypes } from './types';
|
||||
import { pendantConfig } from './config';
|
||||
import { PendantConfig, PendantTypes } from './types';
|
||||
type Props = {
|
||||
recastBlockId: string;
|
||||
blockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
};
|
||||
|
||||
type StorageMap = {
|
||||
[recastBlockId: string]: {
|
||||
[propertyId: RecastPropertyId]: string;
|
||||
};
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_NAME = 'TEMPORARY_PENDANT_DATA';
|
||||
|
||||
const ensureLocalStorage = () => {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
|
||||
if (!data) {
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
|
||||
}
|
||||
};
|
||||
|
||||
export const setPendantHistory = ({
|
||||
recastBlockId,
|
||||
blockId,
|
||||
propertyId,
|
||||
}: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
if (!data[recastBlockId]) {
|
||||
data[recastBlockId] = {};
|
||||
}
|
||||
const propertyValueRecord = data[recastBlockId];
|
||||
propertyValueRecord[propertyId] = blockId;
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const getPendantHistory = ({
|
||||
recastBlockId,
|
||||
}: {
|
||||
recastBlockId: string;
|
||||
}) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
return data[recastBlockId] ?? {};
|
||||
};
|
||||
|
||||
export const removePropertyValueRecord = ({
|
||||
recastBlockId,
|
||||
propertyId,
|
||||
}: {
|
||||
recastBlockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
}) => {
|
||||
ensureLocalStorage();
|
||||
const data: StorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
if (!data[recastBlockId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete data[recastBlockId][propertyId];
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
/**
|
||||
* In select pendant panel, use mock options instead of use `createSelect` when add or delete option
|
||||
@@ -107,7 +30,7 @@ export const getOfficialSelected = ({
|
||||
.map(id => {
|
||||
return tempOptions.findIndex((o: OptionType) => o.id === id);
|
||||
})
|
||||
.filter(index => index != -1);
|
||||
.filter(index => index !== -1);
|
||||
selectedId = selectedIndex.map((index: number) => {
|
||||
return options[index].id;
|
||||
});
|
||||
@@ -130,7 +53,7 @@ export const getPendantIconsConfigByName = (
|
||||
return pendantConfig[pendantName];
|
||||
};
|
||||
|
||||
export const genBasicOption = ({
|
||||
export const generateBasicOption = ({
|
||||
index,
|
||||
iconConfig,
|
||||
name = '',
|
||||
@@ -159,22 +82,22 @@ export const genBasicOption = ({
|
||||
/**
|
||||
* Status Pendant is a Select Pendant built-in some options
|
||||
* **/
|
||||
export const genInitialOptions = (
|
||||
export const generateInitialOptions = (
|
||||
type: PendantTypes,
|
||||
iconConfig: PendantConfig
|
||||
) => {
|
||||
if (type === PendantTypes.Status) {
|
||||
return [
|
||||
genBasicOption({ index: 0, iconConfig, name: 'No Started' }),
|
||||
genBasicOption({
|
||||
generateBasicOption({ index: 0, iconConfig, name: 'No Started' }),
|
||||
generateBasicOption({
|
||||
index: 1,
|
||||
iconConfig,
|
||||
name: 'In Progress',
|
||||
}),
|
||||
genBasicOption({ index: 2, iconConfig, name: 'Complete' }),
|
||||
generateBasicOption({ index: 2, iconConfig, name: 'Complete' }),
|
||||
];
|
||||
}
|
||||
return [genBasicOption({ index: 0, iconConfig })];
|
||||
return [generateBasicOption({ index: 0, iconConfig })];
|
||||
};
|
||||
|
||||
export const checkPendantForm = (
|
||||
@@ -222,3 +145,10 @@ export const checkPendantForm = (
|
||||
|
||||
return { passed: true, message: 'Check passed !' };
|
||||
};
|
||||
|
||||
const upperFirst = (str: string) => {
|
||||
return `${str[0].toUpperCase()}${str.slice(1)}`;
|
||||
};
|
||||
|
||||
export const generateRandomFieldName = (type: PendantTypes) =>
|
||||
upperFirst(`${type}#${nanoid(4)}`);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { AsyncBlock, BlockEditor } from '../editor';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
interface DragDropWrapperProps {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
children: ReactElement | null;
|
||||
}
|
||||
|
||||
export function DragDropWrapper({
|
||||
children,
|
||||
editor,
|
||||
block,
|
||||
}: DragDropWrapperProps) {
|
||||
const handlerDragOver: React.DragEventHandler<HTMLDivElement> = event => {
|
||||
event.preventDefault();
|
||||
if (block.dom) {
|
||||
editor.getHooks().afterOnNodeDragOver(event, {
|
||||
blockId: block.id,
|
||||
dom: block.dom,
|
||||
rect: block.dom?.getBoundingClientRect(),
|
||||
type: block.type,
|
||||
properties: block.getProperties(),
|
||||
});
|
||||
}
|
||||
};
|
||||
return <div onDragOver={handlerDragOver}>{children}</div>;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './DragDropWrapper';
|
||||
@@ -159,6 +159,33 @@ export class BlockCommands {
|
||||
return [];
|
||||
}
|
||||
|
||||
public async moveInNewGridItem(
|
||||
blockId: string,
|
||||
gridItemId: string,
|
||||
isBefore = false
|
||||
) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
if (block) {
|
||||
const gridItemBlock = await this._editor.createBlock(
|
||||
Protocol.Block.Type.gridItem
|
||||
);
|
||||
const targetGridItemBlock = await this._editor.getBlockById(
|
||||
gridItemId
|
||||
);
|
||||
await block.remove();
|
||||
await gridItemBlock.append(block);
|
||||
if (targetGridItemBlock && gridItemBlock) {
|
||||
if (isBefore) {
|
||||
await targetGridItemBlock.before(gridItemBlock);
|
||||
} else {
|
||||
await targetGridItemBlock.after(gridItemBlock);
|
||||
}
|
||||
}
|
||||
return gridItemBlock;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async splitGroupFromBlock(blockId: string) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
await splitGroup(this._editor, block);
|
||||
|
||||
27
libs/components/editor-core/src/editor/config/grid.ts
Normal file
27
libs/components/editor-core/src/editor/config/grid.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BlockEditor } from '../..';
|
||||
|
||||
/**
|
||||
*
|
||||
* the global config for the editor
|
||||
* @class GridConfig
|
||||
*/
|
||||
export class GridConfig {
|
||||
private _maxGridItemCount = 6;
|
||||
private _editor: BlockEditor;
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
get maxGridItemCount() {
|
||||
return this._maxGridItemCount;
|
||||
}
|
||||
|
||||
set maxGridItemCount(value) {
|
||||
this._maxGridItemCount = value;
|
||||
}
|
||||
|
||||
get gridItemMinWidth() {
|
||||
return 100 / this.maxGridItemCount;
|
||||
}
|
||||
}
|
||||
23
libs/components/editor-core/src/editor/config/index.ts
Normal file
23
libs/components/editor-core/src/editor/config/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { BlockEditor } from '../..';
|
||||
import { GridConfig } from './grid';
|
||||
|
||||
// TODO: if config be complex, add children config abstract
|
||||
/**
|
||||
*
|
||||
* the global config for the editor
|
||||
* @class EditorConfig
|
||||
*/
|
||||
export class EditorConfig {
|
||||
private _maxGridItemCount = 6;
|
||||
private _editor: BlockEditor;
|
||||
private _grid: GridConfig;
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
this._editor = editor;
|
||||
this._grid = new GridConfig(editor);
|
||||
}
|
||||
|
||||
get grid() {
|
||||
return this._grid;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { domToRect, Point } from '@toeverything/utils';
|
||||
import { AsyncBlock } from '../..';
|
||||
import { GridDropType } from '../commands/types';
|
||||
@@ -5,6 +6,7 @@ import { Editor } from '../editor';
|
||||
import { BlockDropPlacement, GroupDirection } from '../types';
|
||||
// TODO: Evaluate implementing custom events with Rxjs
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
|
||||
enum DragType {
|
||||
dragBlock = 'dragBlock',
|
||||
@@ -86,6 +88,7 @@ export class DragDropManager {
|
||||
while (curr !== this._editor.getRootBlockId()) {
|
||||
if (curr === blockId) return false;
|
||||
const block = await this._editor.getBlockById(curr);
|
||||
if (!block) return false;
|
||||
curr = block.parentId;
|
||||
}
|
||||
return true;
|
||||
@@ -114,6 +117,48 @@ export class DragDropManager {
|
||||
: GridDropType.right
|
||||
);
|
||||
}
|
||||
if (
|
||||
[
|
||||
BlockDropPlacement.outerLeft,
|
||||
BlockDropPlacement.outerRight,
|
||||
].includes(this._blockDragDirection)
|
||||
) {
|
||||
const targetBlock = await this._editor.getBlockById(
|
||||
this._blockDragTargetId
|
||||
);
|
||||
if (targetBlock.type !== Protocol.Block.Type.grid) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
this._blockDragDirection ===
|
||||
BlockDropPlacement.outerLeft
|
||||
? GridDropType.left
|
||||
: GridDropType.right
|
||||
);
|
||||
}
|
||||
if (targetBlock.type === Protocol.Block.Type.grid) {
|
||||
const gridItems = await targetBlock.children();
|
||||
if (
|
||||
BlockDropPlacement.outerRight ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[gridItems.length - 1].id
|
||||
);
|
||||
}
|
||||
if (
|
||||
BlockDropPlacement.outerLeft ===
|
||||
this._blockDragDirection
|
||||
) {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[0].id,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +254,93 @@ export class DragDropManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* check if drag block is out of blocks and return direction
|
||||
* @param {React.DragEvent<Element>} event
|
||||
* @return {
|
||||
* direction: BlockDropPlacement.none, // none, outerLeft, outerRight
|
||||
* block: undefined, // the block in the same clientY
|
||||
* isOuter: false, // if is drag over outer
|
||||
* }
|
||||
*
|
||||
* @memberof DragDropManager
|
||||
*/
|
||||
public async checkOuterBlockDragTypes(event: React.DragEvent<Element>) {
|
||||
const { clientX, clientY } = event;
|
||||
const mousePoint = new Point(clientX, clientY);
|
||||
const rootBlock = await this._editor.getBlockById(
|
||||
this._editor.getRootBlockId()
|
||||
);
|
||||
let direction = BlockDropPlacement.none;
|
||||
const rootBlockRect = domToRect(rootBlock.dom);
|
||||
let targetBlock: AsyncBlock | undefined;
|
||||
let typesInfo = {
|
||||
direction: BlockDropPlacement.none,
|
||||
block: undefined,
|
||||
isOuter: false,
|
||||
} as {
|
||||
direction: BlockDropPlacement;
|
||||
block: AsyncBlock | undefined;
|
||||
isOuter: boolean;
|
||||
};
|
||||
if (rootBlockRect.isPointLeft(mousePoint)) {
|
||||
direction = BlockDropPlacement.outerLeft;
|
||||
typesInfo.isOuter = true;
|
||||
}
|
||||
if (rootBlockRect.isPointRight(mousePoint)) {
|
||||
direction = BlockDropPlacement.outerRight;
|
||||
typesInfo.isOuter = true;
|
||||
}
|
||||
if (direction !== BlockDropPlacement.none) {
|
||||
const blockList = await this._editor.getBlockListByLevelOrder();
|
||||
targetBlock = blockList.find(block => {
|
||||
const domRect = domToRect(block.dom);
|
||||
const pointChecker =
|
||||
direction === BlockDropPlacement.outerLeft
|
||||
? domRect.isPointLeft.bind(domRect)
|
||||
: domRect.isPointRight.bind(domRect);
|
||||
return (
|
||||
block.type !== Protocol.Block.Type.page &&
|
||||
block.type !== Protocol.Block.Type.group &&
|
||||
pointChecker(mousePoint)
|
||||
);
|
||||
});
|
||||
if (targetBlock) {
|
||||
if (targetBlock.type !== Protocol.Block.Type.grid) {
|
||||
this._setBlockDragDirection(direction);
|
||||
this._setBlockDragTargetId(targetBlock.id);
|
||||
typesInfo = {
|
||||
direction,
|
||||
block: targetBlock,
|
||||
isOuter: true,
|
||||
};
|
||||
}
|
||||
if (targetBlock.type === Protocol.Block.Type.grid) {
|
||||
const children = await targetBlock.children();
|
||||
if (
|
||||
children.length <
|
||||
this._editor.configManager.grid.maxGridItemCount
|
||||
) {
|
||||
typesInfo = {
|
||||
direction,
|
||||
block: targetBlock,
|
||||
isOuter: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
typesInfo.direction !== BlockDropPlacement.none &&
|
||||
typesInfo.block
|
||||
) {
|
||||
this._setBlockDragTargetId(targetBlock.id);
|
||||
}
|
||||
this._setBlockDragDirection(typesInfo.direction);
|
||||
return typesInfo;
|
||||
}
|
||||
|
||||
public async checkBlockDragTypes(
|
||||
event: React.DragEvent<Element>,
|
||||
blockDom: HTMLElement,
|
||||
@@ -216,10 +348,25 @@ export class DragDropManager {
|
||||
) {
|
||||
const { clientX, clientY } = event;
|
||||
this._setBlockDragTargetId(blockId);
|
||||
|
||||
const path = await this._editor.getBlockPath(blockId);
|
||||
const mousePoint = new Point(clientX, clientY);
|
||||
const rect = domToRect(blockDom);
|
||||
/**
|
||||
* IMP: compute the level of the target block
|
||||
* future feature drag drop has level support do not delete
|
||||
* const levelUnderGrid = Array.from(path)
|
||||
.reverse()
|
||||
.findIndex(block => block.type === Protocol.Block.Type.gridItem);
|
||||
const levelUnderGroup = Array.from(path)
|
||||
.reverse()
|
||||
.findIndex(block => block.type === Protocol.Block.Type.group);
|
||||
const blockLevel =
|
||||
levelUnderGrid > 0 ? levelUnderGrid : levelUnderGroup;
|
||||
console.log({ blockLevel, levelUnderGrid, levelUnderGroup });
|
||||
*
|
||||
*/
|
||||
let direction = BlockDropPlacement.bottom;
|
||||
|
||||
if (mousePoint.x - rect.left <= this._dragBlockHotDistance) {
|
||||
direction = BlockDropPlacement.left;
|
||||
}
|
||||
@@ -236,9 +383,10 @@ export class DragDropManager {
|
||||
direction === BlockDropPlacement.left ||
|
||||
direction === BlockDropPlacement.right
|
||||
) {
|
||||
const path = await this._editor.getBlockPath(blockId);
|
||||
const gridBlocks = path.filter(block => block.type === 'grid');
|
||||
// limit grid block floor counts
|
||||
const gridBlocks = path.filter(
|
||||
block => block.type === Protocol.Block.Type.grid
|
||||
);
|
||||
// limit grid block floor counts, when drag block to init grid
|
||||
if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
|
||||
direction = BlockDropPlacement.none;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ export enum BlockDropPlacement {
|
||||
right = 'right',
|
||||
top = 'top',
|
||||
bottom = 'bottom',
|
||||
outerLeft = 'outer-left',
|
||||
outerRight = 'outer-right',
|
||||
none = 'none',
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { BrowserClipboard } from './clipboard/browser-clipboard';
|
||||
import { ClipboardPopulator } from './clipboard/clipboard-populator';
|
||||
import { BlockHelper } from './block/block-helper';
|
||||
import { DragDropManager } from './drag-drop';
|
||||
import { EditorConfig } from './config';
|
||||
|
||||
export interface EditorCtorProps {
|
||||
workspace: string;
|
||||
@@ -56,6 +57,7 @@ export class Editor implements Virgo {
|
||||
public dragDropManager = new DragDropManager(this);
|
||||
public commands = new EditorCommands(this);
|
||||
public blockHelper = new BlockHelper(this);
|
||||
public configManager = new EditorConfig(this);
|
||||
public bdCommands: Commands;
|
||||
public ui_container?: HTMLDivElement;
|
||||
public version = '0.0.1';
|
||||
@@ -343,6 +345,23 @@ export class Editor implements Virgo {
|
||||
return [...blockList, ...(await this.getOffspring(rootBlockId))];
|
||||
}
|
||||
|
||||
async getBlockListByLevelOrder() {
|
||||
const rootBlockId = this.getRootBlockId();
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
const blockList: Array<AsyncBlock> = [];
|
||||
let nextToVisit: Array<AsyncBlock> = rootBlock ? [rootBlock] : [];
|
||||
while (nextToVisit.length) {
|
||||
let next: Array<AsyncBlock> = [];
|
||||
for (const block of nextToVisit) {
|
||||
const children = await block.children();
|
||||
blockList.push(block);
|
||||
next = next.concat(children);
|
||||
}
|
||||
nextToVisit = next;
|
||||
}
|
||||
return blockList;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get all offspring of block
|
||||
@@ -367,15 +386,6 @@ export class Editor implements Virgo {
|
||||
return blockList;
|
||||
}
|
||||
|
||||
async getRootLastChildrenBlock(rootBlockId = this.getRootBlockId()) {
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
if (!rootBlock) {
|
||||
throw new Error('root block is not found');
|
||||
}
|
||||
const lastChildren = await rootBlock.lastChild();
|
||||
return lastChildren ?? rootBlock;
|
||||
}
|
||||
|
||||
async getLastBlock(rootBlockId = this.getRootBlockId()) {
|
||||
const rootBlock = await this.getBlockById(rootBlockId);
|
||||
if (!rootBlock) {
|
||||
|
||||
@@ -7,12 +7,6 @@ export * from './commands/types';
|
||||
export { Editor as BlockEditor } from './editor';
|
||||
export * from './selection';
|
||||
export { BlockDropPlacement, HookType, GroupDirection } from './types';
|
||||
export type {
|
||||
BlockDomInfo,
|
||||
Plugin,
|
||||
PluginCreator,
|
||||
PluginHooks,
|
||||
Virgo,
|
||||
} from './types';
|
||||
export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
|
||||
export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
|
||||
export type { ChildrenView, CreateView } from './views/base-view';
|
||||
|
||||
@@ -35,20 +35,6 @@ export class KeyboardManager {
|
||||
}
|
||||
this.handler_map = {};
|
||||
|
||||
// WARNING: Remove the filter of hotkeys, the input event of input/select/textarea will be filtered out by default
|
||||
// When there is a problem with the input of the text component, you need to pay attention to this
|
||||
const old_filter = HotKeys.filter;
|
||||
HotKeys.filter = event => {
|
||||
let parent = (event.target as Element).parentElement;
|
||||
while (parent) {
|
||||
if (parent === editor.container) {
|
||||
return old_filter(event);
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
HotKeys.setScope('editor');
|
||||
|
||||
// this.init_common_shortcut_cb();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DragEvent } from 'react';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { HooksRunner, HookType, BlockDomInfo, PluginHooks } from '../types';
|
||||
import { HooksRunner, HookType, PluginHooks } from '../types';
|
||||
|
||||
export class Hooks implements HooksRunner, PluginHooks {
|
||||
private _subject: Record<string, Subject<unknown>> = {};
|
||||
@@ -113,13 +112,6 @@ export class Hooks implements HooksRunner, PluginHooks {
|
||||
this._runHook(HookType.ON_ROOTNODE_DRAG_OVER_CAPTURE, e);
|
||||
}
|
||||
|
||||
public afterOnNodeDragOver(
|
||||
e: React.DragEvent<Element>,
|
||||
node: BlockDomInfo
|
||||
): void {
|
||||
this._runHook(HookType.AFTER_ON_NODE_DRAG_OVER, e, node);
|
||||
}
|
||||
|
||||
public onSearch(): void {
|
||||
this._runHook(HookType.ON_SEARCH);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export class ScrollManager {
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
this._editor = editor;
|
||||
(window as any).scrollManager = this;
|
||||
}
|
||||
|
||||
private _updateScrollInfo(left: number, top: number) {
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface Virgo {
|
||||
getBlockById(blockId: string): Promise<AsyncBlock | null>;
|
||||
setHotKeysScope(scope?: string): void;
|
||||
getBlockList: () => Promise<AsyncBlock[]>;
|
||||
getBlockListByLevelOrder: () => Promise<AsyncBlock[]>;
|
||||
// removeBlocks: () => void;
|
||||
storageManager: StorageManager | undefined;
|
||||
selection: VirgoSelection;
|
||||
@@ -177,20 +178,11 @@ export enum HookType {
|
||||
ON_ROOTNODE_DRAG_END = 'onRootNodeDragEnd',
|
||||
ON_ROOTNODE_DRAG_OVER_CAPTURE = 'onRootNodeDragOverCapture',
|
||||
ON_ROOTNODE_DROP = 'onRootNodeDrop',
|
||||
AFTER_ON_NODE_DRAG_OVER = 'afterOnNodeDragOver',
|
||||
BEFORE_COPY = 'beforeCopy',
|
||||
BEFORE_CUT = 'beforeCut',
|
||||
ON_ROOTNODE_SCROLL = 'onRootNodeScroll',
|
||||
}
|
||||
|
||||
export interface BlockDomInfo {
|
||||
blockId: string;
|
||||
dom: HTMLElement;
|
||||
type: BlockFlavorKeys;
|
||||
rect: DOMRect;
|
||||
properties: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Editor's various callbacks, used in Editor
|
||||
export interface HooksRunner {
|
||||
init: () => void;
|
||||
@@ -219,10 +211,6 @@ export interface HooksRunner {
|
||||
onRootNodeDragEnd: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDragLeave: (e: React.DragEvent<Element>) => void;
|
||||
onRootNodeDrop: (e: React.DragEvent<Element>) => void;
|
||||
afterOnNodeDragOver: (
|
||||
e: React.DragEvent<Element>,
|
||||
node: BlockDomInfo
|
||||
) => void;
|
||||
beforeCopy: (e: ClipboardEvent) => void;
|
||||
beforeCut: (e: ClipboardEvent) => void;
|
||||
onRootNodeScroll: (e: React.UIEvent) => void;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { noop, Point } from '@toeverything/utils';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEditor } from './Contexts';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockEditor,
|
||||
@@ -5,9 +8,6 @@ import {
|
||||
SelectionInfo,
|
||||
SelectionSettingsMap,
|
||||
} from './editor';
|
||||
import { noop, Point } from '@toeverything/utils';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { RootContext } from './contexts';
|
||||
|
||||
function useRequestReRender() {
|
||||
const [, setUpdateCounter] = useState(0);
|
||||
@@ -56,7 +56,7 @@ function useRequestReRender() {
|
||||
export const useBlock = (blockId: string) => {
|
||||
const [block, setBlock] = useState<AsyncBlock>();
|
||||
const requestReRender = useRequestReRender();
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
if (!blockId) {
|
||||
return undefined;
|
||||
@@ -95,7 +95,7 @@ export const useOnSelect = (
|
||||
blockId: string,
|
||||
cb: (isSelect: boolean) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(blockId, SelectEventTypes.onSelect, cb);
|
||||
return () => {
|
||||
@@ -117,7 +117,7 @@ export const useOnSelectActive = (
|
||||
blockId: string,
|
||||
cb: (position: Point | undefined) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(blockId, SelectEventTypes.active, cb);
|
||||
return () => {
|
||||
@@ -139,7 +139,7 @@ export const useOnSelectSetSelection = <T extends keyof SelectionSettingsMap>(
|
||||
blockId: string,
|
||||
cb: (args: SelectionSettingsMap[T]) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.observe(
|
||||
blockId,
|
||||
@@ -162,7 +162,7 @@ export const useOnSelectSetSelection = <T extends keyof SelectionSettingsMap>(
|
||||
* @export
|
||||
*/
|
||||
export const useOnSelectChange = (cb: (info: SelectionInfo) => void) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.onSelectionChange(cb);
|
||||
return () => {
|
||||
@@ -177,7 +177,7 @@ export const useOnSelectChange = (cb: (info: SelectionInfo) => void) => {
|
||||
* @export
|
||||
*/
|
||||
export const useOnSelectEnd = (cb: (info: SelectionInfo) => void) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.selectionManager.onSelectEnd(cb);
|
||||
return () => {
|
||||
@@ -195,7 +195,7 @@ export const useOnSelectStartWith = (
|
||||
blockId: string,
|
||||
cb: (args: MouseEvent) => void
|
||||
) => {
|
||||
const { editor } = useContext(RootContext);
|
||||
const { editor } = useEditor();
|
||||
useEffect(() => {
|
||||
editor.mouseManager.onSelectStartWith(blockId, cb);
|
||||
return () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { ColumnsContext, RootContext } from './contexts';
|
||||
export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot';
|
||||
export * from './render-block';
|
||||
export * from './hooks';
|
||||
@@ -15,7 +14,6 @@ export * from './kanban/types';
|
||||
|
||||
export * from './utils';
|
||||
|
||||
export * from './drag-drop-wrapper';
|
||||
export * from './block-content-wrapper';
|
||||
|
||||
export * from './editor';
|
||||
|
||||
export { RefPageProvider, useRefPage } from './ref-page';
|
||||
|
||||
@@ -6,10 +6,15 @@ import {
|
||||
PropertyType,
|
||||
RecastBlockValue,
|
||||
RecastMetaProperty,
|
||||
RecastPropertyId,
|
||||
} from '../recast-block/types';
|
||||
import type { DefaultGroup, KanbanGroup } from './types';
|
||||
import { DEFAULT_GROUP_ID } from './types';
|
||||
import {
|
||||
generateInitialOptions,
|
||||
generateRandomFieldName,
|
||||
getPendantIconsConfigByName,
|
||||
} from '../block-pendant/utils';
|
||||
import { SelectOption } from '../recast-block';
|
||||
|
||||
/**
|
||||
* - If the `groupBy` is `SelectProperty` or `MultiSelectProperty`, return `(Multi)SelectProperty.options`.
|
||||
@@ -23,6 +28,7 @@ export const getGroupOptions = async (
|
||||
return [];
|
||||
}
|
||||
switch (groupBy.type) {
|
||||
case PropertyType.Status:
|
||||
case PropertyType.Select:
|
||||
case PropertyType.MultiSelect: {
|
||||
return groupBy.options.map(option => ({
|
||||
@@ -51,15 +57,13 @@ const isValueBelongOption = (
|
||||
option: KanbanGroup
|
||||
) => {
|
||||
switch (propertyValue.type) {
|
||||
case PropertyType.Select: {
|
||||
case PropertyType.Select:
|
||||
case PropertyType.Status: {
|
||||
return propertyValue.value === option.id;
|
||||
}
|
||||
case PropertyType.MultiSelect: {
|
||||
return propertyValue.value.some(i => i === option.id);
|
||||
}
|
||||
// case PropertyType.Text: {
|
||||
// TOTODO:DO support this type
|
||||
// }
|
||||
default: {
|
||||
console.error(propertyValue, option);
|
||||
throw new Error('Not support group by type');
|
||||
@@ -96,40 +100,67 @@ export const calcCardGroup = (
|
||||
/**
|
||||
* Set group value for the card block
|
||||
*/
|
||||
export const moveCardToGroup = async (
|
||||
groupById: RecastPropertyId,
|
||||
cardBlock: RecastItem,
|
||||
group: KanbanGroup
|
||||
) => {
|
||||
export const moveCardToGroup = async ({
|
||||
groupBy,
|
||||
cardBlock,
|
||||
group,
|
||||
recastBlock,
|
||||
}: {
|
||||
groupBy: RecastMetaProperty;
|
||||
cardBlock: RecastItem;
|
||||
group: KanbanGroup;
|
||||
recastBlock: RecastBlock;
|
||||
}) => {
|
||||
const { setValue, removeValue } = getRecastItemValue(cardBlock);
|
||||
let success = false;
|
||||
if (group.id === DEFAULT_GROUP_ID) {
|
||||
success = await removeValue(groupById);
|
||||
success = await removeValue(groupBy.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (group.type) {
|
||||
case PropertyType.Select: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.Status: {
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.MultiSelect: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: [group.id],
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: [group.id],
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
case PropertyType.Text: {
|
||||
success = await setValue({
|
||||
id: groupById,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
});
|
||||
success = await setValue(
|
||||
{
|
||||
id: groupBy.id,
|
||||
type: group.type,
|
||||
value: group.id,
|
||||
},
|
||||
recastBlock.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -194,14 +225,18 @@ export const genDefaultGroup = (groupBy: RecastMetaProperty): DefaultGroup => ({
|
||||
items: [],
|
||||
});
|
||||
|
||||
export const DEFAULT_GROUP_BY_PROPERTY = {
|
||||
name: 'Status',
|
||||
options: [
|
||||
{ name: 'No Started', color: '#E53535', background: '#FFCECE' },
|
||||
{ name: 'In Progress', color: '#A77F1A', background: '#FFF5AB' },
|
||||
{ name: 'Complete', color: '#3C8867', background: '#C5FBE0' },
|
||||
],
|
||||
};
|
||||
export const generateDefaultGroupByProperty = (): {
|
||||
name: string;
|
||||
options: Omit<SelectOption, 'id'>[];
|
||||
type: PropertyType.Status;
|
||||
} => ({
|
||||
name: generateRandomFieldName(PropertyType.Status),
|
||||
type: PropertyType.Status,
|
||||
options: generateInitialOptions(
|
||||
PropertyType.Status,
|
||||
getPendantIconsConfigByName(PropertyType.Status)
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
* Unwrap blocks from the grid recursively.
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useKanbanGroup = (groupBy: RecastMetaProperty) => {
|
||||
const { updateSelect } = useSelectProperty();
|
||||
|
||||
switch (groupBy.type) {
|
||||
case PropertyType.Status:
|
||||
case PropertyType.MultiSelect:
|
||||
case PropertyType.Select: {
|
||||
const {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useEditor } from '../contexts';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { useRecastView } from '../recast-block';
|
||||
import { useRecastBlock } from '../recast-block/Context';
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
import { supportChildren } from '../utils';
|
||||
import {
|
||||
calcCardGroup,
|
||||
DEFAULT_GROUP_BY_PROPERTY,
|
||||
genDefaultGroup,
|
||||
generateDefaultGroupByProperty,
|
||||
getCardGroup,
|
||||
getGroupOptions,
|
||||
moveCardToAfter,
|
||||
@@ -48,6 +48,7 @@ export const useRecastKanbanGroupBy = () => {
|
||||
// Add other type groupBy support
|
||||
const supportedGroupBy = getProperties().filter(
|
||||
prop =>
|
||||
prop.type === PropertyType.Status ||
|
||||
prop.type === PropertyType.Select ||
|
||||
prop.type === PropertyType.MultiSelect
|
||||
);
|
||||
@@ -88,7 +89,8 @@ export const useRecastKanbanGroupBy = () => {
|
||||
// TODO: support other property type
|
||||
if (
|
||||
groupByProperty.type !== PropertyType.Select &&
|
||||
groupByProperty.type !== PropertyType.MultiSelect
|
||||
groupByProperty.type !== PropertyType.MultiSelect &&
|
||||
groupByProperty.type !== PropertyType.Status
|
||||
) {
|
||||
console.warn('Not support groupBy type', groupByProperty);
|
||||
|
||||
@@ -134,7 +136,7 @@ export const useInitKanbanEffect = ():
|
||||
}
|
||||
// 3. no group by, no properties
|
||||
// create a new property and set it as group by
|
||||
const prop = await createSelect(DEFAULT_GROUP_BY_PROPERTY);
|
||||
const prop = await createSelect(generateDefaultGroupByProperty());
|
||||
await setGroupBy(prop.id);
|
||||
};
|
||||
|
||||
@@ -197,7 +199,12 @@ export const useRecastKanban = () => {
|
||||
beforeBlock: string | null,
|
||||
afterBlock: string | null
|
||||
) => {
|
||||
await moveCardToGroup(groupBy.id, child, kanbanMap[id]);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: child,
|
||||
group: kanbanMap[id],
|
||||
recastBlock,
|
||||
});
|
||||
if (beforeBlock) {
|
||||
const block = await editor.getBlockById(
|
||||
beforeBlock
|
||||
@@ -286,7 +293,12 @@ export const useKanban = () => {
|
||||
);
|
||||
if (isChangedGroup) {
|
||||
// 1.2 Move to the target group
|
||||
await moveCardToGroup(groupBy.id, targetCard, targetGroup);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: targetCard,
|
||||
group: targetGroup,
|
||||
recastBlock,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Reorder the card
|
||||
@@ -324,7 +336,12 @@ export const useKanban = () => {
|
||||
}
|
||||
recastBlock.append(newBlock);
|
||||
const newCard = newBlock as unknown as RecastItem;
|
||||
await moveCardToGroup(groupBy.id, newCard, group);
|
||||
await moveCardToGroup({
|
||||
groupBy,
|
||||
cardBlock: newCard,
|
||||
group,
|
||||
recastBlock,
|
||||
});
|
||||
},
|
||||
[editor, groupBy.id, recastBlock]
|
||||
);
|
||||
|
||||
@@ -46,7 +46,10 @@ export type DefaultGroup = KanbanGroupBase & {
|
||||
|
||||
type SelectGroup = KanbanGroupBase &
|
||||
SelectOption & {
|
||||
type: PropertyType.Select | PropertyType.MultiSelect;
|
||||
type:
|
||||
| PropertyType.Select
|
||||
| PropertyType.MultiSelect
|
||||
| PropertyType.Status;
|
||||
};
|
||||
|
||||
type TextGroup = KanbanGroupBase & {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { ComponentType, createContext, ReactNode, useContext } from 'react';
|
||||
import { RecastBlock } from './types';
|
||||
import { RefPageProvider } from '../ref-page';
|
||||
|
||||
/**
|
||||
* Determine whether the block supports RecastBlock
|
||||
@@ -47,7 +48,7 @@ export const RecastBlockProvider = ({
|
||||
|
||||
return (
|
||||
<RecastBlockContext.Provider value={block}>
|
||||
{children}
|
||||
<RefPageProvider>{children}</RefPageProvider>
|
||||
</RecastBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -60,7 +61,7 @@ export const useRecastBlock = () => {
|
||||
const recastBlock = useContext(RecastBlockContext);
|
||||
if (!recastBlock) {
|
||||
throw new Error(
|
||||
'Failed to find recastBlock! Please use the hook under `RecastTableProvider`.'
|
||||
'Failed to find recastBlock! Please use the hook under `RecastBlockProvider`.'
|
||||
);
|
||||
}
|
||||
return recastBlock;
|
||||
|
||||
@@ -49,22 +49,3 @@ const SomeBlock = () => {
|
||||
return <div>...</div>;
|
||||
};
|
||||
```
|
||||
|
||||
## Scene
|
||||
|
||||
**Notice: The scene API will refactor at next version.**
|
||||
|
||||
```tsx
|
||||
const SomeBlock = () => {
|
||||
const { scene, setScene, setPage, setTable, setKanban } =
|
||||
useRecastBlockScene();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Scene: {scene}</div>
|
||||
<button onClick={setPage}>list</button>
|
||||
<button onClick={setKanban}>kanban</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -32,7 +32,7 @@ export const mergeGroup = async (...groups: AsyncBlock[]) => {
|
||||
);
|
||||
}
|
||||
|
||||
await mergeGroupProperties(...(groups as RecastBlock[]));
|
||||
await mergeGroupProperties(...(groups as unknown as RecastBlock[]));
|
||||
|
||||
const [headGroup, ...restGroups] = groups;
|
||||
// Add all children to the head group
|
||||
@@ -174,7 +174,7 @@ export const splitGroup = async (
|
||||
}
|
||||
|
||||
splitGroupProperties(
|
||||
group as RecastBlock,
|
||||
group as unknown as RecastBlock,
|
||||
newGroupBlock as unknown as RecastBlock
|
||||
);
|
||||
await group.after(newGroupBlock);
|
||||
@@ -185,6 +185,22 @@ export const splitGroup = async (
|
||||
return newGroupBlock;
|
||||
};
|
||||
|
||||
export const appendNewGroup = async (
|
||||
editor: BlockEditor,
|
||||
parentBlock: AsyncBlock,
|
||||
active = false
|
||||
) => {
|
||||
const newGroupBlock = await createGroupWithEmptyText(editor);
|
||||
await parentBlock.append(newGroupBlock);
|
||||
if (active) {
|
||||
// Active text block
|
||||
await editor.selectionManager.activeNodeByNodeId(
|
||||
newGroupBlock.childrenIds[0]
|
||||
);
|
||||
}
|
||||
return newGroupBlock;
|
||||
};
|
||||
|
||||
export const addNewGroup = async (
|
||||
editor: BlockEditor,
|
||||
previousBlock: AsyncBlock,
|
||||
|
||||
84
libs/components/editor-core/src/recast-block/history.ts
Normal file
84
libs/components/editor-core/src/recast-block/history.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { RecastPropertyId } from './types';
|
||||
|
||||
// TODO: The logic for keeping history should be supported by the network layer
|
||||
type Props = {
|
||||
recastBlockId: string;
|
||||
blockId: string;
|
||||
propertyId: RecastPropertyId;
|
||||
};
|
||||
|
||||
type HistoryStorageMap = {
|
||||
[recastBlockId: string]: {
|
||||
[propertyId: RecastPropertyId]: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_NAME = 'TEMPORARY_HISTORY_DATA';
|
||||
|
||||
const ensureLocalStorage = () => {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_NAME);
|
||||
if (!data) {
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify({}));
|
||||
}
|
||||
};
|
||||
const ensureHistoryAtom = (
|
||||
data: HistoryStorageMap,
|
||||
recastBlockId: string,
|
||||
propertyId: RecastPropertyId
|
||||
): HistoryStorageMap => {
|
||||
if (!data[recastBlockId]) {
|
||||
data[recastBlockId] = {};
|
||||
}
|
||||
if (!data[recastBlockId][propertyId]) {
|
||||
data[recastBlockId][propertyId] = [];
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const setHistory = ({ recastBlockId, blockId, propertyId }: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
ensureHistoryAtom(data, recastBlockId, propertyId);
|
||||
const propertyHistory = data[recastBlockId][propertyId];
|
||||
|
||||
if (propertyHistory.includes(blockId)) {
|
||||
const idIndex = propertyHistory.findIndex(id => id === blockId);
|
||||
propertyHistory.splice(idIndex, 1);
|
||||
}
|
||||
|
||||
propertyHistory.push(blockId);
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
|
||||
export const getHistory = ({ recastBlockId }: { recastBlockId: string }) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
|
||||
return data[recastBlockId] ?? {};
|
||||
};
|
||||
|
||||
export const removeHistory = ({
|
||||
recastBlockId,
|
||||
blockId,
|
||||
propertyId,
|
||||
}: Props) => {
|
||||
ensureLocalStorage();
|
||||
const data: HistoryStorageMap = JSON.parse(
|
||||
localStorage.getItem(LOCAL_STORAGE_NAME) as string
|
||||
);
|
||||
ensureHistoryAtom(data, recastBlockId, propertyId);
|
||||
|
||||
const propertyHistory = data[recastBlockId][propertyId];
|
||||
|
||||
if (propertyHistory.includes(blockId)) {
|
||||
const idIndex = propertyHistory.findIndex(id => id === blockId);
|
||||
propertyHistory.splice(idIndex, 1);
|
||||
}
|
||||
|
||||
localStorage.setItem(LOCAL_STORAGE_NAME, JSON.stringify(data));
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
SelectProperty,
|
||||
TABLE_VALUES_KEY,
|
||||
} from './types';
|
||||
import { getHistory, removeHistory, setHistory } from './history';
|
||||
|
||||
/**
|
||||
* Generate a unique id for a property
|
||||
@@ -240,7 +241,13 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
|
||||
return props[id];
|
||||
};
|
||||
|
||||
const setValue = (newValue: RecastBlockValue) => {
|
||||
const setValue = (newValue: RecastBlockValue, recastBlockId: string) => {
|
||||
setHistory({
|
||||
recastBlockId: recastBlockId,
|
||||
blockId: block.id,
|
||||
propertyId: newValue.id,
|
||||
});
|
||||
|
||||
return recastItem.setProperty(TABLE_VALUES_KEY, {
|
||||
...props,
|
||||
[newValue.id]: newValue,
|
||||
@@ -249,22 +256,30 @@ export const getRecastItemValue = (block: RecastItem | AsyncBlock) => {
|
||||
|
||||
const removeValue = (propertyId: RecastPropertyId) => {
|
||||
const { [propertyId]: omitted, ...restProps } = props;
|
||||
|
||||
removeHistory({
|
||||
recastBlockId: block.id,
|
||||
propertyId: propertyId,
|
||||
blockId: block.id,
|
||||
});
|
||||
|
||||
return recastItem.setProperty(TABLE_VALUES_KEY, restProps);
|
||||
};
|
||||
return { getAllValue, getValue, setValue, removeValue };
|
||||
|
||||
const getValueHistory = getHistory;
|
||||
|
||||
return { getAllValue, getValue, setValue, removeValue, getValueHistory };
|
||||
};
|
||||
|
||||
const isSelectLikeProperty = (
|
||||
metaProperty?: RecastMetaProperty
|
||||
): metaProperty is SelectProperty | MultiSelectProperty => {
|
||||
if (
|
||||
!metaProperty ||
|
||||
(metaProperty.type !== PropertyType.Select &&
|
||||
metaProperty.type !== PropertyType.MultiSelect)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
): metaProperty is SelectProperty | MultiSelectProperty | StatusProperty => {
|
||||
return (
|
||||
metaProperty &&
|
||||
(metaProperty.type === PropertyType.Status ||
|
||||
metaProperty.type === PropertyType.Select ||
|
||||
metaProperty.type === PropertyType.MultiSelect)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -312,7 +327,7 @@ export const useSelectProperty = () => {
|
||||
};
|
||||
|
||||
const updateSelect = (
|
||||
selectProperty: SelectProperty | MultiSelectProperty
|
||||
selectProperty: StatusProperty | SelectProperty | MultiSelectProperty
|
||||
) => {
|
||||
// if (typeof selectProperty === 'string') {
|
||||
// const maybeSelectProperty = getProperty(selectProperty);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
import { MutableRefObject, useCallback, useEffect, useState } from 'react';
|
||||
import { useRecastBlock } from './Context';
|
||||
import {
|
||||
KanbanView,
|
||||
@@ -50,7 +50,33 @@ export const useCurrentView = () => {
|
||||
);
|
||||
return [currentView, setCurrentView] as const;
|
||||
};
|
||||
export const useLazyIframe = (
|
||||
link: string,
|
||||
timers: number,
|
||||
container: MutableRefObject<HTMLElement>
|
||||
) => {
|
||||
const [iframeShow, setIframeShow] = useState(false);
|
||||
useEffect(() => {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = link;
|
||||
iframe.onload = () => {
|
||||
setTimeout(() => {
|
||||
// Prevent iframe from scrolling parent container
|
||||
// TODO W3C https://github.com/w3c/csswg-drafts/issues/7134
|
||||
// https://forum.figma.com/t/prevent-figmas-embed-code-from-automatically-scrolling-to-it-on-page-load/26029/6
|
||||
setIframeShow(true);
|
||||
}, timers);
|
||||
};
|
||||
if (container?.current) {
|
||||
container.current.appendChild(iframe);
|
||||
}
|
||||
return () => {
|
||||
iframe.remove();
|
||||
};
|
||||
}, [link, container]);
|
||||
|
||||
return iframeShow;
|
||||
};
|
||||
export const useRecastView = () => {
|
||||
const recastBlock = useRecastBlock();
|
||||
const recastViews =
|
||||
|
||||
87
libs/components/editor-core/src/ref-page/ModalPage.tsx
Normal file
87
libs/components/editor-core/src/ref-page/ModalPage.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RenderBlock } from '../render-block';
|
||||
|
||||
const Dialog = styled('div')({
|
||||
flex: 1,
|
||||
width: '880px',
|
||||
margin: '72px auto',
|
||||
background: '#fff',
|
||||
boxShadow: '0px 1px 10px rgba(152, 172, 189, 0.6)',
|
||||
borderRadius: '10px',
|
||||
padding: '72px 120px',
|
||||
overflow: 'scroll',
|
||||
});
|
||||
|
||||
const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
const theme = useTheme();
|
||||
const { closeSubPage } = useRefPage();
|
||||
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
</MuiBackdrop>,
|
||||
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
const ModalPage = ({ blockId }: { blockId: string | null }) => {
|
||||
return (
|
||||
<Modal open={!!blockId}>
|
||||
{blockId && <RenderBlock blockId={blockId} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const RefPageContext = createContext<
|
||||
ReturnType<typeof useState<string | null>> | undefined
|
||||
>(undefined);
|
||||
|
||||
export const RefPageProvider = ({ children }: { children: ReactNode }) => {
|
||||
const state = useState<string | null>();
|
||||
const [blockId, setBlockId] = state;
|
||||
|
||||
return (
|
||||
<RefPageContext.Provider value={state}>
|
||||
{children}
|
||||
<ModalPage blockId={blockId ?? null} />
|
||||
</RefPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useRefPage = () => {
|
||||
const context = useContext(RefPageContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'Wrap your app inside of a `SubPageProvider` to have access to the hook context!'
|
||||
);
|
||||
}
|
||||
const [blockId, setBlockId] = context;
|
||||
const openSubPage = (blockId: string) => {
|
||||
setBlockId(blockId);
|
||||
};
|
||||
const closeSubPage = () => {
|
||||
setBlockId(null);
|
||||
};
|
||||
|
||||
return { blockId, open: !!blockId, openSubPage, closeSubPage };
|
||||
};
|
||||
|
||||
// export const openSubPage = () => {};
|
||||
1
libs/components/editor-core/src/ref-page/index.ts
Normal file
1
libs/components/editor-core/src/ref-page/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useRefPage, RefPageProvider } from './ModalPage';
|
||||
@@ -1,8 +1,8 @@
|
||||
import { styled, Theme } from '@toeverything/components/ui';
|
||||
import { FC, useContext, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { FC, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
|
||||
// import { RenderChildren } from './RenderChildren';
|
||||
import { RootContext } from '../contexts';
|
||||
import { useEditor } from '../Contexts';
|
||||
import { useBlock } from '../hooks';
|
||||
|
||||
interface RenderBlockProps {
|
||||
@@ -14,7 +14,7 @@ export const RenderBlock: FC<RenderBlockProps> = ({
|
||||
blockId,
|
||||
hasContainer = true,
|
||||
}) => {
|
||||
const { editor, editorElement } = useContext(RootContext);
|
||||
const { editor, editorElement } = useEditor();
|
||||
const { block } = useBlock(blockId);
|
||||
const blockRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user