merge branch develop into branch feat/doublelink220820

This commit is contained in:
xiaodong zuo 2022-09-05 11:45:19 +08:00
commit 3a9aa99ee9
175 changed files with 6576 additions and 4627 deletions

View File

@ -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"
]
}
]
}

View File

@ -5,6 +5,7 @@
"editor.codeActionsOnSave": ["source.fixAll", "source.organizeImports"],
"prettier.prettierPath": "./node_modules/prettier",
"cSpell.words": [
"aboutus",
"AUTOINCREMENT",
"Backlinks",
"blockdb",

132
README.md
View File

@ -2,12 +2,13 @@
<b>
<a href="https://affine.pro">AFFiNE.PRO</a><br>
</b>
The Next-Gen Knowledge Base to Replace Notion & Miro.
The Next-Gen Collaborative Knowledge Base
<br>
</h1>
<p align="center">
Planning, Sorting and Creating all Together. Open-source, Privacy-First, and Free to use.
Open-source and privacy-first. <br />
A free replacement for Notion & Miro.
</p>
<div align="center">
@ -18,13 +19,14 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
-->
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[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
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![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)
<br/>
[![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
<p align="center">
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
&nbsp;
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
&nbsp;
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
&nbsp;
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
&nbsp;
<a href="https://medium.com/@affineworkos"><img src="https://img.shields.io/badge/-Medium-red?style=social&logo=medium" height=25></a>
</p>
@ -48,100 +50,52 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<p align="center"><img width="1920" alt="affine_screen" src="https://user-images.githubusercontent.com/21084335/182552060-972cac0e-6258-4ccb-85bd-3bb466c30ccd.png"><p/>
# Stay Up-to-Date and Support Us
# :star: Support Us and Keep Updated :star:
![952cd7a5-70fe-48ab-b74f-23981d94d2c5](https://user-images.githubusercontent.com/79301703/182365526-df074c64-cee4-45f6-b8e0-b912f17332c6.gif)
# How to use
# Getting Started
<h3 align="center">
🥳🥳🥳 Our web live demo is ready! 🥳🥳🥳
</h3>
[![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.
<h3 align="center">
Start to play with <a href="https://affine.pro"><b>AFFiNE web version</b></a> on our landing page:
</h3>
<p align="center"><a href="https://affine.pro">
<img
src="https://user-images.githubusercontent.com/79301703/184340907-5aaa4e6e-7d37-4523-a06c-7d630e7864d1.jpeg"
width="600px"
alt="check_live_demo"
/>
</a></p>
If you have experience in front-end development, you may wish to refer to our [documentation](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine) to learn more about deploying your own version or contributing further to development.
Also, thanks to Lee who has made a [desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out.
Please notice that AFFiNE is still under Alpha stage and is not ready for production use.
# Table of contents
- [Stay Up-to-Date and Support Us](#stay-up-to-date-and-support-us)
- [How to Use](#how-to-use)
- [Table of contents](#table-of-contents)
- [Shape your page](#shape-your-page)
- [Plan your task](#plan-your-task)
- [Sort your knowledge](#sort-your-knowledge)
- [Create your story](#create-your-story)
- [Documentation](#documentation)
- [Getting Started with development](#getting-started-with-development)
- [Roadmap](#roadmap)
- [Releases](#releases)
- [Feature requests](#feature-requests)
- [FAQ](#faq)
- [The Philosophy of AFFiNE](#the-philosophy-of-affine)
- [Community](#community)
- [Contributors](#contributors)
- [Acknowledgments](#acknowledgments)
- [License](#license)
## Shape your page
![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. <br />
You can refer to our documentation which can be found from the [useful links](#useful-links) section - where you will also find ways to help contribute to the project and join our communities.
<br /><br />
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
## Create your story
We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features.
And when it comes to collaboration, these features are not just necessarily for teams -- you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators.
Affine is fully built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released.
There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together. Privacy first, open-source, customizable and ready to use, built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, with full support for real-time collaboration through peer-to-peer technology. We don't think "privacy-first" is a good excuse for not supporting modern web features.
And when it comes to collaboration, these features are not just necessarily for teams - you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators.
# Documentation
### Shape your page
AFFiNE is not yet ready for production use. For installation, you may check how to build or deploy AFFiNE from our [quick-start](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine/quick-start) guide. Alternatively, you can view our [full documentation](https://docs.affine.pro/affine/).
![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
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@ -199,45 +147,47 @@ For help, discussion about best practices, or any other conversation that would
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/doodlewind"><img src="https://avatars.githubusercontent.com/u/7312949?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Yifeng Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=doodlewind" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=doodlewind" title="Documentation">📖</a></td>
<td align="center"><a href="https://darksky.eu.org/"><img src="https://avatars.githubusercontent.com/u/25152247?v=4?s=50" width="50px;" alt=""/><br /><sub><b>DarkSky</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=darkskygit" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=darkskygit" title="Documentation">📖</a></td>
<td align="center"><a href="http://zhangchi.page/"><img src="https://avatars.githubusercontent.com/u/5910926?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Chi Zhang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=tzhangchi" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=tzhangchi" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/alt1o"><img src="https://avatars.githubusercontent.com/u/21084335?v=4?s=50" width="50px;" alt=""/><br /><sub><b>wang xinglong</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=alt1o" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=alt1o" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/DiamondThree"><img src="https://avatars.githubusercontent.com/u/24630517?v=4?s=50" width="50px;" alt=""/><br /><sub><b>DiamondThree</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=DiamondThree" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=DiamondThree" title="Documentation">📖</a></td>
<td align="center"><a href="https://lawvs.github.io/profile/"><img src="https://avatars.githubusercontent.com/u/18554747?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Whitewater</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lawvs" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=lawvs" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/zuoxiaodong0815"><img src="https://avatars.githubusercontent.com/u/53252747?v=4?s=50" width="50px;" alt=""/><br /><sub><b>xiaodong zuo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=zuoxiaodong0815" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=zuoxiaodong0815" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/SaikaSakura"><img src="https://avatars.githubusercontent.com/u/11530942?v=4?s=50" width="50px;" alt=""/><br /><sub><b>MingLIang Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/SaikaSakura"><img src="https://avatars.githubusercontent.com/u/11530942?v=4?s=50" width="50px;" alt=""/><br /><sub><b>MingLIang Wang</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=SaikaSakura" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/QiShaoXuan"><img src="https://avatars.githubusercontent.com/u/22772830?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Qi</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=QiShaoXuan" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=QiShaoXuan" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/mitsuhatu"><img src="https://avatars.githubusercontent.com/u/110213079?v=4?s=50" width="50px;" alt=""/><br /><sub><b>mitsuhatu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=mitsuhatu" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=mitsuhatu" title="Documentation">📖</a></td>
<td align="center"><a href="https://shockwave.me/"><img src="https://avatars.githubusercontent.com/u/15013925?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Austaras</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Austaras" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=Austaras" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/uptonking"><img src="https://avatars.githubusercontent.com/u/11391549?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Jin Yao</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=uptonking" title="Code">💻</a> <a href="https://github.com/toeverything/AFFiNE/commits?author=uptonking" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/HeJiachen-PM"><img src="https://avatars.githubusercontent.com/u/79301703?v=4?s=50" width="50px;" alt=""/><br /><sub><b>HeJiachen-PM</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=HeJiachen-PM" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Yipei-Operation"><img src="https://avatars.githubusercontent.com/u/79373028?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Yipei Wei</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Yipei-Operation" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/fanjing22"><img src="https://avatars.githubusercontent.com/u/109729699?v=4?s=50" width="50px;" alt=""/><br /><sub><b>fanjing22</b></sub></a><br /><a href="#design-fanjing22" title="Design">🎨</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/fanjing22"><img src="https://avatars.githubusercontent.com/u/109729699?v=4?s=50" width="50px;" alt=""/><br /><sub><b>fanjing22</b></sub></a><br /><a href="#design-fanjing22" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/Svaney-ssman"><img src="https://avatars.githubusercontent.com/u/110808979?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Svaney</b></sub></a><br /><a href="#design-Svaney-ssman" title="Design">🎨</a></td>
<td align="center"><a href="http://xell.me/"><img src="https://avatars.githubusercontent.com/u/132558?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Guozhu Liu</b></sub></a><br /><a href="#design-xell" title="Design">🎨</a></td>
<td align="center"><a href="https://github.com/fyZheng07"><img src="https://avatars.githubusercontent.com/u/63830919?v=4?s=50" width="50px;" alt=""/><br /><sub><b>fyZheng07</b></sub></a><br /><a href="#eventOrganizing-fyZheng07" title="Event Organizing">📋</a> <a href="#userTesting-fyZheng07" title="User Testing">📓</a></td>
<td align="center"><a href="https://github.com/CJSS"><img src="https://avatars.githubusercontent.com/u/4605025?v=4?s=50" width="50px;" alt=""/><br /><sub><b>CJSS</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=CJSS" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/clean-software"><img src="https://avatars.githubusercontent.com/u/62192072?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Carlos Rafael </b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=CarlosZoft" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/caleboleary"><img src="https://avatars.githubusercontent.com/u/12816579?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Caleb OLeary</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=caleboleary" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/JimmFly"><img src="https://avatars.githubusercontent.com/u/102217452?v=4?s=50" width="50px;" alt=""/><br /><sub><b>JimmFly</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=JimmFly" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/JimmFly"><img src="https://avatars.githubusercontent.com/u/102217452?v=4?s=50" width="50px;" alt=""/><br /><sub><b>JimmFly</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=JimmFly" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/westongraham"><img src="https://avatars.githubusercontent.com/u/89493023?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Weston Graham</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=westongraham" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/pointmax"><img src="https://avatars.githubusercontent.com/u/49361135?v=4?s=50" width="50px;" alt=""/><br /><sub><b>pointmax</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=pointmax" title="Documentation">📖</a></td>
<td align="center"><a href="https://liby.github.io/notes"><img src="https://avatars.githubusercontent.com/u/38807139?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Bryan Lee</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=liby" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chenmoonmo"><img src="https://avatars.githubusercontent.com/u/36295999?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Simon Li</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=chenmoonmo" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/githbq"><img src="https://avatars.githubusercontent.com/u/10009709?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Bob Hu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=githbq" title="Code">💻</a></td>
<td align="center"><a href="https://quavo.vercel.app/"><img src="https://avatars.githubusercontent.com/u/67266933?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Quavo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lucky-chap" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/LuciNyan"><img src="https://avatars.githubusercontent.com/u/22126563?v=4?s=50" width="50px;" alt=""/><br /><sub><b>子瞻 Luci</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=LuciNyan" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/LuciNyan"><img src="https://avatars.githubusercontent.com/u/22126563?v=4?s=50" width="50px;" alt=""/><br /><sub><b>子瞻 Luci</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=LuciNyan" title="Code">💻</a></td>
<td align="center"><a href="http://blog.ipili.me/"><img src="https://avatars.githubusercontent.com/u/4948120?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Horus</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=m1911star" title="Code">💻</a> <a href="#platform-m1911star" title="Packaging/porting to new platform">📦</a></td>
<td align="center"><a href="https://segmentfault.com/u/qzuser_584786517d31a"><img src="https://avatars.githubusercontent.com/u/15103283?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Super.x</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=fanshyiis" title="Code">💻</a></td>
<td align="center"><a href="https://wangyu-1999.github.io/"><img src="https://avatars.githubusercontent.com/u/80874770?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Wang Yu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=wangyu-1999" title="Code">💻</a></td>
<td align="center"><a href="https://felixc.at/"><img src="https://avatars.githubusercontent.com/u/1006477?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Felix Yan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=felixonmars" title="Code">💻</a></td>
</tr>
</table>
@ -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.

View File

@ -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"
}

View File

@ -22,16 +22,23 @@ import { type BlockEditor } from '@toeverything/components/editor-core';
import { useFlag } from '@toeverything/datasource/feature-flags';
import { CollapsiblePageTree } from './collapsible-page-tree';
import { Tabs } from './components/tabs';
import { TabMap, TAB_TITLE } from './components/tabs/Tabs';
import { TOC } from './components/toc';
import { WorkspaceName } from './workspace-name';
type PageProps = {
workspace: string;
};
export function Page(props: PageProps) {
const [activeTab, setActiveTab] = useState(
TabMap.get(TAB_TITLE.PAGES).value
);
const { page_id } = useParams();
const { showSpaceSidebar, fixedDisplay, setSpaceSidebarVisible } =
useShowSpaceSidebar();
const dailyNotesFlag = useFlag('BooleanDailyNotes', false);
const onTabChange = v => setActiveTab(v);
return (
<LigoApp>
@ -50,31 +57,37 @@ export function Page(props: PageProps) {
>
<WorkspaceName />
<Tabs />
<Tabs activeTab={activeTab} onTabChange={onTabChange} />
<WorkspaceSidebarContent>
<div>
{dailyNotesFlag && (
{activeTab === TabMap.get(TAB_TITLE.PAGES).value && (
<div>
{dailyNotesFlag && (
<div>
<CollapsibleTitle title="Daily Notes">
<CalendarHeatmap />
</CollapsibleTitle>
</div>
)}
<div>
<CollapsibleTitle title="Daily Notes">
<CalendarHeatmap />
<CollapsibleTitle
title="ACTIVITIES"
initialOpen={false}
>
<Activities />
</CollapsibleTitle>
</div>
)}
<div>
<CollapsibleTitle
title="ACTIVITIES"
initialOpen={false}
>
<Activities />
</CollapsibleTitle>
<div>
<CollapsiblePageTree title="PAGES">
{page_id ? <PageTree /> : null}
</CollapsiblePageTree>
</div>
</div>
<div>
<CollapsiblePageTree title="PAGES">
{page_id ? <PageTree /> : null}
</CollapsiblePageTree>
</div>
</div>
)}
{activeTab === TabMap.get(TAB_TITLE.TOC).value && (
<TOC />
)}
</WorkspaceSidebarContent>
</WorkspaceSidebar>
</LigoLeftContainer>
@ -105,6 +118,7 @@ const EditorContainer = ({
const obv = new ResizeObserver(e => {
setPageClientWidth(e[0].contentRect.width);
});
obv.observe(scrollContainer);
return () => obv.disconnect();
}
@ -175,7 +189,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({
width: 300,
minWidth: 300,
borderRadius: '0px 10px 10px 0px',
boxShadow: theme.affine.shadows.shadow1,
boxShadow: theme.affine.shadows.shadow2,
backgroundColor: '#FFFFFF',
transitionProperty: 'left',
transitionDuration: '0.35s',

View File

@ -1,6 +1,5 @@
import { styled } from '@toeverything/components/ui';
import type { ValueOf } from '@toeverything/utils';
import { useState } from 'react';
const StyledTabs = styled('div')(({ theme }) => {
return {
@ -56,32 +55,35 @@ const StyledTabTitle = styled('div')<{
}
`;
const TAB_TITLE = {
PAGES: 'pages',
GALLERY: 'gallery',
TOC: 'toc',
export const TAB_TITLE = {
PAGES: 'PAGES',
GALLERY: 'GALLERY',
TOC: 'TOC',
} as const;
const TabMap = new Map<TabKey, { value: TabValue; disabled?: boolean }>([
['PAGES', { value: 'pages' }],
['GALLERY', { value: 'gallery', disabled: true }],
['TOC', { value: 'toc' }],
export const TabMap = new Map<
TabValue,
{ value: TabValue; disabled?: boolean }
>([
[TAB_TITLE.PAGES, { value: TAB_TITLE.PAGES }],
[TAB_TITLE.GALLERY, { value: TAB_TITLE.GALLERY, disabled: true }],
[TAB_TITLE.TOC, { value: TAB_TITLE.TOC }],
]);
type TabKey = keyof typeof TAB_TITLE;
type TabValue = ValueOf<typeof TAB_TITLE>;
const Tabs = () => {
const [activeValue, setActiveTab] = useState<TabValue>(TAB_TITLE.PAGES);
interface Props {
activeTab: TabValue;
onTabChange: (v: TabValue) => void;
}
const onClick = (v: TabValue) => {
setActiveTab(v);
};
const Tabs = (props: Props) => {
const { activeTab, onTabChange } = props;
return (
<StyledTabs>
{[...TabMap.entries()].map(([k, { value, disabled = false }]) => {
const isActive = activeValue === value;
const isActive = activeTab === value;
return (
<StyledTabTitle
@ -89,7 +91,7 @@ const Tabs = () => {
className={isActive ? 'active' : ''}
isActive={isActive}
isDisabled={disabled}
onClick={() => onClick(value)}
onClick={() => onTabChange(value)}
>
{k}
</StyledTabTitle>

View File

@ -0,0 +1,202 @@
import type { Virgo } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service';
import { useCurrentEditors } from '@toeverything/datasource/state';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useParams } from 'react-router';
import { BLOCK_TYPES } from './toc-enum';
import {
destroyEventList,
getContentByAsyncBlocks,
getPageTOC,
} from './toc-util';
import type { ListenerMap, TOCType } from './types';
const StyledTOCItem = styled('a')<{ type?: string; isActive?: boolean }>(
({ type, isActive }) => {
const common = {
height: '32px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
color: isActive ? '#3E6FDB' : '#4C6275',
};
if (type === BLOCK_TYPES.HEADING1) {
return {
...common,
padding: '0 12px',
fontWeight: '600',
fontSize: '16px',
};
}
if (type === BLOCK_TYPES.HEADING2) {
return {
...common,
padding: '0 32px',
fontSize: '14px',
};
}
if (type === BLOCK_TYPES.HEADING3) {
return {
...common,
padding: '0 52px',
fontSize: '12px',
};
}
if (type === BLOCK_TYPES.GROUP) {
return {
...common,
margin: '6px 0px',
height: '46px',
padding: '6px 12px',
fontWeight: '600',
fontSize: '16px',
borderTop: '0.5px solid #E0E6EB',
borderBottom: '0.5px solid #E0E6EB',
color: isActive ? '#3E6FDB' : '#98ACBD',
};
}
return {};
}
);
const StyledItem = styled('div')(props => {
return {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
});
const TOCContext = createContext(null);
const TOCItem = props => {
const { activeBlockId, onClick } = useContext(TOCContext);
const { id, type, text } = props;
const isActive = id === activeBlockId;
return (
<StyledTOCItem
key={id}
isActive={isActive}
type={type}
onClick={() => onClick(id)}
>
<StyledItem>{text}</StyledItem>
</StyledTOCItem>
);
};
const renderTOCContent = tocDataSource => {
return (
<>
{tocDataSource.map(tocItem => {
if (tocItem?.length) {
return renderTOCContent(tocItem);
}
const { id, type, text } = tocItem;
return <TOCItem key={id} id={id} type={type} text={text} />;
})}
</>
);
};
export const TOC = () => {
const { page_id } = useParams();
const [tocDataSource, setTocDataSource] = useState<TOCType[]>([]);
const [activeBlockId, setActiveBlockId] = useState('');
/* store page/block unmount-listener */
const listenerMapRef = useRef<ListenerMap>(new Map());
const { currentEditors } = useCurrentEditors();
const editor = currentEditors[page_id] as Virgo;
const updateTocDataSource = useCallback(async () => {
if (!editor) {
return null;
}
const listenerMap = listenerMapRef.current;
/* page listener: trigger update-notice when add new group */
const pageAsyncBlock = (await editor.getBlockByIds([page_id]))?.[0];
if (!listenerMap.has(pageAsyncBlock.id)) {
listenerMap.set(
pageAsyncBlock.id,
pageAsyncBlock.onUpdate(updateTocDataSource)
);
}
/* block listener: trigger update-notice when change block content */
const { children = [] } =
(await editor.queryByPageId(page_id))?.[0] || {};
const asyncBlocks = (await editor.getBlockByIds(children)) || [];
const { tocContents } = await getContentByAsyncBlocks(
asyncBlocks,
updateTocDataSource,
listenerMap
);
/* toc: flat content */
const tocDataSource = getPageTOC(asyncBlocks, tocContents);
setTocDataSource(tocDataSource);
}, [editor, page_id]);
/* init toc and add page/block update-listener & unmount-listener */
useEffect(() => {
(async () => {
await updateTocDataSource();
})();
/* remove listener when unmount component */
return () => destroyEventList(listenerMapRef.current);
}, [updateTocDataSource]);
const onClick = async (blockId?: string) => {
setActiveBlockId(blockId);
const block = await editor.getBlockById(blockId);
await editor.scrollManager.scrollIntoViewByBlockId(blockId);
if (!block || block.type === Protocol.Block.Type.group) {
// the group block has its own background
return;
}
// See https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
block.dom?.animate(
[
{
backgroundColor: 'rgba(152, 172, 189, 0.1)',
},
{
backgroundColor: 'rgba(152, 172, 189, 0)',
},
],
{
delay: 500,
duration: 700,
easing: 'linear',
}
);
};
return (
<TOCContext.Provider value={{ activeBlockId, onClick }}>
<div>{renderTOCContent(tocDataSource)}</div>
</TOCContext.Provider>
);
};

View File

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

View File

@ -0,0 +1,6 @@
export enum BLOCK_TYPES {
GROUP = 'group',
HEADING1 = 'heading1',
HEADING2 = 'heading2',
HEADING3 = 'heading3',
}

View File

@ -0,0 +1,96 @@
import { AsyncBlock } from '@toeverything/components/editor-core';
import { BLOCK_TYPES } from './toc-enum';
import type { ListenerMap, TOCType } from './types';
/* 😞😞sorry, I don't know how to define unlimited dimensions array */
const getContentByAsyncBlocks = async (
asyncBlocks: AsyncBlock[] = [],
callback: () => void,
listenerMap: ListenerMap
): Promise<{
tocContents: any[];
}> => {
const collect = async (asyncBlocks): Promise<any[]> => {
/* maybe should recast it to tail recursion */
return await Promise.all(
asyncBlocks.map(async (asyncBlock: AsyncBlock) => {
const asyncBlocks = await asyncBlock?.children();
if (asyncBlocks?.length) {
return collect(asyncBlocks);
}
/* add only once event listener for every block */
if (!listenerMap.has(asyncBlock?.id)) {
/* get update notice */
const destroyHandler = asyncBlock?.onUpdate(callback);
/* collect destroy handlers */
listenerMap.set(asyncBlock?.id, destroyHandler);
}
const { id, type } = asyncBlock;
switch (type) {
case BLOCK_TYPES.GROUP:
case BLOCK_TYPES.HEADING1:
case BLOCK_TYPES.HEADING2:
case BLOCK_TYPES.HEADING3: {
const properties = await asyncBlock?.getProperties();
return {
id,
type,
text: properties?.text?.value?.[0]?.text || '',
};
}
default:
return null;
}
})
);
};
return {
tocContents: await collect(asyncBlocks),
};
};
/**
* get flat toc
* @param asyncBlocks
* @param tocContents
*/
const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TOCType[] => {
return tocContents
.reduce((tocGroupContent, tocContent, index) => {
const { id, type } = asyncBlocks[index];
const groupContent = {
id,
type,
text: 'Untitled Group',
};
tocGroupContent.push(
!tocContent.flat(Infinity).filter(Boolean).length
? groupContent
: tocContent
);
return tocGroupContent;
}, [])
.flat(Infinity)
.filter(Boolean);
};
/* destroy page/block update-listener */
const destroyEventList = (listenerMap: ListenerMap) => {
const eventListeners = listenerMap.values();
listenerMap.clear();
for (const eventListener of eventListeners) {
eventListener?.();
}
};
export { getPageTOC, getContentByAsyncBlocks, destroyEventList };

View File

@ -0,0 +1,7 @@
export type TOCType = {
id: string;
type: string;
text: string;
};
export type ListenerMap = Map<string, () => void>;

View File

@ -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)$/,

View File

@ -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",

View File

@ -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 (
<>
<AFFiNEHeader />
<Grid xs={12} sx={{ display: 'flex', marginTop: '4vh!important' }}>
<Box
sx={{
display: 'inline-flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
fontWeight: 'bold',
textAlign: 'center',
}}
>
<Typography
fontSize="64px"
fontWeight={900}
sx={{
marginRight: '0.25em',
'@media (max-width: 1024px)': {
fontSize: '32px',
marginRight: 0,
},
}}
>
To Shape, not to adapt.
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography
level="h3"
fontSize="24px"
fontWeight={'400'}
sx={{
color: '#888',
'@media (max-width: 1024px)': {
fontSize: '16px',
marginRight: 0,
},
}}
>
Deliver Building Blocks for Future SaaS Applications.
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Box
component="article"
className="markdown-body"
sx={{
minWidth: '200px',
maxWidth: '720px',
}}
>
<AboutText />
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Box
component="article"
className="markdown-body"
sx={{
minWidth: '200px',
maxWidth: '720px',
}}
>
<AFFiNEImage
src={KeepUpdate}
alt="AFFiNE keep update"
sx={{ cursor: 'pointer' }}
onClick={() =>
window.open(
'https://github.com/toeverything/AFFiNE'
)
}
/>
</Box>
</Grid>
<AFFiNEFooter keepupdate={false} />
</>
);
};

600
apps/venus/src/app/App.tsx Normal file
View File

@ -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 (
<Alternatives
width={`${maxWidth}em`}
sx={{
margin: 'auto',
marginRight: '1em',
transition: 'width .5s',
'@media (max-width: 1024px)': {
width: '8em',
},
}}
>
<Box
className={clsx(
'scroll-element',
'primary',
active && 'active'
)}
>
<Typography
fontSize="96px"
fontWeight={900}
sx={{
color: '#06449d',
textAlign: 'right',
overflow: 'hidden',
'@media (max-width: 1024px)': {
fontSize: '32px',
},
}}
>
{last}
</Typography>
</Box>
<Box
className={clsx(
'scroll-element',
'primary',
active && 'active'
)}
sx={{
marginTop: '96px',
textAlign: 'right',
overflow: 'hidden',
'@media (max-width: 1024px)': {
marginTop: '48px',
},
}}
>
<Typography
fontSize="96px"
fontWeight={900}
sx={{
color: '#06449d',
overflow: 'hidden',
'@media (max-width: 1024px)': {
fontSize: '32px',
},
}}
>
{current}
</Typography>
</Box>
</Alternatives>
);
};
const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => {
const matches = useMediaQuery('(max-width: 1024px)');
const { t } = useTranslation();
return (
<Button
onClick={() => {
window.open('https://livedemo.affine.pro/');
}}
{...(props.flat ? { variant: 'plain' } : {})}
{...{
sx: {
margin: 'auto 1em',
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
...(props.flat
? {
padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' },
}
: {}),
...(props.center
? {
padding: '0.5em 1em',
fontSize: '2em',
backgroundColor: '#000',
':hover': {
backgroundColor: '#0c60d9',
boxShadow: '2px 2px 20px #08f4',
},
}
: {}),
},
}}
startIcon={<LogoIcon />}
size="lg"
>
{t('Try it Online')}
</Button>
);
};
export function App() {
const matches = useMediaQuery('(max-width: 1024px)');
const navigate = useNavigate();
const { t, i18n } = useTranslation();
const changeLanguage = (event: any) => {
i18n.changeLanguage(event);
};
return (
<>
<AFFiNEHeader />
<Grid xs={12} sx={{ display: 'flex', marginTop: '12vh!important' }}>
<Box
sx={{
display: 'inline-flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
fontWeight: 'bold',
textAlign: 'center',
}}
>
<Typography
fontSize="96px"
fontWeight={900}
sx={{
marginRight: '0.25em',
'@media (max-width: 1024px)': {
fontSize: '32px',
marginRight: 0,
},
}}
>
{t('Open Source')},
</Typography>
<Typography
fontSize="96px"
fontWeight={900}
sx={{
'@media (max-width: 1024px)': {
fontSize: '32px',
},
}}
>
{t('Privacy First')}
</Typography>
</Box>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
flexFlow: 'wrap',
overflow: 'auto',
}}
>
<Box
sx={{
display: 'inline-flex',
flexFlow: 'wrap',
margin: 'auto',
fontWeight: 'bold',
textAlign: 'center',
}}
>
<Product />
<Typography
fontSize="96px"
fontWeight={900}
sx={{
color: '#06449d',
margin: 'auto',
'@media (max-width: 1024px)': {
fontSize: '32px',
},
}}
>
{t('Alternative')}
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography
level="h3"
fontWeight={'400'}
sx={{ color: '#888' }}
>
{t('description1.part1')}
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
marginTop: '1.5em',
marginBottom: '12vh!important',
rawGap: '1em',
}}
>
<GitHub center />
<AFFiNEOnline center />
</Box>
</Grid>
<Grid
xs={12}
sx={{ display: 'flex', maxWidth: '1200px', margin: 'auto' }}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
margin: 'auto',
transition: 'all .5s',
transform: 'scale(0.98)',
boxShadow: '2px 2px 40px #0002',
':hover': {
transform: 'scale(1)',
boxShadow: '2px 2px 40px #0004',
},
}}
>
<AFFiNEImage src={PageImage} alt="AFFiNE main ui" />
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
margin: 'auto',
marginTop: '12em',
}}
>
<Typography
level={matches ? 'h2' : 'h1'}
fontWeight={'bold'}
>
{t('description1.part2')}
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
margin: 'auto',
justifyContent: 'center',
textAlign: 'center',
marginBottom: '12em',
}}
>
<Typography fontSize="1.2em">
{t('description1.part3')}
</Typography>
<Typography fontSize="1.2em">
{t('description1.part4')}
</Typography>
</Box>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
flexDirection: matches ? 'column' : 'row',
marginBottom: '12em',
}}
>
<Grid
xs={matches ? 12 : 3}
sx={{
display: 'flex',
...(matches
? {}
: { marginLeft: '4em', marginRight: '2em' }),
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'left',
alignSelf: 'center',
textAlign: 'left',
width: '100%',
}}
>
<Typography
level="h2"
fontWeight={'bold'}
style={{ marginBottom: '0.5em' }}
>
{t('description2.part1')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description2.part2')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description2.part3')}
</Typography>
</Box>
</Grid>
<Grid
xs={matches ? 12 : 9}
sx={{ display: 'flex', width: '100%' }}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'left',
textAlign: 'left',
transition: 'all .5s',
transform: 'scale(0.98)',
boxShadow: '2px 2px 40px #0002',
':hover': {
transform: 'scale(1)',
boxShadow: '2px 2px 40px #0004',
},
}}
>
<AFFiNEImage
src={ShapeImage}
alt="AFFiNE Shape Your Page"
/>
</Box>
</Grid>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
flexDirection: matches ? 'column' : 'row-reverse',
marginBottom: '12em',
}}
>
<Grid
xs={matches ? 12 : 6}
sx={{
display: 'flex',
...(matches ? {} : { marginLeft: '4em' }),
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'left',
alignSelf: 'center',
textAlign: 'left',
width: '100%',
}}
>
<Typography
level="h2"
fontWeight={'bold'}
style={{ marginBottom: '0.5em' }}
>
{t('description3.part1')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description3.part2')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description3.part3')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description3.part4')}
</Typography>
</Box>
</Grid>
<Grid
xs={matches ? 12 : 6}
sx={{ display: 'flex', width: '100%' }}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'left',
textAlign: 'left',
transition: 'all .5s',
transform: 'scale(0.98)',
boxShadow: '2px 2px 40px #0002',
':hover': {
transform: 'scale(1)',
boxShadow: '2px 2px 40px #0004',
},
}}
>
<AFFiNEImage
src={TaskImage}
alt="AFFiNE Plan Your Task"
/>
</Box>
</Grid>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
margin: 'auto',
textAlign: 'center',
marginBottom: '4em',
}}
>
<Typography
level="h2"
fontWeight={'bold'}
style={{ marginBottom: '0.5em' }}
>
{t('description4.part1')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description4.part2')}
</Typography>
<Typography
fontSize="1.2em"
style={{ marginBottom: '0.25em' }}
>
{t('description4.part3')}
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex', marginBottom: '12em' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
margin: 'auto',
transition: 'all .5s',
transform: 'scale(0.98)',
':hover': {
transform: 'scale(1)',
},
}}
>
<AFFiNEImage
src={CollaborationImage}
alt="AFFiNE Privacy-first, and collaborative"
/>
</Box>
</Grid>
<AFFiNEFooter />
</>
);
}

View File

@ -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 ? (
<>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
<AFFiNEImage src={LogoImage} alt="AFFiNE Logo" />
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography fontSize={'1.5em'}>
{t('BuildFor')}
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex', marginBottom: '8em' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography level="h3" sx={{ display: 'flex' }}>
<span style={{ alignSelf: 'center' }}>
{t('KeepUpdated')}
</span>
<GitHub />
</Typography>
</Box>
</Grid>
</>
) : null}
<Grid xs={12} sx={{ display: 'flex', marginBottom: '2em' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography level="h2" sx={{ display: 'flex' }}>
{t('Join')}
</Typography>
</Box>
</Grid>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
maxWidth: '400px',
margin: 'auto',
marginBottom: '2em',
'--joy-shadow-sm': 0,
}}
>
<Box sx={{ display: 'flex', width: '100%' }}>
<Button
variant="plain"
sx={{
display: 'flex',
flexDirection: 'column',
margin: 'auto',
padding: '1em',
minWidth: '6em',
}}
onClick={() =>
window.open(
'https://github.com/toeverything/AFFiNE/'
)
}
>
<Grid
xs={12}
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<GitHubIcon
sx={{ width: '36px', height: '36px' }}
/>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
margin: 'auto',
marginTop: '1em',
}}
>
<Typography
sx={{
display: 'flex',
color: '#888',
fontSize: '0.5em',
}}
>
GitHub
</Typography>
</Grid>
</Button>
</Box>
<Box sx={{ display: 'flex', width: '100%' }}>
<Button
variant="plain"
sx={{
display: 'flex',
flexDirection: 'column',
margin: 'auto',
padding: '1em',
minWidth: '6em',
}}
onClick={() =>
window.open('https://www.reddit.com/r/Affine/')
}
>
<Grid
xs={12}
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<RedditIcon
sx={{ width: '36px', height: '36px' }}
/>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
margin: 'auto',
marginTop: '1em',
}}
>
<Typography
sx={{
display: 'flex',
color: '#888',
fontSize: '0.5em',
}}
>
Reddit
</Typography>
</Grid>
</Button>
</Box>
<Box
sx={{
display: 'flex',
width: '100%',
}}
>
<Button
variant="plain"
sx={{
display: 'flex',
flexDirection: 'column',
margin: 'auto',
padding: '1em',
minWidth: '6em',
}}
onClick={() => window.open('https://t.me/affineworkos')}
>
<Grid
xs={12}
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<TelegramIcon
sx={{ width: '36px', height: '36px' }}
/>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
margin: 'auto',
marginTop: '1em',
}}
>
<Typography
sx={{
display: 'flex',
color: '#888',
fontSize: '0.5em',
}}
>
Telegram
</Typography>
</Grid>
</Button>
</Box>
<Box sx={{ display: 'flex', width: '100%' }}>
<Button
variant="plain"
sx={{
display: 'flex',
flexDirection: 'column',
margin: 'auto',
padding: '1em',
minWidth: '6em',
}}
onClick={() =>
window.open('https://discord.gg/yz6tGVsf5p')
}
>
<Grid
xs={12}
sx={{
display: 'flex',
justifyContent: 'center',
}}
>
<DiscordIcon
sx={{
width: '36px',
height: '36px',
color: '#09449d',
}}
/>
</Grid>
<Grid
xs={12}
sx={{
display: 'flex',
margin: 'auto',
marginTop: '1em',
}}
>
<Typography
sx={{
display: 'flex',
color: '#888',
fontSize: '0.5em',
}}
>
Discord
</Typography>
</Grid>
</Button>
</Box>
</Box>
<Grid xs={12} sx={{ display: 'flex', marginBottom: '2em' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography sx={{ display: 'flex', color: '#888' }}>
AFFiNE is an
<span
style={{
color: '#5085f6cc',
margin: 'auto 0.25em',
}}
>
#OpenSource
</span>
company
</Typography>
</Box>
</Grid>
<Grid xs={12} sx={{ display: 'flex', marginBottom: '2em' }}>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
margin: 'auto',
textAlign: 'center',
}}
>
<Typography sx={{ display: 'flex', color: '#888' }}>
Copyright © 2022 AFFiNE.
</Typography>
</Box>
</Grid>
</>
);
};
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 (
<Grid
container
spacing={2}
sx={{
maxWidth: '1280px',
margin: 'auto',
}}
>
<Grid xs={6}>
<Button
size="lg"
variant="plain"
sx={{
padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' },
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
}}
onClick={() => navigate('/')}
>
AFFiNE
</Button>
</Grid>
<Grid xs={6} sx={{ display: 'flex', justifyContent: 'right' }}>
<GitHub flat />
<Button
onClick={() => window.open('https://blog.affine.pro')}
variant="plain"
sx={{
padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' },
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
}}
size="lg"
>
Blog
</Button>
<Button
onClick={() => navigate('/aboutus')}
variant="plain"
sx={{
padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' },
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
}}
size="lg"
>
About Us
</Button>
<Select
defaultValue="en"
sx={{ display: matchesIPAD ? 'none' : 'intial' }}
onChange={changeLanguage}
>
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.text}
</Option>
))}
</Select>
</Grid>
</Grid>
);
};

View File

@ -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 (
<SvgIcon
{...props}
width="71"
height="55"
viewBox="0 0 71 55"
fill="currentcolor"
>
<g clipPath="url(#clip0)">
<path
d="M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z"
fill="currentcolor"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="71" height="55" fill="white" />
</clipPath>
</defs>
</SvgIcon>
);
};
export const GitHub = (props: { center?: boolean; flat?: boolean }) => {
const matches = useMediaQuery('(max-width: 1024px)');
const { t } = useTranslation();
return (
<Button
onClick={() => {
window.open('https://github.com/toeverything/AFFiNE');
}}
{...(props.flat ? { variant: 'plain' } : {})}
{...{
sx: {
margin: 'auto 1em',
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
...(props.flat
? {
padding: matches ? '0' : '0 0.5em',
margin: 'auto 0',
':hover': { backgroundColor: 'unset' },
}
: {}),
...(props.center
? {
padding: '0.5em 1em',
fontSize: '2em',
':hover': {
backgroundColor: '#0c60d9',
boxShadow: '2px 2px 20px #08f4',
},
}
: {}),
},
}}
startIcon={<GitHubIcon />}
size="lg"
>
{props.center ? t('Check GitHub') : t('GitHub')}
</Button>
);
};

View File

@ -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 (
<>
<Typography>
<span style={{ fontSize: '1em' }}>
<Box
component="a"
href={props.link}
sx={{
pointerEvents: 'none',
color: '#000!important',
'&:hover': {
color: 'unset',
},
}}
>
{props.name}
</Box>
</span>
<span style={{ color: '#57606a' }}>
{' | '}
{props.title}
</span>
</Typography>
{props.description ? (
<Typography>
<span style={{ color: '#aaa' }}>{props.description}</span>
</Typography>
) : null}
</>
);
};
export const Padding = () => {
return <div style={{ paddingTop: '1em' }} />;
};

View File

@ -0,0 +1,88 @@
import { Name, Padding } from './MdxMarks';
<Padding />
## 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)
<Padding />
## Team Member
### Founder & Co-founders
<Name
name="Jiachen He"
link="https://github.com/HeJiachen-PM"
title="Founder & Product Owner"
description="The PM and CEO guy."
/>
<Name
name="Chi Zhang"
link="https://github.com/tzhangchi"
title="Co-founder & Head of Engineering"
description="He builds AFFiNE."
/>
<Name
name="Xiang Wang"
link="https://github.com/xiangwang1223"
title="Co-founder & Head of Machine Learning Algorithms"
description="Making everyone's life easier by embracing the power of AI."
/>
<Name
name="Yipei Wei"
link="https://github.com/Yipei-Operation"
title="Co-founder & Head of Community Support"
description="She talks to people so that AFFiNE is something people want."
/>
### Architectural Developers
<Name
name="Yifeng Wang"
link="https://github.com/doodlewind"
title="Head of Graphics Architecture"
/>
<Name
name="Xiaodong Zuo"
link="https://github.com/zuoxiaodong0815"
title="Head of Software Architecture"
/>
<Name
name="Wenhao Tan"
link="https://github.com/"
title="Director of Performance
and Security"
/>
<Name
name="Xinglong Wang"
link="https://github.com/alt1o"
title="Head of Collaboration and Creativity"
/>
<Name
name="Mingliang Wang"
link="https://github.com/SaikaSakura"
title="Head of Structural Editing"
/>
<Padding />
## 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.

View File

@ -1,5 +1,7 @@
{
"translation": {
"Blog": "Blog",
"AboutUs": "About AboutUs",
"Open Source": "Open Source",
"Privacy First": "Privacy First",
"Alternative": "Alternative",

View File

@ -1,5 +1,7 @@
{
"translation": {
"Blog": "博客",
"AboutUs": "关于我们",
"Open Source": "开源",
"Privacy First": "隐私第一",
"Alternative": "的另一种选择",

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -6,6 +6,27 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://affine.pro/" />
<meta name="twitter:title" content="AFFiNE - All In One Workos" />
<meta
name="twitter:description"
content="Affine is the next-generation collaborative knowledge base for professionals."
/>
<meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.png" />
<meta property="og:type" content="website" />
<meta property="og:title" content="AFFiNE - All In One Workos" />
<meta property="og:site_name" content="AFFiNE - All In One Workos" />
<meta property="og:url" content="https://affine.pro/" />
<meta property="og:image" content="https://affine.pro/og.png" />
<meta
property="og:description"
content="Affine is the next-generation collaborative knowledge base for professionals."
/>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>

View File

@ -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(
<StrictMode>
<BrowserRouter>
<App />
<VenusRoutes />
</BrowserRouter>
</StrictMode>
);

4
apps/venus/src/mdx.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.mdx' {
let MDXComponent: (props: any) => JSX.Element;
export default MDXComponent;
}

View File

@ -19,5 +19,5 @@
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.d.ts"]
}

View File

@ -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'],

View File

@ -1,8 +1,8 @@
# Welcome to ourcontributing guide <!-- omit in toc -->
# Welcome to our contributing guide <!-- omit in toc -->
Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:.
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 dont 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 dont 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/)

View File

@ -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.

View File

@ -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),
},
},
});
},
}}
/>

View File

@ -5,24 +5,12 @@ import { services } from '@toeverything/datasource/db-service';
import { usePageClientWidth } from '@toeverything/datasource/state';
import { useEffect, useState } from 'react';
const getBindings = (workspace: string, rootBlockId: string) => {
return services.api.editorBlock
.get({
workspace: workspace,
ids: [rootBlockId],
})
.then(blcoks => {
return blcoks[0].properties.bindings?.value;
});
};
export const useShapes = (workspace: string, rootBlockId: string) => {
const { pageClientWidth } = usePageClientWidth();
// page padding left and right total 300px
const editorShapeInitSize = pageClientWidth - 300;
const [blocks, setBlocks] = useState<{
shapes: [ReturnEditorBlock[]];
bindings: string;
}>();
useEffect(() => {
Promise.all([
@ -43,11 +31,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
return shapes;
}),
]).then(shapes => {
getBindings(workspace, rootBlockId).then(bindings => {
setBlocks({
shapes,
bindings: bindings,
});
setBlocks({
shapes: shapes,
});
});
@ -65,11 +50,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
return childBlock;
})
).then(shapes => {
getBindings(workspace, rootBlockId).then(bindings => {
setBlocks({
shapes: [shapes],
bindings: bindings,
});
setBlocks({
shapes: [shapes],
});
});
})
@ -104,8 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
return acc;
}, {} as Record<string, TDShape>);
return {
shapes: blocksShapes,
bindings: JSON.parse(blocks?.bindings ?? '{}'),
};
};

View File

@ -1,7 +1,6 @@
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import {
RenderBlock,
RenderRoot,
type BlockEditor,
} from '@toeverything/components/editor-core';
@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
editor={editor}
editorElement={AffineEditor as any}
scrollBlank={scrollBlank}
>
<RenderBlock blockId={editor.getRootBlockId()} />
</RenderRoot>
/>
);
}
);

View File

@ -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;

View File

@ -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': {

View File

@ -21,6 +21,9 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
let activeShape = shapes[0];
let toNextShapBindings: ArrowBinding[] = [];
let bindingId = '';
if (!activeShape) {
return;
}
Object.keys(bindings).forEach(key => {
if (bindings[key].toId === activeShape.id) {
bindingId = bindings[key].fromId;
@ -35,7 +38,6 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
toNextShapBindings.forEach(binding => {
if (binding.toId !== activeShape.id) {
allShape.forEach(item => {
console.log(item);
if (item.id === binding.toId) {
ArrowToArr.push(item);
}
@ -44,7 +46,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
});
setarrowToArr(ArrowToArr);
return () => {};
}, [app.page.bindings, app.shapes]);
}, [app.page.bindings]);
const jumpToNextShap = (shape: TDShape) => {
app.zoomToShapes([shape]);
};
@ -68,7 +70,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
</div>
}
>
<Tooltip content="Font Size" placement="top-start">
<Tooltip content="ArrowToEditor" placement="top-start">
<IconButton>
<ConnectorIcon></ConnectorIcon>
</IconButton>

View File

@ -2,7 +2,6 @@ import { TLDR, TldrawApp } from '@toeverything/components/board-state';
import { Divider, Popover, styled } from '@toeverything/components/ui';
import { Fragment } from 'react';
import { AlignOperation } from './AlignOperation';
import { ArrowTo } from './ArrowTo';
import { BorderColorConfig } from './BorderColorConfig';
import { DeleteShapes } from './DeleteOperation';
import { FillColorConfig } from './FillColorConfig';
@ -107,12 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
shapes={config.deleteShapes.selectedShapes}
></AlignOperation>
) : null,
toNextShap: (
<ArrowTo
app={app}
shapes={config.deleteShapes.selectedShapes}
></ArrowTo>
),
// toNextShap: (
// <ArrowTo
// app={app}
// shapes={config.deleteShapes.selectedShapes}
// ></ArrowTo>
// ),
};
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);

View File

@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
export const LineTools = ({ app }: { app: TldrawApp }) => {
const activeTool = app.useStore(activeToolSelector);
const [visible, setVisible] = useState(false);
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
TDShapeType.Line
@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
return (
<Popover
visible={visible}
placement="right-start"
trigger="click"
onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={
<ShapesContainer>
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
<IconButton
onClick={() => {
app.selectTool(type);
setVisible(false);
setLastActiveTool(type);
}}
>

View File

@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
const activeTool = app.useStore(activeToolSelector);
const [visible, setVisible] = useState(false);
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
TDShapeType.Rectangle
@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
return (
<Popover
visible={visible}
placement="right-start"
trigger="click"
onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={
<ShapesContainer>
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
<IconButton
onClick={() => {
app.selectTool(type);
setVisible(false);
setLastActiveTool(type);
}}
>

View File

@ -12,12 +12,12 @@ import {
import {
IconButton,
PopoverContainer,
styled,
// MuiIconButton as IconButton,
// MuiTooltip as Tooltip,
Tooltip,
useTheme,
} from '@toeverything/components/ui';
import style9 from 'style9';
import { TldrawApp } from '@toeverything/components/board-state';
import {
@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
}}
direction="none"
>
<div className={styles('container')}>
<div className={styles('toolBar')}>
<Container>
<ToolBar>
{tools.map(
({
type,
@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
</Tooltip>
)
)}
</div>
</div>
</ToolBar>
</Container>
</PopoverContainer>
);
};
const styles = style9.create({
container: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
height: '100%',
},
toolBar: {
display: 'flex',
flexDirection: 'column',
backgroundColor: '#ffffff',
borderRadius: '10px',
padding: '4px 4px',
},
const Container = styled('div')({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
height: '100%',
});
const ToolBar = styled('div')({
display: 'flex',
flexDirection: 'column',
backgroundColor: '#ffffff',
borderRadius: '10px',
padding: '4px 4px',
});

View File

@ -12,7 +12,7 @@ import {
styled,
Tooltip,
} from '@toeverything/components/ui';
import { ReactElement, type CSSProperties } from 'react';
import { useState, type CSSProperties, type ReactElement } from 'react';
import { Palette } from '../../palette';
import { Pen } from './Pen';
@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
export const PenTools = ({ app }: { app: TldrawApp }) => {
const appCurrentTool = app.useStore(state => state.appState.activeTool);
const [visible, setVisible] = useState(false);
const chosenPen =
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
PENCIL_CONFIGS[0];
@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
return (
<Popover
visible={visible}
placement="right-start"
trigger="click"
onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={
<Container>
<PensContainer>
@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
name={title}
primaryColor={color_vars['--color-0']}
secondaryColor={color_vars['--color-1']}
onClick={() => setPen(key)}
onClick={() => {
setVisible(false);
setPen(key);
}}
/>
);
}
@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
<Palette
selected={chosenColor}
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
onSelect={color => setPenColor(color)}
onSelect={color => {
setVisible(false);
setPenColor(color);
}}
/>
<div />
</Container>

View File

@ -42,7 +42,7 @@ type E = HTMLDivElement;
export class ArrowUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Arrow as const;
override hideBounds = true;
override hideBounds = false;
override canEdit = true;

View File

@ -1,6 +1,8 @@
import { Utils } from '@tldraw/core';
import type { ShapeStyles } from '@toeverything/components/board-types';
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
import {
BINDING_DISTANCE,
ShapeStyles,
} from '@toeverything/components/board-types';
import * as React from 'react';
import { getShapeStyle } from '../../shared';
@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
return (
<>
<rect
className={
isSelected || style.isFilled
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
className={'tl-fill-hitarea'}
x={sw / 2}
y={sw / 2}
width={w}
height={h}
opacity={1}
strokeWidth={BINDING_DISTANCE}
/>
{style.isFilled && (
<rect
x={sw / 2}
y={sw / 2}
width={w}
height={h}
width={w - sw / 2}
height={h - sw / 2}
fill={fill}
pointerEvents="none"
/>

View File

@ -175,6 +175,10 @@ interface TDCallbacks {
* (optional) A callback to run when the shape is copied.
*/
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
/**
* (optional) A callback to run when the shape is paste.
*/
onPaste?: (e: ClipboardEvent, data: any) => void;
}
export interface TldrawAppCtorProps {
@ -626,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
private prev_bindings = this.page.bindings;
private prev_assets = this.document.assets;
private _broadcastPageChanges = () => {
private _broadcastPageChanges = async () => {
const visited = new Set<string>();
const changedShapes: Record<string, TDShape | undefined> = {};
@ -683,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
Object.keys(changedAssets).length > 0
) {
this.just_sent = true;
this.callbacks.onChangePage?.(
await this.callbacks.onChangePage?.(
this,
changedShapes,
changedBindings,
@ -1956,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
paste = async (point?: number[], e?: ClipboardEvent) => {
if (this.readOnly) return;
if (e) {
const data = e.clipboardData?.getData('text/html');
const paste_as_html = (html: string) => {
try {
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
if (!maybeJson) return;
const json: {
type: string;
shapes: TDShape[];
bindings: TDBinding[];
assets: TDAsset[];
} = JSON.parse(maybeJson);
return json;
} catch (e) {
return;
}
};
this.callbacks.onPaste?.(e, paste_as_html(data));
return this;
}
const paste_as_html = (html: string) => {
try {
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
if (!maybeJson) return;
const json: {
type: string;
shapes: TDShape[];
bindings: TDBinding[];
assets: TDAsset[];
} = JSON.parse(maybeJson);
if (json.type === 'tldr/clipboard') {
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
return;
} else {
throw Error('Not tldraw data!');
}
} catch (e) {
pasteTextAsShape(html);
return;
}
};
const pasteInCurrentPage = (
shapes: TDShape[],
bindings: TDBinding[],
@ -2107,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// this.select(shapeId);
};
const paste_as_html = (html: string) => {
try {
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
if (!maybeJson) return;
const json: {
type: string;
shapes: TDShape[];
bindings: TDBinding[];
assets: TDAsset[];
} = JSON.parse(maybeJson);
if (json.type === 'tldr/clipboard') {
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
return;
} else {
throw Error('Not tldraw data!');
}
} catch (e) {
pasteTextAsShape(html);
return;
}
};
if (e !== undefined) {
const items: DataTransferItemList =
e.clipboardData?.items ?? ({} as DataTransferItemList);

View File

@ -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',
}}
>

View File

@ -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 `<strong ${style}>${text}</strong>`;
}
if (textValue.italic) {
return `<em ${style}>${text}</em>`;
}
if (textValue.underline) {
return `<u ${style}>${text}</u>`;
}
if (textValue.inlinecode) {
return `<code ${style}>${text}</code>`;
}
if (textValue.strikethrough) {
return `<s ${style}>${text}</s>`;
}
if (textValue.type === 'link') {
return `<a href='${textValue.url}' ${style}>${this.convertLeaf2Html(
textValue.children
)}</a>`;
}
return `<span ${style}>${text}</span>`;
}
public getStartSelection() {

View File

@ -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",

View File

@ -1,29 +1,28 @@
import type { TextProps } from '@toeverything/components/common';
import {
ContentColumnValue,
services,
Protocol,
services,
} from '@toeverything/datasource/db-service';
import { type CreateView } from '@toeverything/framework/virgo';
import { useEffect, useRef, useState } from 'react';
import {
BlockPendantProvider,
RenderBlockChildren,
supportChildren,
useOnSelect,
} from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { BlockContainer } from '../../components/BlockContainer';
import { List } from '../../components/style-container';
import {
TextManage,
type ExtendedTextUtils,
} from '../../components/text-manage';
import { tabBlock } from '../../utils/indent';
import { BulletIcon, getChildrenType, NumberType } from './data';
import { BulletBlock, BulletProperties } from './types';
import {
supportChildren,
RenderBlockChildren,
useOnSelect,
BlockPendantProvider,
} from '@toeverything/components/editor-core';
import { List } from '../../components/style-container';
import { getChildrenType, BulletIcon, NumberType } from './data';
import { IndentWrapper } from '../../components/IndentWrapper';
import { BlockContainer } from '../../components/BlockContainer';
import { styled } from '@toeverything/components/ui';
export const defaultBulletProps: BulletProperties = {
text: { value: [{ text: '' }] },
@ -189,7 +188,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
return (
<BlockContainer editor={editor} block={block} selected={isSelect}>
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<List>
<BulletLeft>
<BulletIcon numberType={properties.numberType} />
@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
</div>
</List>
</BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} />
</IndentWrapper>
<RenderBlockChildren block={block} />
</BlockContainer>
);
};

View File

@ -1,18 +1,17 @@
import { Protocol } from '@toeverything/datasource/db-service';
import {
AsyncBlock,
BaseView,
CreateView,
getTextProperties,
SelectBlock,
getTextHtml,
BlockEditor,
HTML2BlockResult,
} from '@toeverything/framework/virgo';
import {
Protocol,
DefaultColumnsValue,
} from '@toeverything/datasource/db-service';
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
import { defaultBulletProps, BulletView } from './BulletView';
import { IndentWrapper } from '../../components/IndentWrapper';
Block2HtmlProps,
commonBlock2HtmlContent,
commonHTML2block,
} from '../../utils/commonBlockClip';
import { BulletView, defaultBulletProps } from './BulletView';
export class BulletBlock extends BaseView {
public type = Protocol.Block.Type.bullet;
@ -27,66 +26,44 @@ export class BulletBlock extends BaseView {
}
return block;
}
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
if (element.tagName === 'UL') {
const firstList = element.querySelector('li');
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'UL') {
const result = [];
for (let i = 0; i < el.children.length; i++) {
const blocks_info = parseEl(el.children[i]);
result.push(...blocks_info);
if (!firstList || firstList.innerText.startsWith('[ ] ')) {
return null;
}
return result.length > 0 ? result : null;
const children = Array.from(element.children);
const childrenBlockInfos = (
await Promise.all(
children.map(childElement =>
this.html2block({
editor,
element: childElement,
})
)
)
)
.flat()
.filter(v => v);
return childrenBlockInfos.length ? childrenBlockInfos : null;
}
if (tag_name == 'LI' && !el.textContent.startsWith('[ ] ')) {
const childNodes = el.childNodes;
const texts = [];
const children = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
} else {
children.push(blocks_info[j]);
}
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: children,
},
];
}
return null;
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'LI',
});
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<ul><li>${content}</li></ul>`;
override async block2html(props: Block2HtmlProps) {
return `<ul><li>${await commonBlock2HtmlContent(props)}</li></ul>`;
}
}

View File

@ -1,58 +1,54 @@
import { useState, useRef, useEffect } from 'react';
import { StyleWithAtRules } from 'style9';
import { CreateView } from '@toeverything/framework/virgo';
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
import { styled } from '@toeverything/components/ui';
import { javascript } from '@codemirror/lang-javascript';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json } from '@codemirror/lang-json';
import { python } from '@codemirror/lang-python';
import { markdown } from '@codemirror/lang-markdown';
import { xml } from '@codemirror/lang-xml';
import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql';
import { java } from '@codemirror/lang-java';
import { rust } from '@codemirror/lang-rust';
import { cpp } from '@codemirror/lang-cpp';
import { css } from '@codemirror/lang-css';
import { html } from '@codemirror/lang-html';
import { java } from '@codemirror/lang-java';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { lezer } from '@codemirror/lang-lezer';
import { markdown } from '@codemirror/lang-markdown';
import { php } from '@codemirror/lang-php';
import { python } from '@codemirror/lang-python';
import { rust } from '@codemirror/lang-rust';
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
import { xml } from '@codemirror/lang-xml';
import { StreamLanguage } from '@codemirror/language';
import { go } from '@codemirror/legacy-modes/mode/go';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { lua } from '@codemirror/legacy-modes/mode/lua';
import { swift } from '@codemirror/legacy-modes/mode/swift';
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { vb } from '@codemirror/legacy-modes/mode/vb';
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
import { elixir } from 'codemirror-lang-elixir';
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
import { perl } from '@codemirror/legacy-modes/mode/perl';
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
import { toml } from '@codemirror/legacy-modes/mode/toml';
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
import { go } from '@codemirror/legacy-modes/mode/go';
import { julia } from '@codemirror/legacy-modes/mode/julia';
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
import { lua } from '@codemirror/legacy-modes/mode/lua';
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
import { perl } from '@codemirror/legacy-modes/mode/perl';
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
import { r } from '@codemirror/legacy-modes/mode/r';
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
import { shell } from '@codemirror/legacy-modes/mode/shell';
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
import { swift } from '@codemirror/legacy-modes/mode/swift';
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
import { toml } from '@codemirror/legacy-modes/mode/toml';
import { vb } from '@codemirror/legacy-modes/mode/vb';
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
import { Extension } from '@codemirror/state';
import { Option, Select } from '@toeverything/components/ui';
import {
useOnSelect,
BlockPendantProvider,
useOnSelect,
} from '@toeverything/components/editor-core';
import { copyToClipboard } from '@toeverything/utils';
import { DuplicateIcon } from '@toeverything/components/icons';
import { Option, Select, styled } from '@toeverything/components/ui';
import { CreateView } from '@toeverything/framework/virgo';
import { copyToClipboard } from '@toeverything/utils';
import { elixir } from 'codemirror-lang-elixir';
import { useEffect, useRef, useState } from 'react';
import { StyleWithAtRules } from 'style9';
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
interface CreateCodeView extends CreateView {
style9?: StyleWithAtRules;
@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
borderRadius: theme.affine.shape.borderRadius,
'&:hover': {
'.operation': {
display: 'flex',
opacity: 1,
},
},
'.operation': {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
opacity: 0,
transition: 'opacity 1.5s',
},
'.copy-block': {
padding: '0px 10px',
@ -172,7 +170,7 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
editor.selectionManager.activePreviousNode(block.id, 'start');
};
return (
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<CodeBlock
onKeyDown={e => {
e.stopPropagation();
@ -200,7 +198,8 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
</div>
<div>
<div className="copy-block" onClick={copyCode}>
<DuplicateIcon></DuplicateIcon>Copy
<DuplicateIcon />
Copy
</div>
</div>
</div>

View File

@ -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<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'CODE',
});
}
// TODO: internal format not implemented yet
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'CODE') {
const childNodes = el.childNodes;
let text_value = '';
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
if (block_texts.length > 0) {
text_value += block_texts[0].text;
}
}
}
}
return [
{
type: this.type,
properties: {
text: { value: [{ text: text_value }] },
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<code>${content}</code>`;
override async block2html(props: Block2HtmlProps) {
return `<code>${await commonBlock2HtmlContent(props)}<code/>`;
}
}

View File

@ -1,39 +1,35 @@
import {
AsyncBlock,
BaseView,
BlockEditor,
HTML2BlockResult,
SelectBlock,
} from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { DividerView } from './divider-view';
import { Block2HtmlProps, commonHTML2block } from '../../utils/commonBlockClip';
export class DividerBlock extends BaseView {
type = Protocol.Block.Type.divider;
View = DividerView;
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'HR') {
return [
{
type: this.type,
properties: {
text: {},
},
children: [],
},
];
}
return null;
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'HR',
ignoreEmptyElement: false,
});
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
return `<hr>`;
override async block2html(props: Block2HtmlProps) {
return `<hr/>`;
}
}

View File

@ -33,7 +33,7 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
};
return (
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<LinkContainer>
{embedLinkUrl ? (
<SourceView

View File

@ -5,6 +5,7 @@ import {
} from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { EmbedLinkView } from './EmbedLinkView';
import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class EmbedLinkBlock extends BaseView {
public override selectable = true;
@ -12,36 +13,8 @@ export class EmbedLinkBlock extends BaseView {
type = Protocol.Block.Type.embedLink;
View = EmbedLinkView;
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
return [
{
type: this.type,
properties: {
// TODO: Not sure what value to fill for name
embedLink: {
name: this.type,
value: el.getAttribute('href'),
},
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
const figma_url = block.getProperty('embedLink')?.value;
return `<p><a src=${figma_url}>${figma_url}</p>`;
override async block2html({ block }: Block2HtmlProps) {
const url = block.getProperty('embedLink')?.value;
return `<p><a href="${url}">${url}</a></p>`;
}
}

View File

@ -1,13 +1,12 @@
import { useState } from 'react';
import { CreateView } from '@toeverything/framework/virgo';
import {
useOnSelect,
BlockPendantProvider,
useOnSelect,
} from '@toeverything/components/editor-core';
import { Upload } from '../../components/upload/upload';
import { CreateView } from '@toeverything/framework/virgo';
import { useState } from 'react';
import { SourceView } from '../../components/source-view';
import { styled } from '@toeverything/components/ui';
import { LinkContainer } from '../../components/style-container';
import { Upload } from '../../components/upload/upload';
const MESSAGES = {
ADD_FIGMA_LINK: 'Add figma link',
@ -30,7 +29,7 @@ export const FigmaView = ({ block, editor }: FigmaView) => {
setIsSelect(isSelect);
});
return (
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<LinkContainer>
{figmaUrl ? (
<SourceView

View File

@ -5,6 +5,7 @@ import {
SelectBlock,
} from '@toeverything/framework/virgo';
import { FigmaView } from './FigmaView';
import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class FigmaBlock extends BaseView {
public override selectable = true;
@ -12,42 +13,8 @@ export class FigmaBlock extends BaseView {
type = Protocol.Block.Type.figma;
View = FigmaView;
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
const href = el.getAttribute('href');
const allowedHosts = ['www.figma.com'];
const host = new URL(href).host;
if (allowedHosts.includes(host)) {
return [
{
type: this.type,
properties: {
// TODO: Not sure what value to fill for name
embedLink: {
name: this.type,
value: el.getAttribute('href'),
},
},
children: [],
},
];
}
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
const figma_url = block.getProperty('embedLink')?.value;
return `<p><a src=${figma_url}>${figma_url}</p>`;
override async block2html({ block }: Block2HtmlProps) {
const figmaUrl = block.getProperty('embedLink')?.value;
return `<p><a href="${figmaUrl}">${figmaUrl}</a></p>`;
}
}

View File

@ -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<string>
): Promise<string> {
const file_property =
block.getProperty('file') || ({} as FileColumnValue);
const file_id = file_property.value;
let file_info = null;
if (file_id) {
file_info = await services.api.file.get(file_id, block.workspace);
}
return `<p><a src=${file_info?.url}>${file_property?.name}</p>`;
return fileInfo
? `<p><a href=${fileInfo?.url}>${fileProperty?.name}</a></p>`
: '';
}
}

View File

@ -1,4 +1,4 @@
import { RenderBlock } from '@toeverything/components/editor-core';
import { RenderBlockChildren } from '@toeverything/components/editor-core';
import { ChildrenView, CreateView } from '@toeverything/framework/virgo';
export const GridItemRender = function (
@ -6,13 +6,7 @@ export const GridItemRender = function (
) {
const GridItem = function (props: CreateView) {
const { block } = props;
const children = (
<>
{block.childrenIds.map(id => {
return <RenderBlock key={id} blockId={id} />;
})}
</>
);
const children = <RenderBlockChildren block={block} indent={false} />;
return <>{creator({ ...props, children })}</>;
};
return GridItem;

View File

@ -1,16 +1,16 @@
import { RenderBlock } from '@toeverything/components/editor-core';
import { CreateView } from '@toeverything/framework/virgo';
import React, { useEffect, useRef, useState } from 'react';
import { GridHandle } from './GirdHandle';
import { BlockRender } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service';
import { CreateView } from '@toeverything/framework/virgo';
import { debounce, domToRect, Point } from '@toeverything/utils';
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import {
GRID_ITEM_CLASS_NAME,
GRID_ITEM_CONTENT_CLASS_NAME,
} from '../grid-item/GridItem';
import { debounce, domToRect, Point } from '@toeverything/utils';
import clsx from 'clsx';
import { Protocol } from '@toeverything/datasource/db-service';
import { GridHandle } from './GirdHandle';
const DB_UPDATE_DELAY = 50;
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
@ -226,7 +226,7 @@ export const Grid = function (props: CreateView) {
key={id}
className={GRID_ITEM_CLASS_NAME}
>
<RenderBlock hasContainer={false} blockId={id} />
<BlockRender hasContainer={false} blockId={id} />
<GridHandle
onDrag={event => handleDragGrid(event, i)}
editor={editor}

View File

@ -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<AsyncBlock> {
return block;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
const content = await generateHtml(children);
return `<div>${content}<div>`;
override async block2html({ editor, selectInfo, block }: Block2HtmlProps) {
const childrenHtml =
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
block,
selectInfo?.children
);
return `<div>${childrenHtml}<code/>`;
}
}

View File

@ -21,19 +21,6 @@ const SceneMap: Record<RecastScene, ComponentType<CreateView>> = {
kanban: SceneKanban,
} as const;
const GroupBox = styled('div')(({ theme }) => {
return {
'&:hover': {
// Workaround referring to other components
// See https://emotion.sh/docs/styled#targeting-another-emotion-component
// [GroupActionWrapper.toString()]: {},
'& > *': {
visibility: 'visible',
},
},
};
});
const GroupActionWrapper = styled('div')(({ theme }) => ({
height: '30px',
display: 'flex',
@ -59,6 +46,14 @@ const GroupActionWrapper = styled('div')(({ theme }) => ({
},
}));
const GroupBox = styled('div')({
'&:hover': {
[GroupActionWrapper.toString()]: {
visibility: 'visible',
},
},
});
const GroupContainer = styled('div')<{ isSelect?: boolean }>(
({ isSelect, theme }) => ({
background: theme.affine.palette.white,

View File

@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
import type { CreateView } from '@toeverything/framework/virgo';
export const ScenePage = ({ block }: CreateView) => {
return <RenderBlockChildren block={block} />;
return <RenderBlockChildren block={block} indent={false} />;
};

View File

@ -41,7 +41,7 @@ export const AddViewMenu = () => {
onClick={() => setActivePanel(!activePanel)}
>
<AddViewIcon fontSize="small" />
<span>Add View</span>
<span style={{ userSelect: 'none' }}>Add View</span>
{activePanel && (
<Panel>
<PanelItem>
@ -66,10 +66,10 @@ export const AddViewMenu = () => {
key={name}
active={viewType === scene}
onClick={() => {
if (scene === RecastScene.Table) {
// The table view is under progress
return;
}
// if (scene === RecastScene.Table) {
// // The table view is under progress
// return;
// }
setViewType(scene);
}}
style={{ textTransform: 'uppercase' }}

View File

@ -20,7 +20,7 @@ export const ViewsMenu = () => {
useRecastView();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setViewName(e.target.value.trim());
setViewName(e.target.value);
};
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
@ -36,7 +36,7 @@ export const ViewsMenu = () => {
}
await updateView({
...activeView,
name: viewName,
name: viewName.trim(),
type: viewType,
});
setActiveView(null);
@ -99,10 +99,10 @@ export const ViewsMenu = () => {
key={name}
active={viewType === scene}
onClick={() => {
if (scene === RecastScene.Table) {
// The table view is under progress
return;
}
// if (scene === RecastScene.Table) {
// // The table view is under progress
// return;
// }
setViewType(scene);
}}
style={{ textTransform: 'uppercase' }}

View File

@ -1,9 +1,5 @@
import { RecastScene } from '@toeverything/components/editor-core';
import {
KanBanIcon,
TableIcon,
TodoListIcon,
} from '@toeverything/components/icons';
import { KanBanIcon, TodoListIcon } from '@toeverything/components/icons';
export const VIEW_LIST = [
{
@ -16,9 +12,9 @@ export const VIEW_LIST = [
scene: RecastScene.Kanban,
icon: <KanBanIcon fontSize="small" />,
},
{
name: 'Table',
scene: RecastScene.Table,
icon: <TableIcon fontSize="small" />,
},
// {
// name: 'Table',
// scene: RecastScene.Table,
// icon: <TableIcon fontSize="small" />,
// },
] as const;

View File

@ -1,12 +1,12 @@
import { useCallback } from 'react';
import { CardItem } from './CardItem';
import { styled } from '@toeverything/components/ui';
import { useKanban } from '@toeverything/components/editor-core';
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
import type {
KanbanCard,
KanbanGroup,
} from '@toeverything/components/editor-core';
import { useKanban } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { useCallback } from 'react';
import { CardItem } from './CardItem';
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
const AddCardWrapper = styled('div')({
display: 'flex',
@ -48,7 +48,7 @@ export const CardContext = (props: Props) => {
item={item}
active={activeId === id}
>
<CardItem id={id} block={block} />
<CardItem block={block} />
</CardItemPanelWrapper>
</StyledCardContainer>
);

View File

@ -1,6 +1,6 @@
import {
KanbanBlockRender,
KanbanCard,
RenderBlock,
useEditor,
useKanban,
} from '@toeverything/components/editor-core';
@ -10,7 +10,6 @@ import {
MuiClickAwayListener,
styled,
} from '@toeverything/components/ui';
import { useFlag } from '@toeverything/datasource/feature-flags';
import { useState, type MouseEvent } from 'react';
import { useRefPage } from './RefPage';
@ -82,41 +81,37 @@ const Overlay = styled('div')({
},
});
export const CardItem = ({
id,
block,
}: {
id: KanbanCard['id'];
block: KanbanCard['block'];
}) => {
export const CardItem = ({ block }: { block: KanbanCard['block'] }) => {
const { addSubItem } = useKanban();
const { openSubPage } = useRefPage();
const [editable, setEditable] = useState(false);
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
const [editableBlock, setEditableBlock] = useState<string | null>(null);
const { editor } = useEditor();
const onAddItem = async () => {
setEditable(true);
await addSubItem(block);
const newItem = await addSubItem(block);
setEditableBlock(newItem.id);
};
const onClickCard = async () => {
openSubPage(id);
openSubPage(block.id);
};
const onClickPen = (e: MouseEvent<Element>) => {
e.stopPropagation();
setEditable(true);
setEditableBlock(block.id);
editor.selectionManager.activeNodeByNodeId(block.id);
};
return (
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
<MuiClickAwayListener onClickAway={() => setEditableBlock(null)}>
<CardContainer>
<CardContent>
<RenderBlock blockId={id} />
<KanbanBlockRender
blockId={block.id}
activeBlock={editableBlock}
/>
</CardContent>
{showKanbanRefPageFlag && !editable && (
{!editableBlock && (
<Overlay onClick={onClickCard}>
<IconButton backgroundColor="#fff" onClick={onClickPen}>
<PenIcon />

View File

@ -21,13 +21,17 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
return createPortal(
<MuiBackdrop
open={open}
onMouseDown={(e: { stopPropagation: () => void }) => {
// Prevent trigger the bottom editor's selection
e.stopPropagation();
}}
onClick={closeSubPage}
style={{
display: 'flex',
flexDirection: 'column',
background: 'rgba(58, 76, 92, 0.4)',
zIndex: theme.affine.zIndex.popover,
}}
onClick={closeSubPage}
>
<Dialog
onClick={(e: { stopPropagation: () => void }) => {

View File

@ -1,7 +1,6 @@
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
import { CardItem } from '../../CardItem';
import type { KanbanCard } from '@toeverything/components/editor-core';
import type { DndableItems } from '../type';
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
export function renderContainerDragOverlay({
containerId,
@ -18,7 +17,7 @@ export function renderContainerDragOverlay({
return (
<CardItemWrapper
key={id}
card={<CardItem key={id} id={id} block={block} />}
card={<CardItem key={id} block={block} />}
index={index}
/>
);

View File

@ -5,35 +5,13 @@ import {
} from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { GroupDividerView } from './groupDividerView';
import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class GroupDividerBlock extends BaseView {
type = Protocol.Block.Type.groupDivider;
View = GroupDividerView;
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'HR') {
return [
{
type: this.type,
properties: {
text: {},
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
return `<hr>`;
override async block2html(props: Block2HtmlProps) {
return `<hr/>`;
}
}

View File

@ -165,12 +165,10 @@ export const ImageView = ({ block, editor }: ImageView) => {
};
return (
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<ImageBlock>
<div style={{ position: 'relative' }} ref={resizeBox}>
{!isSelect ? (
<ImageShade onClick={handleClick}></ImageShade>
) : null}
{!isSelect ? <ImageShade onClick={handleClick} /> : null}
{imgUrl ? (
<div
onMouseDown={e => {

View File

@ -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<HTML2BlockResult> {
if (element.tagName === 'IMG') {
return [
{
type: this.type,
properties: {
value: '',
url: el.getAttribute('src'),
name: el.getAttribute('src'),
size: 0,
type: 'link',
image: {
value: getRandomString('image'),
url: element.getAttribute('src'),
name: element.getAttribute('src'),
size: 0,
type: 'link',
},
},
children: [],
},
];
}
return null;
}
// TODO:
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
const text = block.getProperty('text');
override async block2html({ block, editor }: Block2HtmlProps) {
const textValue = block.getProperty('text');
const content = '';
if (text) {
text.value.map(text => `<span>${text}</span>`).join('');
}
// TODO: child
return `<p><img src=${content}></p>`;
// TODO: text.value should export with style??
const figcaption = (textValue?.value ?? [])
.map(({ text }) => `<span>${text}</span>`)
.join('');
return `<figure><img src="${content}" alt="${figcaption}"/><figcaption>${figcaption}<figcaption/></figure>`;
}
}

View File

@ -1,8 +1,8 @@
import { TextProps } from '@toeverything/components/common';
import {
ContentColumnValue,
services,
Protocol,
services,
} from '@toeverything/datasource/db-service';
import { type CreateView } from '@toeverything/framework/virgo';
import { useEffect, useRef, useState } from 'react';
@ -11,18 +11,17 @@ import {
type ExtendedTextUtils,
} from '../../components/text-manage';
import { tabBlock } from '../../utils/indent';
import { IndentWrapper } from '../../components/IndentWrapper';
import type { Numbered, NumberedAsyncBlock } from './types';
import { getChildrenType, getNumber } from './data';
import {
supportChildren,
RenderBlockChildren,
useOnSelect,
BlockPendantProvider,
RenderBlockChildren,
supportChildren,
useOnSelect,
} from '@toeverything/components/editor-core';
import { List } from '../../components/style-container';
import { BlockContainer } from '../../components/BlockContainer';
import { List } from '../../components/style-container';
import { getChildrenType, getNumber } from './data';
export const defaultTodoProps: Numbered = {
text: { value: [{ text: '' }] },
@ -184,7 +183,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
return (
<BlockContainer editor={editor} block={block} selected={isSelect}>
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<List>
<div className={'checkBoxContainer'}>
{getNumber(properties.numberType, number)}.
@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
</List>
</BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} />
</IndentWrapper>
<RenderBlockChildren block={block} />
</BlockContainer>
);
};

View File

@ -1,17 +1,16 @@
import { Protocol } from '@toeverything/datasource/db-service';
import {
AsyncBlock,
BaseView,
getTextProperties,
SelectBlock,
getTextHtml,
BlockEditor,
HTML2BlockResult,
} from '@toeverything/framework/virgo';
import {
Protocol,
DefaultColumnsValue,
} from '@toeverything/datasource/db-service';
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
Block2HtmlProps,
commonBlock2HtmlContent,
commonHTML2block,
} from '../../utils/commonBlockClip';
import { defaultTodoProps, NumberedView } from './NumberedView';
import { IndentWrapper } from '../../components/IndentWrapper';
export class NumberedBlock extends BaseView {
public type = Protocol.Block.Type.numbered;
@ -29,71 +28,39 @@ export class NumberedBlock extends BaseView {
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'OL') {
const result = [];
for (let i = 0; i < el.children.length; i++) {
const blocks_info = parseEl(el.children[i]);
result.push(...blocks_info);
}
return result.length > 0 ? result : null;
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
if (element.tagName === 'OL') {
const children = Array.from(element.children);
const childrenBlockInfos = (
await Promise.all(
children.map(childElement =>
this.html2block({
editor,
element: childElement,
})
)
)
)
.flat()
.filter(v => v);
return childrenBlockInfos.length ? childrenBlockInfos : null;
}
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
const childNodes = el.childNodes;
let texts = [];
const children = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
} else {
children.push(blocks_info[j]);
}
}
}
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
texts[0].text = texts[0].text.substring('[ ] '.length);
if (!texts[0].text) {
texts = texts.slice(1);
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: children,
},
];
}
return null;
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'LI',
});
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<ol><li>${content}</li></ol>`;
override async block2html(props: Block2HtmlProps) {
return `<ol><li>${await commonBlock2HtmlContent(props)}</li></ol>`;
}
}

View File

@ -1,14 +1,14 @@
import { useRef, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { BackLink, TextProps } from '@toeverything/components/common';
import {
RenderBlockChildren,
BlockPendantProvider,
RenderBlockChildren,
} from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import { ContentColumnValue } from '@toeverything/datasource/db-service';
import { CreateView } from '@toeverything/framework/virgo';
import { Theme, styled } from '@toeverything/components/ui';
import {
TextManage,
@ -81,7 +81,7 @@ export const PageView = ({ block, editor }: CreateView) => {
return (
<PageTitleBlock>
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<TextManage
alwaysShowPlaceholder
ref={textRef}

View File

@ -1,21 +1,9 @@
import { withRecastBlock } from '@toeverything/components/editor-core';
import {
Protocol,
DefaultColumnsValue,
} from '@toeverything/datasource/db-service';
import {
AsyncBlock,
BaseView,
ChildrenView,
getTextHtml,
getTextProperties,
SelectBlock,
} from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
import { PageView } from './PageView';
export const PageChildrenView: (prop: ChildrenView) => JSX.Element = props =>
props.children;
import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class PageBlock extends BaseView {
type = Protocol.Block.Type.page;
@ -35,21 +23,17 @@ export class PageBlock extends BaseView {
return this.get_decoration<any>(content, 'text')?.value?.[0].text;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
const content = getTextHtml(block);
const childrenContent = await generateHtml(children);
return `<h1>${content}</h1> ${childrenContent}`;
override async block2html({ block, editor, selectInfo }: Block2HtmlProps) {
const header =
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
block,
selectInfo
);
const childrenHtml =
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
block,
selectInfo?.children
);
return `<h1>${header}</h1>${childrenHtml}`;
}
}

View File

@ -1,15 +1,16 @@
import {
DefaultColumnsValue,
Protocol,
} from '@toeverything/datasource/db-service';
import { Protocol } from '@toeverything/datasource/db-service';
import {
AsyncBlock,
BaseView,
BlockEditor,
CreateView,
getTextHtml,
getTextProperties,
SelectBlock,
HTML2BlockResult,
} from '@toeverything/framework/virgo';
import {
Block2HtmlProps,
commonBlock2HtmlContent,
commonHTML2block,
} from '../../utils/commonBlockClip';
import { TextView } from './TextView';
@ -29,54 +30,25 @@ export class QuoteBlock extends BaseView {
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'BLOCKQUOTE',
});
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'BLOCKQUOTE') {
const childNodes = el.childNodes;
const texts = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
}
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<blockquote>${content}</blockquote>`;
override async block2html(props: Block2HtmlProps) {
return `<blockquote>${await commonBlock2HtmlContent(
props
)}</blockquote>`;
}
}
@ -96,69 +68,22 @@ export class CalloutBlock extends BaseView {
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'ASIDE',
});
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (
tag_name === 'ASIDE' ||
el.firstChild?.nodeValue?.startsWith('<aside>')
) {
const childNodes = el.childNodes;
let texts = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
}
}
}
if (
texts.length > 0 &&
(texts[0].text || '').startsWith('<aside>')
) {
texts[0].text = texts[0].text.substring('<aside>'.length);
if (!texts[0].text) {
texts = texts.slice(1);
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
];
}
if (el.firstChild?.nodeValue?.startsWith('</aside>')) {
return [];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<aside>${content}</aside>`;
override async block2html(props: Block2HtmlProps) {
return `<aside>${await commonBlock2HtmlContent(props)}</aside>`;
}
}

View File

@ -2,17 +2,16 @@ import {
BaseView,
CreateView,
AsyncBlock,
getTextProperties,
SelectBlock,
getTextHtml,
HTML2BlockResult,
BlockEditor,
} from '@toeverything/framework/virgo';
import {
DefaultColumnsValue,
Protocol,
} from '@toeverything/datasource/db-service';
import { Protocol } from '@toeverything/datasource/db-service';
import { TextView } from './TextView';
import { getRandomString } from '@toeverything/components/common';
import {
Block2HtmlProps,
commonBlock2HtmlContent,
commonHTML2block,
} from '../../utils/commonBlockClip';
export class TextBlock extends BaseView {
type = Protocol.Block.Type.text;
@ -28,106 +27,30 @@ export class TextBlock extends BaseView {
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
if (el instanceof Text) {
// TODO: parsing style
return el.textContent.split('\n').map(text => {
return {
type: this.type,
properties: {
text: { value: [{ text: text }] },
},
children: [],
};
});
}
const tag_name = el.tagName;
const block_style: any = {};
switch (tag_name) {
case 'STRONG':
case 'B':
block_style.bold = true;
break;
case 'A':
block_style.type = 'link';
block_style.url = el.getAttribute('href');
block_style.id = getRandomString('link');
block_style.children = [];
break;
case 'EM':
block_style.italic = true;
break;
case 'U':
block_style.underline = true;
break;
case 'CODE':
block_style.inlinecode = true;
break;
case 'S':
case 'DEL':
block_style.strikethrough = true;
break;
default:
break;
}
const child_nodes = el.childNodes;
let texts = [];
if (Object.keys(block_style).length > 0) {
for (let i = 0; i < child_nodes.length; i++) {
const blocks_info: any[] = parseEl(child_nodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
const block = blocks_info[j];
if (block.type === this.type) {
const block_texts = block.properties.text.value.map(
(text_value: any) => {
return tag_name === 'A'
? { ...text_value }
: { ...block_style, ...text_value };
}
);
texts.push(...block_texts);
}
}
}
}
if (tag_name === 'A') {
block_style.children.push(...texts);
texts = [block_style];
}
return texts.length > 0
? [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
]
: null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<p>${content}</p>`;
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: [
'DIV',
'P',
'PRE',
'B',
'A',
'EM',
'U',
'CODE',
'S',
'DEL',
],
});
}
}
@ -144,55 +67,23 @@ export class Heading1Block extends BaseView {
}
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'H1',
});
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'H1') {
const childNodes = el.childNodes;
const texts = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
}
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<h1>${content}</h1>`;
override async block2html(props: Block2HtmlProps) {
return `<h1>${await commonBlock2HtmlContent(props)}</h1>`;
}
}
@ -209,55 +100,23 @@ export class Heading2Block extends BaseView {
}
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'H2',
});
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'H2') {
const childNodes = el.childNodes;
const texts = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
}
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<h2>${content}</h2>`;
override async block2html(props: Block2HtmlProps) {
return `<h2>${await commonBlock2HtmlContent(props)}</h2>`;
}
}
@ -275,53 +134,22 @@ export class Heading3Block extends BaseView {
return block;
}
override getSelProperties(
block: AsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'H3',
});
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'H3') {
const childNodes = el.childNodes;
const texts = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
}
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: [],
},
];
}
return null;
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<h3>${content}</h3>`;
override async block2html(props: Block2HtmlProps) {
return `<h3>${await commonBlock2HtmlContent(props)}</h3>`;
}
}

View File

@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service';
import { CreateView } from '@toeverything/framework/virgo';
import { BlockContainer } from '../../components/BlockContainer';
import { IndentWrapper } from '../../components/IndentWrapper';
import { TextManage } from '../../components/text-manage';
import { dedentBlock, tabBlock } from '../../utils/indent';
interface CreateTextView extends CreateView {
@ -243,7 +242,7 @@ export const TextView = ({
selected={isSelect}
className={containerClassName}
>
<BlockPendantProvider block={block}>
<BlockPendantProvider editor={editor} block={block}>
<TextBlock
block={block}
type={block.type}
@ -255,9 +254,7 @@ export const TextView = ({
handleTab={onTab}
/>
</BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} />
</IndentWrapper>
<RenderBlockChildren block={block} />
</BlockContainer>
);
};

View File

@ -1,11 +1,17 @@
import { TextProps } from '@toeverything/components/common';
import {
AsyncBlock,
BlockPendantProvider,
CreateView,
useOnSelect,
} from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui';
import {
ContentColumnValue,
Protocol,
} from '@toeverything/datasource/db-service';
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { BlockContainer } from '../../components/BlockContainer';
import {
TextManage,
type ExtendedTextUtils,
@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
export const TodoView = ({ block, editor }: CreateView) => {
const properties = { ...defaultTodoProps, ...block.getProperties() };
const text_ref = useRef<ExtendedTextUtils>(null);
const [isSelect, setIsSelect] = useState<boolean>(false);
useOnSelect(block.id, (isSelect: boolean) => {
setIsSelect(isSelect);
});
const turn_into_text_block = async () => {
// Convert to text block
@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
};
return (
<TodoBlock>
<div className={'checkBoxContainer'}>
<CheckBox
checked={properties.checked?.value}
onChange={on_checked_change}
/>
</div>
<BlockContainer editor={editor} block={block} selected={isSelect}>
<BlockPendantProvider editor={editor} block={block}>
<TodoBlock>
<div className={'checkBoxContainer'}>
<CheckBox
checked={properties.checked?.value}
onChange={on_checked_change}
/>
</div>
<div className={'textContainer'}>
<TextManage
className={properties.checked?.value ? 'checked' : ''}
ref={text_ref}
editor={editor}
block={block}
supportMarkdown
placeholder="To-do"
handleEnter={on_text_enter}
handleBackSpace={on_backspace}
handleTab={on_tab}
/>
</div>
</TodoBlock>
<div className={'textContainer'}>
<TextManage
className={
properties.checked?.value ? 'checked' : ''
}
ref={text_ref}
editor={editor}
block={block}
supportMarkdown
placeholder="To-do"
handleEnter={on_text_enter}
handleBackSpace={on_backspace}
handleTab={on_tab}
/>
</div>
</TodoBlock>
</BlockPendantProvider>
</BlockContainer>
);
};

View File

@ -1,18 +1,17 @@
import { Protocol } from '@toeverything/datasource/db-service';
import {
BaseView,
getTextProperties,
AsyncBlock,
SelectBlock,
getTextHtml,
BlockEditor,
HTML2BlockResult,
withTreeViewChildren,
} from '@toeverything/framework/virgo';
// import type { CreateView } from '@toeverything/framework/virgo';
import { defaultTodoProps, TodoView } from './TodoView';
import {
Protocol,
DefaultColumnsValue,
} from '@toeverything/datasource/db-service';
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
import { TodoView, defaultTodoProps } from './TodoView';
Block2HtmlProps,
commonBlock2HtmlContent,
commonHTML2block,
} from '../../utils/commonBlockClip';
import type { TodoAsyncBlock } from './types';
export class TodoBlock extends BaseView {
@ -29,71 +28,44 @@ export class TodoBlock extends BaseView {
return block;
}
override getSelProperties(
block: TodoAsyncBlock,
selectInfo: any
): DefaultColumnsValue {
const properties = super.getSelProperties(block, selectInfo);
return getTextProperties(properties, selectInfo);
}
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'UL') {
const result = [];
for (let i = 0; i < el.children.length; i++) {
const blocks_info = parseEl(el.children[i]);
result.push(...blocks_info);
override async html2block({
element,
editor,
}: {
element: Element;
editor: BlockEditor;
}): Promise<HTML2BlockResult> {
if (element.tagName === 'UL') {
const firstList = element.querySelector('li');
if (!firstList || !firstList.innerText.startsWith('[ ] ')) {
return null;
}
return result.length > 0 ? result : null;
const children = Array.from(element.children);
const childrenBlockInfos = (
await Promise.all(
children.map(childElement =>
this.html2block({
editor,
element: childElement,
})
)
)
)
.flat()
.filter(v => v);
return childrenBlockInfos.length ? childrenBlockInfos : null;
}
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
const childNodes = el.childNodes;
let texts = [];
const children = [];
for (let i = 0; i < childNodes.length; i++) {
const blocks_info = parseEl(childNodes[i] as Element);
for (let j = 0; j < blocks_info.length; j++) {
if (blocks_info[j].type === 'text') {
const block_texts =
blocks_info[j].properties.text.value;
texts.push(...block_texts);
} else {
children.push(blocks_info[j]);
}
}
}
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
texts[0].text = texts[0].text.substring('[ ] '.length);
if (!texts[0].text) {
texts = texts.slice(1);
}
}
return [
{
type: this.type,
properties: {
text: { value: texts },
},
children: children,
},
];
}
return null;
return commonHTML2block({
element,
editor,
type: this.type,
tagName: 'LI',
});
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
let content = getTextHtml(block);
content += await generateHtml(children);
return `<ul><li>[ ] ${content}</li></ul>`;
override async block2html(props: Block2HtmlProps) {
return `<ul><li>[ ] ${await commonBlock2HtmlContent(props)}</li></ul>`;
}
}

View File

@ -5,6 +5,10 @@ import {
SelectBlock,
} from '@toeverything/framework/virgo';
import { YoutubeView } from './YoutubeView';
import {
Block2HtmlProps,
commonBlock2HtmlContent,
} from '../../utils/commonBlockClip';
export class YoutubeBlock extends BaseView {
public override selectable = true;
@ -12,42 +16,11 @@ export class YoutubeBlock extends BaseView {
type = Protocol.Block.Type.youtube;
View = YoutubeView;
override html2block(
el: Element,
parseEl: (el: Element) => any[]
): any[] | null {
const tag_name = el.tagName;
if (tag_name === 'A' && el.parentElement?.childElementCount === 1) {
const href = el.getAttribute('href');
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
const host = new URL(href).host;
if (allowedHosts.includes(host)) {
return [
{
type: this.type,
properties: {
// TODO: is not sure what value to fill in name
embedLink: {
name: this.type,
value: el.getAttribute('href'),
},
},
children: [],
},
];
}
}
return null;
override async block2Text(block: AsyncBlock, selectInfo: SelectBlock) {
return block.getProperty('embedLink')?.value ?? '';
}
override async block2html(
block: AsyncBlock,
children: SelectBlock[],
generateHtml: (el: any[]) => Promise<string>
): Promise<string> {
override async block2html({ block }: Block2HtmlProps) {
const url = block.getProperty('embedLink')?.value;
return `<p><a src=${url}>${url}</p>`;
return `<p><a href="${url}">${url}</a></p>`;
}
}

View File

@ -23,7 +23,7 @@ export const BlockContainer = function ({
);
};
export const Container = styled('div')<{ selected: boolean }>(
export const Container = styled('div')<{ selected?: boolean }>(
({ selected, theme }) => ({
backgroundColor: selected ? theme.affine.palette.textSelected : '',
marginBottom: '2px',

View File

@ -1,17 +0,0 @@
import { PropsWithChildren } from 'react';
import { ChildrenView } from '@toeverything/framework/virgo';
import { styled } from '@toeverything/components/ui';
/**
* Indent rendering child nodes
*/
export const IndentWrapper = (props: PropsWithChildren) => {
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
};
const StyledIdentWrapper = styled('div')({
display: 'flex',
flexDirection: 'column',
// TODO: marginLeft should use theme provided by styled
marginLeft: '30px',
});

View File

@ -1 +0,0 @@
export * from './IndentWrapper';

View File

@ -1,4 +1,5 @@
export const isYoutubeUrl = (url?: string): boolean => {
if (!url) return false;
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
const host = new URL(url).host;
return allowedHosts.includes(host);

View File

@ -1,6 +0,0 @@
.v-basic-table-body {
overflow: hidden !important;
&:hover {
overflow: auto !important;
}
}

View File

@ -1,19 +1,19 @@
import {
useMemo,
memo,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
useLayoutEffect,
useCallback,
} from 'react';
import { VariableSizeGrid, areEqual } from 'react-window';
import type {
GridChildComponentProps,
GridItemKeySelector,
} from 'react-window';
import { VariableSizeGrid } from 'react-window';
import style9 from 'style9';
import './basic-table.scss';
export interface TableColumn {
dataKey: string;
label: string;

View File

@ -0,0 +1,57 @@
import {
AsyncBlock,
BlockEditor,
HTML2BlockResult,
SelectBlock,
} from '@toeverything/components/editor-core';
import { BlockFlavorKeys } from '@toeverything/datasource/db-service';
export type Block2HtmlProps = {
editor: BlockEditor;
block: AsyncBlock;
// The selectInfo parameter is not passed when the block is selected in ful, the selectInfo.type is Range
selectInfo?: SelectBlock;
};
export const commonBlock2HtmlContent = async ({
editor,
block,
selectInfo,
}: Block2HtmlProps) => {
const html =
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
block,
selectInfo
);
const childrenHtml =
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
block,
selectInfo?.children
);
return `${html}${childrenHtml}`;
};
export const commonHTML2block = ({
element,
editor,
tagName,
type,
ignoreEmptyElement = true,
}: {
element: Element;
editor: BlockEditor;
tagName: string | string[];
type: BlockFlavorKeys;
ignoreEmptyElement?: boolean;
}): HTML2BlockResult => {
const tagNames = typeof tagName === 'string' ? [tagName] : tagName;
if (tagNames.includes(element.tagName)) {
const res = editor.clipboard.clipboardUtils.commonHTML2Block(
element,
type,
ignoreEmptyElement
);
return res ? [res] : null;
}
return null;
};

View File

@ -4,12 +4,14 @@
"license": "MIT",
"dependencies": {
"@mui/icons-material": "^5.8.4",
"date-fns": "^2.28.0",
"date-fns": "^2.29.2",
"eventemitter3": "^4.0.7",
"hotkeys-js": "^3.9.4",
"html-escaper": "^3.0.3",
"lru-cache": "^7.10.1",
"marked": "^4.0.19",
"nanoid": "^4.0.0",
"slate": "^0.81.0",
"style9": "^0.13.3"
"style9": "^0.14.0"
}
}

View File

@ -1,22 +1,34 @@
import { createContext, useContext } from 'react';
import type { BlockEditor, AsyncBlock } from './editor';
import { genErrorObj } from '@toeverything/utils';
import { createContext, PropsWithChildren, useContext } from 'react';
import type { AsyncBlock, BlockEditor } from './editor';
const RootContext = createContext<{
type EditorProps = {
editor: BlockEditor;
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
editorElement: () => JSX.Element;
}>(
};
const EditorContext = createContext<EditorProps>(
genErrorObj(
'Failed to get context! The context only can use under the "render-root"'
'Failed to get EditorContext! The context only can use under the "render-root"'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
);
export const EditorProvider = RootContext.Provider;
export const useEditor = () => {
return useContext(RootContext);
return useContext(EditorContext);
};
export const EditorProvider = ({
editor,
editorElement,
children,
}: PropsWithChildren<EditorProps>) => {
return (
<EditorContext.Provider value={{ editor, editorElement }}>
{children}
</EditorContext.Provider>
);
};
/**

View File

@ -4,12 +4,12 @@ import {
services,
type ReturnUnobserve,
} from '@toeverything/datasource/db-service';
import type { PropsWithChildren } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { EditorProvider } from './Contexts';
import type { BlockEditor } from './editor';
import { useIsOnDrag } from './hooks';
import { addNewGroup, appendNewGroup } from './recast-block';
import { BlockRenderProvider, RenderBlock } from './render-block';
import { SelectionRect, SelectionRef } from './Selection';
interface RenderRootProps {
@ -24,11 +24,7 @@ interface RenderRootProps {
const MAX_PAGE_WIDTH = 5000;
export const MIN_PAGE_WIDTH = 1480;
export const RenderRoot = ({
editor,
editorElement,
children,
}: PropsWithChildren<RenderRootProps>) => {
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
const selectionRef = useRef<SelectionRef>(null);
const triggeredBySelect = useRef(false);
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
@ -158,39 +154,43 @@ export const RenderRoot = ({
};
return (
<EditorProvider value={{ editor, editorElement }}>
<Container
isEdgeless={editor.isEdgeless}
ref={ref => {
if (ref != null && ref !== editor.container) {
editor.container = ref;
editor.getHooks().render();
}
}}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
onMouseOut={onMouseOut}
onContextMenu={onContextmenu}
onKeyDown={onKeyDown}
onKeyDownCapture={onKeyDownCapture}
onKeyUp={onKeyUp}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDragOverCapture={onDragOverCapture}
onDragEnd={onDragEnd}
onDrop={onDrop}
isOnDrag={isOnDrag}
>
<Content style={{ maxWidth: pageWidth + 'px' }}>
{children}
</Content>
{/** TODO: remove selectionManager insert */}
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
{patchedNodes}
</Container>
<EditorProvider editor={editor} editorElement={editorElement}>
<BlockRenderProvider blockRender={RenderBlock}>
<Container
isEdgeless={editor.isEdgeless}
ref={ref => {
if (ref != null && ref !== editor.container) {
editor.container = ref;
editor.getHooks().render();
}
}}
onMouseMove={onMouseMove}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
onMouseOut={onMouseOut}
onContextMenu={onContextmenu}
onKeyDown={onKeyDown}
onKeyDownCapture={onKeyDownCapture}
onKeyUp={onKeyUp}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDragOverCapture={onDragOverCapture}
onDragEnd={onDragEnd}
onDrop={onDrop}
isOnDrag={isOnDrag}
>
<Content style={{ maxWidth: pageWidth + 'px' }}>
<RenderBlock blockId={editor.getRootBlockId()} />
</Content>
{/** TODO: remove selectionManager insert */}
{editor && (
<SelectionRect ref={selectionRef} editor={editor} />
)}
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
{patchedNodes}
</Container>
</BlockRenderProvider>
</EditorProvider>
);
};
@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
);
return (
<ScrollBlankContainter
<ScrollBlankContainer
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onClick={onClick}
@ -283,7 +283,7 @@ const Content = styled('div')({
transitionTimingFunction: 'ease-in',
});
const ScrollBlankContainter = styled('div')({
const ScrollBlankContainer = styled('div')({
paddingBottom: '30vh',
margin: `0 -${PADDING_X}px`,
});

View File

@ -1,18 +1,25 @@
import type { PropsWithChildren } from 'react';
import { styled } from '@toeverything/components/ui';
import type { AsyncBlock } from '../editor';
import { containerFlavor } from '@toeverything/datasource/db-service';
import {
useCallback,
useRef,
type MouseEvent,
type PropsWithChildren,
} from 'react';
import type { AsyncBlock, BlockEditor } from '../editor';
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
import { PendantPopover } from './pendant-popover';
import { PendantRender } from './pendant-render';
import { useRef } from 'react';
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
/**
* @deprecated
*/
interface BlockTagProps {
editor: BlockEditor;
block: AsyncBlock;
}
export const BlockPendantProvider = ({
editor,
block,
children,
}: PropsWithChildren<BlockTagProps>) => {
@ -23,8 +30,23 @@ export const BlockPendantProvider = ({
const showTriggerLine =
properties.filter(property => getValue(property.id)).length === 0;
const onClick = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (containerFlavor.includes(block.type)) {
return;
}
if (e.target === e.currentTarget) {
const rect = e.currentTarget.getBoundingClientRect();
const middle = (rect.left + rect.right) / 2;
const position = e.clientX < middle ? 'start' : 'end';
editor.selectionManager.activeNodeByNodeId(block.id, position);
}
},
[editor, block]
);
return (
<Container>
<Container onClick={onClick}>
{children}
{showTriggerLine ? (

View File

@ -180,9 +180,49 @@ export class AsyncBlock {
return this.event_emitter.emit(eventName, eventData);
}
onUpdate(callback: (event: EventData) => void) {
this.on('update', callback);
/**
* @param deep observe deep
*
* NOTICE: the observe of children is async,
* so there maybe have some delay before observe done.
*/
onUpdate(callback: (event: EventData) => void, deep = 0) {
let expired = false;
const unobserveMap: Record<string, () => void> = {};
const observeChildren = () => {
if (deep <= 0) {
return;
}
this.children().then(children => {
// Check current event listeners is not expired
if (expired) {
return;
}
children.forEach(child => {
if (unobserveMap[child.id]) return;
const unobserve = child.onUpdate(callback, deep - 1);
unobserveMap[child.id] = unobserve;
});
});
};
const unobserveChildren = () => {
Object.values(unobserveMap).forEach(unobserve => {
unobserve();
});
};
this.on('update', e => {
callback(e);
// Update children observe
observeChildren();
});
observeChildren();
return () => {
expired = true;
unobserveChildren();
this.off('update', callback);
};
}

View File

@ -11,7 +11,9 @@ import {
Point,
Selection as SlateSelection,
} from 'slate';
import { AsyncBlock } from '../block';
import { Editor } from '../editor';
import { SelectBlock } from '../selection';
type TextUtilsFunctions =
| 'getString'
@ -43,7 +45,9 @@ type TextUtilsFunctions =
| 'setSelection'
| 'insertNodes'
| 'getNodeByPath'
| 'wrapLink';
| 'wrapLink'
| 'getNodeByRange'
| 'convertLeaf2Html';
type ExtendedTextUtils = SlateUtils & {
setLinkModalVisible: (visible: boolean) => void;
@ -104,15 +108,116 @@ export class BlockHelper {
return '';
}
public getBlockTextBetweenSelection(blockId: string) {
public async isBlockEditable(blockOrBlockId: AsyncBlock | string) {
const block =
typeof blockOrBlockId === 'string'
? await this._editor.getBlockById(blockOrBlockId)
: blockOrBlockId;
const blockView = this._editor.getView(block.type);
return blockView.editable;
}
public async getFlatBlocksUnderParent(
parentBlockId: string,
includeParent = false
): Promise<AsyncBlock[]> {
const blocks = [];
const block = await this._editor.getBlockById(parentBlockId);
if (includeParent) {
blocks.push(block);
}
const children = await block.children();
(
await Promise.all(
children.map(child => {
return this.getFlatBlocksUnderParent(child.id, true);
})
)
).forEach(editableChildren => {
blocks.push(...editableChildren);
});
return blocks;
}
public getBlockTextBetweenSelection(
blockId: string,
shouldUsePreviousSelection = true
) {
const text_utils = this._blockTextUtilsMap[blockId];
if (text_utils) {
return text_utils.getStringBetweenSelection(true);
return text_utils.getStringBetweenSelection(
shouldUsePreviousSelection
);
}
console.warn('Could find the block text utils');
return '';
}
public async getEditableBlockPropertiesBySelectInfo(
block: AsyncBlock,
selectInfo?: SelectBlock
) {
const properties = block.getProperties();
if (properties.text.value.length === 0) {
return properties;
}
const text_value = properties.text.value;
const {
text: { value: originTextValue, ...otherTextProperties },
...otherProperties
} = properties;
// Use deepClone method will throw incomprehensible error
let textValue = JSON.parse(JSON.stringify(originTextValue));
if (selectInfo?.endInfo) {
textValue = textValue.slice(0, selectInfo.endInfo.arrayIndex + 1);
textValue[textValue.length - 1].text = text_value[
textValue.length - 1
].text.substring(0, selectInfo.endInfo.offset);
}
if (selectInfo?.startInfo) {
textValue = textValue.slice(selectInfo.startInfo.arrayIndex);
textValue[0].text = textValue[0].text.substring(
selectInfo.startInfo.offset
);
}
return {
...otherProperties,
text: {
...otherTextProperties,
value: textValue,
},
};
}
// For editable blocks, the properties containing the selected text will be returned with the selection information
public async getBlockPropertiesBySelectInfo(selectBlockInfo: SelectBlock) {
const block = await this._editor.getBlockById(selectBlockInfo.blockId);
const blockView = this._editor.getView(block.type);
if (blockView.editable) {
return this.getEditableBlockPropertiesBySelectInfo(
block,
selectBlockInfo
);
} else {
return block?.getProperties();
}
}
public convertTextValue2Html(blockId: string, textValue: any) {
const text_utils = this._blockTextUtilsMap[blockId];
if (!text_utils) {
return '';
}
return textValue.reduce((html: string, textValueItem: any) => {
const fragment = text_utils.convertLeaf2Html(textValueItem);
return `${html}${fragment}`;
}, '');
}
public setBlockBlur(blockId: string) {
const text_utils = this._blockTextUtilsMap[blockId];
if (text_utils) {

View File

@ -1,128 +0,0 @@
import { HooksRunner } from '../types';
import { Editor } from '../editor';
import ClipboardParse from './clipboard-parse';
import { MarkdownParser } from './markdown-parse';
import { shouldHandlerContinue } from './utils';
import { Paste } from './paste';
// todo needs to be a switch
enum ClipboardAction {
COPY = 'copy',
CUT = 'cut',
PASTE = 'paste',
}
//TODO: need to consider the cursor position after inserting the children
class BrowserClipboard {
private _eventTarget: Element;
private _hooks: HooksRunner;
private _editor: Editor;
private _clipboardParse: ClipboardParse;
private _markdownParse: MarkdownParser;
private _paste: Paste;
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
this._eventTarget = eventTarget;
this._hooks = hooks;
this._editor = editor;
this._clipboardParse = new ClipboardParse(editor);
this._markdownParse = new MarkdownParser();
this._paste = new Paste(
editor,
this._clipboardParse,
this._markdownParse
);
this._initialize();
}
public getClipboardParse() {
return this._clipboardParse;
}
private _initialize() {
this._handleCopy = this._handleCopy.bind(this);
this._handleCut = this._handleCut.bind(this);
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
document.addEventListener(ClipboardAction.CUT, this._handleCut);
document.addEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
this._eventTarget.addEventListener(
ClipboardAction.COPY,
this._handleCopy
);
this._eventTarget.addEventListener(
ClipboardAction.CUT,
this._handleCut
);
this._eventTarget.addEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
}
private _handleCopy(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
}
private _handleCut(e: Event) {
if (!shouldHandlerContinue(e, this._editor)) {
return;
}
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
}
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
switch (action) {
case ClipboardAction.COPY:
this._hooks.beforeCopy(e);
break;
case ClipboardAction.CUT:
this._hooks.beforeCut(e);
break;
}
}
private _dispatchClipboardEvent(
action: ClipboardAction,
e: ClipboardEvent
) {
this._preCopyCut(action, e);
}
dispose() {
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
document.removeEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
this._eventTarget.removeEventListener(
ClipboardAction.COPY,
this._handleCopy
);
this._eventTarget.removeEventListener(
ClipboardAction.CUT,
this._handleCut
);
this._eventTarget.removeEventListener(
ClipboardAction.PASTE,
this._paste.handlePaste
);
this._clipboardParse.dispose();
this._clipboardParse = null;
this._eventTarget = null;
this._hooks = null;
this._editor = null;
}
}
export { BrowserClipboard };

View File

@ -1,207 +0,0 @@
import { Protocol, BlockFlavorKeys } from '@toeverything/datasource/db-service';
import { escape } from '@toeverything/utils';
import { Editor } from '../editor';
import { SelectBlock } from '../selection';
import { ClipBlockInfo } from './types';
class DefaultBlockParse {
public static html2block(el: Element): ClipBlockInfo[] | undefined | null {
const tag_name = el.tagName;
if (tag_name === 'DIV' || el instanceof Text) {
return el.textContent?.split('\n').map(str => {
const data = {
text: escape(str),
};
return {
type: 'text',
properties: {
text: { value: [data] },
},
children: [],
};
});
}
return null;
}
}
export default class ClipboardParse {
private editor: Editor;
private static block_types: BlockFlavorKeys[] = [
Protocol.Block.Type.page,
Protocol.Block.Type.reference,
Protocol.Block.Type.heading1,
Protocol.Block.Type.heading2,
Protocol.Block.Type.heading3,
Protocol.Block.Type.quote,
Protocol.Block.Type.todo,
Protocol.Block.Type.code,
Protocol.Block.Type.text,
Protocol.Block.Type.toc,
Protocol.Block.Type.file,
Protocol.Block.Type.image,
Protocol.Block.Type.divider,
Protocol.Block.Type.callout,
Protocol.Block.Type.youtube,
Protocol.Block.Type.figma,
Protocol.Block.Type.group,
Protocol.Block.Type.embedLink,
Protocol.Block.Type.numbered,
Protocol.Block.Type.bullet,
];
private static break_flags: Set<string> = new Set([
'BLOCKQUOTE',
'BODY',
'CENTER',
'DD',
'DIR',
'DIV',
'DL',
'DT',
'FORM',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HEAD',
'HTML',
'ISINDEX',
'MENU',
'NOFRAMES',
'P',
'PRE',
'TABLE',
'TD',
'TH',
'TITLE',
'TR',
]);
constructor(editor: Editor) {
this.editor = editor;
this.generate_html = this.generate_html.bind(this);
this.parse_dom = this.parse_dom.bind(this);
}
// TODO: escape
public text2blocks(clipData: string): ClipBlockInfo[] {
return (clipData || '').split('\n').map((str: string) => {
const block_info: ClipBlockInfo = {
type: 'text',
properties: {
text: { value: [{ text: str }] },
},
children: [] as ClipBlockInfo[],
};
return block_info;
});
}
public html2blocks(clipData: string): ClipBlockInfo[] {
return this.common_html2blocks(clipData);
}
private common_html2blocks(clipData: string): ClipBlockInfo[] {
const html_el = document.createElement('html');
html_el.innerHTML = clipData;
return this.parse_dom(html_el);
}
// tTODO:odo escape
private parse_dom(el: Element): ClipBlockInfo[] {
for (let i = 0; i < ClipboardParse.block_types.length; i++) {
const block_utils = this.editor.getView(
ClipboardParse.block_types[i]
);
const blocks = block_utils?.html2block?.(el, this.parse_dom);
if (blocks) {
return blocks;
}
}
const blocks: ClipBlockInfo[] = [];
// blocks = DefaultBlockParse.html2block(el);
for (let i = 0; i < el.childNodes.length; i++) {
const child = el.childNodes[i];
const last_block_info =
blocks.length === 0 ? null : blocks[blocks.length - 1];
let blocks_info = this.parse_dom(child as Element);
if (
last_block_info &&
last_block_info.type === 'text' &&
!ClipboardParse.break_flags.has((child as Element).tagName)
) {
const texts = last_block_info.properties?.text?.value || [];
let j = 0;
for (; j < blocks_info.length; j++) {
const block = blocks_info[j];
if (block.type === 'text') {
const block_texts = block.properties.text.value;
texts.push(...block_texts);
}
}
last_block_info.properties = {
text: { value: texts },
};
blocks_info = blocks_info.slice(j);
}
blocks.push(...blocks_info);
}
return blocks;
}
public async generateHtml(): Promise<string> {
const select_info = await this.editor.selectionManager.getSelectInfo();
return await this.generate_html(select_info.blocks);
}
public async page2html(): Promise<string> {
const root_block_id = this.editor.getRootBlockId();
if (!root_block_id) {
return '';
}
const block_info = await this.get_select_info(root_block_id);
return await this.generate_html([block_info]);
}
private async get_select_info(blockId: string) {
const block = await this.editor.getBlockById(blockId);
if (!block) return null;
const block_info: SelectBlock = {
blockId: block.id,
children: [],
};
const children_ids = block.childrenIds;
for (let i = 0; i < children_ids.length; i++) {
block_info.children.push(
await this.get_select_info(children_ids[i])
);
}
return block_info;
}
private async generate_html(selectBlocks: SelectBlock[]): Promise<string> {
let result = '';
for (let i = 0; i < selectBlocks.length; i++) {
const sel_block = selectBlocks[i];
if (!sel_block || !sel_block.blockId) continue;
const block = await this.editor.getBlockById(sel_block.blockId);
if (!block) continue;
const block_utils = this.editor.getView(block.type);
const html = await block_utils.block2html(
block,
sel_block.children,
this.generate_html
);
result += html;
}
return result;
}
public dispose() {
this.editor = null;
}
}

View File

@ -1,38 +0,0 @@
import { Editor } from '../editor';
import { SelectionManager } from '../selection';
import { HookType, PluginHooks } from '../types';
import ClipboardParse from './clipboard-parse';
import { Subscription } from 'rxjs';
import { Copy } from './copy';
class ClipboardPopulator {
private _editor: Editor;
private _hooks: PluginHooks;
private _selectionManager: SelectionManager;
private _clipboardParse: ClipboardParse;
private _sub = new Subscription();
private _copy: Copy;
constructor(
editor: Editor,
hooks: PluginHooks,
selectionManager: SelectionManager
) {
this._editor = editor;
this._hooks = hooks;
this._selectionManager = selectionManager;
this._clipboardParse = new ClipboardParse(editor);
this._copy = new Copy(editor);
this._sub.add(
hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy)
);
this._sub.add(
hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy)
);
}
disposeInternal() {
this._sub.unsubscribe();
this._hooks = null;
}
}
export { ClipboardPopulator };

Some files were not shown because too many files have changed in this diff Show More