diff --git a/.all-contributorsrc b/.all-contributorsrc index 8993984505..4b1cc35879 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -12,6 +12,16 @@ "contributorsPerLine": 7, "badgeTemplate": "\n[all-contributors-badge]: https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square\n", "contributors": [ + { + "login": "doodlewind", + "name": "Yifeng Wang", + "avatar_url": "https://avatars.githubusercontent.com/u/7312949?v=4", + "profile": "https://github.com/doodlewind", + "contributions": [ + "code", + "doc" + ] + }, { "login": "darkskygit", "name": "DarkSky", @@ -303,6 +313,15 @@ "contributions": [ "code" ] + }, + { + "login": "felixonmars", + "name": "Felix Yan", + "avatar_url": "https://avatars.githubusercontent.com/u/1006477?v=4", + "profile": "https://felixc.at/", + "contributions": [ + "code" + ] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index e69da8bc93..18f956c613 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ "editor.codeActionsOnSave": ["source.fixAll", "source.organizeImports"], "prettier.prettierPath": "./node_modules/prettier", "cSpell.words": [ + "aboutus", "AUTOINCREMENT", "Backlinks", "blockdb", diff --git a/README.md b/README.md index 19e0db1307..d4f92bc519 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ AFFiNE.PRO
- The Next-Gen Knowledge Base to Replace Notion & Miro. + The Next-Gen Collaborative Knowledge Base

-Planning, Sorting and Creating all Together. Open-source, Privacy-First, and Free to use. +Open-source and privacy-first.
+A free replacement for Notion & Miro.

@@ -18,13 +19,14 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066 --> -[all-contributors-badge]: https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square +[all-contributors-badge]: https://img.shields.io/badge/all_contributors-33-orange.svg?style=flat-square -[![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://affine.pro) +[![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://livedemo.affine.pro) [![stars](https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars)](https://github.com/toeverything/AFFiNE) [![All Contributors][all-contributors-badge]](#contributors) +
[![Node](https://img.shields.io/badge/node->=16.0-success)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/TypeScript-4.7-3178c6)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/) @@ -35,11 +37,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066

  - +   - +   - +  

@@ -48,100 +50,52 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066

affine_screen

-# Stay Up-to-Date and Support Us +# :star: Support Us and Keep Updated :star: ![952cd7a5-70fe-48ab-b74f-23981d94d2c5](https://user-images.githubusercontent.com/79301703/182365526-df074c64-cee4-45f6-b8e0-b912f17332c6.gif) -# How to use +# Getting Started -

-🥳🥳🥳 Our web live demo is ready! 🥳🥳🥳 -

+[![affine.pro](https://img.shields.io/static/v1?label=Try%20it%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&message=%E2%86%92&style=for-the-badge)](https://affine.pro) No installation or registration required! Head over to our website and try it out now. -

-Start to play with AFFiNE web version on our landing page: -

- -

- check_live_demo -

- -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 - -![546163d6-4c39-4128-ae7f-55d59bc3b76b](https://user-images.githubusercontent.com/79301703/182365611-b0ba3690-21c0-4d9b-bfbc-0bc15da05aeb.gif) - -## Plan your task - -![41a7b3a4-32f2-4d18-ac6b-57d1e1fda753](https://user-images.githubusercontent.com/79301703/182366553-1f6558a7-f17b-4611-ab95-aea3ec997154.gif) - -## Sort your knowledge - -![c9e1ff46-cec2-411b-b89d-6727a5e6f6c3](https://user-images.githubusercontent.com/79301703/182366602-08e44d28-a031-4097-9904-52fb9b1e9e17.gif) +Want to deploy it yourself? AFFiNE can run just about anywhere.
+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. +

+⚠️ 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/). +![546163d6-4c39-4128-ae7f-55d59bc3b76b](https://user-images.githubusercontent.com/79301703/182365611-b0ba3690-21c0-4d9b-bfbc-0bc15da05aeb.gif) -## Getting Started with development +### Plan your task -Please view the path Contribute-to-AFFiNE/Software-Contributions/Quick-Start in the documentation. +![41a7b3a4-32f2-4d18-ac6b-57d1e1fda753](https://user-images.githubusercontent.com/79301703/182366553-1f6558a7-f17b-4611-ab95-aea3ec997154.gif) -# Roadmap +### Sort your knowledge -Yes! Permanent storage, collaboration, stable release is planned! Check it [here](https://github.com/toeverything/AFFiNE/issues/293) +![c9e1ff46-cec2-411b-b89d-6727a5e6f6c3](https://user-images.githubusercontent.com/79301703/182366602-08e44d28-a031-4097-9904-52fb9b1e9e17.gif) -# 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 - -Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors. - -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/). +- 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) # Contact Us -You may contact us by emailing to: contact@toeverything.info +Feel free to send us an email: contact@toeverything.info # The Philosophy of AFFiNE @@ -164,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: @@ -186,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 @@ -199,45 +147,47 @@ For help, discussion about best practices, or any other conversation that would + - + - + - + - + +

Yifeng Wang

💻 📖

DarkSky

💻 📖

Chi Zhang

💻 📖

wang xinglong

💻 📖

DiamondThree

💻 📖

Whitewater

💻 📖

xiaodong zuo

💻 📖

MingLIang Wang

💻 📖

MingLIang Wang

💻 📖

Qi

💻 📖

mitsuhatu

💻 📖

Austaras

💻 📖

Jin Yao

💻 📖

HeJiachen-PM

📖

Yipei Wei

📖

fanjing22

🎨

fanjing22

🎨

Svaney

🎨

Guozhu Liu

🎨

fyZheng07

📋 📓

CJSS

📖

Carlos Rafael

💻

Caleb OLeary

💻

JimmFly

💻

JimmFly

💻

Weston Graham

📖

pointmax

📖

Bryan Lee

💻

Simon Li

💻

Bob Hu

💻

Quavo

📖

子瞻 Luci

💻

子瞻 Luci

💻

Horus

💻 📦

Super.x

💻

Wang Yu

💻

Felix Yan

💻
@@ -250,4 +200,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. diff --git a/apps/ligo-virgo/package.json b/apps/ligo-virgo/package.json index 5bb0d78eaa..e8a18e071c 100644 --- a/apps/ligo-virgo/package.json +++ b/apps/ligo-virgo/package.json @@ -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" } diff --git a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx index a2440a1757..ba1897cc4a 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx @@ -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 ( @@ -50,31 +57,37 @@ export function Page(props: PageProps) { > - + -
- {dailyNotesFlag && ( + {activeTab === TabMap.get(TAB_TITLE.PAGES).value && ( +
+ {dailyNotesFlag && ( +
+ + + +
+ )}
- - + +
- )} -
- - - +
+ + {page_id ? : null} + +
-
- - {page_id ? : null} - -
-
+ )} + + {activeTab === TabMap.get(TAB_TITLE.TOC).value && ( + + )} @@ -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', diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx b/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx index 74a8dcc5ae..aea0c88374 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/tabs/Tabs.tsx @@ -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([ - ['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; -const Tabs = () => { - const [activeValue, setActiveTab] = useState(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 ( {[...TabMap.entries()].map(([k, { value, disabled = false }]) => { - const isActive = activeValue === value; + const isActive = activeTab === value; return ( { className={isActive ? 'active' : ''} isActive={isActive} isDisabled={disabled} - onClick={() => onClick(value)} + onClick={() => onTabChange(value)} > {k} diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx new file mode 100644 index 0000000000..0a5c442590 --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx @@ -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 ( + onClick(id)} + > + {text} + + ); +}; + +const renderTOCContent = tocDataSource => { + return ( + <> + {tocDataSource.map(tocItem => { + if (tocItem?.length) { + return renderTOCContent(tocItem); + } + + const { id, type, text } = tocItem; + + return ; + })} + + ); +}; + +export const TOC = () => { + const { page_id } = useParams(); + const [tocDataSource, setTocDataSource] = useState([]); + const [activeBlockId, setActiveBlockId] = useState(''); + + /* store page/block unmount-listener */ + const listenerMapRef = useRef(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 ( + +
{renderTOCContent(tocDataSource)}
+
+ ); +}; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts new file mode 100644 index 0000000000..e603af8242 --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/index.ts @@ -0,0 +1 @@ +export { TOC } from './TOC'; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts new file mode 100644 index 0000000000..5c9ad0c59e --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-enum.ts @@ -0,0 +1,6 @@ +export enum BLOCK_TYPES { + GROUP = 'group', + HEADING1 = 'heading1', + HEADING2 = 'heading2', + HEADING3 = 'heading3', +} diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts new file mode 100644 index 0000000000..aa1154a944 --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/toc-util.ts @@ -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 => { + /* 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 }; diff --git a/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts new file mode 100644 index 0000000000..9d84eec52a --- /dev/null +++ b/apps/ligo-virgo/src/pages/workspace/docs/components/toc/types.ts @@ -0,0 +1,7 @@ +export type TOCType = { + id: string; + type: string; + text: string; +}; + +export type ListenerMap = Map void>; diff --git a/apps/ligo-virgo/webpack.config.js b/apps/ligo-virgo/webpack.config.js index 4bf721409c..11be2e1c76 100644 --- a/apps/ligo-virgo/webpack.config.js +++ b/apps/ligo-virgo/webpack.config.js @@ -43,6 +43,7 @@ module.exports = function (webpackConfig) { ...config.output, filename: '[name].[contenthash:8].js', chunkFilename: '[name].[chunkhash:8].js', + hashDigestLength: 8, hashFunction: undefined, }; config.optimization = { @@ -67,21 +68,21 @@ module.exports = function (webpackConfig) { chunks: 'all', enforce: true, }, - auth: { - test: /[\\/]node_modules[\\/](@authing|@?firebase)/, - name: 'auth', - priority: -5, - chunks: 'all', - }, + // auth: { + // test: /[\\/]node_modules[\\/](@authing|@?firebase)/, + // name: 'auth', + // priority: -5, + // chunks: 'all', + // }, edgeless: { test: /(libs\/components\/board-|[\\/]node_modules[\\/]@tldraw)/, name: 'edgeless', priority: -7, chunks: 'all', }, - editor: { + paper: { test: /(libs\/framework\/(ligo|virgo|editor)|[\\/]node_modules[\\/](@codemirror|@lezer|slate))/, - name: 'editor', + name: 'paper', priority: -8, chunks: 'all', }, @@ -176,7 +177,11 @@ module.exports = function (webpackConfig) { publicPath: '/', }), new Style9Plugin(), - isProd && new MiniCssExtractPlugin(), + isProd && + new MiniCssExtractPlugin({ + filename: '[name].[contenthash:8].css', + chunkFilename: '[id].[chunkhash:8].css', + }), isProd && new CompressionPlugin({ test: /\.(js|css|html|svg|ttf|woff)$/, diff --git a/apps/venus/package.json b/apps/venus/package.json index 321da2b80d..87065b9d79 100644 --- a/apps/venus/package.json +++ b/apps/venus/package.json @@ -9,13 +9,15 @@ "dependencies": { "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", - "@mui/joy": "^5.0.0-alpha.39", + "@mui/joy": "^5.0.0-alpha.42", "i18next": "^21.9.1", "lozad": "^1.16.0", "react-i18next": "^11.18.4" }, "devDependencies": { - "image-minimizer-webpack-plugin": "^3.2.3", + "@mdx-js/loader": "^2.1.3", + "github-markdown-css": "^5.1.0", + "image-minimizer-webpack-plugin": "^3.3.0", "imagemin": "^8.0.1", "imagemin-optipng": "^8.0.0", "mini-css-extract-plugin": "^2.6.1", diff --git a/apps/venus/src/app/AboutUs.tsx b/apps/venus/src/app/AboutUs.tsx new file mode 100644 index 0000000000..b20420a556 --- /dev/null +++ b/apps/venus/src/app/AboutUs.tsx @@ -0,0 +1,110 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { Box, Grid, Typography } from '@mui/joy'; +// eslint-disable-next-line no-restricted-imports +import { useMediaQuery } from '@mui/material'; +import 'github-markdown-css'; +import AboutText from './about.mdx'; +import { AFFiNEFooter, AFFiNEHeader, AFFiNEImage } from './Common'; +import KeepUpdate from './keeupdate.png'; +export const AboutUs = () => { + const matches = useMediaQuery('(max-width: 1024px)'); + const navigate = useNavigate(); + const { i18n } = useTranslation(); + + const changeLanguage = (event: any) => { + i18n.changeLanguage(event); + }; + return ( + <> + + + + + To Shape, not to adapt. + + + + + + + Deliver Building Blocks for Future SaaS Applications. + + + + + + + + + + + + window.open( + 'https://github.com/toeverything/AFFiNE' + ) + } + /> + + + + + ); +}; diff --git a/apps/venus/src/app/App.tsx b/apps/venus/src/app/App.tsx new file mode 100644 index 0000000000..df1e8be681 --- /dev/null +++ b/apps/venus/src/app/App.tsx @@ -0,0 +1,600 @@ +/* eslint-disable max-lines */ +/* eslint-disable @typescript-eslint/naming-convention */ +import clsx from 'clsx'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { Box, Button, Grid, Typography } from '@mui/joy'; +import { styled } from '@mui/joy/styles'; +import { LogoIcon } from '@toeverything/components/icons'; +// eslint-disable-next-line no-restricted-imports +import { useMediaQuery } from '@mui/material'; + +import CollaborationImage from './collaboration.png'; +import { AFFiNEFooter, AFFiNEHeader, AFFiNEImage } from './Common'; +import { GitHub } from './Icons'; +import PageImage from './page.png'; +import ShapeImage from './shape.png'; +import TaskImage from './task.png'; + +const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({ + position: 'relative', + width: '24em', + height: '128px', + transform: 'translateY(-8px)', + overflowY: 'hidden', + '@media (max-width: 1024px)': { + width, + height: '48px', + transform: 'translateY(0)', + }, + '& .scroll-element': { + width: 'inherit', + height: 'inherit', + position: 'absolute', + left: '0%', + top: '0%', + lineHeight: '96px', + '@media (max-width: 1024px)': { + lineHeight: '32px', + }, + }, + '& .scroll-element.active': { + animation: 'primary 500ms linear infinite', + }, + '.primary.active': { + animation: 'primary 500ms linear infinite', + }, + '.secondary.active': { + animation: 'secondary 500ms linear infinite', + }, + '@keyframes primary': { + from: { + top: '0%', + }, + to: { + top: '-100%', + }, + }, + '@keyframes secondary': { + from: { + top: '100%', + }, + to: { + top: '0%', + }, + }, +})); + +const _alternatives = ['Notion', 'Miro', 'Monday']; +const _alternativesSize = [8, 6, 10]; + +const Product = () => { + const [idx, setIdx] = useState(0); + const [last, current] = useMemo( + () => [ + _alternatives[idx], + _alternatives[idx + 1] ? _alternatives[idx + 1] : _alternatives[0], + ], + [idx] + ); + const maxWidth = useMemo(() => _alternativesSize[idx], [idx]); + const [active, setActive] = useState(false); + const matches = useMediaQuery('(max-width: 1024px)'); + + useEffect(() => { + const handle = setInterval(() => { + setActive(true); + setTimeout( + () => { + setIdx(idx => (_alternatives[idx + 1] ? idx + 1 : 0)); + setActive(false); + }, + matches ? 450 : 380 + ); + }, 2000); + return () => clearInterval(handle); + }, [matches]); + + return ( + + + + {last} + + + + + {current} + + + + ); +}; + +const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => { + const matches = useMediaQuery('(max-width: 1024px)'); + const { t } = useTranslation(); + return ( + + ); +}; + +export function App() { + const matches = useMediaQuery('(max-width: 1024px)'); + const navigate = useNavigate(); + const { t, i18n } = useTranslation(); + + const changeLanguage = (event: any) => { + i18n.changeLanguage(event); + }; + return ( + <> + + + + + {t('Open Source')}, + + + {t('Privacy First')} + + + + + + + + {t('Alternative')} + + + + + + + {t('description1.part1')} + + + + + + + + + + + + + + + + + + {t('description1.part2')} + + + + + + + {t('description1.part3')} + + + {t('description1.part4')} + + + + + + + + {t('description2.part1')} + + + {t('description2.part2')} + + + {t('description2.part3')} + + + + + + + + + + + + + + {t('description3.part1')} + + + {t('description3.part2')} + + + {t('description3.part3')} + + + {t('description3.part4')} + + + + + + + + + + + + + {t('description4.part1')} + + + {t('description4.part2')} + + + {t('description4.part3')} + + + + + + + + + + + ); +} diff --git a/apps/venus/src/app/Common.tsx b/apps/venus/src/app/Common.tsx new file mode 100644 index 0000000000..87347f4fa1 --- /dev/null +++ b/apps/venus/src/app/Common.tsx @@ -0,0 +1,426 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import { Box, Button, Grid, Typography } from '@mui/joy'; +import Option from '@mui/joy/Option'; +import Select from '@mui/joy/Select'; +import { styled } from '@mui/joy/styles'; +// eslint-disable-next-line no-restricted-imports +import { useMediaQuery } from '@mui/material'; + +import GitHubIcon from '@mui/icons-material/GitHub'; +import RedditIcon from '@mui/icons-material/Reddit'; +import TelegramIcon from '@mui/icons-material/Telegram'; +import { options } from './i18n'; +import { DiscordIcon, GitHub } from './Icons'; +// eslint-disable-next-line no-restricted-imports +import LogoImage from './logo.png'; + +export const AFFiNEImage = styled('img')({ + maxWidth: '100%', + objectFit: 'contain', +}); + +export const AFFiNEFooter = ({ + keepupdate = true, +}: { + keepupdate?: boolean; +}) => { + const { t } = useTranslation(); + + return ( + <> + {keepupdate ? ( + <> + + + + + + + + + {t('BuildFor')} + + + + + + + + + {t('KeepUpdated')} + + + + + + + ) : null} + + + + {t('Join')} + + + + + + + + + + + + + + + + + + + + + AFFiNE is an + + #OpenSource + + company + + + + + + + Copyright © 2022 AFFiNE. + + + + + ); +}; + +export const AFFiNEHeader = () => { + const matches = useMediaQuery('(max-width: 1024px)'); + const navigate = useNavigate(); + const { i18n } = useTranslation(); + + const changeLanguage = (event: any) => { + i18n.changeLanguage(event); + }; + const matchesIPAD = useMediaQuery('(max-width: 768px)'); + return ( + + + + + + + + + + + + ); +}; diff --git a/apps/venus/src/app/Icons.tsx b/apps/venus/src/app/Icons.tsx new file mode 100644 index 0000000000..ec30f2067c --- /dev/null +++ b/apps/venus/src/app/Icons.tsx @@ -0,0 +1,75 @@ +/* eslint-disable max-lines */ +/* eslint-disable @typescript-eslint/naming-convention */ +import GitHubIcon from '@mui/icons-material/GitHub'; +import { Button, SvgIcon } from '@mui/joy'; +// eslint-disable-next-line no-restricted-imports +import { useMediaQuery } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export const DiscordIcon = (props: any) => { + return ( + + + + + + + + + + + ); +}; + +export const GitHub = (props: { center?: boolean; flat?: boolean }) => { + const matches = useMediaQuery('(max-width: 1024px)'); + const { t } = useTranslation(); + + return ( + + ); +}; diff --git a/apps/venus/src/app/MdxMarks.tsx b/apps/venus/src/app/MdxMarks.tsx new file mode 100644 index 0000000000..1e3890cae1 --- /dev/null +++ b/apps/venus/src/app/MdxMarks.tsx @@ -0,0 +1,45 @@ +import { Box, Typography } from '@mui/joy'; + +type NameProps = { + name: string; + link: string; + title: string; + description?: string; +}; + +export const Name = (props: NameProps) => { + return ( + <> + + + + {props.name} + + + + {' | '} + {props.title} + + + {props.description ? ( + + {props.description} + + ) : null} + + ); +}; + +export const Padding = () => { + return
; +}; diff --git a/apps/venus/src/app/about.mdx b/apps/venus/src/app/about.mdx new file mode 100644 index 0000000000..d05c285003 --- /dev/null +++ b/apps/venus/src/app/about.mdx @@ -0,0 +1,88 @@ +import { Name, Padding } from './MdxMarks'; + + + +## Do Contact US if you + +- Want to know more about AFFiNE as a collaborative knowledge base; +- Want to join us; +- Want to build your own block-based applications. + +General contact: [contact@toeverything.info](mailto:contact@toeverything.info) + +Send Resume to: [hr@toeverything.info](mailto:hr@toeverything.info) + + + +## Team Member + +### Founder & Co-founders + + + + + + + + + +### Architectural Developers + + + + + + + + + +## The Philosophy of AFFiNE + +People need better building blocks for future applications. And it should not be so hard to develop collaborative, transferable, smart spreadsheets or block editors. + +Timothy Berners-Lee once taught us about the idea of the semantic web, where all the data can be interpreted in any form while the "truth" is kept. This gives our best image of an ideal knowledge base by far, that sorting of information, planning of project and goals as well as creating of knowledge can be all together. + +We have witnessed waves of paradigm shift so many times. At first, everything was noted on office-like apps or DSL like LaTeX, then we found todo-list apps and WYSIWYG markdown editors better for writing and planning. + +Finally, here comes Notion and Miro, who take advantage of the idea of blocks to further liberate our creativity. It is all perfect... without waste operations and redundant information. And, we insist that privacy first should always be given by default. That's why we are making AFFiNE. diff --git a/apps/venus/src/app/i18n/resources/en.json b/apps/venus/src/app/i18n/resources/en.json index cd7a377352..2cffda93bd 100644 --- a/apps/venus/src/app/i18n/resources/en.json +++ b/apps/venus/src/app/i18n/resources/en.json @@ -1,5 +1,7 @@ { "translation": { + "Blog": "Blog", + "AboutUs": "About AboutUs", "Open Source": "Open Source", "Privacy First": "Privacy First", "Alternative": "Alternative", diff --git a/apps/venus/src/app/i18n/resources/zh.json b/apps/venus/src/app/i18n/resources/zh.json index eb4cf432dd..ca0a060072 100644 --- a/apps/venus/src/app/i18n/resources/zh.json +++ b/apps/venus/src/app/i18n/resources/zh.json @@ -1,5 +1,7 @@ { "translation": { + "Blog": "博客", + "AboutUs": "关于我们", "Open Source": "开源", "Privacy First": "隐私第一", "Alternative": "的另一种选择", diff --git a/apps/venus/src/app/index.tsx b/apps/venus/src/app/index.tsx index e4d9c7854e..8464ca5b4a 100644 --- a/apps/venus/src/app/index.tsx +++ b/apps/venus/src/app/index.tsx @@ -1,1050 +1,37 @@ -/* eslint-disable max-lines */ -/* eslint-disable @typescript-eslint/naming-convention */ -import clsx from 'clsx'; -import { useEffect, useMemo, useState } from 'react'; +import { Outlet, Route, Routes } from 'react-router-dom'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import RedditIcon from '@mui/icons-material/Reddit'; -import TelegramIcon from '@mui/icons-material/Telegram'; -import { Box, Button, Container, Grid, SvgIcon, Typography } from '@mui/joy'; -import Option from '@mui/joy/Option'; -import Select from '@mui/joy/Select'; -import { CssVarsProvider, styled } from '@mui/joy/styles'; -import { LogoIcon } from '@toeverything/components/icons'; -// eslint-disable-next-line no-restricted-imports -import { useMediaQuery } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import CollaborationImage from './collaboration.png'; -import { options } from './i18n'; -import LogoImage from './logo.png'; -import PageImage from './page.png'; -import ShapeImage from './shape.png'; -import TaskImage from './task.png'; +import { Container } from '@mui/joy'; +import { CssVarsProvider } from '@mui/joy/styles'; -const DiscordIcon = (props: any) => { - return ( - - - - - - - - - - - ); -}; +import { AboutUs } from './AboutUs'; +import { App } from './App'; -const VenusContainer = styled(Container)({ - margin: '1em auto', -}); - -const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({ - position: 'relative', - width: '24em', - height: '128px', - transform: 'translateY(-8px)', - overflowY: 'hidden', - '@media (max-width: 1024px)': { - width, - height: '48px', - transform: 'translateY(0)', - }, - '& .scroll-element': { - width: 'inherit', - height: 'inherit', - position: 'absolute', - left: '0%', - top: '0%', - lineHeight: '96px', - '@media (max-width: 1024px)': { - lineHeight: '32px', - }, - }, - '& .scroll-element.active': { - animation: 'primary 500ms linear infinite', - }, - '.primary.active': { - animation: 'primary 500ms linear infinite', - }, - '.secondary.active': { - animation: 'secondary 500ms linear infinite', - }, - '@keyframes primary': { - from: { - top: '0%', - }, - to: { - top: '-100%', - }, - }, - '@keyframes secondary': { - from: { - top: '100%', - }, - to: { - top: '0%', - }, - }, -})); - -const _alternatives = ['Notion', 'Miro', 'Monday']; -const _alternativesSize = [8, 6, 10]; - -const Product = () => { - const [idx, setIdx] = useState(0); - const [last, current] = useMemo( - () => [ - _alternatives[idx], - _alternatives[idx + 1] ? _alternatives[idx + 1] : _alternatives[0], - ], - [idx] - ); - const maxWidth = useMemo(() => _alternativesSize[idx], [idx]); - const [active, setActive] = useState(false); - const matches = useMediaQuery('(max-width: 1024px)'); - - useEffect(() => { - const handle = setInterval(() => { - setActive(true); - setTimeout( - () => { - setIdx(idx => (_alternatives[idx + 1] ? idx + 1 : 0)); - setActive(false); - }, - matches ? 450 : 380 - ); - }, 2000); - return () => clearInterval(handle); - }, [matches]); - - return ( - - - - {last} - - - - - {current} - - - - ); -}; - -const AffineImage = styled('img')({ - maxWidth: '100%', - objectFit: 'contain', -}); - -const GitHub = (props: { center?: boolean; flat?: boolean }) => { - const matches = useMediaQuery('(max-width: 1024px)'); - const { t } = useTranslation(); - - return ( - - ); -}; - -const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => { - const matches = useMediaQuery('(max-width: 1024px)'); - const { t } = useTranslation(); - return ( - - ); -}; - -export function App() { - const matches = useMediaQuery('(max-width: 1024px)'); - const { t, i18n } = useTranslation(); - - const changeLanguage = (event: any) => { - i18n.changeLanguage(event); - }; +const VenusContainer = () => { return ( - div': { marginTop: '1em', }, }} > - - - - - - - - - - - - - - {t('Open Source')}, - - - {t('Privacy First')} - - - - - - - - {t('Alternative')} - - - - - - - {t('description1.part1')} - - - - - - - - - - - - - - - - - - {t('description1.part2')} - - - - - - - {t('description1.part3')} - - - {t('description1.part4')} - - - - - - - - {t('description2.part1')} - - - {t('description2.part2')} - - - {t('description2.part3')} - - - - - - - - - - - - - - {t('description3.part1')} - - - {t('description3.part2')} - - - {t('description3.part3')} - - - {t('description3.part4')} - - - - - - - - - - - - - {t('description4.part1')} - - - {t('description4.part2')} - - - {t('description4.part3')} - - - - - - - - - - - - - - - - - {t('BuildFor')} - - - - - - - - {t('KeepUpdated')} - - - - - - - - - {t('Join')} - - - - - - - - - - - - - - - - - - - - - AFFiNE is an - - #OpenSource - - company - - - - - - - Copyright © 2022 AFFiNE. - - - - + + ); -} +}; -export default App; +export function VenusRoutes() { + return ( + + }> + } /> + } /> + + + ); +} diff --git a/apps/venus/src/app/keeupdate.png b/apps/venus/src/app/keeupdate.png new file mode 100644 index 0000000000..7bdc36986b Binary files /dev/null and b/apps/venus/src/app/keeupdate.png differ diff --git a/apps/venus/src/assets/images/og.png b/apps/venus/src/assets/images/og.png new file mode 100644 index 0000000000..ad0ea8088c Binary files /dev/null and b/apps/venus/src/assets/images/og.png differ diff --git a/apps/venus/src/index.html b/apps/venus/src/index.html index 232e6299d1..b139b035ef 100644 --- a/apps/venus/src/index.html +++ b/apps/venus/src/index.html @@ -6,6 +6,27 @@ + + + + + + + + + + + + + + + diff --git a/apps/venus/src/index.tsx b/apps/venus/src/index.tsx index 5fdc926e59..2bd7c320cd 100644 --- a/apps/venus/src/index.tsx +++ b/apps/venus/src/index.tsx @@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import './app/i18n'; -import App from './app'; +import { VenusRoutes } from './app'; const container = document.getElementById('root'); if (!container) { @@ -13,7 +13,7 @@ const root = createRoot(container); root.render( - + ); diff --git a/apps/venus/src/mdx.d.ts b/apps/venus/src/mdx.d.ts new file mode 100644 index 0000000000..e6f1511e1d --- /dev/null +++ b/apps/venus/src/mdx.d.ts @@ -0,0 +1,4 @@ +declare module '*.mdx' { + let MDXComponent: (props: any) => JSX.Element; + export default MDXComponent; +} diff --git a/apps/venus/tsconfig.app.json b/apps/venus/tsconfig.app.json index 054e3ea559..a4f9eff7dc 100644 --- a/apps/venus/tsconfig.app.json +++ b/apps/venus/tsconfig.app.json @@ -19,5 +19,5 @@ "**/*.spec.jsx", "**/*.test.jsx" ], - "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.d.ts"] } diff --git a/apps/venus/webpack.config.js b/apps/venus/webpack.config.js index 88df817a12..57c78f9f3b 100644 --- a/apps/venus/webpack.config.js +++ b/apps/venus/webpack.config.js @@ -9,7 +9,6 @@ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); -const Style9Plugin = require('style9/webpack'); const enableBundleAnalyzer = process.env.BUNDLE_ANALYZER; @@ -18,15 +17,13 @@ module.exports = function (webpackConfig) { const isProd = config.mode === 'production'; - const style9 = { - test: /\.(tsx|ts|js|mjs|jsx)$/, + const mdx = { + test: /\.mdx?$/, use: [ { - loader: Style9Plugin.loader, - options: { - minifyProperties: isProd, - incrementalClassnames: isProd, - }, + loader: '@mdx-js/loader', + /** @type {import('@mdx-js/loader').Options} */ + options: {}, }, ], }; @@ -34,7 +31,6 @@ module.exports = function (webpackConfig) { config.experiments.topLevelAwait = true; if (isProd) { - config.module.rules.unshift(style9); config.entry = { main: [...config.entry.main, ...config.entry.polyfills], }; @@ -43,6 +39,7 @@ module.exports = function (webpackConfig) { ...config.output, filename: '[name].[contenthash:8].js', chunkFilename: '[name].[chunkhash:8].js', + hashDigestLength: 8, hashFunction: undefined, }; config.optimization = { @@ -102,24 +99,8 @@ module.exports = function (webpackConfig) { }, ], }); - config.module.rules.unshift({ - test: /\.scss$/i, - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - sourceMap: false, - }, - }, - { - loader: 'postcss-loader', - }, - ], - }); - config.module.rules.splice(6); + config.module.rules.splice(4); } else { - config.module.rules.push(style9); config.output = { ...config.output, publicPath: '/', @@ -138,6 +119,8 @@ module.exports = function (webpackConfig) { } } + config.module.rules.push(mdx); + addEmotionBabelPlugin(config); config.plugins = [ @@ -158,8 +141,11 @@ module.exports = function (webpackConfig) { template: path.resolve(__dirname, './src/template.html'), publicPath: '/', }), - new Style9Plugin(), - isProd && new MiniCssExtractPlugin(), + isProd && + new MiniCssExtractPlugin({ + filename: '[name].[contenthash:8].css', + chunkFilename: '[id].[chunkhash:8].css', + }), isProd && new CompressionPlugin({ test: /\.(js|css|html|svg|ttf|woff)$/, @@ -205,15 +191,6 @@ const addEmotionBabelPlugin = config => { // See https://github.com/mui/material-ui/issues/27380#issuecomment-928973157 // See https://github.com/emotion-js/emotion/tree/main/packages/babel-plugin#importmap importMap: { - '@toeverything/components/ui': { - styled: { - canonicalImport: ['@emotion/styled', 'default'], - styledBaseImport: [ - '@toeverything/components/ui', - 'styled', - ], - }, - }, '@mui/material': { styled: { canonicalImport: ['@emotion/styled', 'default'], diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 83% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 7b7b63a1a5..5dfbae23c6 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,8 +1,8 @@ -# Welcome to ourcontributing guide +# Welcome to our contributing guide Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:. -Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. +Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. Join our [dicord](https://discord.com/invite/yz6tGVsf5p) server for more. In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. @@ -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 @@ -65,6 +63,8 @@ For more information about using a codespace for working on GitHub documentation Commit the changes once you are happy with them. +Reach out the community members for necessary help. + Once your changes are ready, don't forget to self-review to speed up the review process:zap:. ### Pull Request @@ -83,6 +83,6 @@ When you're finished with the changes, create a pull request, also known as a PR Congratulations :tada::tada: The AFFiNE team thanks you :sparkles:. -Once your PR is merged, your contributions will be publicly visible on the our GitHub. +Once your PR is merged, your contributions will be publicly visible on our GitHub. Now that you are part of the AFFiNE community, see how else you can join and help over at [Gitbook](https://docs.affine.pro/affine/) diff --git a/docs/types-of-contributions.md b/docs/types-of-contributions.md new file mode 100644 index 0000000000..fd448200e9 --- /dev/null +++ b/docs/types-of-contributions.md @@ -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. diff --git a/libs/components/affine-board/src/Board.tsx b/libs/components/affine-board/src/Board.tsx index af9e8982fe..8c521705e7 100644 --- a/libs/components/affine-board/src/Board.tsx +++ b/libs/components/affine-board/src/Board.tsx @@ -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), - }, - }, - }); }, }} /> diff --git a/libs/components/affine-board/src/hooks/use-shapes.ts b/libs/components/affine-board/src/hooks/use-shapes.ts index beb8c50efa..23270b6c19 100644 --- a/libs/components/affine-board/src/hooks/use-shapes.ts +++ b/libs/components/affine-board/src/hooks/use-shapes.ts @@ -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); + return { shapes: blocksShapes, - bindings: JSON.parse(blocks?.bindings ?? '{}'), }; }; diff --git a/libs/components/affine-editor/src/Editor.tsx b/libs/components/affine-editor/src/Editor.tsx index c280bea984..f1edb2ce74 100644 --- a/libs/components/affine-editor/src/Editor.tsx +++ b/libs/components/affine-editor/src/Editor.tsx @@ -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( editor={editor} editorElement={AffineEditor as any} scrollBlank={scrollBlank} - > - - + /> ); } ); diff --git a/libs/components/board-draw/src/TlDraw.tsx b/libs/components/board-draw/src/TlDraw.tsx index 1961367235..a1444b76c0 100644 --- a/libs/components/board-draw/src/TlDraw.tsx +++ b/libs/components/board-draw/src/TlDraw.tsx @@ -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; diff --git a/libs/components/board-draw/src/components/align-panel/index.tsx b/libs/components/board-draw/src/components/align-panel/index.tsx index d0ff48390d..ddc43b1254 100644 --- a/libs/components/board-draw/src/components/align-panel/index.tsx +++ b/libs/components/board-draw/src/components/align-panel/index.tsx @@ -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': { diff --git a/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx b/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx index 21f37a9fbc..84ef142ef0 100644 --- a/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx +++ b/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx @@ -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) => {
} > - + diff --git a/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx b/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx index 94e28bdc2f..031d4240a6 100644 --- a/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx +++ b/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx @@ -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} > ) : null, - toNextShap: ( - - ), + // toNextShap: ( + // + // ), }; const nodes = Object.entries(configNodes).filter(([key, node]) => !!node); diff --git a/libs/components/board-draw/src/components/tools-panel/LineTools.tsx b/libs/components/board-draw/src/components/tools-panel/LineTools.tsx index 3ef546ca3c..ce6cb97574 100644 --- a/libs/components/board-draw/src/components/tools-panel/LineTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/LineTools.tsx @@ -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( TDShapeType.Line @@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => { return ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ {shapes.map(({ type, label, tooltip, icon: Icon }) => ( @@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => { { app.selectTool(type); + setVisible(false); setLastActiveTool(type); }} > diff --git a/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx b/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx index eb2fb5c564..0c04126c95 100644 --- a/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx @@ -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( TDShapeType.Rectangle @@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => { return ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ {shapes.map(({ type, label, tooltip, icon: Icon }) => ( @@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => { { app.selectTool(type); + setVisible(false); setLastActiveTool(type); }} > diff --git a/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx b/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx index f659fdff1c..82a5315816 100644 --- a/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx +++ b/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx @@ -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" > -
-
+ + {tools.map( ({ type, @@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => { ) )} -
-
+ + ); }; -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', }); diff --git a/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx b/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx index 0536acfce1..c955afba0d 100644 --- a/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx @@ -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 ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ @@ -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 }) => { setPenColor(color)} + onSelect={color => { + setVisible(false); + setPenColor(color); + }} />
diff --git a/libs/components/board-shapes/src/arrow-util/arrow-util.tsx b/libs/components/board-shapes/src/arrow-util/arrow-util.tsx index 42045918f4..dace7eeb38 100644 --- a/libs/components/board-shapes/src/arrow-util/arrow-util.tsx +++ b/libs/components/board-shapes/src/arrow-util/arrow-util.tsx @@ -42,7 +42,7 @@ type E = HTMLDivElement; export class ArrowUtil extends TDShapeUtil { type = TDShapeType.Arrow as const; - override hideBounds = true; + override hideBounds = false; override canEdit = true; diff --git a/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx b/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx index 77ee2cec6a..36d4475b92 100644 --- a/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx +++ b/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx @@ -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 ( <> {style.isFilled && ( diff --git a/libs/components/board-state/src/tldraw-app.ts b/libs/components/board-state/src/tldraw-app.ts index 34a392db72..1e95912db5 100644 --- a/libs/components/board-state/src/tldraw-app.ts +++ b/libs/components/board-state/src/tldraw-app.ts @@ -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 { private prev_bindings = this.page.bindings; private prev_assets = this.document.assets; - private _broadcastPageChanges = () => { + private _broadcastPageChanges = async () => { const visited = new Set(); const changedShapes: Record = {}; @@ -683,7 +687,7 @@ export class TldrawApp extends StateManager { 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 { 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>/)?.[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>/)?.[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 { // this.select(shapeId); }; - const paste_as_html = (html: string) => { - try { - const maybeJson = html.match(/(.*)<\/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); diff --git a/libs/components/common/src/lib/text/EditableText.tsx b/libs/components/common/src/lib/text/EditableText.tsx index 3722b90c3b..87c111544a 100644 --- a/libs/components/common/src/lib/text/EditableText.tsx +++ b/libs/components/common/src/lib/text/EditableText.tsx @@ -819,7 +819,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => { backgroundColor: '#F2F5F9', borderRadius: '5px', color: '#3A4C5C', - padding: '3px 8px', + padding: '1px 8px', margin: '0 2px', }} > diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 0b35877974..2f7684b547 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -978,7 +978,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 `${text}`; + } + if (textValue.italic) { + return `${text}`; + } + if (textValue.underline) { + return `${text}`; + } + if (textValue.inlinecode) { + return `${text}`; + } + if (textValue.strikethrough) { + return `${text}`; + } + if (textValue.type === 'link') { + return `${this.convertLeaf2Html( + textValue.children + )}`; + } + return `${text}`; } public getStartSelection() { diff --git a/libs/components/editor-blocks/package.json b/libs/components/editor-blocks/package.json index d9b9c66f72..04d8e0e544 100644 --- a/libs/components/editor-blocks/package.json +++ b/libs/components/editor-blocks/package.json @@ -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", diff --git a/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx b/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx index 9f77eda6cb..0bdbd7ca5b 100644 --- a/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx +++ b/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx @@ -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 ( - + @@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
- - - + ); }; diff --git a/libs/components/editor-blocks/src/blocks/bullet/index.ts b/libs/components/editor-blocks/src/blocks/bullet/index.ts index 087afb3056..6044fdb4a8 100644 --- a/libs/components/editor-blocks/src/blocks/bullet/index.ts +++ b/libs/components/editor-blocks/src/blocks/bullet/index.ts @@ -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 { + 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 - ): Promise { - let content = getTextHtml(block); - content += await generateHtml(children); - return `
  • ${content}
`; + override async block2html(props: Block2HtmlProps) { + return `
  • ${await commonBlock2HtmlContent(props)}
`; } } diff --git a/libs/components/editor-blocks/src/blocks/code/CodeView.tsx b/libs/components/editor-blocks/src/blocks/code/CodeView.tsx index 2739c8a70c..f5b91d7fcf 100644 --- a/libs/components/editor-blocks/src/blocks/code/CodeView.tsx +++ b/libs/components/editor-blocks/src/blocks/code/CodeView.tsx @@ -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 ( - + { e.stopPropagation(); @@ -200,7 +198,8 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
- Copy + + Copy
diff --git a/libs/components/editor-blocks/src/blocks/code/index.ts b/libs/components/editor-blocks/src/blocks/code/index.ts index ce8d7badfb..4239add745 100644 --- a/libs/components/editor-blocks/src/blocks/code/index.ts +++ b/libs/components/editor-blocks/src/blocks/code/index.ts @@ -1,17 +1,16 @@ 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; @@ -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 { + 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 - ): Promise { - let content = getTextHtml(block); - content += await generateHtml(children); - return `${content}`; + override async block2html(props: Block2HtmlProps) { + return `${await commonBlock2HtmlContent(props)}`; } } diff --git a/libs/components/editor-blocks/src/blocks/divider/index.ts b/libs/components/editor-blocks/src/blocks/divider/index.ts index 6d67611cda..f77722838f 100644 --- a/libs/components/editor-blocks/src/blocks/divider/index.ts +++ b/libs/components/editor-blocks/src/blocks/divider/index.ts @@ -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 { + return commonHTML2block({ + element, + editor, + type: this.type, + tagName: 'HR', + ignoreEmptyElement: false, + }); } - override async block2html( - block: AsyncBlock, - children: SelectBlock[], - generateHtml: (el: any[]) => Promise - ): Promise { - return `
`; + override async block2html(props: Block2HtmlProps) { + return `
`; } } diff --git a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx index 86b43908d1..c3e7199c29 100644 --- a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx +++ b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx @@ -33,7 +33,7 @@ export const EmbedLinkView = (props: EmbedLinkView) => { }; return ( - + {embedLinkUrl ? ( 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 - ): Promise { - const figma_url = block.getProperty('embedLink')?.value; - return `

${figma_url}

`; + override async block2html({ block }: Block2HtmlProps) { + const url = block.getProperty('embedLink')?.value; + return `

${url}

`; } } diff --git a/libs/components/editor-blocks/src/blocks/figma/FigmaView.tsx b/libs/components/editor-blocks/src/blocks/figma/FigmaView.tsx index 7375546dad..6949280646 100644 --- a/libs/components/editor-blocks/src/blocks/figma/FigmaView.tsx +++ b/libs/components/editor-blocks/src/blocks/figma/FigmaView.tsx @@ -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 ( - + {figmaUrl ? ( 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 - ): Promise { - const figma_url = block.getProperty('embedLink')?.value; - return `

${figma_url}

`; + override async block2html({ block }: Block2HtmlProps) { + const figmaUrl = block.getProperty('embedLink')?.value; + return `

${figmaUrl}

`; } } diff --git a/libs/components/editor-blocks/src/blocks/file/index.ts b/libs/components/editor-blocks/src/blocks/file/index.ts index e3119e9a16..c2f12c0c6d 100644 --- a/libs/components/editor-blocks/src/blocks/file/index.ts +++ b/libs/components/editor-blocks/src/blocks/file/index.ts @@ -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 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 - ): Promise { - 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 `

${file_property?.name}

`; + return fileInfo + ? `

${fileProperty?.name}

` + : ''; } } diff --git a/libs/components/editor-blocks/src/blocks/grid-item/GridItemRender.tsx b/libs/components/editor-blocks/src/blocks/grid-item/GridItemRender.tsx index 3a598b3145..620a25dba6 100644 --- a/libs/components/editor-blocks/src/blocks/grid-item/GridItemRender.tsx +++ b/libs/components/editor-blocks/src/blocks/grid-item/GridItemRender.tsx @@ -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 ; - })} - - ); + const children = ; return <>{creator({ ...props, children })}; }; return GridItem; diff --git a/libs/components/editor-blocks/src/blocks/grid/Grid.tsx b/libs/components/editor-blocks/src/blocks/grid/Grid.tsx index 4ecf47f3b5..a8e69125d0 100644 --- a/libs/components/editor-blocks/src/blocks/grid/Grid.tsx +++ b/libs/components/editor-blocks/src/blocks/grid/Grid.tsx @@ -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} > - + handleDragGrid(event, i)} editor={editor} diff --git a/libs/components/editor-blocks/src/blocks/group/Group.tsx b/libs/components/editor-blocks/src/blocks/group/Group.tsx index 787c537f18..ea27bbbd38 100644 --- a/libs/components/editor-blocks/src/blocks/group/Group.tsx +++ b/libs/components/editor-blocks/src/blocks/group/Group.tsx @@ -6,6 +6,10 @@ 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; @@ -25,13 +29,12 @@ export class Group extends BaseView { override async onCreate(block: AsyncBlock): Promise { return block; } - - override async block2html( - block: AsyncBlock, - children: SelectBlock[], - generateHtml: (el: any[]) => Promise - ): Promise { - const content = await generateHtml(children); - return `
${content}
`; + override async block2html({ editor, selectInfo, block }: Block2HtmlProps) { + const childrenHtml = + await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos( + block, + selectInfo?.children + ); + return `
${childrenHtml}`; } } diff --git a/libs/components/editor-blocks/src/blocks/group/GroupView.tsx b/libs/components/editor-blocks/src/blocks/group/GroupView.tsx index 791059e36c..f0d6e56cc5 100644 --- a/libs/components/editor-blocks/src/blocks/group/GroupView.tsx +++ b/libs/components/editor-blocks/src/blocks/group/GroupView.tsx @@ -21,19 +21,6 @@ const SceneMap: Record> = { 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, diff --git a/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx b/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx index 6145bec114..739927626d 100644 --- a/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx @@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core'; import type { CreateView } from '@toeverything/framework/virgo'; export const ScenePage = ({ block }: CreateView) => { - return ; + return ; }; diff --git a/libs/components/editor-blocks/src/blocks/group/group-menu/AddViewMenu.tsx b/libs/components/editor-blocks/src/blocks/group/group-menu/AddViewMenu.tsx index a376785b52..e1cee48f30 100644 --- a/libs/components/editor-blocks/src/blocks/group/group-menu/AddViewMenu.tsx +++ b/libs/components/editor-blocks/src/blocks/group/group-menu/AddViewMenu.tsx @@ -41,7 +41,7 @@ export const AddViewMenu = () => { onClick={() => setActivePanel(!activePanel)} > - Add View + Add View {activePanel && ( @@ -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' }} diff --git a/libs/components/editor-blocks/src/blocks/group/group-menu/ViewsMenu.tsx b/libs/components/editor-blocks/src/blocks/group/group-menu/ViewsMenu.tsx index 1fee99465e..273911a94d 100644 --- a/libs/components/editor-blocks/src/blocks/group/group-menu/ViewsMenu.tsx +++ b/libs/components/editor-blocks/src/blocks/group/group-menu/ViewsMenu.tsx @@ -20,7 +20,7 @@ export const ViewsMenu = () => { useRecastView(); const handleChange = (e: ChangeEvent) => { - setViewName(e.target.value.trim()); + setViewName(e.target.value); }; const handleKeyDown = (event: KeyboardEvent) => { @@ -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' }} diff --git a/libs/components/editor-blocks/src/blocks/group/group-menu/constant.tsx b/libs/components/editor-blocks/src/blocks/group/group-menu/constant.tsx index 0f2a1887d4..7a057eccbd 100644 --- a/libs/components/editor-blocks/src/blocks/group/group-menu/constant.tsx +++ b/libs/components/editor-blocks/src/blocks/group/group-menu/constant.tsx @@ -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: , }, - { - name: 'Table', - scene: RecastScene.Table, - icon: , - }, + // { + // name: 'Table', + // scene: RecastScene.Table, + // icon: , + // }, ] as const; diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardContext.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardContext.tsx index 13fcaa45e3..74afbe087d 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardContext.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardContext.tsx @@ -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} > - + ); diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx index 837772bed2..b527711217 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx @@ -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(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) => { e.stopPropagation(); - setEditable(true); + setEditableBlock(block.id); editor.selectionManager.activeNodeByNodeId(block.id); }; return ( - setEditable(false)}> + setEditableBlock(null)}> - + - {showKanbanRefPageFlag && !editable && ( + {!editableBlock && ( diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx index 3d223d121d..34d6dbb583 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx @@ -21,13 +21,17 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { return createPortal( 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} > void }) => { diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/dndable/drag-overlay/renderContainerDragOverlay.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/dndable/drag-overlay/renderContainerDragOverlay.tsx index 957ee8599f..b2b1382e5c 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/dndable/drag-overlay/renderContainerDragOverlay.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/dndable/drag-overlay/renderContainerDragOverlay.tsx @@ -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 ( } + card={} index={index} /> ); diff --git a/libs/components/editor-blocks/src/blocks/groupDvider/index.ts b/libs/components/editor-blocks/src/blocks/groupDvider/index.ts index 4120f1a525..458ef2f708 100644 --- a/libs/components/editor-blocks/src/blocks/groupDvider/index.ts +++ b/libs/components/editor-blocks/src/blocks/groupDvider/index.ts @@ -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 - ): Promise { - return `
`; + override async block2html(props: Block2HtmlProps) { + return `
`; } } diff --git a/libs/components/editor-blocks/src/blocks/image/ImageView.tsx b/libs/components/editor-blocks/src/blocks/image/ImageView.tsx index 886c10fc97..3bc5435575 100644 --- a/libs/components/editor-blocks/src/blocks/image/ImageView.tsx +++ b/libs/components/editor-blocks/src/blocks/image/ImageView.tsx @@ -165,12 +165,10 @@ export const ImageView = ({ block, editor }: ImageView) => { }; return ( - +
- {!isSelect ? ( - - ) : null} + {!isSelect ? : null} {imgUrl ? (
{ diff --git a/libs/components/editor-blocks/src/blocks/image/index.ts b/libs/components/editor-blocks/src/blocks/image/index.ts index e5c33514e2..b170d44413 100644 --- a/libs/components/editor-blocks/src/blocks/image/index.ts +++ b/libs/components/editor-blocks/src/blocks/image/index.ts @@ -1,10 +1,12 @@ 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; @@ -13,42 +15,40 @@ export class ImageBlock extends BaseView { 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 { + 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 - ): Promise { - const text = block.getProperty('text'); + override async block2html({ block, editor }: Block2HtmlProps) { + const textValue = block.getProperty('text'); const content = ''; - if (text) { - text.value.map(text => `${text}`).join(''); - } - // TODO: child - return `

`; + // TODO: text.value should export with style?? + const figcaption = (textValue?.value ?? []) + .map(({ text }) => `${text}`) + .join(''); + return `
${figcaption}
${figcaption}
`; } } diff --git a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx index 38dac6fc75..c45321a138 100644 --- a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx +++ b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx @@ -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 ( - +
{getNumber(properties.numberType, number)}. @@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => { - - - + ); }; diff --git a/libs/components/editor-blocks/src/blocks/numbered/index.ts b/libs/components/editor-blocks/src/blocks/numbered/index.ts index b8b3c3dc0a..3dd6f74934 100644 --- a/libs/components/editor-blocks/src/blocks/numbered/index.ts +++ b/libs/components/editor-blocks/src/blocks/numbered/index.ts @@ -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 { + 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 - ): Promise { - let content = getTextHtml(block); - content += await generateHtml(children); - return `
  1. ${content}
`; + override async block2html(props: Block2HtmlProps) { + return `
  1. ${await commonBlock2HtmlContent(props)}
`; } } diff --git a/libs/components/editor-blocks/src/blocks/page/PageView.tsx b/libs/components/editor-blocks/src/blocks/page/PageView.tsx index 64fba3ca83..123021e361 100644 --- a/libs/components/editor-blocks/src/blocks/page/PageView.tsx +++ b/libs/components/editor-blocks/src/blocks/page/PageView.tsx @@ -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 ( - + 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(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 - ): Promise { - const content = getTextHtml(block); - const childrenContent = await generateHtml(children); - return `

${content}

${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 `

${header}

${childrenHtml}`; } } diff --git a/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx b/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx index f62dfae040..7789ec5f1c 100644 --- a/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx +++ b/libs/components/editor-blocks/src/blocks/text/QuoteBlock.tsx @@ -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 { + 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 - ): Promise { - let content = getTextHtml(block); - content += await generateHtml(children); - return `
${content}
`; + override async block2html(props: Block2HtmlProps) { + return `
${await commonBlock2HtmlContent( + props + )}
`; } } @@ -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 { + 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('