mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-13 16:16:46 +08:00
@@ -2,12 +2,13 @@
|
||||
<b>
|
||||
<a href="https://affine.pro">AFFiNE.PRO</a><br>
|
||||
</b>
|
||||
The Next-Gen Knowledge Base to Replace Notion & Miro.
|
||||
The Next-Gen Collaborative Knowledge Base
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
Planning, Sorting and Creating all Together. Open-source, Privacy-First, and Free to use.
|
||||
Open-source and privacy-first. <br />
|
||||
A free replacement for Notion & Miro.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -22,9 +23,10 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://affine.pro)
|
||||
[](https://livedemo.affine.pro)
|
||||
[](https://github.com/toeverything/AFFiNE)
|
||||
[![All Contributors][all-contributors-badge]](#contributors)
|
||||
<br/>
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://reactjs.org/)
|
||||
@@ -35,11 +37,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
<p align="center">
|
||||
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
|
||||
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
|
||||
<a href="https://medium.com/@affineworkos"><img src="https://img.shields.io/badge/-Medium-red?style=social&logo=medium" height=25></a>
|
||||
</p>
|
||||
@@ -48,96 +50,52 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
|
||||
<p align="center"><img width="1920" alt="affine_screen" src="https://user-images.githubusercontent.com/21084335/182552060-972cac0e-6258-4ccb-85bd-3bb466c30ccd.png"><p/>
|
||||
|
||||
# Stay Up-to-Date and Support Us
|
||||
# :star: Support Us and Keep Updated :star:
|
||||
|
||||

|
||||
|
||||
# How to use
|
||||
# Getting Started
|
||||
|
||||
<h3 align="center">
|
||||
🥳🥳🥳 Our web live demo is ready! 🥳🥳🥳
|
||||
</h3>
|
||||
[](https://affine.pro) No installation or registration required! Head over to our website and try it out now.
|
||||
|
||||
<h3 align="center">
|
||||
Start to play with <a href="https://affine.pro"><b>AFFiNE web version</b></a> on our landing page:
|
||||
</h3>
|
||||
|
||||
<p align="center"><a href="https://affine.pro">
|
||||
<img
|
||||
src="https://user-images.githubusercontent.com/79301703/184340907-5aaa4e6e-7d37-4523-a06c-7d630e7864d1.jpeg"
|
||||
width="600px"
|
||||
alt="check_live_demo"
|
||||
/>
|
||||
</a></p>
|
||||
|
||||
If you have experience in front-end development, you may wish to refer to our [documentation](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine) to learn more about deploying your own version or contributing further to development.
|
||||
Also, thanks to Lee who has made a [desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out.
|
||||
Please notice that AFFiNE is still under Alpha stage and is not ready for production use.
|
||||
|
||||
# Table of contents
|
||||
|
||||
- [Stay Up-to-Date and Support Us](#stay-up-to-date-and-support-us)
|
||||
- [How to Use](#how-to-use)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Shape your page](#shape-your-page)
|
||||
- [Plan your task](#plan-your-task)
|
||||
- [Sort your knowledge](#sort-your-knowledge)
|
||||
- [Create your story](#create-your-story)
|
||||
- [Documentation](#documentation)
|
||||
- [Getting Started with development](#getting-started-with-development)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Releases](#releases)
|
||||
- [Feature requests](#feature-requests)
|
||||
- [FAQ](#faq)
|
||||
- [The Philosophy of AFFiNE](#the-philosophy-of-affine)
|
||||
- [Community](#community)
|
||||
- [Contributors](#contributors)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
- [License](#license)
|
||||
|
||||
## Shape your page
|
||||
|
||||

|
||||
|
||||
## Plan your task
|
||||
|
||||

|
||||
|
||||
## Sort your knowledge
|
||||
|
||||

|
||||
Want to deploy it yourself? AFFiNE can run just about anywhere. <br />
|
||||
You can refer to our documentation which can be found from the [useful links](#useful-links) section - where you will also find ways to help contribute to the project and join our communities.
|
||||
<br /><br />
|
||||
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
|
||||
|
||||
## Create your story
|
||||
|
||||
We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features.
|
||||
And when it comes to collaboration, these features are not just necessarily for teams -- you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators.
|
||||
Affine is fully built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released.
|
||||
There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together. Privacy first, open-source, customizable and ready to use, built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, with full support for real-time collaboration through peer-to-peer technology. We don't think "privacy-first" is a good excuse for not supporting modern web features.
|
||||
And when it comes to collaboration, these features are not just necessarily for teams - you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators.
|
||||
|
||||
# Documentation
|
||||
### Shape your page
|
||||
|
||||
AFFiNE is not yet ready for production use. For installation, you may check how to build or deploy AFFiNE from our [quick-start](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine/quick-start) guide. Alternatively, you can view our [full documentation](https://docs.affine.pro/affine/).
|
||||

|
||||
|
||||
## Getting Started with development
|
||||
### Plan your task
|
||||
|
||||
Please view the path Contribute-to-AFFiNE/Software-Contributions/Quick-Start in the documentation.
|
||||

|
||||
|
||||
# Roadmap
|
||||
### Sort your knowledge
|
||||
|
||||
Yes! Permanent storage, collaboration, stable release is planned! Check it [here](https://github.com/toeverything/AFFiNE/issues/293)
|
||||

|
||||
|
||||
# Releases
|
||||
# Useful Links
|
||||
|
||||
Get our latest [release notes](https://github.com/toeverything/AFFiNE/wiki) from here.
|
||||
- [AFFiNE Documentation](https://docs.affine.pro/affine/) - More detailed documentation on how to use and develop with AFFiNE
|
||||
|
||||
# Feature requests
|
||||
- [Feature Roadmap](https://github.com/toeverything/AFFiNE/issues/293) - Looking for a feature? It might already be planned for release - you can check here
|
||||
- [Release Notes](https://github.com/toeverything/AFFiNE/wiki) - Find out what changes we are making and how we are improving AFFiNE
|
||||
|
||||
Please go to [feature requests](https://github.com/toeverything/AFFiNE/issues).
|
||||
- [Contributing Guide](/docs/CONTRIBUTING.md) - Want to help improve AFFiNE? You might not even need to write a line of code. Find out how you can contribute.
|
||||
- [Code of Conduct](/docs/CODE_OF_CONDUCT.md) - How we promote and maintain a harassment-free experience for everyone in our community.
|
||||
|
||||
# FAQ
|
||||
- AFFiNE Communities: [Discord](https://discord.gg/yz6tGVsf5p) | [Telegram](https://t.me/affineworkos) | [Twitter](https://twitter.com/AffineOfficial) |
|
||||
[Medium](https://medium.com/@affineworkos) | [AFFiNE Blog](https://blog.affine.pro)
|
||||
|
||||
Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors.
|
||||
# Contact Us
|
||||
|
||||
Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/).
|
||||
Feel free to send us an email: contact@toeverything.info
|
||||
|
||||
# The Philosophy of AFFiNE
|
||||
|
||||
@@ -160,7 +118,7 @@ That's why we are making AFFiNE. Some of the most important features are:
|
||||
- Data is always stored locally by default
|
||||
- CRDTs are applied so that peer-to-peer collaboration is possible.
|
||||
|
||||
We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth.
|
||||
We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a single source of truth.
|
||||
|
||||
We would like to give special thanks to the innovators and pioneers who greatly inspired us:
|
||||
|
||||
@@ -182,12 +140,6 @@ We would also like to give thanks to open-source projects that make affine possi
|
||||
|
||||
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
|
||||
|
||||
# Community
|
||||
|
||||
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
||||
|
||||
[Discuss AFFiNE on GitHub](https://github.com/toeverything/AFFiNE/discussions)
|
||||
|
||||
# Contributors
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
@@ -246,4 +198,4 @@ For help, discussion about best practices, or any other conversation that would
|
||||
|
||||
AFFiNE is distributed under the terms of MIT license.
|
||||
|
||||
See LICENSE for details.
|
||||
See [LICENSE](/LICENSE) for details.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@mui/icons-material": "^5.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"firebase": "^9.9.2",
|
||||
"firebase": "^9.9.3",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"webpack": "^5.74.0"
|
||||
}
|
||||
|
||||
@@ -22,16 +22,23 @@ import { type BlockEditor } from '@toeverything/components/editor-core';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { CollapsiblePageTree } from './collapsible-page-tree';
|
||||
import { Tabs } from './components/tabs';
|
||||
import { TabMap, TAB_TITLE } from './components/tabs/Tabs';
|
||||
import { TOC } from './components/toc';
|
||||
import { WorkspaceName } from './workspace-name';
|
||||
|
||||
type PageProps = {
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
export function Page(props: PageProps) {
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
TabMap.get(TAB_TITLE.PAGES).value
|
||||
);
|
||||
const { page_id } = useParams();
|
||||
const { showSpaceSidebar, fixedDisplay, setSpaceSidebarVisible } =
|
||||
useShowSpaceSidebar();
|
||||
const dailyNotesFlag = useFlag('BooleanDailyNotes', false);
|
||||
const onTabChange = v => setActiveTab(v);
|
||||
|
||||
return (
|
||||
<LigoApp>
|
||||
@@ -50,31 +57,37 @@ export function Page(props: PageProps) {
|
||||
>
|
||||
<WorkspaceName />
|
||||
|
||||
<Tabs />
|
||||
<Tabs activeTab={activeTab} onTabChange={onTabChange} />
|
||||
|
||||
<WorkspaceSidebarContent>
|
||||
<div>
|
||||
{dailyNotesFlag && (
|
||||
{activeTab === TabMap.get(TAB_TITLE.PAGES).value && (
|
||||
<div>
|
||||
{dailyNotesFlag && (
|
||||
<div>
|
||||
<CollapsibleTitle title="Daily Notes">
|
||||
<CalendarHeatmap />
|
||||
</CollapsibleTitle>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CollapsibleTitle title="Daily Notes">
|
||||
<CalendarHeatmap />
|
||||
<CollapsibleTitle
|
||||
title="ACTIVITIES"
|
||||
initialOpen={false}
|
||||
>
|
||||
<Activities />
|
||||
</CollapsibleTitle>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CollapsibleTitle
|
||||
title="ACTIVITIES"
|
||||
initialOpen={false}
|
||||
>
|
||||
<Activities />
|
||||
</CollapsibleTitle>
|
||||
<div>
|
||||
<CollapsiblePageTree title="PAGES">
|
||||
{page_id ? <PageTree /> : null}
|
||||
</CollapsiblePageTree>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CollapsiblePageTree title="PAGES">
|
||||
{page_id ? <PageTree /> : null}
|
||||
</CollapsiblePageTree>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabMap.get(TAB_TITLE.TOC).value && (
|
||||
<TOC />
|
||||
)}
|
||||
</WorkspaceSidebarContent>
|
||||
</WorkspaceSidebar>
|
||||
</LigoLeftContainer>
|
||||
@@ -105,6 +118,7 @@ const EditorContainer = ({
|
||||
const obv = new ResizeObserver(e => {
|
||||
setPageClientWidth(e[0].contentRect.width);
|
||||
});
|
||||
|
||||
obv.observe(scrollContainer);
|
||||
return () => obv.disconnect();
|
||||
}
|
||||
@@ -175,7 +189,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({
|
||||
width: 300,
|
||||
minWidth: 300,
|
||||
borderRadius: '0px 10px 10px 0px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
boxShadow: theme.affine.shadows.shadow2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
transitionProperty: 'left',
|
||||
transitionDuration: '0.35s',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { ValueOf } from '@toeverything/utils';
|
||||
import { useState } from 'react';
|
||||
|
||||
const StyledTabs = styled('div')(({ theme }) => {
|
||||
return {
|
||||
@@ -56,32 +55,35 @@ const StyledTabTitle = styled('div')<{
|
||||
}
|
||||
`;
|
||||
|
||||
const TAB_TITLE = {
|
||||
PAGES: 'pages',
|
||||
GALLERY: 'gallery',
|
||||
TOC: 'toc',
|
||||
export const TAB_TITLE = {
|
||||
PAGES: 'PAGES',
|
||||
GALLERY: 'GALLERY',
|
||||
TOC: 'TOC',
|
||||
} as const;
|
||||
|
||||
const TabMap = new Map<TabKey, { value: TabValue; disabled?: boolean }>([
|
||||
['PAGES', { value: 'pages' }],
|
||||
['GALLERY', { value: 'gallery', disabled: true }],
|
||||
['TOC', { value: 'toc' }],
|
||||
export const TabMap = new Map<
|
||||
TabValue,
|
||||
{ value: TabValue; disabled?: boolean }
|
||||
>([
|
||||
[TAB_TITLE.PAGES, { value: TAB_TITLE.PAGES }],
|
||||
[TAB_TITLE.GALLERY, { value: TAB_TITLE.GALLERY, disabled: true }],
|
||||
[TAB_TITLE.TOC, { value: TAB_TITLE.TOC }],
|
||||
]);
|
||||
|
||||
type TabKey = keyof typeof TAB_TITLE;
|
||||
type TabValue = ValueOf<typeof TAB_TITLE>;
|
||||
|
||||
const Tabs = () => {
|
||||
const [activeValue, setActiveTab] = useState<TabValue>(TAB_TITLE.PAGES);
|
||||
interface Props {
|
||||
activeTab: TabValue;
|
||||
onTabChange: (v: TabValue) => void;
|
||||
}
|
||||
|
||||
const onClick = (v: TabValue) => {
|
||||
setActiveTab(v);
|
||||
};
|
||||
const Tabs = (props: Props) => {
|
||||
const { activeTab, onTabChange } = props;
|
||||
|
||||
return (
|
||||
<StyledTabs>
|
||||
{[...TabMap.entries()].map(([k, { value, disabled = false }]) => {
|
||||
const isActive = activeValue === value;
|
||||
const isActive = activeTab === value;
|
||||
|
||||
return (
|
||||
<StyledTabTitle
|
||||
@@ -89,7 +91,7 @@ const Tabs = () => {
|
||||
className={isActive ? 'active' : ''}
|
||||
isActive={isActive}
|
||||
isDisabled={disabled}
|
||||
onClick={() => onClick(value)}
|
||||
onClick={() => onTabChange(value)}
|
||||
>
|
||||
{k}
|
||||
</StyledTabTitle>
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import type { Virgo } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { useCurrentEditors } from '@toeverything/datasource/state';
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { BLOCK_TYPES } from './toc-enum';
|
||||
import {
|
||||
destroyEventList,
|
||||
getContentByAsyncBlocks,
|
||||
getPageTOC,
|
||||
} from './toc-util';
|
||||
import type { ListenerMap, TOCType } from './types';
|
||||
|
||||
const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>(
|
||||
({ type, isActive }) => {
|
||||
const common = {
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
color: isActive ? '#3E6FDB' : '#4C6275',
|
||||
};
|
||||
|
||||
if (type === BLOCK_TYPES.HEADING1) {
|
||||
return {
|
||||
...common,
|
||||
padding: '0 12px',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
};
|
||||
}
|
||||
|
||||
if (type === BLOCK_TYPES.HEADING2) {
|
||||
return {
|
||||
...common,
|
||||
padding: '0 32px',
|
||||
fontSize: '14px',
|
||||
};
|
||||
}
|
||||
|
||||
if (type === BLOCK_TYPES.HEADING3) {
|
||||
return {
|
||||
...common,
|
||||
padding: '0 52px',
|
||||
fontSize: '12px',
|
||||
};
|
||||
}
|
||||
|
||||
if (type === BLOCK_TYPES.GROUP) {
|
||||
return {
|
||||
...common,
|
||||
margin: '6px 0px',
|
||||
height: '46px',
|
||||
padding: '6px 12px',
|
||||
fontWeight: '600',
|
||||
fontSize: '16px',
|
||||
borderTop: '0.5px solid #E0E6EB',
|
||||
borderBottom: '0.5px solid #E0E6EB',
|
||||
color: isActive ? '#3E6FDB' : '#98ACBD',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
);
|
||||
|
||||
const StyledItem = styled('div')(props => {
|
||||
return {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
});
|
||||
|
||||
const TOCContext = createContext(null);
|
||||
|
||||
const TOCItem = props => {
|
||||
const { activeBlockId, onClick } = useContext(TOCContext);
|
||||
const { id, type, text } = props;
|
||||
const isActive = id === activeBlockId;
|
||||
|
||||
return (
|
||||
<StyledTOCItem
|
||||
key={id}
|
||||
isActive={isActive}
|
||||
type={type}
|
||||
onClick={() => onClick(id)}
|
||||
>
|
||||
<StyledItem>{text}</StyledItem>
|
||||
</StyledTOCItem>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTOCContent = tocDataSource => {
|
||||
return (
|
||||
<>
|
||||
{tocDataSource.map(tocItem => {
|
||||
if (tocItem?.length) {
|
||||
return renderTOCContent(tocItem);
|
||||
}
|
||||
|
||||
const { id, type, text } = tocItem;
|
||||
|
||||
return <TOCItem key={id} id={id} type={type} text={text} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TOC = () => {
|
||||
const { page_id } = useParams();
|
||||
const [tocDataSource, setTocDataSource] = useState<TOCType[]>([]);
|
||||
const [activeBlockId, setActiveBlockId] = useState('');
|
||||
|
||||
/* store page/block unmount-listener */
|
||||
const listenerMapRef = useRef<ListenerMap>(new Map());
|
||||
|
||||
const { currentEditors } = useCurrentEditors();
|
||||
const editor = currentEditors[page_id] as Virgo;
|
||||
|
||||
const updateTocDataSource = useCallback(async () => {
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listenerMap = listenerMapRef.current;
|
||||
|
||||
/* page listener: trigger update-notice when add new group */
|
||||
const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0];
|
||||
if (!listenerMap.has(pageAsyncBlock.id)) {
|
||||
listenerMap.set(
|
||||
pageAsyncBlock.id,
|
||||
pageAsyncBlock.onUpdate(updateTocDataSource)
|
||||
);
|
||||
}
|
||||
|
||||
/* block listener: trigger update-notice when change block content */
|
||||
const { children = [] } =
|
||||
(await editor.queryByPageId(page_id))?.[0] || {};
|
||||
const asyncBlocks = (await editor.getBlockByIds(children)) || [];
|
||||
const { tocContents } = await getContentByAsyncBlocks(
|
||||
asyncBlocks,
|
||||
updateTocDataSource,
|
||||
listenerMap
|
||||
);
|
||||
|
||||
/* toc: flat content */
|
||||
const tocDataSource = getPageTOC(asyncBlocks, tocContents);
|
||||
setTocDataSource(tocDataSource);
|
||||
}, [editor, page_id]);
|
||||
|
||||
/* init toc and add page/block update-listener & unmount-listener */
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await updateTocDataSource();
|
||||
})();
|
||||
|
||||
/* remove listener when unmount component */
|
||||
return () => destroyEventList(listenerMapRef.current);
|
||||
}, [updateTocDataSource]);
|
||||
|
||||
const onClick = async (blockId?: string) => {
|
||||
setActiveBlockId(blockId);
|
||||
const block = await editor.getBlockById(blockId);
|
||||
await editor.scrollManager.scrollIntoViewByBlockId(blockId);
|
||||
|
||||
if (!block || block.type === Protocol.Block.Type.group) {
|
||||
// the group block has its own background
|
||||
return;
|
||||
}
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
|
||||
block.dom?.animate(
|
||||
[
|
||||
{
|
||||
backgroundColor: 'rgba(152, 172, 189, 0.1)',
|
||||
},
|
||||
{
|
||||
backgroundColor: 'rgba(152, 172, 189, 0)',
|
||||
},
|
||||
],
|
||||
{
|
||||
delay: 500,
|
||||
duration: 700,
|
||||
easing: 'linear',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TOCContext.Provider value={{ activeBlockId, onClick }}>
|
||||
<div>{renderTOCContent(tocDataSource)}</div>
|
||||
</TOCContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { TOC } from './TOC';
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum BLOCK_TYPES {
|
||||
GROUP = 'group',
|
||||
HEADING1 = 'heading1',
|
||||
HEADING2 = 'heading2',
|
||||
HEADING3 = 'heading3',
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { AsyncBlock } from '@toeverything/components/editor-core';
|
||||
import { BLOCK_TYPES } from './toc-enum';
|
||||
import type { ListenerMap, TOCType } from './types';
|
||||
|
||||
/* 😞😞sorry, I don't know how to define unlimited dimensions array */
|
||||
const getContentByAsyncBlocks = async (
|
||||
asyncBlocks: AsyncBlock[] = [],
|
||||
callback: () => void,
|
||||
listenerMap: ListenerMap
|
||||
): Promise<{
|
||||
tocContents: any[];
|
||||
}> => {
|
||||
const collect = async (asyncBlocks): Promise<any[]> => {
|
||||
/* maybe should recast it to tail recursion */
|
||||
return await Promise.all(
|
||||
asyncBlocks.map(async (asyncBlock: AsyncBlock) => {
|
||||
const asyncBlocks = await asyncBlock?.children();
|
||||
|
||||
if (asyncBlocks?.length) {
|
||||
return collect(asyncBlocks);
|
||||
}
|
||||
|
||||
/* add only once event listener for every block */
|
||||
if (!listenerMap.has(asyncBlock?.id)) {
|
||||
/* get update notice */
|
||||
const destroyHandler = asyncBlock?.onUpdate(callback);
|
||||
|
||||
/* collect destroy handlers */
|
||||
listenerMap.set(asyncBlock?.id, destroyHandler);
|
||||
}
|
||||
|
||||
const { id, type } = asyncBlock;
|
||||
|
||||
switch (type) {
|
||||
case BLOCK_TYPES.GROUP:
|
||||
case BLOCK_TYPES.HEADING1:
|
||||
case BLOCK_TYPES.HEADING2:
|
||||
case BLOCK_TYPES.HEADING3: {
|
||||
const properties = await asyncBlock?.getProperties();
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
text: properties?.text?.value?.[0]?.text || '',
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
tocContents: await collect(asyncBlocks),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get flat toc
|
||||
* @param asyncBlocks
|
||||
* @param tocContents
|
||||
*/
|
||||
const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TOCType[] => {
|
||||
return tocContents
|
||||
.reduce((tocGroupContent, tocContent, index) => {
|
||||
const { id, type } = asyncBlocks[index];
|
||||
const groupContent = {
|
||||
id,
|
||||
type,
|
||||
text: 'Untitled Group',
|
||||
};
|
||||
|
||||
tocGroupContent.push(
|
||||
!tocContent.flat(Infinity).filter(Boolean).length
|
||||
? groupContent
|
||||
: tocContent
|
||||
);
|
||||
|
||||
return tocGroupContent;
|
||||
}, [])
|
||||
.flat(Infinity)
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/* destroy page/block update-listener */
|
||||
const destroyEventList = (listenerMap: ListenerMap) => {
|
||||
const eventListeners = listenerMap.values();
|
||||
listenerMap.clear();
|
||||
|
||||
for (const eventListener of eventListeners) {
|
||||
eventListener?.();
|
||||
}
|
||||
};
|
||||
|
||||
export { getPageTOC, getContentByAsyncBlocks, destroyEventList };
|
||||
@@ -0,0 +1,7 @@
|
||||
export type TOCType = {
|
||||
id: string;
|
||||
type: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ListenerMap = Map<string, () => void>;
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"Blog": "Blog",
|
||||
"AboutUs": "About AboutUs",
|
||||
"Open Source": "Open Source",
|
||||
"Privacy First": "Privacy First",
|
||||
"Alternative": "Alternative",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"Blog": "博客",
|
||||
"AboutUs": "关于我们",
|
||||
"Open Source": "开源",
|
||||
"Privacy First": "隐私第一",
|
||||
"Alternative": "的另一种选择",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -6,6 +6,27 @@
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://affine.pro/" />
|
||||
<meta name="twitter:title" content="AFFiNE - All In One Workos" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Affine is the next-generation collaborative knowledge base for professionals."
|
||||
/>
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.png" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="AFFiNE - All In One Workos" />
|
||||
<meta property="og:site_name" content="AFFiNE - All In One Workos" />
|
||||
<meta property="og:url" content="https://affine.pro/" />
|
||||
<meta property="og:image" content="https://affine.pro/og.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Affine is the next-generation collaborative knowledge base for professionals."
|
||||
/>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Welcome to ourcontributing guide <!-- omit in toc -->
|
||||
# Welcome to our contributing guide <!-- omit in toc -->
|
||||
|
||||
Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:.
|
||||
|
||||
@@ -10,7 +10,7 @@ Use the table of contents icon on the top left corner of this document to get to
|
||||
|
||||
## New contributor guide
|
||||
|
||||
To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions:
|
||||
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
|
||||
|
||||
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
|
||||
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
|
||||
@@ -19,19 +19,17 @@ To get an overview of the project, read the [README](README.md). Here are some r
|
||||
|
||||
## Getting started
|
||||
|
||||
To navigate our codebase with confidence, see [the introduction to working in the docs repository](/contributing/working-in-docs-repository.md) :confetti_ball:. For more information on how we write our markdown files, see [the GitHub Markdown reference](contributing/content-markup-reference.md).
|
||||
|
||||
Check to see what [types of contributions](/contributing/types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:.
|
||||
Check to see what [types of contributions](types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:.
|
||||
|
||||
### Issues
|
||||
|
||||
#### Create a new issue
|
||||
#### Create a new issue or feature request
|
||||
|
||||
If you spot a problem, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/toeverything/AFFiNE/issues/new/choose).
|
||||
|
||||
#### Solve an issue
|
||||
|
||||
Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See [Labels](/contributing/how-to-use-labels.md) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See our [Labels](https://github.com/toeverything/AFFiNE/labels) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
|
||||
### Make Changes
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Types of contributions :memo:
|
||||
|
||||
You can contribute to AFFiNE in several ways. This repo is a place to discuss and collaborate on AFFiNE!
|
||||
|
||||
### :mega: Discussions
|
||||
|
||||
Discussions are where we have conversations.
|
||||
|
||||
If you'd like help troubleshooting a docs PR you're working on, have a great new idea, or want to share something amazing you've learned in our docs, join us in [discussions](https://github.com/toeverything/AFFiNE/discussions).
|
||||
|
||||
### :lady_beetle: Issues
|
||||
|
||||
[Issues](https://docs.github.com/en/github/managing-your-work-on-github/about-issues) are used to track tasks that contributors can help with. If an issue has a triage label, we haven't reviewed it yet, and you shouldn't begin work on it.
|
||||
|
||||
If you've found something in the content or the website that should be updated, search open issues to see if someone else has reported the same thing. If it's something new, open an issue using a [template](https://github.com/toeverything/AFFiNE/issues/new/choose). We'll use the issue to have a conversation about the problem you want to fix.
|
||||
|
||||
### :hammer_and_wrench: Pull requests
|
||||
|
||||
A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository. When we merge those changes, they should be deployed to the live site within 24 hours. :earth_africa:
|
||||
You can [create a new pull request](https://github.com/toeverything/AFFiNE/compare) and view [current pull requests](https://github.com/toeverything/AFFiNE/pulls).
|
||||
|
||||
### :question: Support
|
||||
|
||||
We are a small team working hard to keep up with the documentation demands of a continuously changing product.
|
||||
You may be able to find additional help and information on our social media platforms and groups - the links to these can be found in our [README](../README.md).
|
||||
|
||||
### :earth_asia: Translations
|
||||
|
||||
AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content.
|
||||
|
||||
**We do not currently offer this feature**, but we hope to in the future.
|
||||
@@ -5,10 +5,7 @@ import { getSession } from '@toeverything/components/board-sessions';
|
||||
import { deepCopy, TldrawApp } from '@toeverything/components/board-state';
|
||||
import { tools } from '@toeverything/components/board-tools';
|
||||
import { TDShapeType } from '@toeverything/components/board-types';
|
||||
import {
|
||||
getClipDataOfBlocksById,
|
||||
RecastBlockProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { RecastBlockProvider } from '@toeverything/components/editor-core';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -51,10 +48,10 @@ const AffineBoard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const { shapes, bindings } = useShapes(workspace, rootBlockId);
|
||||
const { shapes } = useShapes(workspace, rootBlockId);
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
app.replacePageContent(shapes || {}, bindings, {});
|
||||
app.replacePageContent(shapes || {}, {}, {});
|
||||
}
|
||||
}, [app, shapes]);
|
||||
|
||||
@@ -68,12 +65,14 @@ const AffineBoard = ({
|
||||
onMount(app) {
|
||||
set_app(app);
|
||||
},
|
||||
|
||||
async onPaste(e, data) {
|
||||
console.log('e,data: ', e, data);
|
||||
},
|
||||
async onCopy(e, groupIds) {
|
||||
const clip = await getClipDataOfBlocksById(
|
||||
editor,
|
||||
groupIds
|
||||
);
|
||||
const clip =
|
||||
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
|
||||
groupIds
|
||||
);
|
||||
|
||||
e.clipboardData?.setData(
|
||||
clip.getMimeType(),
|
||||
@@ -109,19 +108,6 @@ const AffineBoard = ({
|
||||
});
|
||||
}
|
||||
shape.affineId = block.id;
|
||||
|
||||
Object.keys(bindings).forEach(bilingKey => {
|
||||
if (
|
||||
bindings[bilingKey]?.fromId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].fromId = block.id;
|
||||
}
|
||||
if (
|
||||
bindings[bilingKey]?.toId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].toId = block.id;
|
||||
}
|
||||
});
|
||||
return await services.api.editorBlock.update({
|
||||
workspace: shape.workspace,
|
||||
id: block.id,
|
||||
@@ -134,32 +120,6 @@ const AffineBoard = ({
|
||||
}
|
||||
})
|
||||
);
|
||||
let pageBindingsString = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
)?.[0].properties.bindings?.value;
|
||||
console.log(123123123);
|
||||
let pageBindings = JSON.parse(pageBindingsString ?? '{}');
|
||||
console.log(pageBindings, 3333, bindings);
|
||||
Object.keys(bindings).forEach(bindingsKey => {
|
||||
console.log(345345345345345);
|
||||
if (!bindings[bindingsKey]) {
|
||||
delete pageBindings[bindingsKey];
|
||||
} else {
|
||||
Object.assign(pageBindings, bindings);
|
||||
}
|
||||
});
|
||||
services.api.editorBlock.update({
|
||||
workspace: workspace,
|
||||
id: rootBlockId,
|
||||
properties: {
|
||||
bindings: {
|
||||
value: JSON.stringify(pageBindings),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,24 +5,12 @@ import { services } from '@toeverything/datasource/db-service';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const getBindings = (workspace: string, rootBlockId: string) => {
|
||||
return services.api.editorBlock
|
||||
.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
.then(blcoks => {
|
||||
return blcoks[0].properties.bindings?.value;
|
||||
});
|
||||
};
|
||||
|
||||
export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
const [blocks, setBlocks] = useState<{
|
||||
shapes: [ReturnEditorBlock[]];
|
||||
bindings: string;
|
||||
}>();
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -43,11 +31,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return shapes;
|
||||
}),
|
||||
]).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes,
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: shapes,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,11 +50,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return childBlock;
|
||||
})
|
||||
).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -104,8 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, TDShape>);
|
||||
|
||||
return {
|
||||
shapes: blocksShapes,
|
||||
bindings: JSON.parse(blocks?.bindings ?? '{}'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import {
|
||||
RenderBlock,
|
||||
RenderRoot,
|
||||
type BlockEditor,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
editor={editor}
|
||||
editorElement={AffineEditor as any}
|
||||
scrollBlank={scrollBlank}
|
||||
>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</RenderRoot>
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -362,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name === 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId;
|
||||
|
||||
@@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px',
|
||||
padding: '3px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
'&:hover': {
|
||||
|
||||
@@ -21,6 +21,9 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
let activeShape = shapes[0];
|
||||
let toNextShapBindings: ArrowBinding[] = [];
|
||||
let bindingId = '';
|
||||
if (!activeShape) {
|
||||
return;
|
||||
}
|
||||
Object.keys(bindings).forEach(key => {
|
||||
if (bindings[key].toId === activeShape.id) {
|
||||
bindingId = bindings[key].fromId;
|
||||
@@ -35,7 +38,6 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
toNextShapBindings.forEach(binding => {
|
||||
if (binding.toId !== activeShape.id) {
|
||||
allShape.forEach(item => {
|
||||
console.log(item);
|
||||
if (item.id === binding.toId) {
|
||||
ArrowToArr.push(item);
|
||||
}
|
||||
@@ -44,7 +46,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
});
|
||||
setarrowToArr(ArrowToArr);
|
||||
return () => {};
|
||||
}, [app.page.bindings, app.shapes]);
|
||||
}, [app.page.bindings]);
|
||||
const jumpToNextShap = (shape: TDShape) => {
|
||||
app.zoomToShapes([shape]);
|
||||
};
|
||||
@@ -68,7 +70,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip content="Font Size" placement="top-start">
|
||||
<Tooltip content="ArrowToEditor" placement="top-start">
|
||||
<IconButton>
|
||||
<ConnectorIcon></ConnectorIcon>
|
||||
</IconButton>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { TLDR, TldrawApp } from '@toeverything/components/board-state';
|
||||
import { Divider, Popover, styled } from '@toeverything/components/ui';
|
||||
import { Fragment } from 'react';
|
||||
import { AlignOperation } from './AlignOperation';
|
||||
import { ArrowTo } from './ArrowTo';
|
||||
import { BorderColorConfig } from './BorderColorConfig';
|
||||
import { DeleteShapes } from './DeleteOperation';
|
||||
import { FillColorConfig } from './FillColorConfig';
|
||||
@@ -107,12 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></AlignOperation>
|
||||
) : null,
|
||||
toNextShap: (
|
||||
<ArrowTo
|
||||
app={app}
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></ArrowTo>
|
||||
),
|
||||
// toNextShap: (
|
||||
// <ArrowTo
|
||||
// app={app}
|
||||
// shapes={config.deleteShapes.selectedShapes}
|
||||
// ></ArrowTo>
|
||||
// ),
|
||||
};
|
||||
|
||||
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);
|
||||
|
||||
@@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Line
|
||||
@@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Rectangle
|
||||
@@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
import {
|
||||
IconButton,
|
||||
PopoverContainer,
|
||||
styled,
|
||||
// MuiIconButton as IconButton,
|
||||
// MuiTooltip as Tooltip,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { TldrawApp } from '@toeverything/components/board-state';
|
||||
import {
|
||||
@@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
}}
|
||||
direction="none"
|
||||
>
|
||||
<div className={styles('container')}>
|
||||
<div className={styles('toolBar')}>
|
||||
<Container>
|
||||
<ToolBar>
|
||||
{tools.map(
|
||||
({
|
||||
type,
|
||||
@@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolBar>
|
||||
</Container>
|
||||
</PopoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
toolBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
},
|
||||
const Container = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
const ToolBar = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
styled,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { ReactElement, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties, type ReactElement } from 'react';
|
||||
import { Palette } from '../../palette';
|
||||
import { Pen } from './Pen';
|
||||
|
||||
@@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
|
||||
|
||||
export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
const appCurrentTool = app.useStore(state => state.appState.activeTool);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const chosenPen =
|
||||
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
|
||||
PENCIL_CONFIGS[0];
|
||||
@@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<Container>
|
||||
<PensContainer>
|
||||
@@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
name={title}
|
||||
primaryColor={color_vars['--color-0']}
|
||||
secondaryColor={color_vars['--color-1']}
|
||||
onClick={() => setPen(key)}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
setPen(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
<Palette
|
||||
selected={chosenColor}
|
||||
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
|
||||
onSelect={color => setPenColor(color)}
|
||||
onSelect={color => {
|
||||
setVisible(false);
|
||||
setPenColor(color);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
</Container>
|
||||
|
||||
@@ -42,7 +42,7 @@ type E = HTMLDivElement;
|
||||
export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Arrow as const;
|
||||
|
||||
override hideBounds = true;
|
||||
override hideBounds = false;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import {
|
||||
BINDING_DISTANCE,
|
||||
ShapeStyles,
|
||||
} from '@toeverything/components/board-types';
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
@@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
className={'tl-fill-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={1}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
width={w - sw / 2}
|
||||
height={h - sw / 2}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
@@ -175,6 +175,10 @@ interface TDCallbacks {
|
||||
* (optional) A callback to run when the shape is copied.
|
||||
*/
|
||||
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
|
||||
/**
|
||||
* (optional) A callback to run when the shape is paste.
|
||||
*/
|
||||
onPaste?: (e: ClipboardEvent, data: any) => void;
|
||||
}
|
||||
|
||||
export interface TldrawAppCtorProps {
|
||||
@@ -626,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
private prev_bindings = this.page.bindings;
|
||||
private prev_assets = this.document.assets;
|
||||
|
||||
private _broadcastPageChanges = () => {
|
||||
private _broadcastPageChanges = async () => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {};
|
||||
@@ -683,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.just_sent = true;
|
||||
this.callbacks.onChangePage?.(
|
||||
await this.callbacks.onChangePage?.(
|
||||
this,
|
||||
changedShapes,
|
||||
changedBindings,
|
||||
@@ -1956,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return;
|
||||
|
||||
if (e) {
|
||||
const data = e.clipboardData?.getData('text/html');
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
return json;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
this.callbacks.onPaste?.(e, paste_as_html(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const pasteInCurrentPage = (
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[],
|
||||
@@ -2107,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
// this.select(shapeId);
|
||||
};
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (e !== undefined) {
|
||||
const items: DataTransferItemList =
|
||||
e.clipboardData?.items ?? ({} as DataTransferItemList);
|
||||
|
||||
@@ -804,7 +804,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
|
||||
backgroundColor: '#F2F5F9',
|
||||
borderRadius: '5px',
|
||||
color: '#3A4C5C',
|
||||
padding: '3px 8px',
|
||||
padding: '1px 8px',
|
||||
margin: '0 2px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -968,7 +968,41 @@ class SlateUtils {
|
||||
}
|
||||
|
||||
public getNodeByPath(path: Path) {
|
||||
Editor.node(this.editor, path);
|
||||
return Editor.node(this.editor, path);
|
||||
}
|
||||
|
||||
public getNodeByRange(range: Range) {
|
||||
return Editor.node(this.editor, range);
|
||||
}
|
||||
|
||||
// This may should write with logic of render slate
|
||||
public convertLeaf2Html(textValue: any) {
|
||||
const { text, fontColor, fontBgColor } = textValue;
|
||||
|
||||
const style = `style="${fontColor ? `color: ${fontColor};` : ''}${
|
||||
fontBgColor ? `backgroundColor: ${fontBgColor};` : ''
|
||||
}"`;
|
||||
if (textValue.bold) {
|
||||
return `<strong ${style}>${text}</strong>`;
|
||||
}
|
||||
if (textValue.italic) {
|
||||
return `<em ${style}>${text}</em>`;
|
||||
}
|
||||
if (textValue.underline) {
|
||||
return `<u ${style}>${text}</u>`;
|
||||
}
|
||||
if (textValue.inlinecode) {
|
||||
return `<code ${style}>${text}</code>`;
|
||||
}
|
||||
if (textValue.strikethrough) {
|
||||
return `<s ${style}>${text}</s>`;
|
||||
}
|
||||
if (textValue.type === 'link') {
|
||||
return `<a href='${textValue.url}' ${style}>${this.convertLeaf2Html(
|
||||
textValue.children
|
||||
)}</a>`;
|
||||
}
|
||||
return `<span ${style}>${text}</span>`;
|
||||
}
|
||||
|
||||
public getStartSelection() {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"react-window": "^1.8.7",
|
||||
"slate": "^0.81.1",
|
||||
"slate-react": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codemirror": "^5.60.5",
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { BulletIcon, getChildrenType, NumberType } from './data';
|
||||
import { BulletBlock, BulletProperties } from './types';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
export const defaultBulletProps: BulletProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -189,7 +188,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<BulletLeft>
|
||||
<BulletIcon numberType={properties.numberType} />
|
||||
@@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
</div>
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
CreateView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { defaultBulletProps, BulletView } from './BulletView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { BulletView, defaultBulletProps } from './BulletView';
|
||||
|
||||
export class BulletBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.bullet;
|
||||
@@ -27,66 +26,44 @@ export class BulletBlock extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
if (!firstList || firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && !el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,54 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { cpp } from '@codemirror/lang-cpp';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { lezer } from '@codemirror/lang-lezer';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { julia } from '@codemirror/legacy-modes/mode/julia';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { DuplicateIcon } from '@toeverything/components/icons';
|
||||
import { Option, Select, styled } from '@toeverything/components/ui';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
|
||||
interface CreateCodeView extends CreateView {
|
||||
style9?: StyleWithAtRules;
|
||||
@@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
'&:hover': {
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
opacity: 0,
|
||||
transition: 'opacity 1.5s',
|
||||
},
|
||||
'.copy-block': {
|
||||
padding: '0px 10px',
|
||||
@@ -172,7 +170,7 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
editor.selectionManager.activePreviousNode(block.id, 'start');
|
||||
};
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<CodeBlock
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
@@ -200,7 +198,8 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
</div>
|
||||
<div>
|
||||
<div className="copy-block" onClick={copyCode}>
|
||||
<DuplicateIcon></DuplicateIcon>Copy
|
||||
<DuplicateIcon />
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {
|
||||
BaseView,
|
||||
AsyncBlock,
|
||||
getTextProperties,
|
||||
CreateView,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CodeView } from './CodeView';
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class CodeBlock extends BaseView {
|
||||
type = Protocol.Block.Type.code;
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
// View = CodeView;
|
||||
View = CodeView;
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
@@ -28,56 +27,22 @@ export class CodeBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'CODE',
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: internal format not implemented yet
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'CODE') {
|
||||
const childNodes = el.childNodes;
|
||||
let text_value = '';
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
if (block_texts.length > 0) {
|
||||
text_value += block_texts[0].text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: [{ text: text_value }] },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<code>${content}</code>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<code>${await commonBlock2HtmlContent(props)}<code/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { DividerView } from './divider-view';
|
||||
import { Block2HtmlProps, commonHTML2block } from '../../utils/commonBlockClip';
|
||||
|
||||
export class DividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.divider;
|
||||
View = DividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'HR',
|
||||
ignoreEmptyElement: false,
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
|
||||
@@ -5,43 +5,16 @@ import {
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { EmbedLinkView } from './EmbedLinkView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class EmbedLinkBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.embedLink;
|
||||
View = EmbedLinkView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: Not sure what value to fill for name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const figma_url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${figma_url}>${figma_url}</p>`;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a href="${url}">${url}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { useState } from 'react';
|
||||
import { SourceView } from '../../components/source-view';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { LinkContainer } from '../../components/style-container';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
|
||||
const MESSAGES = {
|
||||
ADD_FIGMA_LINK: 'Add figma link',
|
||||
@@ -30,7 +29,7 @@ export const FigmaView = ({ block, editor }: FigmaView) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{figmaUrl ? (
|
||||
<SourceView
|
||||
|
||||
@@ -5,49 +5,16 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { FigmaView } from './FigmaView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FigmaBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.figma;
|
||||
View = FigmaView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
const href = el.getAttribute('href');
|
||||
const allowedHosts = ['www.figma.com'];
|
||||
const host = new URL(href).host;
|
||||
|
||||
if (allowedHosts.includes(host)) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: Not sure what value to fill for name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const figma_url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${figma_url}>${figma_url}</p>`;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const figmaUrl = block.getProperty('embedLink')?.value;
|
||||
return `<p><a href="${figmaUrl}">${figmaUrl}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,25 +9,22 @@ import {
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { FileView } from './FileView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FileBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.file;
|
||||
View = FileView;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const fileProperty = block.getProperty('file');
|
||||
const fileId = fileProperty?.value;
|
||||
const fileInfo = fileId
|
||||
? await services.api.file.get(fileId, block.workspace)
|
||||
: null;
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const file_property =
|
||||
block.getProperty('file') || ({} as FileColumnValue);
|
||||
const file_id = file_property.value;
|
||||
let file_info = null;
|
||||
if (file_id) {
|
||||
file_info = await services.api.file.get(file_id, block.workspace);
|
||||
}
|
||||
return `<p><a src=${file_info?.url}>${file_property?.name}</p>`;
|
||||
return fileInfo
|
||||
? `<p><a href=${fileInfo?.url}>${fileProperty?.name}</a></p>`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import { ChildrenView, CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const GridItemRender = function (
|
||||
@@ -6,13 +6,7 @@ export const GridItemRender = function (
|
||||
) {
|
||||
const GridItem = function (props: CreateView) {
|
||||
const { block } = props;
|
||||
const children = (
|
||||
<>
|
||||
{block.childrenIds.map(id => {
|
||||
return <RenderBlock key={id} blockId={id} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
const children = <RenderBlockChildren block={block} indent={false} />;
|
||||
return <>{creator({ ...props, children })}</>;
|
||||
};
|
||||
return GridItem;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GridItemRender } from './GridItemRender';
|
||||
|
||||
export class GridItemBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
import { BlockRender } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
GRID_ITEM_CLASS_NAME,
|
||||
GRID_ITEM_CONTENT_CLASS_NAME,
|
||||
} from '../grid-item/GridItem';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
|
||||
const DB_UPDATE_DELAY = 50;
|
||||
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
|
||||
@@ -226,7 +226,7 @@ export const Grid = function (props: CreateView) {
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<BlockRender hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
|
||||
@@ -7,7 +7,7 @@ export { GRID_PROPERTY_KEY, removePercent } from './Grid';
|
||||
|
||||
export class GridBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { GroupView } from './GroupView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class Group extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
|
||||
type = Protocol.Block.Type.group;
|
||||
@@ -25,13 +29,12 @@ export class Group extends BaseView {
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
return block;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const content = await generateHtml(children);
|
||||
return `<div>${content}<div>`;
|
||||
override async block2html({ editor, selectInfo, block }: Block2HtmlProps) {
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `<div>${childrenHtml}<code/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,19 +21,6 @@ const SceneMap: Record<RecastScene, ComponentType<CreateView>> = {
|
||||
kanban: SceneKanban,
|
||||
} as const;
|
||||
|
||||
const GroupBox = styled('div')(({ theme }) => {
|
||||
return {
|
||||
'&:hover': {
|
||||
// Workaround referring to other components
|
||||
// See https://emotion.sh/docs/styled#targeting-another-emotion-component
|
||||
// [GroupActionWrapper.toString()]: {},
|
||||
'& > *': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
@@ -59,6 +46,14 @@ const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const GroupBox = styled('div')({
|
||||
'&:hover': {
|
||||
[GroupActionWrapper.toString()]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
({ isSelect, theme }) => ({
|
||||
background: theme.affine.palette.white,
|
||||
|
||||
@@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const ScenePage = ({ block }: CreateView) => {
|
||||
return <RenderBlockChildren block={block} />;
|
||||
return <RenderBlockChildren block={block} indent={false} />;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const AddViewMenu = () => {
|
||||
onClick={() => setActivePanel(!activePanel)}
|
||||
>
|
||||
<AddViewIcon fontSize="small" />
|
||||
<span>Add View</span>
|
||||
<span style={{ userSelect: 'none' }}>Add View</span>
|
||||
{activePanel && (
|
||||
<Panel>
|
||||
<PanelItem>
|
||||
@@ -66,10 +66,10 @@ export const AddViewMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ViewsMenu = () => {
|
||||
useRecastView();
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setViewName(e.target.value.trim());
|
||||
setViewName(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -36,7 +36,7 @@ export const ViewsMenu = () => {
|
||||
}
|
||||
await updateView({
|
||||
...activeView,
|
||||
name: viewName,
|
||||
name: viewName.trim(),
|
||||
type: viewType,
|
||||
});
|
||||
setActiveView(null);
|
||||
@@ -99,10 +99,10 @@ export const ViewsMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { RecastScene } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
KanBanIcon,
|
||||
TableIcon,
|
||||
TodoListIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { KanBanIcon, TodoListIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const VIEW_LIST = [
|
||||
{
|
||||
@@ -16,9 +12,9 @@ export const VIEW_LIST = [
|
||||
scene: RecastScene.Kanban,
|
||||
icon: <KanBanIcon fontSize="small" />,
|
||||
},
|
||||
{
|
||||
name: 'Table',
|
||||
scene: RecastScene.Table,
|
||||
icon: <TableIcon fontSize="small" />,
|
||||
},
|
||||
// {
|
||||
// name: 'Table',
|
||||
// scene: RecastScene.Table,
|
||||
// icon: <TableIcon fontSize="small" />,
|
||||
// },
|
||||
] as const;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
import type {
|
||||
KanbanCard,
|
||||
KanbanGroup,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
|
||||
const AddCardWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
@@ -48,7 +48,7 @@ export const CardContext = (props: Props) => {
|
||||
item={item}
|
||||
active={activeId === id}
|
||||
>
|
||||
<CardItem id={id} block={block} />
|
||||
<CardItem block={block} />
|
||||
</CardItemPanelWrapper>
|
||||
</StyledCardContainer>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
KanbanBlockRender,
|
||||
KanbanCard,
|
||||
RenderBlock,
|
||||
useEditor,
|
||||
useKanban,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
MuiClickAwayListener,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { useRefPage } from './RefPage';
|
||||
|
||||
@@ -82,41 +81,37 @@ const Overlay = styled('div')({
|
||||
},
|
||||
});
|
||||
|
||||
export const CardItem = ({
|
||||
id,
|
||||
block,
|
||||
}: {
|
||||
id: KanbanCard['id'];
|
||||
block: KanbanCard['block'];
|
||||
}) => {
|
||||
export const CardItem = ({ block }: { block: KanbanCard['block'] }) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const [editable, setEditable] = useState(false);
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const [editableBlock, setEditableBlock] = useState<string | null>(null);
|
||||
const { editor } = useEditor();
|
||||
|
||||
const onAddItem = async () => {
|
||||
setEditable(true);
|
||||
await addSubItem(block);
|
||||
const newItem = await addSubItem(block);
|
||||
setEditableBlock(newItem.id);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
openSubPage(id);
|
||||
openSubPage(block.id);
|
||||
};
|
||||
|
||||
const onClickPen = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
setEditable(true);
|
||||
setEditableBlock(block.id);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
|
||||
<MuiClickAwayListener onClickAway={() => setEditableBlock(null)}>
|
||||
<CardContainer>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
<KanbanBlockRender
|
||||
blockId={block.id}
|
||||
activeBlock={editableBlock}
|
||||
/>
|
||||
</CardContent>
|
||||
{showKanbanRefPageFlag && !editable && (
|
||||
{!editableBlock && (
|
||||
<Overlay onClick={onClickCard}>
|
||||
<IconButton backgroundColor="#fff" onClick={onClickPen}>
|
||||
<PenIcon />
|
||||
|
||||
@@ -21,13 +21,17 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
onMouseDown={(e: { stopPropagation: () => void }) => {
|
||||
// Prevent trigger the bottom editor's selection
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={(e: { stopPropagation: () => void }) => {
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
import { CardItem } from '../../CardItem';
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import type { DndableItems } from '../type';
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
|
||||
export function renderContainerDragOverlay({
|
||||
containerId,
|
||||
@@ -18,7 +17,7 @@ export function renderContainerDragOverlay({
|
||||
return (
|
||||
<CardItemWrapper
|
||||
key={id}
|
||||
card={<CardItem key={id} id={id} block={block} />}
|
||||
card={<CardItem key={id} block={block} />}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,35 +5,13 @@ import {
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GroupDividerView } from './groupDividerView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class GroupDividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.groupDivider;
|
||||
View = GroupDividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +165,10 @@ export const ImageView = ({ block, editor }: ImageView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<ImageBlock>
|
||||
<div style={{ position: 'relative' }} ref={resizeBox}>
|
||||
{!isSelect ? (
|
||||
<ImageShade onClick={handleClick}></ImageShade>
|
||||
) : null}
|
||||
{!isSelect ? <ImageShade onClick={handleClick} /> : null}
|
||||
{imgUrl ? (
|
||||
<div
|
||||
onMouseDown={e => {
|
||||
|
||||
@@ -1,54 +1,54 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
SelectBlock,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { ImageView } from './ImageView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
|
||||
export class ImageBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.image;
|
||||
View = ImageView;
|
||||
|
||||
// TODO: needs to download the image and then upload it to get a new link and then assign it
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'IMG') {
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'IMG') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
value: '',
|
||||
url: el.getAttribute('src'),
|
||||
name: el.getAttribute('src'),
|
||||
size: 0,
|
||||
type: 'link',
|
||||
image: {
|
||||
value: getRandomString('image'),
|
||||
url: element.getAttribute('src'),
|
||||
name: element.getAttribute('src'),
|
||||
size: 0,
|
||||
type: 'link',
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const text = block.getProperty('text');
|
||||
override async block2html({ block, editor }: Block2HtmlProps) {
|
||||
const textValue = block.getProperty('text');
|
||||
const content = '';
|
||||
if (text) {
|
||||
text.value.map(text => `<span>${text}</span>`).join('');
|
||||
}
|
||||
// TODO: child
|
||||
return `<p><img src=${content}></p>`;
|
||||
// TODO: text.value should export with style??
|
||||
const figcaption = (textValue?.value ?? [])
|
||||
.map(({ text }) => `<span>${text}</span>`)
|
||||
.join('');
|
||||
return `<figure><img src="${content}" alt="${figcaption}"/><figcaption>${figcaption}<figcaption/></figure>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -11,18 +11,17 @@ import {
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import type { Numbered, NumberedAsyncBlock } from './types';
|
||||
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
|
||||
export const defaultTodoProps: Numbered = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -184,7 +183,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<div className={'checkBoxContainer'}>
|
||||
{getNumber(properties.numberType, number)}.
|
||||
@@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
|
||||
export class NumberedBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.numbered;
|
||||
@@ -29,71 +28,39 @@ export class NumberedBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'OL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'OL') {
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ol><li>${content}</li></ol>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ol><li>${await commonBlock2HtmlContent(props)}</li></ol>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useRef, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { BackLink, TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
RenderBlockChildren,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { ContentColumnValue } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { Theme, styled } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
TextManage,
|
||||
@@ -81,7 +81,7 @@ export const PageView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<PageTitleBlock>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextManage
|
||||
alwaysShowPlaceholder
|
||||
ref={textRef}
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { withRecastBlock } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
ChildrenView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
|
||||
|
||||
import { PageView } from './PageView';
|
||||
|
||||
export const PageChildrenView: (prop: ChildrenView) => JSX.Element = props =>
|
||||
props.children;
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class PageBlock extends BaseView {
|
||||
type = Protocol.Block.Type.page;
|
||||
@@ -35,21 +23,17 @@ export class PageBlock extends BaseView {
|
||||
return this.get_decoration<any>(content, 'text')?.value?.[0].text;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const content = getTextHtml(block);
|
||||
const childrenContent = await generateHtml(children);
|
||||
return `<h1>${content}</h1> ${childrenContent}`;
|
||||
override async block2html({ block, editor, selectInfo }: Block2HtmlProps) {
|
||||
const header =
|
||||
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
|
||||
block,
|
||||
selectInfo
|
||||
);
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `<h1>${header}</h1>${childrenHtml}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
CreateView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
import { TextView } from './TextView';
|
||||
|
||||
@@ -29,54 +30,25 @@ export class QuoteBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'BLOCKQUOTE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'BLOCKQUOTE') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<blockquote>${content}</blockquote>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<blockquote>${await commonBlock2HtmlContent(
|
||||
props
|
||||
)}</blockquote>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,69 +68,22 @@ export class CalloutBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'ASIDE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (
|
||||
tag_name === 'ASIDE' ||
|
||||
el.firstChild?.nodeValue?.startsWith('<aside>')
|
||||
) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
texts.length > 0 &&
|
||||
(texts[0].text || '').startsWith('<aside>')
|
||||
) {
|
||||
texts[0].text = texts[0].text.substring('<aside>'.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (el.firstChild?.nodeValue?.startsWith('</aside>')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<aside>${content}</aside>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<aside>${await commonBlock2HtmlContent(props)}</aside>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,16 @@ import {
|
||||
BaseView,
|
||||
CreateView,
|
||||
AsyncBlock,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
HTML2BlockResult,
|
||||
BlockEditor,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { TextView } from './TextView';
|
||||
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class TextBlock extends BaseView {
|
||||
type = Protocol.Block.Type.text;
|
||||
@@ -28,106 +27,30 @@ export class TextBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
if (el instanceof Text) {
|
||||
// TODO: parsing style
|
||||
return el.textContent.split('\n').map(text => {
|
||||
return {
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: [{ text: text }] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const tag_name = el.tagName;
|
||||
const block_style: any = {};
|
||||
switch (tag_name) {
|
||||
case 'STRONG':
|
||||
case 'B':
|
||||
block_style.bold = true;
|
||||
break;
|
||||
case 'A':
|
||||
block_style.type = 'link';
|
||||
block_style.url = el.getAttribute('href');
|
||||
block_style.id = getRandomString('link');
|
||||
block_style.children = [];
|
||||
break;
|
||||
case 'EM':
|
||||
block_style.italic = true;
|
||||
break;
|
||||
case 'U':
|
||||
block_style.underline = true;
|
||||
break;
|
||||
case 'CODE':
|
||||
block_style.inlinecode = true;
|
||||
break;
|
||||
case 'S':
|
||||
case 'DEL':
|
||||
block_style.strikethrough = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const child_nodes = el.childNodes;
|
||||
let texts = [];
|
||||
if (Object.keys(block_style).length > 0) {
|
||||
for (let i = 0; i < child_nodes.length; i++) {
|
||||
const blocks_info: any[] = parseEl(child_nodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === this.type) {
|
||||
const block_texts = block.properties.text.value.map(
|
||||
(text_value: any) => {
|
||||
return tag_name === 'A'
|
||||
? { ...text_value }
|
||||
: { ...block_style, ...text_value };
|
||||
}
|
||||
);
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tag_name === 'A') {
|
||||
block_style.children.push(...texts);
|
||||
texts = [block_style];
|
||||
}
|
||||
return texts.length > 0
|
||||
? [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<p>${content}</p>`;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: [
|
||||
'DIV',
|
||||
'P',
|
||||
'PRE',
|
||||
'B',
|
||||
'A',
|
||||
'EM',
|
||||
'U',
|
||||
'CODE',
|
||||
'S',
|
||||
'DEL',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,55 +67,23 @@ export class Heading1Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H1',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H1') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h1>${content}</h1>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h1>${await commonBlock2HtmlContent(props)}</h1>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,55 +100,23 @@ export class Heading2Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H2',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H2') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h2>${content}</h2>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h2>${await commonBlock2HtmlContent(props)}</h2>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,53 +134,22 @@ export class Heading3Block extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H3',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H3') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h3>${content}</h3>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h3>${await commonBlock2HtmlContent(props)}</h3>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { TextManage } from '../../components/text-manage';
|
||||
import { dedentBlock, tabBlock } from '../../utils/indent';
|
||||
interface CreateTextView extends CreateView {
|
||||
@@ -243,7 +242,7 @@ export const TextView = ({
|
||||
selected={isSelect}
|
||||
className={containerClassName}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextBlock
|
||||
block={block}
|
||||
type={block.type}
|
||||
@@ -255,9 +254,7 @@ export const TextView = ({
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
@@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
export const TodoView = ({ block, editor }: CreateView) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
@@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={
|
||||
properties.checked?.value ? 'checked' : ''
|
||||
}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
</BlockPendantProvider>
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
AsyncBlock,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
withTreeViewChildren,
|
||||
} from '@toeverything/framework/virgo';
|
||||
// import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import { defaultTodoProps, TodoView } from './TodoView';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
@@ -29,71 +28,44 @@ export class TodoBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: TodoAsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
if (!firstList || !firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>[ ] ${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>[ ] ${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,49 +5,22 @@ import {
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { YoutubeView } from './YoutubeView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class YoutubeBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.youtube;
|
||||
View = YoutubeView;
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
|
||||
const href = el.getAttribute('href');
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(href).host;
|
||||
|
||||
if (allowedHosts.includes(host)) {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
// TODO: is not sure what value to fill in name
|
||||
embedLink: {
|
||||
name: this.type,
|
||||
value: el.getAttribute('href'),
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
override async block2Text(block: AsyncBlock, selectInfo: SelectBlock) {
|
||||
return block.getProperty('embedLink')?.value ?? '';
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
const url = block.getProperty('embedLink')?.value;
|
||||
return `<p><a src=${url}>${url}</p>`;
|
||||
return `<p><a href="${url}">${url}</a></p>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const BlockContainer = function ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Container = styled('div')<{ selected: boolean }>(
|
||||
export const Container = styled('div')<{ selected?: boolean }>(
|
||||
({ selected, theme }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ChildrenView } from '@toeverything/framework/virgo';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
export const IndentWrapper = (props: PropsWithChildren) => {
|
||||
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
|
||||
};
|
||||
|
||||
const StyledIdentWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
marginLeft: '30px',
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './IndentWrapper';
|
||||
@@ -1,4 +1,5 @@
|
||||
export const isYoutubeUrl = (url?: string): boolean => {
|
||||
if (!url) return false;
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(url).host;
|
||||
return allowedHosts.includes(host);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
.v-basic-table-body {
|
||||
overflow: hidden !important;
|
||||
&:hover {
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
import {
|
||||
useMemo,
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { VariableSizeGrid, areEqual } from 'react-window';
|
||||
import type {
|
||||
GridChildComponentProps,
|
||||
GridItemKeySelector,
|
||||
} from 'react-window';
|
||||
import { VariableSizeGrid } from 'react-window';
|
||||
import style9 from 'style9';
|
||||
import './basic-table.scss';
|
||||
|
||||
export interface TableColumn {
|
||||
dataKey: string;
|
||||
label: string;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
SelectBlock,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
|
||||
export type Block2HtmlProps = {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
// The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range
|
||||
selectInfo?: SelectBlock;
|
||||
};
|
||||
|
||||
export const commonBlock2HtmlContent = async ({
|
||||
editor,
|
||||
block,
|
||||
selectInfo,
|
||||
}: Block2HtmlProps) => {
|
||||
const html =
|
||||
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
|
||||
block,
|
||||
selectInfo
|
||||
);
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `${html}${childrenHtml}`;
|
||||
};
|
||||
|
||||
export const commonHTML2block = ({
|
||||
element,
|
||||
editor,
|
||||
tagName,
|
||||
type,
|
||||
ignoreEmptyElement = true,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
tagName: string | string[];
|
||||
type: BlockFlavorKeys;
|
||||
ignoreEmptyElement?: boolean;
|
||||
}): HTML2BlockResult => {
|
||||
const tagNames = typeof tagName === 'string' ? [tagName] : tagName;
|
||||
if (tagNames.includes(element.tagName)) {
|
||||
const res = editor.clipboard.clipboardUtils.commonHTML2Block(
|
||||
element,
|
||||
type,
|
||||
ignoreEmptyElement
|
||||
);
|
||||
return res ? [res] : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -4,12 +4,14 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns": "^2.29.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"hotkeys-js": "^3.9.4",
|
||||
"html-escaper": "^3.0.3",
|
||||
"lru-cache": "^7.10.1",
|
||||
"marked": "^4.0.19",
|
||||
"nanoid": "^4.0.0",
|
||||
"slate": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from './editor';
|
||||
|
||||
const RootContext = createContext<{
|
||||
type EditorProps = {
|
||||
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;
|
||||
}>(
|
||||
};
|
||||
|
||||
const EditorContext = createContext<EditorProps>(
|
||||
genErrorObj(
|
||||
'Failed to get context! The context only can use under the "render-root"'
|
||||
'Failed to get EditorContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
return useContext(EditorContext);
|
||||
};
|
||||
|
||||
export const EditorProvider = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<EditorProps>) => {
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, editorElement }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import type { BlockEditor } from './editor';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { BlockRenderProvider, RenderBlock } from './render-block';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -24,11 +24,7 @@ interface RenderRootProps {
|
||||
const MAX_PAGE_WIDTH = 5000;
|
||||
export const MIN_PAGE_WIDTH = 1480;
|
||||
|
||||
export const RenderRoot = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<RenderRootProps>) => {
|
||||
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
|
||||
const selectionRef = useRef<SelectionRef>(null);
|
||||
const triggeredBySelect = useRef(false);
|
||||
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
|
||||
@@ -158,39 +154,43 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
{children}
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
<EditorProvider editor={editor} editorElement={editorElement}>
|
||||
<BlockRenderProvider blockRender={RenderBlock}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && (
|
||||
<SelectionRect ref={selectionRef} editor={editor} />
|
||||
)}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</BlockRenderProvider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
@@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBlankContainter
|
||||
<ScrollBlankContainer
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onClick}
|
||||
@@ -283,7 +283,7 @@ const Content = styled('div')({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
});
|
||||
|
||||
const ScrollBlankContainter = styled('div')({
|
||||
const ScrollBlankContainer = styled('div')({
|
||||
paddingBottom: '30vh',
|
||||
margin: `0 -${PADDING_X}px`,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { containerFlavor } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
type MouseEvent,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from '../editor';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
import { PendantPopover } from './pendant-popover';
|
||||
import { PendantRender } from './pendant-render';
|
||||
import { useRef } from 'react';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
interface BlockTagProps {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
}
|
||||
|
||||
export const BlockPendantProvider = ({
|
||||
editor,
|
||||
block,
|
||||
children,
|
||||
}: PropsWithChildren<BlockTagProps>) => {
|
||||
@@ -23,8 +30,23 @@ export const BlockPendantProvider = ({
|
||||
const showTriggerLine =
|
||||
properties.filter(property => getValue(property.id)).length === 0;
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (containerFlavor.includes(block.type)) {
|
||||
return;
|
||||
}
|
||||
if (e.target === e.currentTarget) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const middle = (rect.left + rect.right) / 2;
|
||||
const position = e.clientX < middle ? 'start' : 'end';
|
||||
editor.selectionManager.activeNodeByNodeId(block.id, position);
|
||||
}
|
||||
},
|
||||
[editor, block]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container onClick={onClick}>
|
||||
{children}
|
||||
|
||||
{showTriggerLine ? (
|
||||
|
||||
@@ -180,9 +180,49 @@ export class AsyncBlock {
|
||||
return this.event_emitter.emit(eventName, eventData);
|
||||
}
|
||||
|
||||
onUpdate(callback: (event: EventData) => void) {
|
||||
this.on('update', callback);
|
||||
/**
|
||||
* @param deep observe deep
|
||||
*
|
||||
* NOTICE: the observe of children is async,
|
||||
* so there maybe have some delay before observe done.
|
||||
*/
|
||||
onUpdate(callback: (event: EventData) => void, deep = 0) {
|
||||
let expired = false;
|
||||
const unobserveMap: Record<string, () => void> = {};
|
||||
const observeChildren = () => {
|
||||
if (deep <= 0) {
|
||||
return;
|
||||
}
|
||||
this.children().then(children => {
|
||||
// Check current event listeners is not expired
|
||||
if (expired) {
|
||||
return;
|
||||
}
|
||||
children.forEach(child => {
|
||||
if (unobserveMap[child.id]) return;
|
||||
const unobserve = child.onUpdate(callback, deep - 1);
|
||||
unobserveMap[child.id] = unobserve;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const unobserveChildren = () => {
|
||||
Object.values(unobserveMap).forEach(unobserve => {
|
||||
unobserve();
|
||||
});
|
||||
};
|
||||
|
||||
this.on('update', e => {
|
||||
callback(e);
|
||||
// Update children observe
|
||||
observeChildren();
|
||||
});
|
||||
|
||||
observeChildren();
|
||||
|
||||
return () => {
|
||||
expired = true;
|
||||
unobserveChildren();
|
||||
this.off('update', callback);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
Selection as SlateSelection,
|
||||
} from 'slate';
|
||||
import { Editor } from '../editor';
|
||||
import { AsyncBlock } from '../block';
|
||||
import { SelectBlock } from '../selection';
|
||||
|
||||
type TextUtilsFunctions =
|
||||
| 'getString'
|
||||
@@ -41,7 +43,9 @@ type TextUtilsFunctions =
|
||||
| 'blur'
|
||||
| 'setSelection'
|
||||
| 'insertNodes'
|
||||
| 'getNodeByPath';
|
||||
| 'getNodeByPath'
|
||||
| 'getNodeByRange'
|
||||
| 'convertLeaf2Html';
|
||||
|
||||
type ExtendedTextUtils = SlateUtils & {
|
||||
setLinkModalVisible: (visible: boolean) => void;
|
||||
@@ -102,15 +106,116 @@ export class BlockHelper {
|
||||
return '';
|
||||
}
|
||||
|
||||
public getBlockTextBetweenSelection(blockId: string) {
|
||||
public async isBlockEditable(blockOrBlockId: AsyncBlock | string) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const blockView = this._editor.getView(block.type);
|
||||
|
||||
return blockView.editable;
|
||||
}
|
||||
|
||||
public async getFlatBlocksUnderParent(
|
||||
parentBlockId: string,
|
||||
includeParent = false
|
||||
): Promise<AsyncBlock[]> {
|
||||
const blocks = [];
|
||||
const block = await this._editor.getBlockById(parentBlockId);
|
||||
if (includeParent) {
|
||||
blocks.push(block);
|
||||
}
|
||||
const children = await block.children();
|
||||
(
|
||||
await Promise.all(
|
||||
children.map(child => {
|
||||
return this.getFlatBlocksUnderParent(child.id, true);
|
||||
})
|
||||
)
|
||||
).forEach(editableChildren => {
|
||||
blocks.push(...editableChildren);
|
||||
});
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public getBlockTextBetweenSelection(
|
||||
blockId: string,
|
||||
shouldUsePreviousSelection = true
|
||||
) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
return text_utils.getStringBetweenSelection(true);
|
||||
return text_utils.getStringBetweenSelection(
|
||||
shouldUsePreviousSelection
|
||||
);
|
||||
}
|
||||
console.warn('Could find the block text utils');
|
||||
return '';
|
||||
}
|
||||
|
||||
public async getEditableBlockPropertiesBySelectInfo(
|
||||
block: AsyncBlock,
|
||||
selectInfo?: SelectBlock
|
||||
) {
|
||||
const properties = block.getProperties();
|
||||
if (properties.text.value.length === 0) {
|
||||
return properties;
|
||||
}
|
||||
let text_value = properties.text.value;
|
||||
|
||||
const {
|
||||
text: { value: originTextValue, ...otherTextProperties },
|
||||
...otherProperties
|
||||
} = properties;
|
||||
|
||||
// Use deepClone method will throw incomprehensible error
|
||||
let textValue = JSON.parse(JSON.stringify(originTextValue));
|
||||
|
||||
if (selectInfo?.endInfo) {
|
||||
textValue = textValue.slice(0, selectInfo.endInfo.arrayIndex + 1);
|
||||
textValue[textValue.length - 1].text = text_value[
|
||||
textValue.length - 1
|
||||
].text.substring(0, selectInfo.endInfo.offset);
|
||||
}
|
||||
if (selectInfo?.startInfo) {
|
||||
textValue = textValue.slice(selectInfo.startInfo.arrayIndex);
|
||||
textValue[0].text = textValue[0].text.substring(
|
||||
selectInfo.startInfo.offset
|
||||
);
|
||||
}
|
||||
return {
|
||||
...otherProperties,
|
||||
text: {
|
||||
...otherTextProperties,
|
||||
value: textValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For editable blocks, the properties containing the selected text will be returned with the selection information
|
||||
public async getBlockPropertiesBySelectInfo(selectBlockInfo: SelectBlock) {
|
||||
const block = await this._editor.getBlockById(selectBlockInfo.blockId);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
if (blockView.editable) {
|
||||
return this.getEditableBlockPropertiesBySelectInfo(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
} else {
|
||||
return block?.getProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public convertTextValue2Html(blockId: string, textValue: any) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (!text_utils) {
|
||||
return '';
|
||||
}
|
||||
return textValue.reduce((html: string, textValueItem: any) => {
|
||||
const fragment = text_utils.convertLeaf2Html(textValueItem);
|
||||
return `${html}${fragment}`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
public setBlockBlur(blockId: string) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { HooksRunner } from '../types';
|
||||
import { Editor } from '../editor';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
import { Paste } from './paste';
|
||||
// todo needs to be a switch
|
||||
|
||||
enum ClipboardAction {
|
||||
COPY = 'copy',
|
||||
CUT = 'cut',
|
||||
PASTE = 'paste',
|
||||
}
|
||||
|
||||
//TODO: need to consider the cursor position after inserting the children
|
||||
class BrowserClipboard {
|
||||
private _eventTarget: Element;
|
||||
private _hooks: HooksRunner;
|
||||
private _editor: Editor;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
private _markdownParse: MarkdownParser;
|
||||
private _paste: Paste;
|
||||
|
||||
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||
this._eventTarget = eventTarget;
|
||||
this._hooks = hooks;
|
||||
this._editor = editor;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
this._markdownParse = new MarkdownParser();
|
||||
this._paste = new Paste(
|
||||
editor,
|
||||
this._clipboardParse,
|
||||
this._markdownParse
|
||||
);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
public getClipboardParse() {
|
||||
return this._clipboardParse;
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._handleCopy = this._handleCopy.bind(this);
|
||||
this._handleCut = this._handleCut.bind(this);
|
||||
|
||||
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCopy(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _handleCut(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||
switch (action) {
|
||||
case ClipboardAction.COPY:
|
||||
this._hooks.beforeCopy(e);
|
||||
break;
|
||||
|
||||
case ClipboardAction.CUT:
|
||||
this._hooks.beforeCut(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _dispatchClipboardEvent(
|
||||
action: ClipboardAction,
|
||||
e: ClipboardEvent
|
||||
) {
|
||||
this._preCopyCut(action, e);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._clipboardParse.dispose();
|
||||
this._clipboardParse = null;
|
||||
this._eventTarget = null;
|
||||
this._hooks = null;
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { BrowserClipboard };
|
||||
@@ -1,207 +0,0 @@
|
||||
import { Protocol, BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
import { escape } from '@toeverything/utils';
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock } from '../selection';
|
||||
import { ClipBlockInfo } from './types';
|
||||
|
||||
class DefaultBlockParse {
|
||||
public static html2block(el: Element): ClipBlockInfo[] | undefined | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'DIV' || el instanceof Text) {
|
||||
return el.textContent?.split('\n').map(str => {
|
||||
const data = {
|
||||
text: escape(str),
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [data] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ClipboardParse {
|
||||
private editor: Editor;
|
||||
private static block_types: BlockFlavorKeys[] = [
|
||||
Protocol.Block.Type.page,
|
||||
Protocol.Block.Type.reference,
|
||||
Protocol.Block.Type.heading1,
|
||||
Protocol.Block.Type.heading2,
|
||||
Protocol.Block.Type.heading3,
|
||||
Protocol.Block.Type.quote,
|
||||
Protocol.Block.Type.todo,
|
||||
Protocol.Block.Type.code,
|
||||
Protocol.Block.Type.text,
|
||||
Protocol.Block.Type.toc,
|
||||
Protocol.Block.Type.file,
|
||||
Protocol.Block.Type.image,
|
||||
Protocol.Block.Type.divider,
|
||||
Protocol.Block.Type.callout,
|
||||
Protocol.Block.Type.youtube,
|
||||
Protocol.Block.Type.figma,
|
||||
Protocol.Block.Type.group,
|
||||
Protocol.Block.Type.embedLink,
|
||||
Protocol.Block.Type.numbered,
|
||||
Protocol.Block.Type.bullet,
|
||||
];
|
||||
private static break_flags: Set<string> = new Set([
|
||||
'BLOCKQUOTE',
|
||||
'BODY',
|
||||
'CENTER',
|
||||
'DD',
|
||||
'DIR',
|
||||
'DIV',
|
||||
'DL',
|
||||
'DT',
|
||||
'FORM',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'HEAD',
|
||||
'HTML',
|
||||
'ISINDEX',
|
||||
'MENU',
|
||||
'NOFRAMES',
|
||||
'P',
|
||||
'PRE',
|
||||
'TABLE',
|
||||
'TD',
|
||||
'TH',
|
||||
'TITLE',
|
||||
'TR',
|
||||
]);
|
||||
|
||||
constructor(editor: Editor) {
|
||||
this.editor = editor;
|
||||
this.generate_html = this.generate_html.bind(this);
|
||||
this.parse_dom = this.parse_dom.bind(this);
|
||||
}
|
||||
// TODO: escape
|
||||
public text2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return (clipData || '').split('\n').map((str: string) => {
|
||||
const block_info: ClipBlockInfo = {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [{ text: str }] },
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
return block_info;
|
||||
});
|
||||
}
|
||||
|
||||
public html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return this.common_html2blocks(clipData);
|
||||
}
|
||||
|
||||
private common_html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
const html_el = document.createElement('html');
|
||||
html_el.innerHTML = clipData;
|
||||
return this.parse_dom(html_el);
|
||||
}
|
||||
|
||||
// tTODO:odo escape
|
||||
private parse_dom(el: Element): ClipBlockInfo[] {
|
||||
for (let i = 0; i < ClipboardParse.block_types.length; i++) {
|
||||
const block_utils = this.editor.getView(
|
||||
ClipboardParse.block_types[i]
|
||||
);
|
||||
const blocks = block_utils?.html2block?.(el, this.parse_dom);
|
||||
|
||||
if (blocks) {
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
const blocks: ClipBlockInfo[] = [];
|
||||
// blocks = DefaultBlockParse.html2block(el);
|
||||
for (let i = 0; i < el.childNodes.length; i++) {
|
||||
const child = el.childNodes[i];
|
||||
const last_block_info =
|
||||
blocks.length === 0 ? null : blocks[blocks.length - 1];
|
||||
let blocks_info = this.parse_dom(child as Element);
|
||||
if (
|
||||
last_block_info &&
|
||||
last_block_info.type === 'text' &&
|
||||
!ClipboardParse.break_flags.has((child as Element).tagName)
|
||||
) {
|
||||
const texts = last_block_info.properties?.text?.value || [];
|
||||
let j = 0;
|
||||
for (; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === 'text') {
|
||||
const block_texts = block.properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
last_block_info.properties = {
|
||||
text: { value: texts },
|
||||
};
|
||||
blocks_info = blocks_info.slice(j);
|
||||
}
|
||||
blocks.push(...blocks_info);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public async generateHtml(): Promise<string> {
|
||||
const select_info = await this.editor.selectionManager.getSelectInfo();
|
||||
return await this.generate_html(select_info.blocks);
|
||||
}
|
||||
|
||||
public async page2html(): Promise<string> {
|
||||
const root_block_id = this.editor.getRootBlockId();
|
||||
if (!root_block_id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const block_info = await this.get_select_info(root_block_id);
|
||||
return await this.generate_html([block_info]);
|
||||
}
|
||||
|
||||
private async get_select_info(blockId: string) {
|
||||
const block = await this.editor.getBlockById(blockId);
|
||||
if (!block) return null;
|
||||
const block_info: SelectBlock = {
|
||||
blockId: block.id,
|
||||
children: [],
|
||||
};
|
||||
const children_ids = block.childrenIds;
|
||||
for (let i = 0; i < children_ids.length; i++) {
|
||||
block_info.children.push(
|
||||
await this.get_select_info(children_ids[i])
|
||||
);
|
||||
}
|
||||
return block_info;
|
||||
}
|
||||
|
||||
private async generate_html(selectBlocks: SelectBlock[]): Promise<string> {
|
||||
let result = '';
|
||||
for (let i = 0; i < selectBlocks.length; i++) {
|
||||
const sel_block = selectBlocks[i];
|
||||
if (!sel_block || !sel_block.blockId) continue;
|
||||
const block = await this.editor.getBlockById(sel_block.blockId);
|
||||
if (!block) continue;
|
||||
const block_utils = this.editor.getView(block.type);
|
||||
const html = await block_utils.block2html(
|
||||
block,
|
||||
sel_block.children,
|
||||
this.generate_html
|
||||
);
|
||||
result += html;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Editor } from '../editor';
|
||||
import { SelectionManager } from '../selection';
|
||||
import { HookType, PluginHooks } from '../types';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Copy } from './copy';
|
||||
class ClipboardPopulator {
|
||||
private _editor: Editor;
|
||||
private _hooks: PluginHooks;
|
||||
private _selectionManager: SelectionManager;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
private _sub = new Subscription();
|
||||
private _copy: Copy;
|
||||
constructor(
|
||||
editor: Editor,
|
||||
hooks: PluginHooks,
|
||||
selectionManager: SelectionManager
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._hooks = hooks;
|
||||
this._selectionManager = selectionManager;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
this._copy = new Copy(editor);
|
||||
this._sub.add(
|
||||
hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy)
|
||||
);
|
||||
this._sub.add(
|
||||
hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy)
|
||||
);
|
||||
}
|
||||
|
||||
disposeInternal() {
|
||||
this._sub.unsubscribe();
|
||||
this._hooks = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { ClipboardPopulator };
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ClipboardEventDispatcher } from './clipboardEventDispatcher';
|
||||
import { HookType } from '../types';
|
||||
import { Editor } from '../editor';
|
||||
import { Copy } from './copy';
|
||||
import { Paste } from './paste';
|
||||
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
|
||||
export class Clipboard {
|
||||
private _clipboardEventDispatcher: ClipboardEventDispatcher;
|
||||
private _copy: Copy;
|
||||
private _paste: Paste;
|
||||
public clipboardUtils: ClipboardUtils;
|
||||
private _clipboardTarget: HTMLElement;
|
||||
|
||||
constructor(editor: Editor, clipboardTarget: HTMLElement) {
|
||||
this.clipboardUtils = new ClipboardUtils(editor);
|
||||
this._clipboardTarget = clipboardTarget;
|
||||
this._copy = new Copy(editor);
|
||||
|
||||
this._paste = new Paste(editor);
|
||||
|
||||
this._clipboardEventDispatcher = new ClipboardEventDispatcher(
|
||||
editor,
|
||||
this._clipboardTarget
|
||||
);
|
||||
|
||||
editor
|
||||
.getHooks()
|
||||
.get(HookType.ON_COPY)
|
||||
.subscribe(this._copy.handleCopy);
|
||||
|
||||
editor.getHooks().get(HookType.ON_CUT).subscribe(this._copy.handleCopy);
|
||||
|
||||
editor
|
||||
.getHooks()
|
||||
.get(HookType.ON_PASTE)
|
||||
.subscribe(this._paste.handlePaste);
|
||||
}
|
||||
|
||||
set clipboardTarget(clipboardTarget: HTMLElement) {
|
||||
this._clipboardTarget = clipboardTarget;
|
||||
this._clipboardEventDispatcher.initialClipboardTargetEvent(
|
||||
this._clipboardTarget
|
||||
);
|
||||
}
|
||||
get clipboardTarget() {
|
||||
return this._clipboardTarget;
|
||||
}
|
||||
|
||||
public clipboardEvent2Blocks(e: ClipboardEvent) {
|
||||
return this._paste.clipboardEvent2Blocks(e);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this._clipboardEventDispatcher.dispose(this.clipboardTarget);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Editor } from '../editor';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
|
||||
enum ClipboardAction {
|
||||
copy = 'copy',
|
||||
cut = 'cut',
|
||||
paste = 'paste',
|
||||
}
|
||||
|
||||
export class ClipboardEventDispatcher {
|
||||
private _editor: Editor;
|
||||
private _utils: ClipboardUtils;
|
||||
|
||||
constructor(editor: Editor, clipboardTarget: HTMLElement) {
|
||||
this._editor = editor;
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
|
||||
this._copyHandler = this._copyHandler.bind(this);
|
||||
this._cutHandler = this._cutHandler.bind(this);
|
||||
this._pasteHandler = this._pasteHandler.bind(this);
|
||||
|
||||
this.initialDocumentEvent();
|
||||
if (clipboardTarget) {
|
||||
this.initialClipboardTargetEvent(clipboardTarget);
|
||||
}
|
||||
}
|
||||
initialDocumentEvent() {
|
||||
this.disposeDocumentEvent();
|
||||
|
||||
document.addEventListener(ClipboardAction.copy, this._copyHandler);
|
||||
document.addEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
document.addEventListener(ClipboardAction.paste, this._pasteHandler);
|
||||
}
|
||||
initialClipboardTargetEvent(clipboardTarget: HTMLElement) {
|
||||
if (!clipboardTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.disposeClipboardTargetEvent(clipboardTarget);
|
||||
|
||||
clipboardTarget.addEventListener(
|
||||
ClipboardAction.copy,
|
||||
this._copyHandler
|
||||
);
|
||||
clipboardTarget.addEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
clipboardTarget.addEventListener(
|
||||
ClipboardAction.paste,
|
||||
this._pasteHandler
|
||||
);
|
||||
}
|
||||
disposeDocumentEvent() {
|
||||
document.removeEventListener(ClipboardAction.copy, this._copyHandler);
|
||||
document.removeEventListener(ClipboardAction.cut, this._cutHandler);
|
||||
document.removeEventListener(ClipboardAction.paste, this._pasteHandler);
|
||||
}
|
||||
disposeClipboardTargetEvent(clipboardTarget: HTMLElement) {
|
||||
if (!clipboardTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.copy,
|
||||
this._copyHandler
|
||||
);
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.cut,
|
||||
this._cutHandler
|
||||
);
|
||||
clipboardTarget.removeEventListener(
|
||||
ClipboardAction.paste,
|
||||
this._pasteHandler
|
||||
);
|
||||
}
|
||||
|
||||
private async _copyHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
this._editor.getHooks().onCopy(e);
|
||||
}
|
||||
|
||||
private async _cutHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
this._editor.getHooks().onCut(e);
|
||||
}
|
||||
private async _pasteHandler(e: ClipboardEvent) {
|
||||
if (!(await this._utils.shouldHandlerContinue(e))) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editor.getHooks().onPaste(e);
|
||||
}
|
||||
|
||||
dispose(clipboardTarget: HTMLElement) {
|
||||
this.disposeDocumentEvent();
|
||||
this.disposeClipboardTargetEvent(clipboardTarget);
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock, SelectInfo } from '../selection';
|
||||
import { AsyncBlock } from '../block';
|
||||
import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types';
|
||||
import { Clip } from './clip';
|
||||
import { commonHTML2Block, commonHTML2Text } from './utils';
|
||||
|
||||
export class ClipboardUtils {
|
||||
private _editor: Editor;
|
||||
constructor(editor: Editor) {
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
async shouldHandlerContinue(event: ClipboardEvent) {
|
||||
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
}
|
||||
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||
return false;
|
||||
}
|
||||
const selectInfo = await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
return (
|
||||
selectInfo.blocks.length &&
|
||||
this._editor.selectionManager.currentSelectInfo.type !== 'None'
|
||||
);
|
||||
}
|
||||
|
||||
async getClipInfoOfBlockById(blockId: string) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block.type,
|
||||
properties: block?.getProperties(),
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
const children = (await block?.children()) ?? [];
|
||||
|
||||
await Promise.all(
|
||||
children.map(async childBlock => {
|
||||
const childInfo = await this.getClipInfoOfBlockById(
|
||||
childBlock.id
|
||||
);
|
||||
blockInfo.children.push(childInfo);
|
||||
})
|
||||
);
|
||||
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
async getClipDataOfBlocksById(blockIds: string[]) {
|
||||
const clipInfos = await Promise.all(
|
||||
blockIds.map(blockId => this.getClipInfoOfBlockById(blockId))
|
||||
);
|
||||
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async getClipInfoOfBlockBySelectInfo(selectBlockInfo: SelectBlock) {
|
||||
const block = await this._editor.getBlockById(selectBlockInfo.blockId);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block?.type,
|
||||
properties:
|
||||
await this._editor.blockHelper.getBlockPropertiesBySelectInfo(
|
||||
selectBlockInfo
|
||||
),
|
||||
// Editable has no children
|
||||
children: [],
|
||||
};
|
||||
return blockInfo;
|
||||
}
|
||||
|
||||
async getClipDataOfBlocksBySelectInfo(selectInfo: SelectInfo) {
|
||||
const clipInfos = await Promise.all(
|
||||
selectInfo.blocks.map(selectBlockInfo =>
|
||||
this.getClipInfoOfBlockBySelectInfo(selectBlockInfo)
|
||||
)
|
||||
);
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async convertTextValue2HtmlBySelectInfo(
|
||||
blockOrBlockId: AsyncBlock | string,
|
||||
selectBlockInfo?: SelectBlock
|
||||
) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const selectedProperties =
|
||||
await this._editor.blockHelper.getEditableBlockPropertiesBySelectInfo(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
return this._editor.blockHelper.convertTextValue2Html(
|
||||
block.id,
|
||||
selectedProperties.text.value
|
||||
);
|
||||
}
|
||||
async convertBlock2HtmlBySelectInfos(
|
||||
blockOrBlockId: AsyncBlock | string,
|
||||
selectBlockInfos?: SelectBlock[]
|
||||
) {
|
||||
if (!selectBlockInfos) {
|
||||
const block =
|
||||
typeof blockOrBlockId === 'string'
|
||||
? await this._editor.getBlockById(blockOrBlockId)
|
||||
: blockOrBlockId;
|
||||
const children = await block?.children();
|
||||
return (
|
||||
await Promise.all(
|
||||
children.map(async childBlock => {
|
||||
const blockView = this._editor.getView(childBlock.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block: childBlock,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
}
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
selectBlockInfos.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block,
|
||||
selectInfo: selectBlockInfo,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
}
|
||||
|
||||
async convertHTMLString2Blocks(html: string): Promise<ClipBlockInfo[]> {
|
||||
const htmlEl = document.createElement('html');
|
||||
htmlEl.innerHTML = html;
|
||||
htmlEl.querySelector('head')?.remove();
|
||||
|
||||
return this.convertHtml2Blocks(htmlEl);
|
||||
}
|
||||
async convertHtml2Blocks(element: Element): Promise<ClipBlockInfo[]> {
|
||||
const editableViews = this._editor.getEditableViews();
|
||||
// 如果block能够捕捉htmlElement则返回block的html2block
|
||||
const [clipBlockInfos] = (
|
||||
await Promise.all(
|
||||
editableViews.map(editableView => {
|
||||
return editableView?.html2block?.({
|
||||
editor: this._editor,
|
||||
element: element,
|
||||
});
|
||||
})
|
||||
)
|
||||
).filter(v => v && v.length);
|
||||
|
||||
if (clipBlockInfos) {
|
||||
return clipBlockInfos;
|
||||
}
|
||||
return (
|
||||
await Promise.all(
|
||||
Array.from(element.children).map(async childElement => {
|
||||
const clipBlockInfos = await this.convertHtml2Blocks(
|
||||
childElement
|
||||
);
|
||||
|
||||
if (clipBlockInfos && clipBlockInfos.length) {
|
||||
return clipBlockInfos;
|
||||
}
|
||||
|
||||
return this.commonHTML2Block(childElement);
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
}
|
||||
|
||||
commonHTML2Block = commonHTML2Block;
|
||||
|
||||
commonHTML2Text = commonHTML2Text;
|
||||
|
||||
textToBlock(clipData = ''): ClipBlockInfo[] {
|
||||
return clipData.split('\n').map((str: string) => {
|
||||
return {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [{ text: str }] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async page2html() {
|
||||
const rootBlockId = this._editor.getRootBlockId();
|
||||
if (!rootBlockId) {
|
||||
return '';
|
||||
}
|
||||
const rootBlock = await this._editor.getBlockById(rootBlockId);
|
||||
const blockView = this._editor.getView(rootBlock.type);
|
||||
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block: rootBlock,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,13 @@ import { Editor } from '../editor';
|
||||
import { SelectInfo } from '../selection';
|
||||
import { OFFICE_CLIPBOARD_MIMETYPE } from './types';
|
||||
import { Clip } from './clip';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { getClipDataOfBlocksById } from './utils';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
class Copy {
|
||||
private _editor: Editor;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
|
||||
private _utils: ClipboardUtils;
|
||||
constructor(editor: Editor) {
|
||||
this._editor = editor;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
this.handleCopy = this.handleCopy.bind(this);
|
||||
}
|
||||
public async handleCopy(e: ClipboardEvent) {
|
||||
@@ -22,7 +18,6 @@ class Copy {
|
||||
if (!clips.length) {
|
||||
return;
|
||||
}
|
||||
// TODO: is not compatible with safari
|
||||
const success = this._copyToClipboardFromPc(clips);
|
||||
if (!success) {
|
||||
// This way, not compatible with firefox
|
||||
@@ -49,24 +44,113 @@ class Copy {
|
||||
const affineClip = await this._getAffineClip();
|
||||
clips.push(affineClip);
|
||||
|
||||
// get common html clip
|
||||
const htmlClip = await this._clipboardParse.generateHtml();
|
||||
htmlClip &&
|
||||
clips.push(new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlClip));
|
||||
const textClip = await this._getTextClip();
|
||||
clips.push(textClip);
|
||||
|
||||
const htmlClip = await this._getHtmlClip();
|
||||
|
||||
clips.push(htmlClip);
|
||||
|
||||
return clips;
|
||||
}
|
||||
|
||||
private async _getHtmlClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
const htmlStr = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
return await blockView.block2html({
|
||||
editor: this._editor,
|
||||
block,
|
||||
selectInfo: selectBlockInfo,
|
||||
});
|
||||
})
|
||||
)
|
||||
).join('');
|
||||
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.HTML, htmlStr);
|
||||
}
|
||||
|
||||
private async _getAffineClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
return getClipDataOfBlocksById(
|
||||
this._editor,
|
||||
if (selectInfo.type === 'Range') {
|
||||
return this._utils.getClipDataOfBlocksBySelectInfo(selectInfo);
|
||||
}
|
||||
|
||||
// The only remaining case is that selectInfo.type === 'Block'
|
||||
return this._utils.getClipDataOfBlocksById(
|
||||
selectInfo.blocks.map(block => block.blockId)
|
||||
);
|
||||
}
|
||||
|
||||
private async _getTextClip(): Promise<Clip> {
|
||||
const selectInfo: SelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
if (selectInfo.type === 'Range') {
|
||||
const text = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(async selectBlockInfo => {
|
||||
const block = await this._editor.getBlockById(
|
||||
selectBlockInfo.blockId
|
||||
);
|
||||
const blockView = this._editor.getView(block.type);
|
||||
const block2Text = await blockView.block2Text(
|
||||
block,
|
||||
selectBlockInfo
|
||||
);
|
||||
|
||||
return (
|
||||
block2Text ||
|
||||
this._editor.blockHelper.getBlockTextBetweenSelection(
|
||||
selectBlockInfo.blockId,
|
||||
false
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
).join('\n');
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, text);
|
||||
}
|
||||
|
||||
// The only remaining case is that selectInfo.type === 'Block'
|
||||
const selectedBlocks = (
|
||||
await Promise.all(
|
||||
selectInfo.blocks.map(selectBlockInfo =>
|
||||
this._editor.blockHelper.getFlatBlocksUnderParent(
|
||||
selectBlockInfo.blockId,
|
||||
true
|
||||
)
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
const blockText = (
|
||||
await Promise.all(
|
||||
selectedBlocks.map(async block => {
|
||||
const blockView = this._editor.getView(block.type);
|
||||
const block2Text = await blockView.block2Text(block);
|
||||
return (
|
||||
block2Text ||
|
||||
this._editor.blockHelper.getBlockText(block.id)
|
||||
);
|
||||
})
|
||||
)
|
||||
).join('\n');
|
||||
|
||||
return new Clip(OFFICE_CLIPBOARD_MIMETYPE.TEXT, blockText);
|
||||
}
|
||||
|
||||
// TODO: Optimization
|
||||
// TODO: is not compatible with safari
|
||||
private _copyToClipboardFromPc(clips: any[]) {
|
||||
let success = false;
|
||||
const tempElem = document.createElement('textarea');
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { Clipboard } from './clipboard';
|
||||
export type { ClipBlockInfo, HTML2BlockResult } from './types';
|
||||
@@ -6,17 +6,11 @@ import {
|
||||
} from './types';
|
||||
import { Editor } from '../editor';
|
||||
import { AsyncBlock } from '../block';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { SelectInfo } from '../selection';
|
||||
import {
|
||||
Protocol,
|
||||
BlockFlavorKeys,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
const SUPPORT_MARKDOWN_PASTE = true;
|
||||
|
||||
import { escape } from 'html-escaper';
|
||||
import { marked } from 'marked';
|
||||
import { ClipboardUtils } from './clipboardUtils';
|
||||
type TextValueItem = {
|
||||
text: string;
|
||||
[key: string]: any;
|
||||
@@ -25,93 +19,89 @@ type TextValueItem = {
|
||||
export class Paste {
|
||||
private _editor: Editor;
|
||||
private _markdownParse: MarkdownParser;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
|
||||
constructor(
|
||||
editor: Editor,
|
||||
clipboardParse: ClipboardParse,
|
||||
markdownParse: MarkdownParser
|
||||
) {
|
||||
this._markdownParse = markdownParse;
|
||||
this._clipboardParse = clipboardParse;
|
||||
private _utils: ClipboardUtils;
|
||||
constructor(editor: Editor) {
|
||||
this._markdownParse = new MarkdownParser();
|
||||
this._editor = editor;
|
||||
|
||||
this._utils = new ClipboardUtils(editor);
|
||||
this.handlePaste = this.handlePaste.bind(this);
|
||||
}
|
||||
private static _optimalMimeType: string[] = [
|
||||
// The event handler will get the most needed clipboard data based on this array order
|
||||
private static _optimalMimeTypes: string[] = [
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.HTML,
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT,
|
||||
];
|
||||
public handlePaste(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
public async handlePaste(e: ClipboardEvent) {
|
||||
e.stopPropagation();
|
||||
|
||||
const clipboardData = (e as ClipboardEvent).clipboardData;
|
||||
|
||||
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||
if (isPureFile) {
|
||||
this._pasteFile(clipboardData);
|
||||
} else {
|
||||
this._pasteContent(clipboardData);
|
||||
}
|
||||
const blocks = await this.clipboardEvent2Blocks(e);
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
public getOptimalClip(clipboardData: any) {
|
||||
const mimeTypeArr = Paste._optimalMimeType;
|
||||
|
||||
for (let i = 0; i < mimeTypeArr.length; i++) {
|
||||
const data =
|
||||
clipboardData[mimeTypeArr[i]] ||
|
||||
clipboardData.getData(mimeTypeArr[i]);
|
||||
public async clipboardEvent2Blocks(e: ClipboardEvent) {
|
||||
const clipboardData = e.clipboardData;
|
||||
const isPureFile = Paste._isPureFileInClipboard(clipboardData);
|
||||
|
||||
if (isPureFile) {
|
||||
return this._file2Blocks(clipboardData);
|
||||
}
|
||||
return this._clipboardData2Blocks(clipboardData);
|
||||
}
|
||||
// Get the most needed clipboard data based on `_optimalMimeTypes` order
|
||||
public getOptimalClip(clipboardData: ClipboardEvent['clipboardData']) {
|
||||
for (let i = 0; i < Paste._optimalMimeTypes.length; i++) {
|
||||
const mimeType = Paste._optimalMimeTypes[i];
|
||||
const data = clipboardData.getData(mimeType);
|
||||
|
||||
if (data) {
|
||||
return {
|
||||
type: mimeTypeArr[i],
|
||||
type: mimeType,
|
||||
data: data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
private _pasteContent(clipboardData: any) {
|
||||
const originClip: { data: any; type: any } = this.getOptimalClip(
|
||||
clipboardData
|
||||
) as { data: any; type: any };
|
||||
private async _clipboardData2Blocks(
|
||||
clipboardData: ClipboardEvent['clipboardData']
|
||||
): Promise<ClipBlockInfo[]> {
|
||||
const optimalClip = this.getOptimalClip(clipboardData);
|
||||
if (
|
||||
optimalClip?.type ===
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED
|
||||
) {
|
||||
const clipInfo: InnerClipInfo = JSON.parse(optimalClip.data);
|
||||
return clipInfo.data;
|
||||
}
|
||||
|
||||
const originTextClipData = clipboardData.getData(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.TEXT
|
||||
const textClipData = escape(
|
||||
clipboardData.getData(OFFICE_CLIPBOARD_MIMETYPE.TEXT)
|
||||
);
|
||||
|
||||
let clipData = originClip['data'];
|
||||
const shouldConvertMarkdown =
|
||||
this._markdownParse.checkIfTextContainsMd(textClipData);
|
||||
|
||||
if (originClip['type'] === OFFICE_CLIPBOARD_MIMETYPE.TEXT) {
|
||||
clipData = Paste._excapeHtml(clipData);
|
||||
if (
|
||||
optimalClip?.type === OFFICE_CLIPBOARD_MIMETYPE.HTML &&
|
||||
!shouldConvertMarkdown
|
||||
) {
|
||||
return this._utils.convertHTMLString2Blocks(optimalClip.data);
|
||||
}
|
||||
|
||||
switch (originClip['type']) {
|
||||
/** Protocol paste */
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED:
|
||||
this._firePasteEditAction(clipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.HTML:
|
||||
this._pasteHtml(clipData, originTextClipData);
|
||||
break;
|
||||
case OFFICE_CLIPBOARD_MIMETYPE.TEXT:
|
||||
this._pasteText(clipData, originTextClipData);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
if (shouldConvertMarkdown) {
|
||||
const md2html = marked.parse(textClipData);
|
||||
return this._utils.convertHTMLString2Blocks(md2html);
|
||||
}
|
||||
|
||||
return this._utils.textToBlock(textClipData);
|
||||
}
|
||||
private async _firePasteEditAction(clipboardData: any) {
|
||||
const clipInfo: InnerClipInfo = JSON.parse(clipboardData);
|
||||
clipInfo && this._insertBlocks(clipInfo.data, clipInfo.select);
|
||||
}
|
||||
private async _pasteFile(clipboardData: any) {
|
||||
|
||||
private async _file2Blocks(clipboardData: any): Promise<ClipBlockInfo[]> {
|
||||
const file = Paste._getImageFile(clipboardData);
|
||||
if (file) {
|
||||
const result = await services.api.file.create({
|
||||
@@ -130,8 +120,9 @@ export class Paste {
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
await this._insertBlocks([blockInfo]);
|
||||
return [blockInfo];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
private static _isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
@@ -144,32 +135,14 @@ export class Paste {
|
||||
);
|
||||
}
|
||||
|
||||
private static _isTextEditBlock(type: BlockFlavorKeys) {
|
||||
return (
|
||||
type === Protocol.Block.Type.page ||
|
||||
type === Protocol.Block.Type.text ||
|
||||
type === Protocol.Block.Type.heading1 ||
|
||||
type === Protocol.Block.Type.heading2 ||
|
||||
type === Protocol.Block.Type.heading3 ||
|
||||
type === Protocol.Block.Type.quote ||
|
||||
type === Protocol.Block.Type.todo ||
|
||||
type === Protocol.Block.Type.code ||
|
||||
type === Protocol.Block.Type.callout ||
|
||||
type === Protocol.Block.Type.numbered ||
|
||||
type === Protocol.Block.Type.bullet
|
||||
);
|
||||
}
|
||||
|
||||
private async _insertBlocks(
|
||||
blocks: ClipBlockInfo[],
|
||||
pasteSelect?: SelectInfo
|
||||
) {
|
||||
private async _insertBlocks(blocks: ClipBlockInfo[]) {
|
||||
if (blocks.length === 0) {
|
||||
return;
|
||||
}
|
||||
const currentSelectInfo =
|
||||
await this._editor.selectionManager.getSelectInfo();
|
||||
|
||||
// TODO: Logic of insert blocks maybe should declare in blockHelper
|
||||
// When the selection is in one of the blocks, select?.type === 'Range'
|
||||
// Currently the selection does not support cross-blocking, so this case is not considered
|
||||
if (currentSelectInfo.type === 'Range') {
|
||||
@@ -177,15 +150,16 @@ export class Paste {
|
||||
const selectedBlock = await this._editor.getBlockById(
|
||||
currentSelectInfo.blocks[0].blockId
|
||||
);
|
||||
const isSelectedBlockCanEdit = Paste._isTextEditBlock(
|
||||
selectedBlock.type
|
||||
const isSelectedBlockCanEdit = this._editor.isEditableView(
|
||||
selectedBlock?.type
|
||||
);
|
||||
|
||||
const blockView = this._editor.getView(selectedBlock.type);
|
||||
const isSelectedBlockEmpty = blockView.isEmpty(selectedBlock);
|
||||
if (isSelectedBlockCanEdit && !isSelectedBlockEmpty) {
|
||||
const shouldSplitBlock =
|
||||
blocks.length > 1 ||
|
||||
!Paste._isTextEditBlock(blocks[0].type);
|
||||
!this._editor.isEditableView(blocks[0].type);
|
||||
const pureText = !shouldSplitBlock
|
||||
? blocks[0].properties.text.value
|
||||
: [{ text: '' }];
|
||||
@@ -405,7 +379,7 @@ export class Paste {
|
||||
}
|
||||
private async _setEndSelectToBlock(blockId: string) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
const isBlockCanEdit = Paste._isTextEditBlock(block.type);
|
||||
const isBlockCanEdit = this._editor.isEditableView(block.type);
|
||||
if (!isBlockCanEdit) {
|
||||
return;
|
||||
}
|
||||
@@ -450,34 +424,6 @@ export class Paste {
|
||||
);
|
||||
}
|
||||
|
||||
private async _pasteHtml(clipData: any, originTextClipData: any) {
|
||||
if (SUPPORT_MARKDOWN_PASTE) {
|
||||
const hasMarkdown =
|
||||
this._markdownParse.checkIfTextContainsMd(originTextClipData);
|
||||
if (hasMarkdown) {
|
||||
try {
|
||||
const convertedDataObj =
|
||||
this._markdownParse.md2Html(originTextClipData);
|
||||
if (convertedDataObj.isConverted) {
|
||||
clipData = convertedDataObj.text;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
clipData = originTextClipData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = this._clipboardParse.html2blocks(clipData);
|
||||
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
|
||||
private async _pasteText(clipData: any, originTextClipData: any) {
|
||||
const blocks = this._clipboardParse.text2blocks(clipData);
|
||||
await this._insertBlocks(blocks);
|
||||
}
|
||||
|
||||
private static _getImageFile(clipboardData: any) {
|
||||
const files = clipboardData.files;
|
||||
if (files && files[0] && files[0].type.indexOf('image') > -1) {
|
||||
@@ -485,16 +431,4 @@ export class Paste {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private static _excapeHtml(data: any, onlySpace?: any) {
|
||||
if (!onlySpace) {
|
||||
// TODO:
|
||||
// data = string.htmlEscape(data);
|
||||
// data = data.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
// data = data.replace(/ /g, ' ');
|
||||
// data = data.replace(/\t/g, ' ');
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import {
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { SelectInfo } from '../selection';
|
||||
|
||||
// export type ClipBlockInfo = Partial<ReturnEditorBlock> & {
|
||||
// children?: ClipBlockInfo[];
|
||||
// };
|
||||
export const OFFICE_CLIPBOARD_MIMETYPE = {
|
||||
DOCS_DOCUMENT_SLICE_CLIP_WRAPPED: 'affine/x-c+w',
|
||||
HTML: 'text/html',
|
||||
@@ -20,8 +23,9 @@ export const OFFICE_CLIPBOARD_MIMETYPE = {
|
||||
export interface ClipBlockInfo {
|
||||
type: BlockFlavorKeys;
|
||||
properties?: Partial<DefaultColumnsValue>;
|
||||
children: ClipBlockInfo[];
|
||||
children?: ClipBlockInfo[];
|
||||
}
|
||||
export type HTML2BlockResult = ClipBlockInfo[] | null;
|
||||
|
||||
export interface InnerClipInfo {
|
||||
select: SelectInfo;
|
||||
|
||||
@@ -1,52 +1,150 @@
|
||||
import { Editor } from '../editor';
|
||||
import { ClipBlockInfo, OFFICE_CLIPBOARD_MIMETYPE } from './types';
|
||||
import { Clip } from './clip';
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
import { BlockFlavorKeys, Protocol } from '@toeverything/datasource/db-service';
|
||||
import { ClipBlockInfo } from './types';
|
||||
|
||||
export const shouldHandlerContinue = (event: Event, editor: Editor) => {
|
||||
const filterNodes = ['INPUT', 'SELECT', 'TEXTAREA'];
|
||||
const getIsLink = (htmlElement: HTMLElement) => {
|
||||
return ['A', 'IMG'].includes(htmlElement.tagName);
|
||||
};
|
||||
const getTextStyle = (htmlElement: HTMLElement) => {
|
||||
const tagName = htmlElement.tagName;
|
||||
const textStyle: { [key: string]: any } = {};
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
return false;
|
||||
const style = (htmlElement.getAttribute('style') || '')
|
||||
.split(';')
|
||||
.reduce((style: { [key: string]: any }, styleString) => {
|
||||
const [key, value] = styleString.split(':');
|
||||
if (key && value) {
|
||||
style[key] = value;
|
||||
}
|
||||
return style;
|
||||
}, {});
|
||||
|
||||
if (
|
||||
style['font-weight'] === 'bold' ||
|
||||
Number(style['font-weight']) > 400 ||
|
||||
['STRONG', 'B', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(
|
||||
htmlElement.tagName
|
||||
)
|
||||
) {
|
||||
textStyle['bold'] = true;
|
||||
}
|
||||
if (filterNodes.includes((event.target as HTMLElement)?.tagName)) {
|
||||
return false;
|
||||
if (getIsLink(htmlElement)) {
|
||||
textStyle['type'] = 'link';
|
||||
textStyle['url'] =
|
||||
htmlElement.getAttribute('href') || htmlElement.getAttribute('src');
|
||||
textStyle['id'] = getRandomString('link');
|
||||
}
|
||||
|
||||
return editor.selectionManager.currentSelectInfo.type !== 'None';
|
||||
if (tagName === 'EM' || style['fontStyle'] === 'italic') {
|
||||
textStyle['italic'] = true;
|
||||
}
|
||||
if (
|
||||
tagName === 'U' ||
|
||||
(style['text-decoration'] &&
|
||||
style['text-decoration'].indexOf('underline') !== -1) ||
|
||||
style['border-bottom']
|
||||
) {
|
||||
textStyle['underline'] = true;
|
||||
}
|
||||
if (tagName === 'CODE') {
|
||||
textStyle['inlinecode'] = true;
|
||||
}
|
||||
if (
|
||||
tagName === 'S' ||
|
||||
tagName === 'DEL' ||
|
||||
(style['text-decoration'] &&
|
||||
style['text-decoration'].indexOf('line-through') !== -1)
|
||||
) {
|
||||
textStyle['strikethrough'] = true;
|
||||
}
|
||||
|
||||
return textStyle;
|
||||
};
|
||||
|
||||
export const getClipInfoOfBlockById = async (
|
||||
editor: Editor,
|
||||
blockId: string
|
||||
) => {
|
||||
const block = await editor.getBlockById(blockId);
|
||||
const blockView = editor.getView(block.type);
|
||||
const blockInfo: ClipBlockInfo = {
|
||||
type: block.type,
|
||||
properties: blockView.getSelProperties(block, {}),
|
||||
children: [] as ClipBlockInfo[],
|
||||
export const commonHTML2Block = (
|
||||
element: HTMLElement | Node,
|
||||
type: BlockFlavorKeys = Protocol.Block.Type.text,
|
||||
ignoreEmptyElement = true
|
||||
): ClipBlockInfo => {
|
||||
const textValue = commonHTML2Text(element, {}, ignoreEmptyElement);
|
||||
if (!textValue.length && ignoreEmptyElement) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type,
|
||||
properties: {
|
||||
text: { value: textValue },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
const children = (await block?.children()) ?? [];
|
||||
};
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childInfo = await getClipInfoOfBlockById(editor, children[i].id);
|
||||
blockInfo.children.push(childInfo);
|
||||
const getSingleLabelHTMLElementContent = (htmlElement: HTMLElement) => {
|
||||
if (htmlElement.tagName === 'IMG') {
|
||||
return (
|
||||
htmlElement.getAttribute('alt') || htmlElement.getAttribute('src')
|
||||
);
|
||||
}
|
||||
return blockInfo;
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getClipDataOfBlocksById = async (
|
||||
editor: Editor,
|
||||
blockIds: string[]
|
||||
export const commonHTML2Text = (
|
||||
element: HTMLElement | Node,
|
||||
textStyle: { [key: string]: any } = {},
|
||||
ignoreEmptyText = true
|
||||
) => {
|
||||
const clipInfos = await Promise.all(
|
||||
blockIds.map(blockId => getClipInfoOfBlockById(editor, blockId))
|
||||
);
|
||||
if (element instanceof Text) {
|
||||
return element.textContent.split('\n').map(text => {
|
||||
return { text: text, ...textStyle };
|
||||
});
|
||||
}
|
||||
const htmlElement = element as HTMLElement;
|
||||
const childNodes = Array.from(htmlElement.childNodes);
|
||||
|
||||
return new Clip(
|
||||
OFFICE_CLIPBOARD_MIMETYPE.DOCS_DOCUMENT_SLICE_CLIP_WRAPPED,
|
||||
JSON.stringify({
|
||||
data: clipInfos,
|
||||
})
|
||||
);
|
||||
const isLink = getIsLink(htmlElement);
|
||||
const currentTextStyle = getTextStyle(htmlElement);
|
||||
|
||||
if (!childNodes.length) {
|
||||
const singleLabelContent =
|
||||
getSingleLabelHTMLElementContent(htmlElement);
|
||||
if (isLink && singleLabelContent) {
|
||||
return [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
text: singleLabelContent,
|
||||
},
|
||||
],
|
||||
...currentTextStyle,
|
||||
},
|
||||
];
|
||||
}
|
||||
return ignoreEmptyText ? [] : [{ text: '', ...currentTextStyle }];
|
||||
}
|
||||
|
||||
const childTexts = childNodes
|
||||
.reduce((result, childNode) => {
|
||||
const textBlocks = commonHTML2Text(
|
||||
childNode,
|
||||
isLink
|
||||
? textStyle
|
||||
: {
|
||||
...textStyle,
|
||||
...currentTextStyle,
|
||||
},
|
||||
ignoreEmptyText
|
||||
);
|
||||
result.push(...textBlocks);
|
||||
return result;
|
||||
}, [])
|
||||
.filter(v => v);
|
||||
|
||||
if (isLink && childTexts.length) {
|
||||
return [
|
||||
{
|
||||
children: childTexts,
|
||||
...currentTextStyle,
|
||||
},
|
||||
];
|
||||
}
|
||||
return childTexts;
|
||||
};
|
||||
|
||||
@@ -13,8 +13,7 @@ import HotKeys from 'hotkeys-js';
|
||||
import type { WorkspaceAndBlockId } from './block';
|
||||
import { AsyncBlock } from './block';
|
||||
import { BlockHelper } from './block/block-helper';
|
||||
import { BrowserClipboard } from './clipboard/browser-clipboard';
|
||||
import { ClipboardPopulator } from './clipboard/clipboard-populator';
|
||||
import { Clipboard } from './clipboard';
|
||||
import { EditorCommands } from './commands';
|
||||
import { EditorConfig } from './config';
|
||||
import { DragDropManager } from './drag-drop';
|
||||
@@ -46,7 +45,7 @@ export class Editor implements Virgo {
|
||||
private _cacheManager = new Map<string, Promise<AsyncBlock | null>>();
|
||||
public mouseManager = new MouseManager(this);
|
||||
public selectionManager = new SelectionManager(this);
|
||||
public keyboardManager = new KeyboardManager(this);
|
||||
public keyboardManager: KeyboardManager;
|
||||
public scrollManager = new ScrollManager(this);
|
||||
public dragDropManager = new DragDropManager(this);
|
||||
public commands = new EditorCommands(this);
|
||||
@@ -63,8 +62,9 @@ export class Editor implements Virgo {
|
||||
readonly = false;
|
||||
private _rootBlockId: string;
|
||||
private storage_manager?: StorageManager;
|
||||
private clipboard?: BrowserClipboard;
|
||||
private clipboard_populator?: ClipboardPopulator;
|
||||
private _clipboard: Clipboard;
|
||||
// private clipboardActionDispacher?: ClipboardEventDispatcher;
|
||||
// private clipboard_populator?: ClipboardPopulator;
|
||||
public reactRenderRoot: {
|
||||
render: PatchNode;
|
||||
has: (key: string) => boolean;
|
||||
@@ -78,10 +78,13 @@ export class Editor implements Virgo {
|
||||
this._rootBlockId = props.rootBlockId;
|
||||
this.hooks = new Hooks();
|
||||
this.plugin_manager = new PluginManager(this, this.hooks);
|
||||
this._clipboard = new Clipboard(this, this.ui_container);
|
||||
this.plugin_manager.registerAll(props.plugins);
|
||||
if (props.isEdgeless) {
|
||||
this.isEdgeless = true;
|
||||
}
|
||||
// Wait for rootId/isEdgeless set
|
||||
this.keyboardManager = new KeyboardManager(this);
|
||||
for (const [name, block] of Object.entries(props.views)) {
|
||||
services.api.editorBlock.registerContentExporter(
|
||||
this.workspace,
|
||||
@@ -136,13 +139,17 @@ export class Editor implements Virgo {
|
||||
|
||||
public set container(v: HTMLDivElement) {
|
||||
this.ui_container = v;
|
||||
this._initClipboard();
|
||||
this._clipboard.clipboardTarget = v;
|
||||
}
|
||||
|
||||
public get container() {
|
||||
return this.ui_container;
|
||||
}
|
||||
|
||||
public get clipboard() {
|
||||
return this._clipboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use it discreetly.
|
||||
* Preference to use {@link withBatch}
|
||||
@@ -189,26 +196,26 @@ export class Editor implements Virgo {
|
||||
};
|
||||
}
|
||||
|
||||
private _disposeClipboard() {
|
||||
this.clipboard?.dispose();
|
||||
this.clipboard_populator?.disposeInternal();
|
||||
}
|
||||
// private _disposeClipboard() {
|
||||
// this.clipboard?.dispose();
|
||||
// this.clipboard_populator?.disposeInternal();
|
||||
// }
|
||||
|
||||
private _initClipboard() {
|
||||
this._disposeClipboard();
|
||||
if (this.ui_container && !this._isDisposed) {
|
||||
this.clipboard = new BrowserClipboard(
|
||||
this.ui_container,
|
||||
this.hooks,
|
||||
this
|
||||
);
|
||||
this.clipboard_populator = new ClipboardPopulator(
|
||||
this,
|
||||
this.hooks,
|
||||
this.selectionManager
|
||||
);
|
||||
}
|
||||
}
|
||||
// private _initClipboard() {
|
||||
// this._disposeClipboard();
|
||||
// if (this.ui_container && !this._isDisposed) {
|
||||
// this.clipboardActionDispacher = new ClipboardEventDispatcher({
|
||||
// clipboardTarget: this.ui_container,
|
||||
// hooks: this.hooks,
|
||||
// editor: this,
|
||||
// });
|
||||
// this.clipboard_populator = new ClipboardPopulator(
|
||||
// this,
|
||||
// this.hooks,
|
||||
// this.selectionManager
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
/** Root Block Id */
|
||||
getRootBlockId() {
|
||||
@@ -249,6 +256,14 @@ export class Editor implements Virgo {
|
||||
getView(type: string) {
|
||||
return this.views[type];
|
||||
}
|
||||
getEditableViews() {
|
||||
return Object.values(this.views)
|
||||
.map(view => (view.editable ? view : null))
|
||||
.filter(v => v);
|
||||
}
|
||||
isEditableView(type: string) {
|
||||
return this.views[type].editable;
|
||||
}
|
||||
|
||||
private async _initBlock(
|
||||
blockData: ReturnEditorBlock
|
||||
@@ -333,6 +348,12 @@ export class Editor implements Virgo {
|
||||
return await this.getBlock({ workspace: this.workspace, id: blockId });
|
||||
}
|
||||
|
||||
async getBlockByIds(ids: string[]): Promise<Awaited<AsyncBlock | null>[]> {
|
||||
return await Promise.all(
|
||||
ids.map(id => this.getBlock({ workspace: this.workspace, id }))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: to be optimized
|
||||
* get block`s dom by block`s id
|
||||
@@ -477,6 +498,13 @@ export class Editor implements Virgo {
|
||||
return await services.api.editorBlock.query(this.workspace, query);
|
||||
}
|
||||
|
||||
async queryByPageId(pageId: string) {
|
||||
return await services.api.editorBlock.get({
|
||||
workspace: this.workspace,
|
||||
ids: [pageId],
|
||||
});
|
||||
}
|
||||
|
||||
/** Hooks */
|
||||
|
||||
public getHooks(): HooksRunner & PluginHooks {
|
||||
@@ -496,12 +524,7 @@ export class Editor implements Virgo {
|
||||
}
|
||||
|
||||
public async page2html(): Promise<string> {
|
||||
const parse = this.clipboard?.getClipboardParse();
|
||||
if (!parse) {
|
||||
return '';
|
||||
}
|
||||
const html_str = await parse.page2html();
|
||||
return html_str;
|
||||
return this.clipboard?.clipboardUtils?.page2html?.();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -517,6 +540,6 @@ export class Editor implements Virgo {
|
||||
this.plugin_manager.dispose();
|
||||
this.selectionManager.dispose();
|
||||
this.dragDropManager.dispose();
|
||||
this._disposeClipboard();
|
||||
this._clipboard?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import ClipboardParseInner from './clipboard/clipboard-parse';
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ClipboardParse = ClipboardParseInner;
|
||||
|
||||
export { AsyncBlock } from './block';
|
||||
export * from './commands/types';
|
||||
export { Editor as BlockEditor } from './editor';
|
||||
export * from './selection';
|
||||
export { BlockDropPlacement, HookType, GroupDirection } from './types';
|
||||
export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
|
||||
export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
|
||||
export { BaseView } from './views/base-view';
|
||||
export type { ChildrenView, CreateView } from './views/base-view';
|
||||
export { getClipDataOfBlocksById } from './clipboard/utils';
|
||||
export type { HTML2BlockResult, ClipBlockInfo } from './clipboard';
|
||||
|
||||
@@ -2,19 +2,19 @@ import HotKeys from 'hotkeys-js';
|
||||
|
||||
import { uaHelper } from '@toeverything/utils';
|
||||
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BlockEditor } from '../..';
|
||||
import { supportChildren } from '../../utils';
|
||||
import { SelectionManager } from '../selection';
|
||||
import {
|
||||
HotKeyTypes,
|
||||
HotkeyMap,
|
||||
MacHotkeyMap,
|
||||
WinHotkeyMap,
|
||||
GlobalHotkeyMap,
|
||||
GlobalMacHotkeyMap,
|
||||
GlobalWinHotkeyMap,
|
||||
HotkeyMap,
|
||||
HotKeyTypes,
|
||||
MacHotkeyMap,
|
||||
WinHotkeyMap,
|
||||
} from './hotkey-map';
|
||||
import { supportChildren } from '../../utils';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
type KeyboardEventHandler = (event: KeyboardEvent) => void;
|
||||
export class KeyboardManager {
|
||||
private _editor: BlockEditor;
|
||||
@@ -22,8 +22,20 @@ export class KeyboardManager {
|
||||
private hotkeys: HotkeyMap;
|
||||
private global_hotkeys: GlobalHotkeyMap;
|
||||
private handler_map: { [k: string]: Array<KeyboardEventHandler> };
|
||||
/**
|
||||
* Every editor should have its own hotkey scope,
|
||||
* but edgeless had multiple editor instances,
|
||||
* all edgeless editors should have shared scope
|
||||
*/
|
||||
private hotkeyScope: string;
|
||||
|
||||
constructor(editor: BlockEditor) {
|
||||
if (editor.isEdgeless) {
|
||||
// edgeless editors should have shared scope
|
||||
this.hotkeyScope = 'whiteboard';
|
||||
} else {
|
||||
this.hotkeyScope = 'editor_' + editor.getRootBlockId();
|
||||
}
|
||||
this._editor = editor;
|
||||
this.selection_manager = this._editor.selectionManager;
|
||||
if (uaHelper.isMacOs) {
|
||||
@@ -35,7 +47,7 @@ export class KeyboardManager {
|
||||
}
|
||||
this.handler_map = {};
|
||||
|
||||
HotKeys.setScope('editor');
|
||||
HotKeys.setScope(this.hotkeyScope);
|
||||
|
||||
// this.init_common_shortcut_cb();
|
||||
this.bind_hot_key_handlers();
|
||||
@@ -44,43 +56,63 @@ export class KeyboardManager {
|
||||
private bind_hot_key_handlers() {
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.selectAll,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_select_all
|
||||
);
|
||||
|
||||
this.bind_hotkey(this.hotkeys.undo, 'editor', this.handle_undo);
|
||||
this.bind_hotkey(this.hotkeys.redo, 'editor', this.handle_redo);
|
||||
this.bind_hotkey(this.hotkeys.remove, 'editor', this.handle_remove);
|
||||
this.bind_hotkey(this.hotkeys.undo, this.hotkeyScope, this.handle_undo);
|
||||
this.bind_hotkey(this.hotkeys.redo, this.hotkeyScope, this.handle_redo);
|
||||
this.bind_hotkey(this.hotkeys.remove, 'all', this.handle_remove);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.checkUncheck,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_check_uncheck
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.preExpendSelect,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_pre_expend_select
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.nextExpendSelect,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.handle_next_expend_select
|
||||
);
|
||||
this.bind_hotkey(this.hotkeys.up, 'editor', this.handle_click_up);
|
||||
this.bind_hotkey(this.hotkeys.down, 'editor', this.handleClickDown);
|
||||
this.bind_hotkey(this.hotkeys.left, 'editor', this.handle_click_up);
|
||||
this.bind_hotkey(this.hotkeys.right, 'editor', this.handleClickDown);
|
||||
this.bind_hotkey(this.hotkeys.mergeGroup, 'editor', this.mergeGroup);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.up,
|
||||
this.hotkeyScope,
|
||||
this.handle_click_up
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.down,
|
||||
this.hotkeyScope,
|
||||
this.handleClickDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.left,
|
||||
this.hotkeyScope,
|
||||
this.handle_click_up
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.right,
|
||||
this.hotkeyScope,
|
||||
this.handleClickDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroup,
|
||||
this.hotkeyScope,
|
||||
this.mergeGroup
|
||||
);
|
||||
this.bind_hotkey(this.hotkeys.enter, 'all', this.handleEnter);
|
||||
this.bind_hotkey(this.global_hotkeys.search, 'all', this.handle_search);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroupDown,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.mergeGroupDown
|
||||
);
|
||||
this.bind_hotkey(
|
||||
this.hotkeys.mergeGroupUp,
|
||||
'editor',
|
||||
this.hotkeyScope,
|
||||
this.mergeGroupUp
|
||||
);
|
||||
}
|
||||
@@ -93,24 +125,12 @@ export class KeyboardManager {
|
||||
handler.forEach(h => {
|
||||
HotKeys(key, scope, h);
|
||||
if (!this.handler_map[key]) {
|
||||
this.handler_map[key] = [h];
|
||||
} else {
|
||||
this.handler_map[key].push(h);
|
||||
this.handler_map[key] = [];
|
||||
}
|
||||
this.handler_map[key].push(h);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* bind global shortcut event
|
||||
* @param {HotkeyMapKeys} type
|
||||
* @param {KeyboardEventHandler} handler
|
||||
* @memberof KeyboardManager
|
||||
*/
|
||||
public bind(type: HotKeyTypes, handler: KeyboardEventHandler) {
|
||||
this.bind_hotkey(this.hotkeys[type], 'editor', handler);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* emit a shortcut event,
|
||||
@@ -127,21 +147,12 @@ export class KeyboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* unbind keyboard event
|
||||
* @param {HotKeyTypes} key
|
||||
* @param {KeyboardEventHandler} cb
|
||||
* @memberof KeyboardManager
|
||||
*/
|
||||
public unbind(key: HotKeyTypes, cb: KeyboardEventHandler) {
|
||||
HotKeys.unbind(key, 'editor', cb);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
Object.keys(this.handler_map).map(key => HotKeys.unbind(key, 'editor'));
|
||||
|
||||
Object.entries(this.handler_map).forEach(([key, fns]) =>
|
||||
fns.forEach(fn => HotKeys.unbind(key, fn))
|
||||
);
|
||||
this.handler_map = {};
|
||||
HotKeys.deleteScope(this.hotkeyScope);
|
||||
}
|
||||
|
||||
private handle_select_all = (event: KeyboardEvent) => {
|
||||
@@ -228,20 +239,20 @@ export class KeyboardManager {
|
||||
);
|
||||
} else {
|
||||
// suspend(true)
|
||||
let textBlock = await this._editor.createBlock('text');
|
||||
const textBlock = await this._editor.createBlock('text');
|
||||
await selectedNode.after(textBlock);
|
||||
this._editor.selectionManager.setActivatedNodeId(textBlock.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
private mergeGroup = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
this._editor.commands.blockCommands.mergeGroup(...selectedGroup);
|
||||
};
|
||||
private mergeGroupDown = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
if (selectedGroup.length) {
|
||||
let nextGroup = await selectedGroup[
|
||||
const nextGroup = await selectedGroup[
|
||||
selectedGroup.length - 1
|
||||
].nextSibling();
|
||||
if (nextGroup?.type === Protocol.Block.Type.group) {
|
||||
@@ -253,9 +264,9 @@ export class KeyboardManager {
|
||||
}
|
||||
};
|
||||
private mergeGroupUp = async (event: Event) => {
|
||||
let selectedGroup = await this.getSelectedGroups();
|
||||
const selectedGroup = await this.getSelectedGroups();
|
||||
if (selectedGroup.length) {
|
||||
let preGroup = await selectedGroup[0].previousSibling();
|
||||
const preGroup = await selectedGroup[0].previousSibling();
|
||||
if (preGroup?.type === Protocol.Block.Type.group) {
|
||||
this._editor.commands.blockCommands.mergeGroup(
|
||||
preGroup,
|
||||
|
||||
@@ -116,12 +116,15 @@ export class Hooks implements HooksRunner, PluginHooks {
|
||||
this._runHook(HookType.ON_SEARCH);
|
||||
}
|
||||
|
||||
public beforeCopy(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.BEFORE_COPY, e);
|
||||
public onCopy(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_COPY, e);
|
||||
}
|
||||
|
||||
public beforeCut(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.BEFORE_CUT, e);
|
||||
public onCut(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_CUT, e);
|
||||
}
|
||||
public onPaste(e: ClipboardEvent): void {
|
||||
this._runHook(HookType.ON_PASTE, e);
|
||||
}
|
||||
|
||||
public onRootNodeScroll(e: React.UIEvent): void {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CSSProperties, type UIEvent } from 'react';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { type UIEvent } from 'react';
|
||||
|
||||
import { domToRect, Rect } from '@toeverything/utils';
|
||||
import type { Editor as BlockEditor } from '../editor';
|
||||
@@ -164,22 +164,9 @@ export class ScrollManager {
|
||||
if (!block.dom) {
|
||||
return console.warn(`Block is not exist.`);
|
||||
}
|
||||
const containerRect = domToRect(this._scrollContainer);
|
||||
const blockRect = domToRect(block.dom);
|
||||
|
||||
const blockRelativeTopToEditor =
|
||||
blockRect.top - containerRect.top - containerRect.height / 4;
|
||||
const blockRelativeLeftToEditor = blockRect.left - containerRect.left;
|
||||
|
||||
this.scrollTo({
|
||||
left: blockRelativeLeftToEditor,
|
||||
top: blockRelativeTopToEditor,
|
||||
behavior,
|
||||
});
|
||||
this._updateScrollInfo(
|
||||
blockRelativeLeftToEditor,
|
||||
blockRelativeTopToEditor
|
||||
);
|
||||
/* use dom primary ability */
|
||||
block.dom.scrollIntoView({ block: 'start', behavior });
|
||||
}
|
||||
|
||||
public async keepBlockInView(
|
||||
@@ -209,11 +196,11 @@ export class ScrollManager {
|
||||
private _getKeepInViewParams(blockRect: Rect) {
|
||||
if (this.scrollContainer == null) return 0;
|
||||
const { top, bottom } = domToRect(this._scrollContainer);
|
||||
if (blockRect.top <= top + blockRect.height * 3) {
|
||||
if (blockRect.top <= top + blockRect.height) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (blockRect.bottom >= bottom - blockRect.height * 3) {
|
||||
if (blockRect.bottom >= bottom - blockRect.height) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -408,13 +408,13 @@ export class SelectionManager implements VirgoSelection {
|
||||
|
||||
/**
|
||||
*
|
||||
* get previous activatable blockNode
|
||||
* get previous editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getPreviousActivatableBlockNode(
|
||||
private async _getPreviousEditableBlockNode(
|
||||
node: AsyncBlock
|
||||
): Promise<AsyncBlock | null> {
|
||||
const preNode = await node.physicallyPerviousSibling();
|
||||
@@ -422,8 +422,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
if (!preNode) {
|
||||
return null;
|
||||
}
|
||||
const { activatable, selectable } = this._editor.getView(preNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(preNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return preNode;
|
||||
}
|
||||
@@ -432,18 +432,18 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getPreviousActivatableBlockNode(preNode);
|
||||
return this._getPreviousEditableBlockNode(preNode);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get next activatable blockNode
|
||||
* get next editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getNextActivatableBlockNode(
|
||||
private async _getNextEditableBlockNode(
|
||||
node: AsyncBlock,
|
||||
ignoreSelf = true
|
||||
): Promise<AsyncBlock | null> {
|
||||
@@ -482,8 +482,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
const { activatable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return nextNode;
|
||||
}
|
||||
@@ -492,7 +492,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getNextActivatableBlockNode(nextNode);
|
||||
return this._getNextEditableBlockNode(nextNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +513,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
return;
|
||||
}
|
||||
}
|
||||
preNode = await this._getPreviousActivatableBlockNode(node);
|
||||
preNode = await this._getPreviousEditableBlockNode(node);
|
||||
if (preNode) {
|
||||
this.activeNodeByNodeId(preNode.id, position);
|
||||
}
|
||||
@@ -622,7 +622,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
nextNode = await this._getNextActivatableBlockNode(node);
|
||||
nextNode = await this._getNextEditableBlockNode(node);
|
||||
if (nextNode) {
|
||||
this.activeNodeByNodeId(nextNode.id, position);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user