mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 21:51:39 +03:00
merge branch develop into branch feat/doublelink220820
This commit is contained in:
commit
3a9aa99ee9
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -5,6 +5,7 @@
|
||||
"editor.codeActionsOnSave": ["source.fixAll", "source.organizeImports"],
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"cSpell.words": [
|
||||
"aboutus",
|
||||
"AUTOINCREMENT",
|
||||
"Backlinks",
|
||||
"blockdb",
|
||||
|
132
README.md
132
README.md
@ -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>
|
||||
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
|
||||
<a href="https://medium.com/@affineworkos"><img src="https://img.shields.io/badge/-Medium-red?style=social&logo=medium" height=25></a>
|
||||
</p>
|
||||
@ -48,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.
|
||||
|
@ -13,7 +13,7 @@
|
||||
"@mui/icons-material": "^5.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"firebase": "^9.9.2",
|
||||
"firebase": "^9.9.3",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"webpack": "^5.74.0"
|
||||
}
|
||||
|
@ -22,16 +22,23 @@ import { type BlockEditor } from '@toeverything/components/editor-core';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { CollapsiblePageTree } from './collapsible-page-tree';
|
||||
import { Tabs } from './components/tabs';
|
||||
import { TabMap, TAB_TITLE } from './components/tabs/Tabs';
|
||||
import { TOC } from './components/toc';
|
||||
import { WorkspaceName } from './workspace-name';
|
||||
|
||||
type PageProps = {
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
export function Page(props: PageProps) {
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
TabMap.get(TAB_TITLE.PAGES).value
|
||||
);
|
||||
const { page_id } = useParams();
|
||||
const { showSpaceSidebar, fixedDisplay, setSpaceSidebarVisible } =
|
||||
useShowSpaceSidebar();
|
||||
const dailyNotesFlag = useFlag('BooleanDailyNotes', false);
|
||||
const onTabChange = v => setActiveTab(v);
|
||||
|
||||
return (
|
||||
<LigoApp>
|
||||
@ -50,31 +57,37 @@ export function Page(props: PageProps) {
|
||||
>
|
||||
<WorkspaceName />
|
||||
|
||||
<Tabs />
|
||||
<Tabs activeTab={activeTab} onTabChange={onTabChange} />
|
||||
|
||||
<WorkspaceSidebarContent>
|
||||
<div>
|
||||
{dailyNotesFlag && (
|
||||
{activeTab === TabMap.get(TAB_TITLE.PAGES).value && (
|
||||
<div>
|
||||
{dailyNotesFlag && (
|
||||
<div>
|
||||
<CollapsibleTitle title="Daily Notes">
|
||||
<CalendarHeatmap />
|
||||
</CollapsibleTitle>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CollapsibleTitle title="Daily Notes">
|
||||
<CalendarHeatmap />
|
||||
<CollapsibleTitle
|
||||
title="ACTIVITIES"
|
||||
initialOpen={false}
|
||||
>
|
||||
<Activities />
|
||||
</CollapsibleTitle>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CollapsibleTitle
|
||||
title="ACTIVITIES"
|
||||
initialOpen={false}
|
||||
>
|
||||
<Activities />
|
||||
</CollapsibleTitle>
|
||||
<div>
|
||||
<CollapsiblePageTree title="PAGES">
|
||||
{page_id ? <PageTree /> : null}
|
||||
</CollapsiblePageTree>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CollapsiblePageTree title="PAGES">
|
||||
{page_id ? <PageTree /> : null}
|
||||
</CollapsiblePageTree>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === TabMap.get(TAB_TITLE.TOC).value && (
|
||||
<TOC />
|
||||
)}
|
||||
</WorkspaceSidebarContent>
|
||||
</WorkspaceSidebar>
|
||||
</LigoLeftContainer>
|
||||
@ -105,6 +118,7 @@ const EditorContainer = ({
|
||||
const obv = new ResizeObserver(e => {
|
||||
setPageClientWidth(e[0].contentRect.width);
|
||||
});
|
||||
|
||||
obv.observe(scrollContainer);
|
||||
return () => obv.disconnect();
|
||||
}
|
||||
@ -175,7 +189,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({
|
||||
width: 300,
|
||||
minWidth: 300,
|
||||
borderRadius: '0px 10px 10px 0px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
boxShadow: theme.affine.shadows.shadow2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
transitionProperty: 'left',
|
||||
transitionDuration: '0.35s',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { ValueOf } from '@toeverything/utils';
|
||||
import { useState } from 'react';
|
||||
|
||||
const StyledTabs = styled('div')(({ theme }) => {
|
||||
return {
|
||||
@ -56,32 +55,35 @@ const StyledTabTitle = styled('div')<{
|
||||
}
|
||||
`;
|
||||
|
||||
const TAB_TITLE = {
|
||||
PAGES: 'pages',
|
||||
GALLERY: 'gallery',
|
||||
TOC: 'toc',
|
||||
export const TAB_TITLE = {
|
||||
PAGES: 'PAGES',
|
||||
GALLERY: 'GALLERY',
|
||||
TOC: 'TOC',
|
||||
} as const;
|
||||
|
||||
const TabMap = new Map<TabKey, { value: TabValue; disabled?: boolean }>([
|
||||
['PAGES', { value: 'pages' }],
|
||||
['GALLERY', { value: 'gallery', disabled: true }],
|
||||
['TOC', { value: 'toc' }],
|
||||
export const TabMap = new Map<
|
||||
TabValue,
|
||||
{ value: TabValue; disabled?: boolean }
|
||||
>([
|
||||
[TAB_TITLE.PAGES, { value: TAB_TITLE.PAGES }],
|
||||
[TAB_TITLE.GALLERY, { value: TAB_TITLE.GALLERY, disabled: true }],
|
||||
[TAB_TITLE.TOC, { value: TAB_TITLE.TOC }],
|
||||
]);
|
||||
|
||||
type TabKey = keyof typeof TAB_TITLE;
|
||||
type TabValue = ValueOf<typeof TAB_TITLE>;
|
||||
|
||||
const Tabs = () => {
|
||||
const [activeValue, setActiveTab] = useState<TabValue>(TAB_TITLE.PAGES);
|
||||
interface Props {
|
||||
activeTab: TabValue;
|
||||
onTabChange: (v: TabValue) => void;
|
||||
}
|
||||
|
||||
const onClick = (v: TabValue) => {
|
||||
setActiveTab(v);
|
||||
};
|
||||
const Tabs = (props: Props) => {
|
||||
const { activeTab, onTabChange } = props;
|
||||
|
||||
return (
|
||||
<StyledTabs>
|
||||
{[...TabMap.entries()].map(([k, { value, disabled = false }]) => {
|
||||
const isActive = activeValue === value;
|
||||
const isActive = activeTab === value;
|
||||
|
||||
return (
|
||||
<StyledTabTitle
|
||||
@ -89,7 +91,7 @@ const Tabs = () => {
|
||||
className={isActive ? 'active' : ''}
|
||||
isActive={isActive}
|
||||
isDisabled={disabled}
|
||||
onClick={() => onClick(value)}
|
||||
onClick={() => onTabChange(value)}
|
||||
>
|
||||
{k}
|
||||
</StyledTabTitle>
|
||||
|
202
apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx
Normal file
202
apps/ligo-virgo/src/pages/workspace/docs/components/toc/TOC.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { TOC } from './TOC';
|
@ -0,0 +1,6 @@
|
||||
export enum BLOCK_TYPES {
|
||||
GROUP = 'group',
|
||||
HEADING1 = 'heading1',
|
||||
HEADING2 = 'heading2',
|
||||
HEADING3 = 'heading3',
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
import { AsyncBlock } from '@toeverything/components/editor-core';
|
||||
import { BLOCK_TYPES } from './toc-enum';
|
||||
import type { ListenerMap, TOCType } from './types';
|
||||
|
||||
/* 😞😞sorry, I don't know how to define unlimited dimensions array */
|
||||
const getContentByAsyncBlocks = async (
|
||||
asyncBlocks: AsyncBlock[] = [],
|
||||
callback: () => void,
|
||||
listenerMap: ListenerMap
|
||||
): Promise<{
|
||||
tocContents: any[];
|
||||
}> => {
|
||||
const collect = async (asyncBlocks): Promise<any[]> => {
|
||||
/* maybe should recast it to tail recursion */
|
||||
return await Promise.all(
|
||||
asyncBlocks.map(async (asyncBlock: AsyncBlock) => {
|
||||
const asyncBlocks = await asyncBlock?.children();
|
||||
|
||||
if (asyncBlocks?.length) {
|
||||
return collect(asyncBlocks);
|
||||
}
|
||||
|
||||
/* add only once event listener for every block */
|
||||
if (!listenerMap.has(asyncBlock?.id)) {
|
||||
/* get update notice */
|
||||
const destroyHandler = asyncBlock?.onUpdate(callback);
|
||||
|
||||
/* collect destroy handlers */
|
||||
listenerMap.set(asyncBlock?.id, destroyHandler);
|
||||
}
|
||||
|
||||
const { id, type } = asyncBlock;
|
||||
|
||||
switch (type) {
|
||||
case BLOCK_TYPES.GROUP:
|
||||
case BLOCK_TYPES.HEADING1:
|
||||
case BLOCK_TYPES.HEADING2:
|
||||
case BLOCK_TYPES.HEADING3: {
|
||||
const properties = await asyncBlock?.getProperties();
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
text: properties?.text?.value?.[0]?.text || '',
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
tocContents: await collect(asyncBlocks),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* get flat toc
|
||||
* @param asyncBlocks
|
||||
* @param tocContents
|
||||
*/
|
||||
const getPageTOC = (asyncBlocks: AsyncBlock[], tocContents): TOCType[] => {
|
||||
return tocContents
|
||||
.reduce((tocGroupContent, tocContent, index) => {
|
||||
const { id, type } = asyncBlocks[index];
|
||||
const groupContent = {
|
||||
id,
|
||||
type,
|
||||
text: 'Untitled Group',
|
||||
};
|
||||
|
||||
tocGroupContent.push(
|
||||
!tocContent.flat(Infinity).filter(Boolean).length
|
||||
? groupContent
|
||||
: tocContent
|
||||
);
|
||||
|
||||
return tocGroupContent;
|
||||
}, [])
|
||||
.flat(Infinity)
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/* destroy page/block update-listener */
|
||||
const destroyEventList = (listenerMap: ListenerMap) => {
|
||||
const eventListeners = listenerMap.values();
|
||||
listenerMap.clear();
|
||||
|
||||
for (const eventListener of eventListeners) {
|
||||
eventListener?.();
|
||||
}
|
||||
};
|
||||
|
||||
export { getPageTOC, getContentByAsyncBlocks, destroyEventList };
|
@ -0,0 +1,7 @@
|
||||
export type TOCType = {
|
||||
id: string;
|
||||
type: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type ListenerMap = Map<string, () => void>;
|
@ -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)$/,
|
||||
|
@ -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",
|
||||
|
110
apps/venus/src/app/AboutUs.tsx
Normal file
110
apps/venus/src/app/AboutUs.tsx
Normal 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
600
apps/venus/src/app/App.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
426
apps/venus/src/app/Common.tsx
Normal file
426
apps/venus/src/app/Common.tsx
Normal 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>
|
||||
);
|
||||
};
|
75
apps/venus/src/app/Icons.tsx
Normal file
75
apps/venus/src/app/Icons.tsx
Normal 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>
|
||||
);
|
||||
};
|
45
apps/venus/src/app/MdxMarks.tsx
Normal file
45
apps/venus/src/app/MdxMarks.tsx
Normal 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' }} />;
|
||||
};
|
88
apps/venus/src/app/about.mdx
Normal file
88
apps/venus/src/app/about.mdx
Normal 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.
|
@ -1,5 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"Blog": "Blog",
|
||||
"AboutUs": "About AboutUs",
|
||||
"Open Source": "Open Source",
|
||||
"Privacy First": "Privacy First",
|
||||
"Alternative": "Alternative",
|
||||
|
@ -1,5 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"Blog": "博客",
|
||||
"AboutUs": "关于我们",
|
||||
"Open Source": "开源",
|
||||
"Privacy First": "隐私第一",
|
||||
"Alternative": "的另一种选择",
|
||||
|
File diff suppressed because it is too large
Load Diff
BIN
apps/venus/src/app/keeupdate.png
Normal file
BIN
apps/venus/src/app/keeupdate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 617 KiB |
BIN
apps/venus/src/assets/images/og.png
Normal file
BIN
apps/venus/src/assets/images/og.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
@ -6,6 +6,27 @@
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://affine.pro/" />
|
||||
<meta name="twitter:title" content="AFFiNE - All In One Workos" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Affine is the next-generation collaborative knowledge base for professionals."
|
||||
/>
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.png" />
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="AFFiNE - All In One Workos" />
|
||||
<meta property="og:site_name" content="AFFiNE - All In One Workos" />
|
||||
<meta property="og:url" content="https://affine.pro/" />
|
||||
<meta property="og:image" content="https://affine.pro/og.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Affine is the next-generation collaborative knowledge base for professionals."
|
||||
/>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
|
@ -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
4
apps/venus/src/mdx.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.mdx' {
|
||||
let MDXComponent: (props: any) => JSX.Element;
|
||||
export default MDXComponent;
|
||||
}
|
@ -19,5 +19,5 @@
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
],
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.d.ts"]
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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 don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See our [Labels](https://github.com/toeverything/AFFiNE/labels) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
|
||||
### Make Changes
|
||||
|
||||
@ -65,6 +63,8 @@ For more information about using a codespace for working on GitHub documentation
|
||||
|
||||
Commit the changes once you are happy with them.
|
||||
|
||||
Reach out the community members for necessary help.
|
||||
|
||||
Once your changes are ready, don't forget to self-review to speed up the review process:zap:.
|
||||
|
||||
### Pull Request
|
||||
@ -83,6 +83,6 @@ When you're finished with the changes, create a pull request, also known as a PR
|
||||
|
||||
Congratulations :tada::tada: The AFFiNE team thanks you :sparkles:.
|
||||
|
||||
Once your PR is merged, your contributions will be publicly visible on the our GitHub.
|
||||
Once your PR is merged, your contributions will be publicly visible on our GitHub.
|
||||
|
||||
Now that you are part of the AFFiNE community, see how else you can join and help over at [Gitbook](https://docs.affine.pro/affine/)
|
31
docs/types-of-contributions.md
Normal file
31
docs/types-of-contributions.md
Normal 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.
|
@ -5,10 +5,7 @@ import { getSession } from '@toeverything/components/board-sessions';
|
||||
import { deepCopy, TldrawApp } from '@toeverything/components/board-state';
|
||||
import { tools } from '@toeverything/components/board-tools';
|
||||
import { TDShapeType } from '@toeverything/components/board-types';
|
||||
import {
|
||||
getClipDataOfBlocksById,
|
||||
RecastBlockProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { RecastBlockProvider } from '@toeverything/components/editor-core';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BlockEditor } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useState } from 'react';
|
||||
@ -51,10 +48,10 @@ const AffineBoard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const { shapes, bindings } = useShapes(workspace, rootBlockId);
|
||||
const { shapes } = useShapes(workspace, rootBlockId);
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
app.replacePageContent(shapes || {}, bindings, {});
|
||||
app.replacePageContent(shapes || {}, {}, {});
|
||||
}
|
||||
}, [app, shapes]);
|
||||
|
||||
@ -68,12 +65,14 @@ const AffineBoard = ({
|
||||
onMount(app) {
|
||||
set_app(app);
|
||||
},
|
||||
|
||||
async onPaste(e, data) {
|
||||
console.log('e,data: ', e, data);
|
||||
},
|
||||
async onCopy(e, groupIds) {
|
||||
const clip = await getClipDataOfBlocksById(
|
||||
editor,
|
||||
groupIds
|
||||
);
|
||||
const clip =
|
||||
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
|
||||
groupIds
|
||||
);
|
||||
|
||||
e.clipboardData?.setData(
|
||||
clip.getMimeType(),
|
||||
@ -109,19 +108,6 @@ const AffineBoard = ({
|
||||
});
|
||||
}
|
||||
shape.affineId = block.id;
|
||||
|
||||
Object.keys(bindings).forEach(bilingKey => {
|
||||
if (
|
||||
bindings[bilingKey]?.fromId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].fromId = block.id;
|
||||
}
|
||||
if (
|
||||
bindings[bilingKey]?.toId === shape.id
|
||||
) {
|
||||
bindings[bilingKey].toId = block.id;
|
||||
}
|
||||
});
|
||||
return await services.api.editorBlock.update({
|
||||
workspace: shape.workspace,
|
||||
id: block.id,
|
||||
@ -134,32 +120,6 @@ const AffineBoard = ({
|
||||
}
|
||||
})
|
||||
);
|
||||
let pageBindingsString = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
)?.[0].properties.bindings?.value;
|
||||
console.log(123123123);
|
||||
let pageBindings = JSON.parse(pageBindingsString ?? '{}');
|
||||
console.log(pageBindings, 3333, bindings);
|
||||
Object.keys(bindings).forEach(bindingsKey => {
|
||||
console.log(345345345345345);
|
||||
if (!bindings[bindingsKey]) {
|
||||
delete pageBindings[bindingsKey];
|
||||
} else {
|
||||
Object.assign(pageBindings, bindings);
|
||||
}
|
||||
});
|
||||
services.api.editorBlock.update({
|
||||
workspace: workspace,
|
||||
id: rootBlockId,
|
||||
properties: {
|
||||
bindings: {
|
||||
value: JSON.stringify(pageBindings),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -5,24 +5,12 @@ import { services } from '@toeverything/datasource/db-service';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const getBindings = (workspace: string, rootBlockId: string) => {
|
||||
return services.api.editorBlock
|
||||
.get({
|
||||
workspace: workspace,
|
||||
ids: [rootBlockId],
|
||||
})
|
||||
.then(blcoks => {
|
||||
return blcoks[0].properties.bindings?.value;
|
||||
});
|
||||
};
|
||||
|
||||
export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
const [blocks, setBlocks] = useState<{
|
||||
shapes: [ReturnEditorBlock[]];
|
||||
bindings: string;
|
||||
}>();
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@ -43,11 +31,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return shapes;
|
||||
}),
|
||||
]).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes,
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: shapes,
|
||||
});
|
||||
});
|
||||
|
||||
@ -65,11 +50,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
return childBlock;
|
||||
})
|
||||
).then(shapes => {
|
||||
getBindings(workspace, rootBlockId).then(bindings => {
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
bindings: bindings,
|
||||
});
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
});
|
||||
});
|
||||
})
|
||||
@ -104,8 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, TDShape>);
|
||||
|
||||
return {
|
||||
shapes: blocksShapes,
|
||||
bindings: JSON.parse(blocks?.bindings ?? '{}'),
|
||||
};
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import {
|
||||
RenderBlock,
|
||||
RenderRoot,
|
||||
type BlockEditor,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
editor={editor}
|
||||
editorElement={AffineEditor as any}
|
||||
scrollBlank={scrollBlank}
|
||||
>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</RenderRoot>
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -362,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
const hideBounds =
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name === 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId;
|
||||
|
@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px',
|
||||
padding: '3px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
'&:hover': {
|
||||
|
@ -21,6 +21,9 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
let activeShape = shapes[0];
|
||||
let toNextShapBindings: ArrowBinding[] = [];
|
||||
let bindingId = '';
|
||||
if (!activeShape) {
|
||||
return;
|
||||
}
|
||||
Object.keys(bindings).forEach(key => {
|
||||
if (bindings[key].toId === activeShape.id) {
|
||||
bindingId = bindings[key].fromId;
|
||||
@ -35,7 +38,6 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
toNextShapBindings.forEach(binding => {
|
||||
if (binding.toId !== activeShape.id) {
|
||||
allShape.forEach(item => {
|
||||
console.log(item);
|
||||
if (item.id === binding.toId) {
|
||||
ArrowToArr.push(item);
|
||||
}
|
||||
@ -44,7 +46,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
});
|
||||
setarrowToArr(ArrowToArr);
|
||||
return () => {};
|
||||
}, [app.page.bindings, app.shapes]);
|
||||
}, [app.page.bindings]);
|
||||
const jumpToNextShap = (shape: TDShape) => {
|
||||
app.zoomToShapes([shape]);
|
||||
};
|
||||
@ -68,7 +70,7 @@ export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip content="Font Size" placement="top-start">
|
||||
<Tooltip content="ArrowToEditor" placement="top-start">
|
||||
<IconButton>
|
||||
<ConnectorIcon></ConnectorIcon>
|
||||
</IconButton>
|
||||
|
@ -2,7 +2,6 @@ import { TLDR, TldrawApp } from '@toeverything/components/board-state';
|
||||
import { Divider, Popover, styled } from '@toeverything/components/ui';
|
||||
import { Fragment } from 'react';
|
||||
import { AlignOperation } from './AlignOperation';
|
||||
import { ArrowTo } from './ArrowTo';
|
||||
import { BorderColorConfig } from './BorderColorConfig';
|
||||
import { DeleteShapes } from './DeleteOperation';
|
||||
import { FillColorConfig } from './FillColorConfig';
|
||||
@ -107,12 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></AlignOperation>
|
||||
) : null,
|
||||
toNextShap: (
|
||||
<ArrowTo
|
||||
app={app}
|
||||
shapes={config.deleteShapes.selectedShapes}
|
||||
></ArrowTo>
|
||||
),
|
||||
// toNextShap: (
|
||||
// <ArrowTo
|
||||
// app={app}
|
||||
// shapes={config.deleteShapes.selectedShapes}
|
||||
// ></ArrowTo>
|
||||
// ),
|
||||
};
|
||||
|
||||
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);
|
||||
|
@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Line
|
||||
@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Rectangle
|
||||
@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
@ -12,12 +12,12 @@ import {
|
||||
import {
|
||||
IconButton,
|
||||
PopoverContainer,
|
||||
styled,
|
||||
// MuiIconButton as IconButton,
|
||||
// MuiTooltip as Tooltip,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { TldrawApp } from '@toeverything/components/board-state';
|
||||
import {
|
||||
@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
}}
|
||||
direction="none"
|
||||
>
|
||||
<div className={styles('container')}>
|
||||
<div className={styles('toolBar')}>
|
||||
<Container>
|
||||
<ToolBar>
|
||||
{tools.map(
|
||||
({
|
||||
type,
|
||||
@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolBar>
|
||||
</Container>
|
||||
</PopoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
toolBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
},
|
||||
const Container = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
const ToolBar = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
styled,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { ReactElement, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties, type ReactElement } from 'react';
|
||||
import { Palette } from '../../palette';
|
||||
import { Pen } from './Pen';
|
||||
|
||||
@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
|
||||
|
||||
export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
const appCurrentTool = app.useStore(state => state.appState.activeTool);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const chosenPen =
|
||||
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
|
||||
PENCIL_CONFIGS[0];
|
||||
@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<Container>
|
||||
<PensContainer>
|
||||
@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
name={title}
|
||||
primaryColor={color_vars['--color-0']}
|
||||
secondaryColor={color_vars['--color-1']}
|
||||
onClick={() => setPen(key)}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
setPen(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
<Palette
|
||||
selected={chosenColor}
|
||||
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
|
||||
onSelect={color => setPenColor(color)}
|
||||
onSelect={color => {
|
||||
setVisible(false);
|
||||
setPenColor(color);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
</Container>
|
||||
|
@ -42,7 +42,7 @@ type E = HTMLDivElement;
|
||||
export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Arrow as const;
|
||||
|
||||
override hideBounds = true;
|
||||
override hideBounds = false;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import {
|
||||
BINDING_DISTANCE,
|
||||
ShapeStyles,
|
||||
} from '@toeverything/components/board-types';
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
className={'tl-fill-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={1}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
width={w - sw / 2}
|
||||
height={h - sw / 2}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
@ -175,6 +175,10 @@ interface TDCallbacks {
|
||||
* (optional) A callback to run when the shape is copied.
|
||||
*/
|
||||
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
|
||||
/**
|
||||
* (optional) A callback to run when the shape is paste.
|
||||
*/
|
||||
onPaste?: (e: ClipboardEvent, data: any) => void;
|
||||
}
|
||||
|
||||
export interface TldrawAppCtorProps {
|
||||
@ -626,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
private prev_bindings = this.page.bindings;
|
||||
private prev_assets = this.document.assets;
|
||||
|
||||
private _broadcastPageChanges = () => {
|
||||
private _broadcastPageChanges = async () => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {};
|
||||
@ -683,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.just_sent = true;
|
||||
this.callbacks.onChangePage?.(
|
||||
await this.callbacks.onChangePage?.(
|
||||
this,
|
||||
changedShapes,
|
||||
changedBindings,
|
||||
@ -1956,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return;
|
||||
|
||||
if (e) {
|
||||
const data = e.clipboardData?.getData('text/html');
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
return json;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
this.callbacks.onPaste?.(e, paste_as_html(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const pasteInCurrentPage = (
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[],
|
||||
@ -2107,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
// this.select(shapeId);
|
||||
};
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (e !== undefined) {
|
||||
const items: DataTransferItemList =
|
||||
e.clipboardData?.items ?? ({} as DataTransferItemList);
|
||||
|
@ -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',
|
||||
}}
|
||||
>
|
||||
|
@ -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() {
|
||||
|
@ -38,7 +38,7 @@
|
||||
"react-window": "^1.8.7",
|
||||
"slate": "^0.81.1",
|
||||
"slate-react": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codemirror": "^5.60.5",
|
||||
|
@ -1,29 +1,28 @@
|
||||
import type { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { BulletIcon, getChildrenType, NumberType } from './data';
|
||||
import { BulletBlock, BulletProperties } from './types';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
export const defaultBulletProps: BulletProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@ -189,7 +188,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<BulletLeft>
|
||||
<BulletIcon numberType={properties.numberType} />
|
||||
@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
</div>
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
CreateView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { defaultBulletProps, BulletView } from './BulletView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { BulletView, defaultBulletProps } from './BulletView';
|
||||
|
||||
export class BulletBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.bullet;
|
||||
@ -27,66 +26,44 @@ export class BulletBlock extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
if (!firstList || firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && !el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,58 +1,54 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { cpp } from '@codemirror/lang-cpp';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { lezer } from '@codemirror/lang-lezer';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { julia } from '@codemirror/legacy-modes/mode/julia';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { DuplicateIcon } from '@toeverything/components/icons';
|
||||
import { Option, Select, styled } from '@toeverything/components/ui';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
|
||||
interface CreateCodeView extends CreateView {
|
||||
style9?: StyleWithAtRules;
|
||||
@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
'&:hover': {
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
opacity: 0,
|
||||
transition: 'opacity 1.5s',
|
||||
},
|
||||
'.copy-block': {
|
||||
padding: '0px 10px',
|
||||
@ -172,7 +170,7 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
editor.selectionManager.activePreviousNode(block.id, 'start');
|
||||
};
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<CodeBlock
|
||||
onKeyDown={e => {
|
||||
e.stopPropagation();
|
||||
@ -200,7 +198,8 @@ export const CodeView = ({ block, editor }: CreateCodeView) => {
|
||||
</div>
|
||||
<div>
|
||||
<div className="copy-block" onClick={copyCode}>
|
||||
<DuplicateIcon></DuplicateIcon>Copy
|
||||
<DuplicateIcon />
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,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/>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +1,35 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { DividerView } from './divider-view';
|
||||
import { Block2HtmlProps, commonHTML2block } from '../../utils/commonBlockClip';
|
||||
|
||||
export class DividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.divider;
|
||||
View = DividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'HR',
|
||||
ignoreEmptyElement: false,
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
|
@ -5,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>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { useState } from 'react';
|
||||
import { SourceView } from '../../components/source-view';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { LinkContainer } from '../../components/style-container';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
|
||||
const MESSAGES = {
|
||||
ADD_FIGMA_LINK: 'Add figma link',
|
||||
@ -30,7 +29,7 @@ export const FigmaView = ({ block, editor }: FigmaView) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<LinkContainer>
|
||||
{figmaUrl ? (
|
||||
<SourceView
|
||||
|
@ -5,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>`;
|
||||
}
|
||||
}
|
||||
|
@ -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>`
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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/>`;
|
||||
}
|
||||
}
|
||||
|
@ -21,19 +21,6 @@ const SceneMap: Record<RecastScene, ComponentType<CreateView>> = {
|
||||
kanban: SceneKanban,
|
||||
} as const;
|
||||
|
||||
const GroupBox = styled('div')(({ theme }) => {
|
||||
return {
|
||||
'&:hover': {
|
||||
// Workaround referring to other components
|
||||
// See https://emotion.sh/docs/styled#targeting-another-emotion-component
|
||||
// [GroupActionWrapper.toString()]: {},
|
||||
'& > *': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
@ -59,6 +46,14 @@ const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const GroupBox = styled('div')({
|
||||
'&:hover': {
|
||||
[GroupActionWrapper.toString()]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
({ isSelect, theme }) => ({
|
||||
background: theme.affine.palette.white,
|
||||
|
@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const ScenePage = ({ block }: CreateView) => {
|
||||
return <RenderBlockChildren block={block} />;
|
||||
return <RenderBlockChildren block={block} indent={false} />;
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ export const AddViewMenu = () => {
|
||||
onClick={() => setActivePanel(!activePanel)}
|
||||
>
|
||||
<AddViewIcon fontSize="small" />
|
||||
<span>Add View</span>
|
||||
<span style={{ userSelect: 'none' }}>Add View</span>
|
||||
{activePanel && (
|
||||
<Panel>
|
||||
<PanelItem>
|
||||
@ -66,10 +66,10 @@ export const AddViewMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
@ -20,7 +20,7 @@ export const ViewsMenu = () => {
|
||||
useRecastView();
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setViewName(e.target.value.trim());
|
||||
setViewName(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||
@ -36,7 +36,7 @@ export const ViewsMenu = () => {
|
||||
}
|
||||
await updateView({
|
||||
...activeView,
|
||||
name: viewName,
|
||||
name: viewName.trim(),
|
||||
type: viewType,
|
||||
});
|
||||
setActiveView(null);
|
||||
@ -99,10 +99,10 @@ export const ViewsMenu = () => {
|
||||
key={name}
|
||||
active={viewType === scene}
|
||||
onClick={() => {
|
||||
if (scene === RecastScene.Table) {
|
||||
// The table view is under progress
|
||||
return;
|
||||
}
|
||||
// if (scene === RecastScene.Table) {
|
||||
// // The table view is under progress
|
||||
// return;
|
||||
// }
|
||||
setViewType(scene);
|
||||
}}
|
||||
style={{ textTransform: 'uppercase' }}
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { RecastScene } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
KanBanIcon,
|
||||
TableIcon,
|
||||
TodoListIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { KanBanIcon, TodoListIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const VIEW_LIST = [
|
||||
{
|
||||
@ -16,9 +12,9 @@ export const VIEW_LIST = [
|
||||
scene: RecastScene.Kanban,
|
||||
icon: <KanBanIcon fontSize="small" />,
|
||||
},
|
||||
{
|
||||
name: 'Table',
|
||||
scene: RecastScene.Table,
|
||||
icon: <TableIcon fontSize="small" />,
|
||||
},
|
||||
// {
|
||||
// name: 'Table',
|
||||
// scene: RecastScene.Table,
|
||||
// icon: <TableIcon fontSize="small" />,
|
||||
// },
|
||||
] as const;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
import type {
|
||||
KanbanCard,
|
||||
KanbanGroup,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { useKanban } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { useCallback } from 'react';
|
||||
import { CardItem } from './CardItem';
|
||||
import { CardItemPanelWrapper } from './dndable/wrapper/CardItemPanelWrapper';
|
||||
|
||||
const AddCardWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
@ -48,7 +48,7 @@ export const CardContext = (props: Props) => {
|
||||
item={item}
|
||||
active={activeId === id}
|
||||
>
|
||||
<CardItem id={id} block={block} />
|
||||
<CardItem block={block} />
|
||||
</CardItemPanelWrapper>
|
||||
</StyledCardContainer>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {
|
||||
KanbanBlockRender,
|
||||
KanbanCard,
|
||||
RenderBlock,
|
||||
useEditor,
|
||||
useKanban,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@ -10,7 +10,6 @@ import {
|
||||
MuiClickAwayListener,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
import { useState, type MouseEvent } from 'react';
|
||||
import { useRefPage } from './RefPage';
|
||||
|
||||
@ -82,41 +81,37 @@ const Overlay = styled('div')({
|
||||
},
|
||||
});
|
||||
|
||||
export const CardItem = ({
|
||||
id,
|
||||
block,
|
||||
}: {
|
||||
id: KanbanCard['id'];
|
||||
block: KanbanCard['block'];
|
||||
}) => {
|
||||
export const CardItem = ({ block }: { block: KanbanCard['block'] }) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const [editable, setEditable] = useState(false);
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const [editableBlock, setEditableBlock] = useState<string | null>(null);
|
||||
const { editor } = useEditor();
|
||||
|
||||
const onAddItem = async () => {
|
||||
setEditable(true);
|
||||
await addSubItem(block);
|
||||
const newItem = await addSubItem(block);
|
||||
setEditableBlock(newItem.id);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
openSubPage(id);
|
||||
openSubPage(block.id);
|
||||
};
|
||||
|
||||
const onClickPen = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
setEditable(true);
|
||||
setEditableBlock(block.id);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
|
||||
<MuiClickAwayListener onClickAway={() => setEditableBlock(null)}>
|
||||
<CardContainer>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
<KanbanBlockRender
|
||||
blockId={block.id}
|
||||
activeBlock={editableBlock}
|
||||
/>
|
||||
</CardContent>
|
||||
{showKanbanRefPageFlag && !editable && (
|
||||
{!editableBlock && (
|
||||
<Overlay onClick={onClickCard}>
|
||||
<IconButton backgroundColor="#fff" onClick={onClickPen}>
|
||||
<PenIcon />
|
||||
|
@ -21,13 +21,17 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
return createPortal(
|
||||
<MuiBackdrop
|
||||
open={open}
|
||||
onMouseDown={(e: { stopPropagation: () => void }) => {
|
||||
// Prevent trigger the bottom editor's selection
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'rgba(58, 76, 92, 0.4)',
|
||||
zIndex: theme.affine.zIndex.popover,
|
||||
}}
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={(e: { stopPropagation: () => void }) => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
import { CardItem } from '../../CardItem';
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import type { DndableItems } from '../type';
|
||||
import { CardItemWrapper } from '../wrapper/CardItemWrapper';
|
||||
|
||||
export function renderContainerDragOverlay({
|
||||
containerId,
|
||||
@ -18,7 +17,7 @@ export function renderContainerDragOverlay({
|
||||
return (
|
||||
<CardItemWrapper
|
||||
key={id}
|
||||
card={<CardItem key={id} id={id} block={block} />}
|
||||
card={<CardItem key={id} block={block} />}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
|
@ -5,35 +5,13 @@ import {
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GroupDividerView } from './groupDividerView';
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class GroupDividerBlock extends BaseView {
|
||||
type = Protocol.Block.Type.groupDivider;
|
||||
View = GroupDividerView;
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'HR') {
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: {},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
return `<hr>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<hr/>`;
|
||||
}
|
||||
}
|
||||
|
@ -165,12 +165,10 @@ export const ImageView = ({ block, editor }: ImageView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<ImageBlock>
|
||||
<div style={{ position: 'relative' }} ref={resizeBox}>
|
||||
{!isSelect ? (
|
||||
<ImageShade onClick={handleClick}></ImageShade>
|
||||
) : null}
|
||||
{!isSelect ? <ImageShade onClick={handleClick} /> : null}
|
||||
{imgUrl ? (
|
||||
<div
|
||||
onMouseDown={e => {
|
||||
|
@ -1,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>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@ -11,18 +11,17 @@ import {
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import type { Numbered, NumberedAsyncBlock } from './types';
|
||||
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
|
||||
export const defaultTodoProps: Numbered = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@ -184,7 +183,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<List>
|
||||
<div className={'checkBoxContainer'}>
|
||||
{getNumber(properties.numberType, number)}.
|
||||
@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,16 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
|
||||
export class NumberedBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.numbered;
|
||||
@ -29,71 +28,39 @@ export class NumberedBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'OL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'OL') {
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ol><li>${content}</li></ol>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ol><li>${await commonBlock2HtmlContent(props)}</li></ol>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { useRef, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { BackLink, TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
RenderBlockChildren,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { ContentColumnValue } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { Theme, styled } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
TextManage,
|
||||
@ -81,7 +81,7 @@ export const PageView = ({ block, editor }: CreateView) => {
|
||||
|
||||
return (
|
||||
<PageTitleBlock>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextManage
|
||||
alwaysShowPlaceholder
|
||||
ref={textRef}
|
||||
|
@ -1,21 +1,9 @@
|
||||
import { withRecastBlock } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
ChildrenView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, BaseView } from '@toeverything/framework/virgo';
|
||||
|
||||
import { PageView } from './PageView';
|
||||
|
||||
export const PageChildrenView: (prop: ChildrenView) => JSX.Element = props =>
|
||||
props.children;
|
||||
import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class PageBlock extends BaseView {
|
||||
type = Protocol.Block.Type.page;
|
||||
@ -35,21 +23,17 @@ export class PageBlock extends BaseView {
|
||||
return this.get_decoration<any>(content, 'text')?.value?.[0].text;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
const content = getTextHtml(block);
|
||||
const childrenContent = await generateHtml(children);
|
||||
return `<h1>${content}</h1> ${childrenContent}`;
|
||||
override async block2html({ block, editor, selectInfo }: Block2HtmlProps) {
|
||||
const header =
|
||||
await editor.clipboard.clipboardUtils.convertTextValue2HtmlBySelectInfo(
|
||||
block,
|
||||
selectInfo
|
||||
);
|
||||
const childrenHtml =
|
||||
await editor.clipboard.clipboardUtils.convertBlock2HtmlBySelectInfos(
|
||||
block,
|
||||
selectInfo?.children
|
||||
);
|
||||
return `<h1>${header}</h1>${childrenHtml}`;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
CreateView,
|
||||
getTextHtml,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
import { TextView } from './TextView';
|
||||
|
||||
@ -29,54 +30,25 @@ export class QuoteBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'BLOCKQUOTE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'BLOCKQUOTE') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<blockquote>${content}</blockquote>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<blockquote>${await commonBlock2HtmlContent(
|
||||
props
|
||||
)}</blockquote>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,69 +68,22 @@ export class CalloutBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'ASIDE',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (
|
||||
tag_name === 'ASIDE' ||
|
||||
el.firstChild?.nodeValue?.startsWith('<aside>')
|
||||
) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
texts.length > 0 &&
|
||||
(texts[0].text || '').startsWith('<aside>')
|
||||
) {
|
||||
texts[0].text = texts[0].text.substring('<aside>'.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (el.firstChild?.nodeValue?.startsWith('</aside>')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<aside>${content}</aside>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<aside>${await commonBlock2HtmlContent(props)}</aside>`;
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,16 @@ import {
|
||||
BaseView,
|
||||
CreateView,
|
||||
AsyncBlock,
|
||||
getTextProperties,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
HTML2BlockResult,
|
||||
BlockEditor,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import {
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { TextView } from './TextView';
|
||||
|
||||
import { getRandomString } from '@toeverything/components/common';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
|
||||
export class TextBlock extends BaseView {
|
||||
type = Protocol.Block.Type.text;
|
||||
@ -28,106 +27,30 @@ export class TextBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
if (el instanceof Text) {
|
||||
// TODO: parsing style
|
||||
return el.textContent.split('\n').map(text => {
|
||||
return {
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: [{ text: text }] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const tag_name = el.tagName;
|
||||
const block_style: any = {};
|
||||
switch (tag_name) {
|
||||
case 'STRONG':
|
||||
case 'B':
|
||||
block_style.bold = true;
|
||||
break;
|
||||
case 'A':
|
||||
block_style.type = 'link';
|
||||
block_style.url = el.getAttribute('href');
|
||||
block_style.id = getRandomString('link');
|
||||
block_style.children = [];
|
||||
break;
|
||||
case 'EM':
|
||||
block_style.italic = true;
|
||||
break;
|
||||
case 'U':
|
||||
block_style.underline = true;
|
||||
break;
|
||||
case 'CODE':
|
||||
block_style.inlinecode = true;
|
||||
break;
|
||||
case 'S':
|
||||
case 'DEL':
|
||||
block_style.strikethrough = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const child_nodes = el.childNodes;
|
||||
let texts = [];
|
||||
if (Object.keys(block_style).length > 0) {
|
||||
for (let i = 0; i < child_nodes.length; i++) {
|
||||
const blocks_info: any[] = parseEl(child_nodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === this.type) {
|
||||
const block_texts = block.properties.text.value.map(
|
||||
(text_value: any) => {
|
||||
return tag_name === 'A'
|
||||
? { ...text_value }
|
||||
: { ...block_style, ...text_value };
|
||||
}
|
||||
);
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tag_name === 'A') {
|
||||
block_style.children.push(...texts);
|
||||
texts = [block_style];
|
||||
}
|
||||
return texts.length > 0
|
||||
? [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
: null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<p>${content}</p>`;
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: [
|
||||
'DIV',
|
||||
'P',
|
||||
'PRE',
|
||||
'B',
|
||||
'A',
|
||||
'EM',
|
||||
'U',
|
||||
'CODE',
|
||||
'S',
|
||||
'DEL',
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,55 +67,23 @@ export class Heading1Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H1',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H1') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h1>${content}</h1>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h1>${await commonBlock2HtmlContent(props)}</h1>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,55 +100,23 @@ export class Heading2Block extends BaseView {
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H2',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H2') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h2>${content}</h2>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h2>${await commonBlock2HtmlContent(props)}</h2>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,53 +134,22 @@ export class Heading3Block extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: AsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'H3',
|
||||
});
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'H3') {
|
||||
const childNodes = el.childNodes;
|
||||
const texts = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<h3>${content}</h3>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<h3>${await commonBlock2HtmlContent(props)}</h3>`;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { TextManage } from '../../components/text-manage';
|
||||
import { dedentBlock, tabBlock } from '../../utils/indent';
|
||||
interface CreateTextView extends CreateView {
|
||||
@ -243,7 +242,7 @@ export const TextView = ({
|
||||
selected={isSelect}
|
||||
className={containerClassName}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TextBlock
|
||||
block={block}
|
||||
type={block.type}
|
||||
@ -255,9 +254,7 @@ export const TextView = ({
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
export const TodoView = ({ block, editor }: CreateView) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider editor={editor} block={block}>
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={
|
||||
properties.checked?.value ? 'checked' : ''
|
||||
}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
</BlockPendantProvider>
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
BaseView,
|
||||
getTextProperties,
|
||||
AsyncBlock,
|
||||
SelectBlock,
|
||||
getTextHtml,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
withTreeViewChildren,
|
||||
} from '@toeverything/framework/virgo';
|
||||
// import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import { defaultTodoProps, TodoView } from './TodoView';
|
||||
|
||||
import {
|
||||
Protocol,
|
||||
DefaultColumnsValue,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
// import { withTreeViewChildren } from '../../utils/with-tree-view-children';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
@ -29,71 +28,44 @@ export class TodoBlock extends BaseView {
|
||||
return block;
|
||||
}
|
||||
|
||||
override getSelProperties(
|
||||
block: TodoAsyncBlock,
|
||||
selectInfo: any
|
||||
): DefaultColumnsValue {
|
||||
const properties = super.getSelProperties(block, selectInfo);
|
||||
return getTextProperties(properties, selectInfo);
|
||||
}
|
||||
|
||||
override html2block(
|
||||
el: Element,
|
||||
parseEl: (el: Element) => any[]
|
||||
): any[] | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'UL') {
|
||||
const result = [];
|
||||
for (let i = 0; i < el.children.length; i++) {
|
||||
const blocks_info = parseEl(el.children[i]);
|
||||
result.push(...blocks_info);
|
||||
override async html2block({
|
||||
element,
|
||||
editor,
|
||||
}: {
|
||||
element: Element;
|
||||
editor: BlockEditor;
|
||||
}): Promise<HTML2BlockResult> {
|
||||
if (element.tagName === 'UL') {
|
||||
const firstList = element.querySelector('li');
|
||||
if (!firstList || !firstList.innerText.startsWith('[ ] ')) {
|
||||
return null;
|
||||
}
|
||||
return result.length > 0 ? result : null;
|
||||
|
||||
const children = Array.from(element.children);
|
||||
const childrenBlockInfos = (
|
||||
await Promise.all(
|
||||
children.map(childElement =>
|
||||
this.html2block({
|
||||
editor,
|
||||
element: childElement,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter(v => v);
|
||||
return childrenBlockInfos.length ? childrenBlockInfos : null;
|
||||
}
|
||||
|
||||
if (tag_name == 'LI' && el.textContent.startsWith('[ ] ')) {
|
||||
const childNodes = el.childNodes;
|
||||
let texts = [];
|
||||
const children = [];
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
const blocks_info = parseEl(childNodes[i] as Element);
|
||||
for (let j = 0; j < blocks_info.length; j++) {
|
||||
if (blocks_info[j].type === 'text') {
|
||||
const block_texts =
|
||||
blocks_info[j].properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
} else {
|
||||
children.push(blocks_info[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (texts.length > 0 && (texts[0].text || '').startsWith('[ ] ')) {
|
||||
texts[0].text = texts[0].text.substring('[ ] '.length);
|
||||
if (!texts[0].text) {
|
||||
texts = texts.slice(1);
|
||||
}
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: this.type,
|
||||
properties: {
|
||||
text: { value: texts },
|
||||
},
|
||||
children: children,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
return commonHTML2block({
|
||||
element,
|
||||
editor,
|
||||
type: this.type,
|
||||
tagName: 'LI',
|
||||
});
|
||||
}
|
||||
|
||||
override async block2html(
|
||||
block: AsyncBlock,
|
||||
children: SelectBlock[],
|
||||
generateHtml: (el: any[]) => Promise<string>
|
||||
): Promise<string> {
|
||||
let content = getTextHtml(block);
|
||||
content += await generateHtml(children);
|
||||
return `<ul><li>[ ] ${content}</li></ul>`;
|
||||
override async block2html(props: Block2HtmlProps) {
|
||||
return `<ul><li>[ ] ${await commonBlock2HtmlContent(props)}</li></ul>`;
|
||||
}
|
||||
}
|
||||
|
@ -5,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>`;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export const BlockContainer = function ({
|
||||
);
|
||||
};
|
||||
|
||||
export const Container = styled('div')<{ selected: boolean }>(
|
||||
export const Container = styled('div')<{ selected?: boolean }>(
|
||||
({ selected, theme }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ChildrenView } from '@toeverything/framework/virgo';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
export const IndentWrapper = (props: PropsWithChildren) => {
|
||||
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
|
||||
};
|
||||
|
||||
const StyledIdentWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
marginLeft: '30px',
|
||||
});
|
@ -1 +0,0 @@
|
||||
export * from './IndentWrapper';
|
@ -1,4 +1,5 @@
|
||||
export const isYoutubeUrl = (url?: string): boolean => {
|
||||
if (!url) return false;
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(url).host;
|
||||
return allowedHosts.includes(host);
|
||||
|
@ -1,6 +0,0 @@
|
||||
.v-basic-table-body {
|
||||
overflow: hidden !important;
|
||||
&:hover {
|
||||
overflow: auto !important;
|
||||
}
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
import {
|
||||
useMemo,
|
||||
memo,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { VariableSizeGrid, areEqual } from 'react-window';
|
||||
import type {
|
||||
GridChildComponentProps,
|
||||
GridItemKeySelector,
|
||||
} from 'react-window';
|
||||
import { VariableSizeGrid } from 'react-window';
|
||||
import style9 from 'style9';
|
||||
import './basic-table.scss';
|
||||
|
||||
export interface TableColumn {
|
||||
dataKey: string;
|
||||
label: string;
|
||||
|
57
libs/components/editor-blocks/src/utils/commonBlockClip.ts
Normal file
57
libs/components/editor-blocks/src/utils/commonBlockClip.ts
Normal 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;
|
||||
};
|
@ -4,12 +4,14 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"date-fns": "^2.28.0",
|
||||
"date-fns": "^2.29.2",
|
||||
"eventemitter3": "^4.0.7",
|
||||
"hotkeys-js": "^3.9.4",
|
||||
"html-escaper": "^3.0.3",
|
||||
"lru-cache": "^7.10.1",
|
||||
"marked": "^4.0.19",
|
||||
"nanoid": "^4.0.0",
|
||||
"slate": "^0.81.0",
|
||||
"style9": "^0.13.3"
|
||||
"style9": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,34 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from './editor';
|
||||
|
||||
const RootContext = createContext<{
|
||||
type EditorProps = {
|
||||
editor: BlockEditor;
|
||||
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
|
||||
editorElement: () => JSX.Element;
|
||||
}>(
|
||||
};
|
||||
|
||||
const EditorContext = createContext<EditorProps>(
|
||||
genErrorObj(
|
||||
'Failed to get context! The context only can use under the "render-root"'
|
||||
'Failed to get EditorContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
return useContext(EditorContext);
|
||||
};
|
||||
|
||||
export const EditorProvider = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<EditorProps>) => {
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, editorElement }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -4,12 +4,12 @@ import {
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import type { BlockEditor } from './editor';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { BlockRenderProvider, RenderBlock } from './render-block';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
|
||||
interface RenderRootProps {
|
||||
@ -24,11 +24,7 @@ interface RenderRootProps {
|
||||
const MAX_PAGE_WIDTH = 5000;
|
||||
export const MIN_PAGE_WIDTH = 1480;
|
||||
|
||||
export const RenderRoot = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<RenderRootProps>) => {
|
||||
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
|
||||
const selectionRef = useRef<SelectionRef>(null);
|
||||
const triggeredBySelect = useRef(false);
|
||||
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
|
||||
@ -158,39 +154,43 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
{children}
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
<EditorProvider editor={editor} editorElement={editorElement}>
|
||||
<BlockRenderProvider blockRender={RenderBlock}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && (
|
||||
<SelectionRect ref={selectionRef} editor={editor} />
|
||||
)}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</BlockRenderProvider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBlankContainter
|
||||
<ScrollBlankContainer
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onClick}
|
||||
@ -283,7 +283,7 @@ const Content = styled('div')({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
});
|
||||
|
||||
const ScrollBlankContainter = styled('div')({
|
||||
const ScrollBlankContainer = styled('div')({
|
||||
paddingBottom: '30vh',
|
||||
margin: `0 -${PADDING_X}px`,
|
||||
});
|
||||
|
@ -1,18 +1,25 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { containerFlavor } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
type MouseEvent,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from '../editor';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
import { PendantPopover } from './pendant-popover';
|
||||
import { PendantRender } from './pendant-render';
|
||||
import { useRef } from 'react';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
interface BlockTagProps {
|
||||
editor: BlockEditor;
|
||||
block: AsyncBlock;
|
||||
}
|
||||
|
||||
export const BlockPendantProvider = ({
|
||||
editor,
|
||||
block,
|
||||
children,
|
||||
}: PropsWithChildren<BlockTagProps>) => {
|
||||
@ -23,8 +30,23 @@ export const BlockPendantProvider = ({
|
||||
const showTriggerLine =
|
||||
properties.filter(property => getValue(property.id)).length === 0;
|
||||
|
||||
const onClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
if (containerFlavor.includes(block.type)) {
|
||||
return;
|
||||
}
|
||||
if (e.target === e.currentTarget) {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const middle = (rect.left + rect.right) / 2;
|
||||
const position = e.clientX < middle ? 'start' : 'end';
|
||||
editor.selectionManager.activeNodeByNodeId(block.id, position);
|
||||
}
|
||||
},
|
||||
[editor, block]
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container onClick={onClick}>
|
||||
{children}
|
||||
|
||||
{showTriggerLine ? (
|
||||
|
@ -180,9 +180,49 @@ export class AsyncBlock {
|
||||
return this.event_emitter.emit(eventName, eventData);
|
||||
}
|
||||
|
||||
onUpdate(callback: (event: EventData) => void) {
|
||||
this.on('update', callback);
|
||||
/**
|
||||
* @param deep observe deep
|
||||
*
|
||||
* NOTICE: the observe of children is async,
|
||||
* so there maybe have some delay before observe done.
|
||||
*/
|
||||
onUpdate(callback: (event: EventData) => void, deep = 0) {
|
||||
let expired = false;
|
||||
const unobserveMap: Record<string, () => void> = {};
|
||||
const observeChildren = () => {
|
||||
if (deep <= 0) {
|
||||
return;
|
||||
}
|
||||
this.children().then(children => {
|
||||
// Check current event listeners is not expired
|
||||
if (expired) {
|
||||
return;
|
||||
}
|
||||
children.forEach(child => {
|
||||
if (unobserveMap[child.id]) return;
|
||||
const unobserve = child.onUpdate(callback, deep - 1);
|
||||
unobserveMap[child.id] = unobserve;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const unobserveChildren = () => {
|
||||
Object.values(unobserveMap).forEach(unobserve => {
|
||||
unobserve();
|
||||
});
|
||||
};
|
||||
|
||||
this.on('update', e => {
|
||||
callback(e);
|
||||
// Update children observe
|
||||
observeChildren();
|
||||
});
|
||||
|
||||
observeChildren();
|
||||
|
||||
return () => {
|
||||
expired = true;
|
||||
unobserveChildren();
|
||||
this.off('update', callback);
|
||||
};
|
||||
}
|
||||
|
@ -11,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) {
|
||||
|
@ -1,128 +0,0 @@
|
||||
import { HooksRunner } from '../types';
|
||||
import { Editor } from '../editor';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { MarkdownParser } from './markdown-parse';
|
||||
import { shouldHandlerContinue } from './utils';
|
||||
import { Paste } from './paste';
|
||||
// todo needs to be a switch
|
||||
|
||||
enum ClipboardAction {
|
||||
COPY = 'copy',
|
||||
CUT = 'cut',
|
||||
PASTE = 'paste',
|
||||
}
|
||||
|
||||
//TODO: need to consider the cursor position after inserting the children
|
||||
class BrowserClipboard {
|
||||
private _eventTarget: Element;
|
||||
private _hooks: HooksRunner;
|
||||
private _editor: Editor;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
private _markdownParse: MarkdownParser;
|
||||
private _paste: Paste;
|
||||
|
||||
constructor(eventTarget: Element, hooks: HooksRunner, editor: Editor) {
|
||||
this._eventTarget = eventTarget;
|
||||
this._hooks = hooks;
|
||||
this._editor = editor;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
this._markdownParse = new MarkdownParser();
|
||||
this._paste = new Paste(
|
||||
editor,
|
||||
this._clipboardParse,
|
||||
this._markdownParse
|
||||
);
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
public getClipboardParse() {
|
||||
return this._clipboardParse;
|
||||
}
|
||||
|
||||
private _initialize() {
|
||||
this._handleCopy = this._handleCopy.bind(this);
|
||||
this._handleCut = this._handleCut.bind(this);
|
||||
|
||||
document.addEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.addEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.addEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
}
|
||||
|
||||
private _handleCopy(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.COPY, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _handleCut(e: Event) {
|
||||
if (!shouldHandlerContinue(e, this._editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dispatchClipboardEvent(ClipboardAction.CUT, e as ClipboardEvent);
|
||||
}
|
||||
|
||||
private _preCopyCut(action: ClipboardAction, e: ClipboardEvent) {
|
||||
switch (action) {
|
||||
case ClipboardAction.COPY:
|
||||
this._hooks.beforeCopy(e);
|
||||
break;
|
||||
|
||||
case ClipboardAction.CUT:
|
||||
this._hooks.beforeCut(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _dispatchClipboardEvent(
|
||||
action: ClipboardAction,
|
||||
e: ClipboardEvent
|
||||
) {
|
||||
this._preCopyCut(action, e);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
document.removeEventListener(ClipboardAction.COPY, this._handleCopy);
|
||||
document.removeEventListener(ClipboardAction.CUT, this._handleCut);
|
||||
document.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.COPY,
|
||||
this._handleCopy
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.CUT,
|
||||
this._handleCut
|
||||
);
|
||||
this._eventTarget.removeEventListener(
|
||||
ClipboardAction.PASTE,
|
||||
this._paste.handlePaste
|
||||
);
|
||||
this._clipboardParse.dispose();
|
||||
this._clipboardParse = null;
|
||||
this._eventTarget = null;
|
||||
this._hooks = null;
|
||||
this._editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { BrowserClipboard };
|
@ -1,207 +0,0 @@
|
||||
import { Protocol, BlockFlavorKeys } from '@toeverything/datasource/db-service';
|
||||
import { escape } from '@toeverything/utils';
|
||||
import { Editor } from '../editor';
|
||||
import { SelectBlock } from '../selection';
|
||||
import { ClipBlockInfo } from './types';
|
||||
|
||||
class DefaultBlockParse {
|
||||
public static html2block(el: Element): ClipBlockInfo[] | undefined | null {
|
||||
const tag_name = el.tagName;
|
||||
if (tag_name === 'DIV' || el instanceof Text) {
|
||||
return el.textContent?.split('\n').map(str => {
|
||||
const data = {
|
||||
text: escape(str),
|
||||
};
|
||||
return {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [data] },
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ClipboardParse {
|
||||
private editor: Editor;
|
||||
private static block_types: BlockFlavorKeys[] = [
|
||||
Protocol.Block.Type.page,
|
||||
Protocol.Block.Type.reference,
|
||||
Protocol.Block.Type.heading1,
|
||||
Protocol.Block.Type.heading2,
|
||||
Protocol.Block.Type.heading3,
|
||||
Protocol.Block.Type.quote,
|
||||
Protocol.Block.Type.todo,
|
||||
Protocol.Block.Type.code,
|
||||
Protocol.Block.Type.text,
|
||||
Protocol.Block.Type.toc,
|
||||
Protocol.Block.Type.file,
|
||||
Protocol.Block.Type.image,
|
||||
Protocol.Block.Type.divider,
|
||||
Protocol.Block.Type.callout,
|
||||
Protocol.Block.Type.youtube,
|
||||
Protocol.Block.Type.figma,
|
||||
Protocol.Block.Type.group,
|
||||
Protocol.Block.Type.embedLink,
|
||||
Protocol.Block.Type.numbered,
|
||||
Protocol.Block.Type.bullet,
|
||||
];
|
||||
private static break_flags: Set<string> = new Set([
|
||||
'BLOCKQUOTE',
|
||||
'BODY',
|
||||
'CENTER',
|
||||
'DD',
|
||||
'DIR',
|
||||
'DIV',
|
||||
'DL',
|
||||
'DT',
|
||||
'FORM',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'HEAD',
|
||||
'HTML',
|
||||
'ISINDEX',
|
||||
'MENU',
|
||||
'NOFRAMES',
|
||||
'P',
|
||||
'PRE',
|
||||
'TABLE',
|
||||
'TD',
|
||||
'TH',
|
||||
'TITLE',
|
||||
'TR',
|
||||
]);
|
||||
|
||||
constructor(editor: Editor) {
|
||||
this.editor = editor;
|
||||
this.generate_html = this.generate_html.bind(this);
|
||||
this.parse_dom = this.parse_dom.bind(this);
|
||||
}
|
||||
// TODO: escape
|
||||
public text2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return (clipData || '').split('\n').map((str: string) => {
|
||||
const block_info: ClipBlockInfo = {
|
||||
type: 'text',
|
||||
properties: {
|
||||
text: { value: [{ text: str }] },
|
||||
},
|
||||
children: [] as ClipBlockInfo[],
|
||||
};
|
||||
return block_info;
|
||||
});
|
||||
}
|
||||
|
||||
public html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
return this.common_html2blocks(clipData);
|
||||
}
|
||||
|
||||
private common_html2blocks(clipData: string): ClipBlockInfo[] {
|
||||
const html_el = document.createElement('html');
|
||||
html_el.innerHTML = clipData;
|
||||
return this.parse_dom(html_el);
|
||||
}
|
||||
|
||||
// tTODO:odo escape
|
||||
private parse_dom(el: Element): ClipBlockInfo[] {
|
||||
for (let i = 0; i < ClipboardParse.block_types.length; i++) {
|
||||
const block_utils = this.editor.getView(
|
||||
ClipboardParse.block_types[i]
|
||||
);
|
||||
const blocks = block_utils?.html2block?.(el, this.parse_dom);
|
||||
|
||||
if (blocks) {
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
const blocks: ClipBlockInfo[] = [];
|
||||
// blocks = DefaultBlockParse.html2block(el);
|
||||
for (let i = 0; i < el.childNodes.length; i++) {
|
||||
const child = el.childNodes[i];
|
||||
const last_block_info =
|
||||
blocks.length === 0 ? null : blocks[blocks.length - 1];
|
||||
let blocks_info = this.parse_dom(child as Element);
|
||||
if (
|
||||
last_block_info &&
|
||||
last_block_info.type === 'text' &&
|
||||
!ClipboardParse.break_flags.has((child as Element).tagName)
|
||||
) {
|
||||
const texts = last_block_info.properties?.text?.value || [];
|
||||
let j = 0;
|
||||
for (; j < blocks_info.length; j++) {
|
||||
const block = blocks_info[j];
|
||||
if (block.type === 'text') {
|
||||
const block_texts = block.properties.text.value;
|
||||
texts.push(...block_texts);
|
||||
}
|
||||
}
|
||||
last_block_info.properties = {
|
||||
text: { value: texts },
|
||||
};
|
||||
blocks_info = blocks_info.slice(j);
|
||||
}
|
||||
blocks.push(...blocks_info);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public async generateHtml(): Promise<string> {
|
||||
const select_info = await this.editor.selectionManager.getSelectInfo();
|
||||
return await this.generate_html(select_info.blocks);
|
||||
}
|
||||
|
||||
public async page2html(): Promise<string> {
|
||||
const root_block_id = this.editor.getRootBlockId();
|
||||
if (!root_block_id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const block_info = await this.get_select_info(root_block_id);
|
||||
return await this.generate_html([block_info]);
|
||||
}
|
||||
|
||||
private async get_select_info(blockId: string) {
|
||||
const block = await this.editor.getBlockById(blockId);
|
||||
if (!block) return null;
|
||||
const block_info: SelectBlock = {
|
||||
blockId: block.id,
|
||||
children: [],
|
||||
};
|
||||
const children_ids = block.childrenIds;
|
||||
for (let i = 0; i < children_ids.length; i++) {
|
||||
block_info.children.push(
|
||||
await this.get_select_info(children_ids[i])
|
||||
);
|
||||
}
|
||||
return block_info;
|
||||
}
|
||||
|
||||
private async generate_html(selectBlocks: SelectBlock[]): Promise<string> {
|
||||
let result = '';
|
||||
for (let i = 0; i < selectBlocks.length; i++) {
|
||||
const sel_block = selectBlocks[i];
|
||||
if (!sel_block || !sel_block.blockId) continue;
|
||||
const block = await this.editor.getBlockById(sel_block.blockId);
|
||||
if (!block) continue;
|
||||
const block_utils = this.editor.getView(block.type);
|
||||
const html = await block_utils.block2html(
|
||||
block,
|
||||
sel_block.children,
|
||||
this.generate_html
|
||||
);
|
||||
result += html;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { Editor } from '../editor';
|
||||
import { SelectionManager } from '../selection';
|
||||
import { HookType, PluginHooks } from '../types';
|
||||
import ClipboardParse from './clipboard-parse';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Copy } from './copy';
|
||||
class ClipboardPopulator {
|
||||
private _editor: Editor;
|
||||
private _hooks: PluginHooks;
|
||||
private _selectionManager: SelectionManager;
|
||||
private _clipboardParse: ClipboardParse;
|
||||
private _sub = new Subscription();
|
||||
private _copy: Copy;
|
||||
constructor(
|
||||
editor: Editor,
|
||||
hooks: PluginHooks,
|
||||
selectionManager: SelectionManager
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._hooks = hooks;
|
||||
this._selectionManager = selectionManager;
|
||||
this._clipboardParse = new ClipboardParse(editor);
|
||||
this._copy = new Copy(editor);
|
||||
this._sub.add(
|
||||
hooks.get(HookType.BEFORE_COPY).subscribe(this._copy.handleCopy)
|
||||
);
|
||||
this._sub.add(
|
||||
hooks.get(HookType.BEFORE_CUT).subscribe(this._copy.handleCopy)
|
||||
);
|
||||
}
|
||||
|
||||
disposeInternal() {
|
||||
this._sub.unsubscribe();
|
||||
this._hooks = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { ClipboardPopulator };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user