Merge remote-tracking branch 'origin/develop' into fix/experience

This commit is contained in:
DarkSky committed 2022-08-24 19:17:53 +08:00
commit 280fb4d8c7
122 files changed
+2212 -1416

No files matched your search

+37
View File
@@ -266,6 +266,43 @@
"contributions": [ "contributions": [
"doc" "doc"
] ]
},
{
"login": "LuciNyan",
"name": "子瞻 Luci",
"avatar_url": "https://avatars.githubusercontent.com/u/22126563?v=4",
"profile": "https://github.com/LuciNyan",
"contributions": [
"code"
]
},
{
"login": "m1911star",
"name": "Horus",
"avatar_url": "https://avatars.githubusercontent.com/u/4948120?v=4",
"profile": "http://blog.ipili.me/",
"contributions": [
"code",
"platform"
]
},
{
"login": "fanshyiis",
"name": "Super.x",
"avatar_url": "https://avatars.githubusercontent.com/u/15103283?v=4",
"profile": "https://segmentfault.com/u/qzuser_584786517d31a",
"contributions": [
"code"
]
},
{
"login": "wangyu-1999",
"name": "Wang Yu",
"avatar_url": "https://avatars.githubusercontent.com/u/80874770?v=4",
"profile": "https://wangyu-1999.github.io/",
"contributions": [
"code"
]
} }
] ]
} }
+3 -2
View File
@@ -1,8 +1,9 @@
FROM node:16-alpine as builder FROM node:16-alpine as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN apk add g++ make python3 git libpng-dev # RUN apk add g++ make python3 git libpng-dev
RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store && pnpm run build:local --skip-nx-cache RUN apk add git
RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store --filter "!ligo-virgo-e2e" --filter "!keck" --filter "!venus" && pnpm run build:local --skip-nx-cache
FROM node:16-alpine as relocate FROM node:16-alpine as relocate
WORKDIR /app WORKDIR /app
+3 -2
View File
@@ -1,8 +1,9 @@
FROM node:16-alpine as builder FROM node:16-alpine as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN apk add g++ make python3 git libpng-dev # RUN apk add g++ make python3 git libpng-dev
RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store && pnpm run build RUN apk add git
RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store --filter "!ligo-virgo-e2e" --filter "!keck" --filter "!venus" && pnpm run build
FROM node:16-alpine as relocate FROM node:16-alpine as relocate
WORKDIR /app WORKDIR /app
+40 -81
View File
@@ -2,12 +2,13 @@
<b> <b>
<a href="https://affine.pro">AFFiNE.PRO</a><br> <a href="https://affine.pro">AFFiNE.PRO</a><br>
</b> </b>
The Next-Gen Knowledge Base to Replace Notion & Miro. The Next-Gen Collaborative Knowledge Base
<br> <br>
</h1> </h1>
<p align="center"> <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> </p>
<div align="center"> <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:START - Do not remove or modify this section -->
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square [all-contributors-badge]: https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- 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) [![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) [![All Contributors][all-contributors-badge]](#contributors)
<br/>
[![Node](https://img.shields.io/badge/node->=16.0-success)](https://www.typescriptlang.org/) [![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/TypeScript-4.7-3178c6)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/) [![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/)
@@ -48,96 +50,53 @@ 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/> <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) ![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"> [![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.
🥳🥳🥳 Our web live demo is ready! 🥳🥳🥳
</h3>
<h3 align="center"> Want to deploy it yourself? AFFiNE can run just about anywhere. Check our
Start to play with <a href="https://affine.pro"><b>AFFiNE web version</b></a> on our landing page: [documentation](#documentation)
</h3> for more information.
<br /><br />
<p align="center"><a href="https://affine.pro"> ⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
<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)
## Create your story ## 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. 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. 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.
# 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
Coming Soon... ![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 - AFFiNE Communities: [Discord](https://discord.gg/yz6tGVsf5p) | [Telegram](https://t.me/affineworkos) | [Twitter](https://twitter.com/AffineOfficial) |
[Medium](https://medium.com/@affineworkos) | [AFFiNE Blog](https://blog.affine.pro)
Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors. # Contact Us
Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/). Feel free to send us an email: contact@toeverything.info
# The Philosophy of AFFiNE # The Philosophy of AFFiNE
@@ -160,7 +119,7 @@ That's why we are making AFFiNE. Some of the most important features are:
- Data is always stored locally by default - Data is always stored locally by default
- CRDTs are applied so that peer-to-peer collaboration is possible. - 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: We would like to give special thanks to the innovators and pioneers who greatly inspired us:
@@ -182,12 +141,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. 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 # Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@@ -228,6 +181,12 @@ For help, discussion about best practices, or any other conversation that would
<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/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://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://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="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>
</tr> </tr>
</table> </table>
@@ -240,4 +199,4 @@ For help, discussion about best practices, or any other conversation that would
AFFiNE is distributed under the terms of MIT license. AFFiNE is distributed under the terms of MIT license.
See LICENSE for details. See [LICENSE](/LICENSE) for details.
@@ -175,7 +175,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({
width: 300, width: 300,
minWidth: 300, minWidth: 300,
borderRadius: '0px 10px 10px 0px', borderRadius: '0px 10px 10px 0px',
boxShadow: theme.affine.shadows.shadow1, boxShadow: theme.affine.shadows.shadow2,
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
transitionProperty: 'left', transitionProperty: 'left',
transitionDuration: '0.35s', transitionDuration: '0.35s',
+5 -3
View File
@@ -7,16 +7,18 @@
"keywords": [], "keywords": [],
"author": "DarkSky <darksky2048@gmail.com>", "author": "DarkSky <darksky2048@gmail.com>",
"dependencies": { "dependencies": {
"@mui/joy": "^5.0.0-alpha.39",
"@emotion/react": "^11.10.0", "@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0", "@emotion/styled": "^11.10.0",
"lozad": "^1.16.0" "@mui/joy": "^5.0.0-alpha.39",
"i18next": "^21.9.1",
"lozad": "^1.16.0",
"react-i18next": "^11.18.4"
}, },
"devDependencies": { "devDependencies": {
"mini-css-extract-plugin": "^2.6.1",
"image-minimizer-webpack-plugin": "^3.2.3", "image-minimizer-webpack-plugin": "^3.2.3",
"imagemin": "^8.0.1", "imagemin": "^8.0.1",
"imagemin-optipng": "^8.0.0", "imagemin-optipng": "^8.0.0",
"mini-css-extract-plugin": "^2.6.1",
"webpack": "^5.74.0" "webpack": "^5.74.0"
} }
} }
+25
View File
@@ -0,0 +1,25 @@
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import en_US from './resources/en.json';
import zh_CN from './resources/zh.json';
const resources = {
en: en_US,
zh: zh_CN,
} as const;
i18next.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
resources,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export const options = [
{ value: 'en', text: 'English' },
{ value: 'zh', text: '简体中文' },
];
export { i18next };
+34
View File
@@ -0,0 +1,34 @@
{
"translation": {
"Open Source": "Open Source",
"Privacy First": "Privacy First",
"Alternative": "Alternative",
"Check GitHub": "Check GitHub",
"Try it Online": "Try it Online",
"description1": {
"part1": "Affine is the next-generation collaborative knowledge base for professionals.",
"part2": "It's not just a collection of Docs, whiteboard, and tables.",
"part3": "Transform any building block as you like.",
"part4": "Say goodbye to redundancy. Store your data once, and keep your data as you like it."
},
"description2": {
"part1": "Shape Your Page",
"part2": "Docs, Kanbans, and Databases are all fully functional anywhere, anytime. A truly what-you-see-is-what-you-get environment for your data.",
"part3": "All pages come with a document (Paper Mode) and whiteboard (Edgeless Mode) view."
},
"description3": {
"part1": "Plan Your Task",
"part2": "No more chaos managing multiple views.",
"part3": "Set a TODO with Markdown, and seamlessly edit it within a Kanban.",
"part4": "Managing multi-dimensional tables should be this simple - and now it is."
},
"description4": {
"part1": "Privacy-first, and collaborative. No compromises whatsoever.",
"part2": "We don't like being locked-in, and neither should you. Privacy is at the foundation of everything we do, but it should not limit us that's why there are no compromises.",
"part3": "Your data is yours;it is always locally stored and secured - available to you always. While still being able to enjoy collaboration features such as real-time editing and sharing with others, without any cloud setup."
},
"BuildFor": "Build for an open and semantic future",
"KeepUpdated": "Keep Updated on",
"Join": "Join Our Community"
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"translation": {
"Open Source": "开源",
"Privacy First": "隐私第一",
"Alternative": "的另一种选择",
"Check GitHub": "GitHub中查看",
"Try it Online": "在线上试试",
"description1": {
"part1": "Affine是面向专业人士的下一代协同知识库",
"part2": "它不仅仅是一个文档、白板和表格的集合。",
"part3": "可以根据需要转换任何构建块。",
"part4": "向冗余说再见吧。将数据存储一次,并保留您喜欢的数据。"
},
"description2": {
"part1": "塑造您的页面",
"part2": "文档、看板和数据库在任何地方、任何时候都是完全可用的,一个真正的“所见即所得”的数据环境。",
"part3": "所有页面都有一个文档(纸张模式)和白板(无边缘模式)视图。"
},
"description3": {
"part1": "计划您的任务",
"part2": "不再混乱地管理多个视图。",
"part3": "使用 Markdown 设置 TODO,并在看板中无缝地编辑它。",
"part4": "管理多维表格本应该就这么简单——现在的它就是这么简单。"
},
"description4": {
"part1": "隐私第一,合作无间,绝不妥协。",
"part2": "我们不喜欢被关起来,您也不应该。隐私是我们做任何事情的基础,但它不应该限制我们,这就是为什么我们决不妥协。",
"part3": "您的数据是您自己的,它总是安全的在本地存储并随时供您使用。您不需要任何云设置就可以享受协作功能,即时编辑和与他人共享。"
},
"BuildFor": "构建一个开放和语义化的未来",
"KeepUpdated": "持续更新在",
"Join": "加入我们的社区"
}
}
+39 -41
View File
@@ -7,12 +7,15 @@ import GitHubIcon from '@mui/icons-material/GitHub';
import RedditIcon from '@mui/icons-material/Reddit'; import RedditIcon from '@mui/icons-material/Reddit';
import TelegramIcon from '@mui/icons-material/Telegram'; import TelegramIcon from '@mui/icons-material/Telegram';
import { Box, Button, Container, Grid, SvgIcon, Typography } from '@mui/joy'; import { Box, Button, Container, Grid, SvgIcon, Typography } from '@mui/joy';
import Option from '@mui/joy/Option';
import Select from '@mui/joy/Select';
import { CssVarsProvider, styled } from '@mui/joy/styles'; import { CssVarsProvider, styled } from '@mui/joy/styles';
import { LogoIcon } from '@toeverything/components/icons'; import { LogoIcon } from '@toeverything/components/icons';
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import { useTranslation } from 'react-i18next';
import CollaborationImage from './collaboration.png'; import CollaborationImage from './collaboration.png';
import { options } from './i18n';
import LogoImage from './logo.png'; import LogoImage from './logo.png';
import PageImage from './page.png'; import PageImage from './page.png';
import ShapeImage from './shape.png'; import ShapeImage from './shape.png';
@@ -199,6 +202,7 @@ const AffineImage = styled('img')({
const GitHub = (props: { center?: boolean; flat?: boolean }) => { const GitHub = (props: { center?: boolean; flat?: boolean }) => {
const matches = useMediaQuery('(max-width: 1024px)'); const matches = useMediaQuery('(max-width: 1024px)');
const { t } = useTranslation();
return ( return (
<Button <Button
@@ -234,14 +238,14 @@ const GitHub = (props: { center?: boolean; flat?: boolean }) => {
startIcon={<GitHubIcon />} startIcon={<GitHubIcon />}
size="lg" size="lg"
> >
{props.center ? 'Check ' : ''}GitHub {props.center ? t('Check GitHub') : t('GitHub')}
</Button> </Button>
); );
}; };
const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => { const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => {
const matches = useMediaQuery('(max-width: 1024px)'); const matches = useMediaQuery('(max-width: 1024px)');
const { t } = useTranslation();
return ( return (
<Button <Button
onClick={() => { onClick={() => {
@@ -277,14 +281,18 @@ const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => {
startIcon={<LogoIcon />} startIcon={<LogoIcon />}
size="lg" size="lg"
> >
Try it Online {t('Try it Online')}
</Button> </Button>
); );
}; };
export function App() { export function App() {
const matches = useMediaQuery('(max-width: 1024px)'); const matches = useMediaQuery('(max-width: 1024px)');
const { t, i18n } = useTranslation();
const changeLanguage = (event: any) => {
i18n.changeLanguage(event);
};
return ( return (
<CssVarsProvider> <CssVarsProvider>
<VenusContainer <VenusContainer
@@ -342,6 +350,13 @@ export function App() {
> >
Blog Blog
</Button> </Button>
<Select defaultValue="en" onChange={changeLanguage}>
{options.map(option => (
<Option key={option.value} value={option.value}>
{option.text}
</Option>
))}
</Select>
</Grid> </Grid>
</Grid> </Grid>
<Grid <Grid
@@ -369,7 +384,7 @@ export function App() {
}, },
}} }}
> >
Open Source, {t('Open Source')},
</Typography> </Typography>
<Typography <Typography
fontSize="96px" fontSize="96px"
@@ -380,7 +395,7 @@ export function App() {
}, },
}} }}
> >
Privacy First {t('Privacy First')}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -413,7 +428,7 @@ export function App() {
}, },
}} }}
> >
Alternative {t('Alternative')}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -432,8 +447,7 @@ export function App() {
fontWeight={'400'} fontWeight={'400'}
sx={{ color: '#888' }} sx={{ color: '#888' }}
> >
Affine is the next-generation collaborative {t('description1.part1')}
knowledge base for professionals.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -488,8 +502,7 @@ export function App() {
level={matches ? 'h2' : 'h1'} level={matches ? 'h2' : 'h1'}
fontWeight={'bold'} fontWeight={'bold'}
> >
It’s not just a collection of Docs, whiteboard, and {t('description1.part2')}
tables.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -506,11 +519,10 @@ export function App() {
}} }}
> >
<Typography fontSize="1.2em"> <Typography fontSize="1.2em">
Transform any building block as you like. {t('description1.part3')}
</Typography> </Typography>
<Typography fontSize="1.2em"> <Typography fontSize="1.2em">
Say goodbye to redundancy. Store your data once, and {t('description1.part4')}
keep your data as you like it.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -547,23 +559,19 @@ export function App() {
fontWeight={'bold'} fontWeight={'bold'}
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
> >
Shape Your Page {t('description2.part1')}
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
Docs, Kanbans, and Databases are all fully {t('description2.part2')}
functional anywhere, anytime. A truly
what-you-see-is-what-you-get environment for
your data.
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
All pages come with a document (Paper Mode) and {t('description2.part3')}
whiteboard (Edgeless Mode) view.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -625,27 +633,25 @@ export function App() {
fontWeight={'bold'} fontWeight={'bold'}
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
> >
Plan Your Task {t('description3.part1')}
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
No more chaos managing multiple views. {t('description3.part2')}
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
Set a TODO with Markdown, and seamlessly edit it {t('description3.part3')}
within a Kanban.
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
Managing multi-dimensional tables should be this {t('description3.part4')}
simple – and now it is.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -697,27 +703,19 @@ export function App() {
fontWeight={'bold'} fontWeight={'bold'}
style={{ marginBottom: '0.5em' }} style={{ marginBottom: '0.5em' }}
> >
Privacy-first, and collaborative. No compromises {t('description4.part1')}
whatsoever.
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
We don’t like being locked-in, and neither should {t('description4.part2')}
you. Privacy is at the foundation of everything we
do, but it should not limit us that’s+ why there are
no compromises.
</Typography> </Typography>
<Typography <Typography
fontSize="1.2em" fontSize="1.2em"
style={{ marginBottom: '0.25em' }} style={{ marginBottom: '0.25em' }}
> >
Your data is yours; it is always locally stored and {t('description4.part3')}
secured - available to you always. While still being
able to enjoy collaboration features such as
real-time editing and sharing with others, without
any cloud setup.
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -762,7 +760,7 @@ export function App() {
}} }}
> >
<Typography fontSize={'1.5em'}> <Typography fontSize={'1.5em'}>
Build for an open and semantic future {t('BuildFor')}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
@@ -778,7 +776,7 @@ export function App() {
> >
<Typography level="h3" sx={{ display: 'flex' }}> <Typography level="h3" sx={{ display: 'flex' }}>
<span style={{ alignSelf: 'center' }}> <span style={{ alignSelf: 'center' }}>
Keep Updated on {t('KeepUpdated')}
</span> </span>
<GitHub /> <GitHub />
</Typography> </Typography>
@@ -795,7 +793,7 @@ export function App() {
}} }}
> >
<Typography level="h2" sx={{ display: 'flex' }}> <Typography level="h2" sx={{ display: 'flex' }}>
Join Our Community {t('Join')}
</Typography> </Typography>
</Box> </Box>
</Grid> </Grid>
+1
View File
@@ -1,6 +1,7 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import './app/i18n';
import App from './app'; import App from './app';
File renamed without changes.
+5 -7
View File
@@ -1,4 +1,4 @@
# Welcome to ourcontributing guide <!-- omit in toc --> # Welcome to our contributing guide <!-- omit in toc -->
Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:. Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:.
@@ -10,7 +10,7 @@ Use the table of contents icon on the top left corner of this document to get to
## New contributor guide ## 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) - [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) - [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 ## 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](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](/contributing/types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:.
### Issues ### 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). 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 #### 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 ### Make Changes
+31
View File
@@ -0,0 +1,31 @@
# Types of contributions :memo:
You can contribute to AFFiNE in several ways. This repo is a place to discuss and collaborate on AFFiNE!
### :mega: Discussions
Discussions are where we have conversations.
If you'd like help troubleshooting a docs PR you're working on, have a great new idea, or want to share something amazing you've learned in our docs, join us in [discussions](https://github.com/toeverything/AFFiNE/discussions).
### :lady_beetle: Issues
[Issues](https://docs.github.com/en/github/managing-your-work-on-github/about-issues) are used to track tasks that contributors can help with. If an issue has a triage label, we haven't reviewed it yet, and you shouldn't begin work on it.
If you've found something in the content or the website that should be updated, search open issues to see if someone else has reported the same thing. If it's something new, open an issue using a [template](https://github.com/toeverything/AFFiNE/issues/new/choose). We'll use the issue to have a conversation about the problem you want to fix.
### :hammer_and_wrench: Pull requests
A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository. When we merge those changes, they should be deployed to the live site within 24 hours. :earth_africa:
You can [create a new pull request](https://github.com/toeverything/AFFiNE/compare) and view [current pull requests](https://github.com/toeverything/AFFiNE/pulls).
### :question: Support
We are a small team working hard to keep up with the documentation demands of a continuously changing product.
You may be able to find additional help and information on our social media platforms and groups - the links to these can be found in our [README](../README.md).
### :earth_asia: Translations
AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content.
**We do not currently offer this feature**, but we hope to in the future.
+6 -4
View File
@@ -22,7 +22,6 @@ const AffineBoard = ({
editor, editor,
}: AffineBoardProps & { editor: BlockEditor }) => { }: AffineBoardProps & { editor: BlockEditor }) => {
const [app, set_app] = useState<TldrawApp>(); const [app, set_app] = useState<TldrawApp>();
const [document] = useState(() => { const [document] = useState(() => {
return { return {
...deepCopy(TldrawApp.default_document), ...deepCopy(TldrawApp.default_document),
@@ -49,7 +48,7 @@ const AffineBoard = ({
}; };
}); });
const shapes = useShapes(workspace, rootBlockId); const { shapes } = useShapes(workspace, rootBlockId);
useEffect(() => { useEffect(() => {
if (app) { if (app) {
app.replacePageContent(shapes || {}, {}, {}); app.replacePageContent(shapes || {}, {}, {});
@@ -66,6 +65,9 @@ const AffineBoard = ({
onMount(app) { onMount(app) {
set_app(app); set_app(app);
}, },
async onPaste(e, data) {
console.log('e,data: ', e, data);
},
async onCopy(e, groupIds) { async onCopy(e, groupIds) {
const clip = const clip =
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById( await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
@@ -77,7 +79,7 @@ const AffineBoard = ({
clip.getData() clip.getData()
); );
}, },
onChangePage(app, shapes, bindings, assets) { async onChangePage(app, shapes, bindings, assets) {
Promise.all( Promise.all(
Object.entries(shapes).map(async ([id, shape]) => { Object.entries(shapes).map(async ([id, shape]) => {
if (shape === undefined) { if (shape === undefined) {
@@ -106,7 +108,7 @@ const AffineBoard = ({
}); });
} }
shape.affineId = block.id; shape.affineId = block.id;
return services.api.editorBlock.update({ return await services.api.editorBlock.update({
workspace: shape.workspace, workspace: shape.workspace,
id: block.id, id: block.id,
properties: { properties: {
@@ -9,8 +9,11 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
const { pageClientWidth } = usePageClientWidth(); const { pageClientWidth } = usePageClientWidth();
// page padding left and right total 300px // page padding left and right total 300px
const editorShapeInitSize = pageClientWidth - 300; const editorShapeInitSize = pageClientWidth - 300;
const [blocks, setBlocks] = useState<ReturnEditorBlock[]>(); const [blocks, setBlocks] = useState<{
shapes: [ReturnEditorBlock[]];
}>();
useEffect(() => { useEffect(() => {
Promise.all([
services.api.editorBlock services.api.editorBlock
.get({ workspace, ids: [rootBlockId] }) .get({ workspace, ids: [rootBlockId] })
.then(async blockData => { .then(async blockData => {
@@ -25,12 +28,18 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
return childBlock; return childBlock;
}) })
); );
setBlocks(shapes); return shapes;
}),
]).then(shapes => {
setBlocks({
shapes: shapes,
}); });
});
let unobserve: () => void; let unobserve: () => void;
services.api.editorBlock services.api.editorBlock
.observe({ workspace, id: rootBlockId }, async blockData => { .observe({ workspace, id: rootBlockId }, async blockData => {
const shapes = await Promise.all( Promise.all(
(blockData?.children || []).map(async childId => { (blockData?.children || []).map(async childId => {
const childBlock = ( const childBlock = (
await services.api.editorBlock.get({ await services.api.editorBlock.get({
@@ -40,8 +49,11 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
)?.[0]; )?.[0];
return childBlock; return childBlock;
}) })
); ).then(shapes => {
setBlocks(shapes); setBlocks({
shapes: [shapes],
});
});
}) })
.then(cb => { .then(cb => {
unobserve = cb; unobserve = cb;
@@ -53,8 +65,7 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
}, [workspace, rootBlockId]); }, [workspace, rootBlockId]);
let groupCount = 0; let groupCount = 0;
let blocksShapes = blocks?.shapes[0]?.reduce((acc, block) => {
return blocks?.reduce((acc, block) => {
const shapeProps = block.properties.shapeProps?.value const shapeProps = block.properties.shapeProps?.value
? JSON.parse(block.properties.shapeProps.value) ? JSON.parse(block.properties.shapeProps.value)
: {}; : {};
@@ -75,4 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
return acc; return acc;
}, {} as Record<string, TDShape>); }, {} as Record<string, TDShape>);
return {
shapes: blocksShapes,
};
}; };
+1 -4
View File
@@ -1,7 +1,6 @@
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { import {
RenderBlock,
RenderRoot, RenderRoot,
type BlockEditor, type BlockEditor,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
@@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
editor={editor} editor={editor}
editorElement={AffineEditor as any} editorElement={AffineEditor as any}
scrollBlank={scrollBlank} scrollBlank={scrollBlank}
> />
<RenderBlock blockId={editor.getRootBlockId()} />
</RenderRoot>
); );
} }
); );
+10 -11
View File
@@ -13,6 +13,8 @@ import {
TDStatus, TDStatus,
} from '@toeverything/components/board-types'; } from '@toeverything/components/board-types';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
// import { FocusButton } from '~components/FocusButton';
import { usePageClientWidth } from '@toeverything/datasource/state';
import { import {
memo, memo,
useEffect, useEffect,
@@ -22,22 +24,20 @@ import {
useState, useState,
type RefObject, type RefObject,
} from 'react'; } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { CommandPanel } from './components/command-panel';
// import { TopPanel } from '~components/TopPanel';
import { ContextMenu } from './components/context-menu';
import { ErrorFallback } from './components/error-fallback';
import { Loading } from './components/loading';
import { ToolsPanel } from './components/tools-panel'; import { ToolsPanel } from './components/tools-panel';
import { ZoomBar } from './components/zoom-bar';
import { import {
TldrawContext, TldrawContext,
useKeyboardShortcuts, useKeyboardShortcuts,
useStylesheet, useStylesheet,
useTldrawApp, useTldrawApp,
} from './hooks'; } from './hooks';
// import { TopPanel } from '~components/TopPanel';
import { ContextMenu } from './components/context-menu';
// import { FocusButton } from '~components/FocusButton';
import { usePageClientWidth } from '@toeverything/datasource/state';
import { ErrorBoundary } from 'react-error-boundary';
import { CommandPanel } from './components/command-panel';
import { ErrorFallback } from './components/error-fallback';
import { Loading } from './components/loading';
import { ZoomBar } from './components/zoom-bar';
export interface TldrawProps extends TldrawAppCtorProps { export interface TldrawProps extends TldrawAppCtorProps {
/** /**
@@ -295,7 +295,6 @@ const InnerTldraw = memo(function InnerTldraw({
const pageState = document.pageStates[page.id]; const pageState = document.pageStates[page.id];
const assets = document.assets; const assets = document.assets;
const { selectedIds } = pageState; const { selectedIds } = pageState;
const isHideBoundsShape = const isHideBoundsShape =
selectedIds.length === 1 && selectedIds.length === 1 &&
page.shapes[selectedIds[0]] && page.shapes[selectedIds[0]] &&
@@ -363,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({
// Hide bounds when not using the select tool, or when the only selected shape has handles // Hide bounds when not using the select tool, or when the only selected shape has handles
const hideBounds = const hideBounds =
(isInSession && app.session?.constructor.name !== 'BrushSession') || (isInSession && app.session?.constructor.name === 'BrushSession') ||
!isSelecting || !isSelecting ||
isHideBoundsShape || isHideBoundsShape ||
!!pageState.editingId; !!pageState.editingId;
@@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
borderRadius: '5px', borderRadius: '5px',
overflow: 'hidden', overflow: 'hidden',
margin: '5px', margin: '5px',
padding: '3px', width: '24px',
height: '24px',
cursor: 'pointer', cursor: 'pointer',
boxSizing: 'border-box', boxSizing: 'border-box',
'&:hover': { '&:hover': {
@@ -0,0 +1,80 @@
import type { TldrawApp } from '@toeverything/components/board-state';
import type {
ArrowBinding,
TDShape,
} from '@toeverything/components/board-types';
import { ConnectorIcon } from '@toeverything/components/icons';
import { IconButton, Popover, Tooltip } from '@toeverything/components/ui';
import { useEffect, useState } from 'react';
import { ListItemContainer, ListItemTitle } from './FontSizeConfig';
interface GroupAndUnGroupProps {
app: TldrawApp;
shapes: TDShape[];
}
export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => {
const [arrowToArr, setarrowToArr] = useState([]);
useEffect(() => {
let allShape = app.shapes;
let bindings = app.page.bindings;
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;
}
});
Object.keys(bindings).forEach(key => {
if (bindings[key].fromId === bindingId) {
toNextShapBindings.push(bindings[key]);
}
});
let ArrowToArr: TDShape[] = [];
toNextShapBindings.forEach(binding => {
if (binding.toId !== activeShape.id) {
allShape.forEach(item => {
if (item.id === binding.toId) {
ArrowToArr.push(item);
}
});
}
});
setarrowToArr(ArrowToArr);
return () => {};
}, [app.page.bindings]);
const jumpToNextShap = (shape: TDShape) => {
app.zoomToShapes([shape]);
};
return (
<Popover
trigger="hover"
placement="bottom-start"
content={
<div>
{arrowToArr.map((arrow: any) => {
return (
<ListItemContainer
key={arrow.id}
onClick={() => jumpToNextShap(arrow)}
>
<ListItemTitle>{arrow.name}</ListItemTitle>
</ListItemContainer>
);
})}
</div>
}
>
<Tooltip content="ArrowToEditor" placement="top-start">
<IconButton>
<ConnectorIcon></ConnectorIcon>
</IconButton>
</Tooltip>
</Popover>
);
};
@@ -106,6 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => {
shapes={config.deleteShapes.selectedShapes} shapes={config.deleteShapes.selectedShapes}
></AlignOperation> ></AlignOperation>
) : null, ) : null,
// toNextShap: (
// <ArrowTo
// app={app}
// shapes={config.deleteShapes.selectedShapes}
// ></ArrowTo>
// ),
}; };
const nodes = Object.entries(configNodes).filter(([key, node]) => !!node); const nodes = Object.entries(configNodes).filter(([key, node]) => !!node);
@@ -86,7 +86,7 @@ export const FontSizeConfig = ({ app, shapes }: FontSizeConfigProps) => {
); );
}; };
const ListItemContainer = styled('div')(({ theme }) => ({ export const ListItemContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
cursor: 'pointer', cursor: 'pointer',
@@ -100,7 +100,7 @@ const ListItemContainer = styled('div')(({ theme }) => ({
}, },
})); }));
const ListItemTitle = styled('span')(({ theme }) => ({ export const ListItemTitle = styled('span')(({ theme }) => ({
marginLeft: '12px', marginLeft: '12px',
color: theme.affine.palette.primaryText, color: theme.affine.palette.primaryText,
})); }));
@@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
export const LineTools = ({ app }: { app: TldrawApp }) => { export const LineTools = ({ app }: { app: TldrawApp }) => {
const activeTool = app.useStore(activeToolSelector); const activeTool = app.useStore(activeToolSelector);
const [visible, setVisible] = useState(false);
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>( const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
TDShapeType.Line TDShapeType.Line
@@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
return ( return (
<Popover <Popover
visible={visible}
placement="right-start" placement="right-start"
trigger="click" onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={ content={
<ShapesContainer> <ShapesContainer>
{shapes.map(({ type, label, tooltip, icon: Icon }) => ( {shapes.map(({ type, label, tooltip, icon: Icon }) => (
@@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
<IconButton <IconButton
onClick={() => { onClick={() => {
app.selectTool(type); app.selectTool(type);
setVisible(false);
setLastActiveTool(type); setLastActiveTool(type);
}} }}
> >
@@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
export const ShapeTools = ({ app }: { app: TldrawApp }) => { export const ShapeTools = ({ app }: { app: TldrawApp }) => {
const activeTool = app.useStore(activeToolSelector); const activeTool = app.useStore(activeToolSelector);
const [visible, setVisible] = useState(false);
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>( const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
TDShapeType.Rectangle TDShapeType.Rectangle
@@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
return ( return (
<Popover <Popover
visible={visible}
placement="right-start" placement="right-start"
trigger="click" onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={ content={
<ShapesContainer> <ShapesContainer>
{shapes.map(({ type, label, tooltip, icon: Icon }) => ( {shapes.map(({ type, label, tooltip, icon: Icon }) => (
@@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
<IconButton <IconButton
onClick={() => { onClick={() => {
app.selectTool(type); app.selectTool(type);
setVisible(false);
setLastActiveTool(type); setLastActiveTool(type);
}} }}
> >
@@ -12,12 +12,12 @@ import {
import { import {
IconButton, IconButton,
PopoverContainer, PopoverContainer,
styled,
// MuiIconButton as IconButton, // MuiIconButton as IconButton,
// MuiTooltip as Tooltip, // MuiTooltip as Tooltip,
Tooltip, Tooltip,
useTheme, useTheme,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import style9 from 'style9';
import { TldrawApp } from '@toeverything/components/board-state'; import { TldrawApp } from '@toeverything/components/board-state';
import { import {
@@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
}} }}
direction="none" direction="none"
> >
<div className={styles('container')}> <Container>
<div className={styles('toolBar')}> <ToolBar>
{tools.map( {tools.map(
({ ({
type, type,
@@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
</Tooltip> </Tooltip>
) )
)} )}
</div> </ToolBar>
</div> </Container>
</PopoverContainer> </PopoverContainer>
); );
}; };
const styles = style9.create({ const Container = styled('div')({
container: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
height: '100%', height: '100%',
}, });
toolBar: { const ToolBar = styled('div')({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
borderRadius: '10px', borderRadius: '10px',
padding: '4px 4px', padding: '4px 4px',
},
}); });
@@ -12,7 +12,7 @@ import {
styled, styled,
Tooltip, Tooltip,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { ReactElement, type CSSProperties } from 'react'; import { useState, type CSSProperties, type ReactElement } from 'react';
import { Palette } from '../../palette'; import { Palette } from '../../palette';
import { Pen } from './Pen'; import { Pen } from './Pen';
@@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
export const PenTools = ({ app }: { app: TldrawApp }) => { export const PenTools = ({ app }: { app: TldrawApp }) => {
const appCurrentTool = app.useStore(state => state.appState.activeTool); const appCurrentTool = app.useStore(state => state.appState.activeTool);
const [visible, setVisible] = useState(false);
const chosenPen = const chosenPen =
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) || PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
PENCIL_CONFIGS[0]; PENCIL_CONFIGS[0];
@@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
return ( return (
<Popover <Popover
visible={visible}
placement="right-start" placement="right-start"
trigger="click" onClick={() => setVisible(prev => !prev)}
onClickAway={() => setVisible(false)}
content={ content={
<Container> <Container>
<PensContainer> <PensContainer>
@@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
name={title} name={title}
primaryColor={color_vars['--color-0']} primaryColor={color_vars['--color-0']}
secondaryColor={color_vars['--color-1']} secondaryColor={color_vars['--color-1']}
onClick={() => setPen(key)} onClick={() => {
setVisible(false);
setPen(key);
}}
/> />
); );
} }
@@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
<Palette <Palette
selected={chosenColor} selected={chosenColor}
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors} colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
onSelect={color => setPenColor(color)} onSelect={color => {
setVisible(false);
setPenColor(color);
}}
/> />
<div /> <div />
</Container> </Container>
@@ -42,7 +42,7 @@ type E = HTMLDivElement;
export class ArrowUtil extends TDShapeUtil<T, E> { export class ArrowUtil extends TDShapeUtil<T, E> {
type = TDShapeType.Arrow as const; type = TDShapeType.Arrow as const;
override hideBounds = true; override hideBounds = false;
override canEdit = true; override canEdit = true;
@@ -8,6 +8,7 @@ import {
TDShapeType, TDShapeType,
TransformInfo, TransformInfo,
} from '@toeverything/components/board-types'; } from '@toeverything/components/board-types';
import type { BlockEditor } from '@toeverything/components/editor-core';
import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core'; import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import type { MouseEvent, SyntheticEvent } from 'react'; import type { MouseEvent, SyntheticEvent } from 'react';
@@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
Component = TDShapeUtil.Component<T, E, TDMeta>( Component = TDShapeUtil.Component<T, E, TDMeta>(
({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => { ({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => {
const containerRef = useRef<HTMLDivElement>(); const containerRef = useRef<HTMLDivElement>();
const editorRef = useRef<BlockEditor>();
const { const {
workspace, workspace,
rootBlockId, rootBlockId,
@@ -135,6 +137,18 @@ export class EditorUtil extends TDShapeUtil<T, E> {
} }
}, [app, state, shape.id, editingText, editingId]); }, [app, state, shape.id, editingText, editingId]);
useEffect(() => {
(async () => {
if (isEditing) {
const lastBlock =
await editorRef.current.getLastBlock();
editorRef.current.selectionManager.activeNodeByNodeId(
lastBlock.id
);
}
})();
}, [isEditing]);
const onMouseDown = useCallback( const onMouseDown = useCallback(
(e: MouseEvent) => { (e: MouseEvent) => {
if (e.detail === 2) { if (e.detail === 2) {
@@ -159,6 +173,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
rootBlockId={rootBlockId} rootBlockId={rootBlockId}
scrollBlank={false} scrollBlank={false}
isEdgeless isEdgeless
ref={editorRef}
/> />
{editingText ? null : <Mask />} {editingText ? null : <Mask />}
</Container> </Container>
@@ -1,6 +1,8 @@
import { Utils } from '@tldraw/core'; import { Utils } from '@tldraw/core';
import type { ShapeStyles } from '@toeverything/components/board-types'; import {
import { BINDING_DISTANCE } from '@toeverything/components/board-types'; BINDING_DISTANCE,
ShapeStyles,
} from '@toeverything/components/board-types';
import * as React from 'react'; import * as React from 'react';
import { getShapeStyle } from '../../shared'; import { getShapeStyle } from '../../shared';
@@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
return ( return (
<> <>
<rect <rect
className={ className={'tl-fill-hitarea'}
isSelected || style.isFilled
? 'tl-fill-hitarea'
: 'tl-stroke-hitarea'
}
x={sw / 2} x={sw / 2}
y={sw / 2} y={sw / 2}
width={w} width={w}
height={h} height={h}
opacity={1}
strokeWidth={BINDING_DISTANCE} strokeWidth={BINDING_DISTANCE}
/> />
{style.isFilled && ( {style.isFilled && (
<rect <rect
x={sw / 2} x={sw / 2}
y={sw / 2} y={sw / 2}
width={w} width={w - sw / 2}
height={h} height={h - sw / 2}
fill={fill} fill={fill}
pointerEvents="none" pointerEvents="none"
/> />
+63 -31
View File
@@ -175,6 +175,10 @@ interface TDCallbacks {
* (optional) A callback to run when the shape is copied. * (optional) A callback to run when the shape is copied.
*/ */
onCopy?: (e: ClipboardEvent, ids: string[]) => void; 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 { export interface TldrawAppCtorProps {
@@ -444,7 +448,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
if (visitedShapes.has(fromShape)) { if (visitedShapes.has(fromShape)) {
return; return;
} }
// We only need to update the binding's "from" shape (an arrow) // We only need to update the binding's "from" shape (an arrow)
const fromDelta = TLDR.update_arrow_bindings( const fromDelta = TLDR.update_arrow_bindings(
page, page,
@@ -627,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
private prev_bindings = this.page.bindings; private prev_bindings = this.page.bindings;
private prev_assets = this.document.assets; private prev_assets = this.document.assets;
private _broadcastPageChanges = () => { private _broadcastPageChanges = async () => {
const visited = new Set<string>(); const visited = new Set<string>();
const changedShapes: Record<string, TDShape | undefined> = {}; const changedShapes: Record<string, TDShape | undefined> = {};
@@ -684,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
Object.keys(changedAssets).length > 0 Object.keys(changedAssets).length > 0
) { ) {
this.just_sent = true; this.just_sent = true;
this.callbacks.onChangePage?.( await this.callbacks.onChangePage?.(
this, this,
changedShapes, changedShapes,
changedBindings, changedBindings,
@@ -860,7 +863,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
if (!page.bindings[binding.id]) { if (!page.bindings[binding.id]) {
return; return;
} }
const fromShape = page.shapes[binding.fromId] as ArrowShape; const fromShape = page.shapes[binding.fromId] as ArrowShape;
if (visitedShapes.has(fromShape)) { if (visitedShapes.has(fromShape)) {
@@ -868,6 +870,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
} }
// We only need to update the binding's "from" shape (an arrow) // We only need to update the binding's "from" shape (an arrow)
const fromDelta = TLDR.update_arrow_bindings(page, fromShape); const fromDelta = TLDR.update_arrow_bindings(page, fromShape);
visitedShapes.add(fromShape); visitedShapes.add(fromShape);
@@ -1957,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
paste = async (point?: number[], e?: ClipboardEvent) => { paste = async (point?: number[], e?: ClipboardEvent) => {
if (this.readOnly) return; 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 = ( const pasteInCurrentPage = (
shapes: TDShape[], shapes: TDShape[],
bindings: TDBinding[], bindings: TDBinding[],
@@ -2108,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// this.select(shapeId); // 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) { if (e !== undefined) {
const items: DataTransferItemList = const items: DataTransferItemList =
e.clipboardData?.items ?? ({} as DataTransferItemList); e.clipboardData?.items ?? ({} as DataTransferItemList);
@@ -3854,7 +3879,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => { private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => {
if (typeof svgStr === 'string') { if (typeof svgStr === 'string') {
let viewBox = new DOMParser().parseFromString(svgStr, 'text/xml'); const viewBox = new DOMParser().parseFromString(svgStr, 'text/xml');
return viewBox.children[0].getAttribute('viewBox'); return viewBox.children[0].getAttribute('viewBox');
} }
@@ -4138,7 +4163,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}; };
onPointerDown: TLPointerEventHandler = (info, e) => { onPointerDown: TLPointerEventHandler = (info, e) => {
if (e.buttons === 4) { if (e.button === 1) {
this.patchState({ this.patchState({
settings: { settings: {
forcePanning: true, forcePanning: true,
@@ -4155,6 +4180,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
}; };
onPointerUp: TLPointerEventHandler = (info, e) => { onPointerUp: TLPointerEventHandler = (info, e) => {
if (e.button === 1) {
this.patchState({
settings: {
forcePanning: false,
},
});
}
this.isPointing = false; this.isPointing = false;
this.updateInputs(info, e); this.updateInputs(info, e);
this.currentTool.onPointerUp?.(info, e); this.currentTool.onPointerUp?.(info, e);
+23 -24
View File
@@ -1,5 +1,6 @@
import { BaseEditor } from 'slate'; import { BaseEditor } from 'slate';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import { DoubleLinkElement } from './text/plugins/DoubleLink';
import { LinkElement } from './text/plugins/link'; import { LinkElement } from './text/plugins/link';
import { RefLinkElement } from './text/plugins/reflink'; import { RefLinkElement } from './text/plugins/reflink';
@@ -8,7 +9,8 @@ export type CustomElement =
| { type: string; children: CustomElement[] } | { type: string; children: CustomElement[] }
| CustomText | CustomText
| LinkElement | LinkElement
| RefLinkElement; | RefLinkElement
| DoubleLinkElement;
declare module 'slate' { declare module 'slate' {
interface CustomTypes { interface CustomTypes {
Editor: BaseEditor & ReactEditor; Editor: BaseEditor & ReactEditor;
@@ -19,35 +21,32 @@ declare module 'slate' {
export { BlockPreview, StyledBlockPreview } from './block-preview'; export { BlockPreview, StyledBlockPreview } from './block-preview';
export { default as Button } from './button'; export { default as Button } from './button';
export type { CommonListItem } from './list';
export { CommonList, BackLink, commonListContainer } from './list';
export * from './Logo';
export { default as Toolbar } from './toolbar';
export { CollapsibleTitle } from './collapsible-title'; export { CollapsibleTitle } from './collapsible-title';
export * from './comming-soon/CommingSoon';
export * from './text';
export { export {
NewpageIcon, AddIcon,
ClockIcon, ClockIcon,
ViewSidebarIcon, CloseIcon,
CodeBlockInlineIcon,
DocumentIcon,
FilterIcon,
FullScreenIcon,
HighlighterDuotoneIcon,
KanbanIcon,
ListIcon, ListIcon,
SpaceIcon, NewpageIcon,
PagesIcon,
PencilDotDuotoneIcon, PencilDotDuotoneIcon,
PencilDuotoneIcon, PencilDuotoneIcon,
HighlighterDuotoneIcon,
CodeBlockInlineIcon,
PagesIcon,
CloseIcon,
DocumentIcon,
TodoListIcon,
KanbanIcon,
TableIcon,
AddIcon,
FilterIcon,
SorterIcon, SorterIcon,
FullScreenIcon, SpaceIcon,
TableIcon,
TodoListIcon,
UnGroupIcon, UnGroupIcon,
ViewSidebarIcon,
} from './icon'; } from './icon';
export { BackLink, CommonList, commonListContainer } from './list';
export * from './comming-soon/CommingSoon'; export type { CommonListItem } from './list';
export * from './Logo';
export * from './text';
export { default as Toolbar } from './toolbar';
+10 -9
View File
@@ -1,8 +1,4 @@
import { useState } from 'react'; import { BackwardUndoIcon } from '@toeverything/components/icons';
import { useNavigate } from 'react-router';
import clsx from 'clsx';
import style9 from 'style9';
import { import {
BaseButton, BaseButton,
ListButton, ListButton,
@@ -12,9 +8,11 @@ import {
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries // eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
import { BlockSearchItem } from '@toeverything/datasource/jwt'; import { BlockSearchItem } from '@toeverything/datasource/jwt';
import clsx from 'clsx';
import { useState } from 'react';
import { useNavigate } from 'react-router';
import style9 from 'style9';
import { BlockPreview } from '../block-preview'; import { BlockPreview } from '../block-preview';
import { BackwardUndoIcon } from '@toeverything/components/icons';
export const commonListContainer = 'commonListContainer'; export const commonListContainer = 'commonListContainer';
@@ -28,6 +26,7 @@ export type CommonListItem = {
divider?: string; divider?: string;
content?: Content; content?: Content;
block?: BlockSearchItem; block?: BlockSearchItem;
renderCustom?: (props: CommonListItem) => JSX.Element;
}; };
type MenuItemsProps = { type MenuItemsProps = {
@@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => {
// ]); // ]);
// TODO Insert bidirectional link to be developed // TODO Insert bidirectional link to be developed
const JSONUnsupportedBlockTypes = ['page']; const JSONUnsupportedBlockTypes = ['page'];
let usedItems = items.filter(item => { const usedItems = items.filter(item => {
return !JSONUnsupportedBlockTypes.includes(item?.content?.id); return !JSONUnsupportedBlockTypes.includes(item?.content?.id);
}); });
return ( return (
@@ -58,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => {
> >
{usedItems?.length ? ( {usedItems?.length ? (
usedItems.map((item, idx) => { usedItems.map((item, idx) => {
if (item.block) { if (item.renderCustom) {
return item.renderCustom(item);
} else if (item.block) {
return ( return (
<BlockPreview <BlockPreview
className={clsx( className={clsx(
@@ -1,54 +1,53 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { ErrorBoundary, isEqual } from '@toeverything/utils';
import isHotkey from 'is-hotkey';
import isUrl from 'is-url';
import React, { import React, {
CSSProperties,
DragEvent,
forwardRef,
KeyboardEvent, KeyboardEvent,
KeyboardEventHandler, KeyboardEventHandler,
MouseEvent,
MouseEventHandler,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
forwardRef,
MouseEventHandler,
useLayoutEffect,
CSSProperties,
MouseEvent,
DragEvent,
} from 'react'; } from 'react';
import isHotkey from 'is-hotkey';
import { import {
createEditor, createEditor,
Descendant, Descendant,
Range,
Element as SlateElement,
Editor, Editor,
Transforms, Element as SlateElement,
Node, Node,
Path, Path,
Range,
Transforms,
} from 'slate'; } from 'slate';
import { import {
Editable, Editable,
withReact,
Slate,
ReactEditor, ReactEditor,
Slate,
useSlateStatic, useSlateStatic,
withReact,
} from 'slate-react'; } from 'slate-react';
import { CustomElement } from '..';
import { ErrorBoundary, isEqual, uaHelper } from '@toeverything/utils'; import { HOTKEYS, INLINE_STYLES } from './constants';
import { TextWithComments } from './element-leaf/TextWithComments';
import { Contents, SlateUtils, isSelectAll } from './slate-utils'; import { InlineDate, withDate } from './plugins/date';
import { DoubleLinkComponent } from './plugins/DoubleLink';
import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link';
import { InlineRefLink } from './plugins/reflink';
import { Contents, isSelectAll, SlateUtils } from './slate-utils';
import { import {
getCommentsIdsOnTextNode, getCommentsIdsOnTextNode,
getExtraPropertiesFromEditorOutmostNode, getExtraPropertiesFromEditorOutmostNode,
isInterceptCharacter, isInterceptCharacter,
matchMarkdown, matchMarkdown,
} from './utils'; } from './utils';
import { HOTKEYS, INLINE_STYLES } from './constants';
import { LinkComponent, LinkModal, withLinks, wrapLink } from './plugins/link';
import { withDate, InlineDate } from './plugins/date';
import { CustomElement } from '..';
import isUrl from 'is-url';
import { InlineRefLink } from './plugins/reflink';
import { TextWithComments } from './element-leaf/TextWithComments';
export interface TextProps { export interface TextProps {
/** read only */ /** read only */
@@ -739,6 +738,9 @@ const EditorElement = (props: any) => {
switch (element.type) { switch (element.type) {
case 'link': { case 'link': {
if (element.linkType === 'doubleLink') {
return <DoubleLinkComponent {...props} editor={editor} />;
}
return ( return (
<LinkComponent <LinkComponent
{...props} {...props}
@@ -802,7 +804,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
backgroundColor: '#F2F5F9', backgroundColor: '#F2F5F9',
borderRadius: '5px', borderRadius: '5px',
color: '#3A4C5C', color: '#3A4C5C',
padding: '3px 8px', padding: '1px 8px',
margin: '0 2px', margin: '0 2px',
}} }}
> >
@@ -839,6 +841,14 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
} }
} }
if (leaf.doubleLinkSearch) {
customChildren = (
<span style={{ backgroundColor: '#eee', borderRadius: '4px' }}>
{customChildren}
</span>
);
}
customChildren = ( customChildren = (
<TextWithComments commentsIds={commentsIds}> <TextWithComments commentsIds={commentsIds}>
{customChildren} {customChildren}
@@ -0,0 +1,40 @@
import { PagesIcon } from '@toeverything/components/icons';
import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { Descendant } from 'slate';
import { RenderElementProps } from 'slate-react';
export type DoubleLinkElement = {
type: 'link';
workspaceId: string;
blockId: string;
children: Descendant[];
id: string;
};
export const DoubleLinkComponent = (props: RenderElementProps) => {
const { attributes, children, element } = props;
const doubleLinkElement = element as DoubleLinkElement;
const navigate = useNavigate();
const handleClickLinkText = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
const { workspaceId, blockId } = doubleLinkElement;
navigate(`/${workspaceId}/${blockId}`);
},
[doubleLinkElement, navigate]
);
return (
<span>
<PagesIcon style={{ verticalAlign: 'middle', height: '20px' }} />
<a
{...attributes}
style={{ cursor: 'pointer' }}
href={`/${doubleLinkElement.workspaceId}/${doubleLinkElement.blockId}`}
>
<span onClick={handleClickLinkText}>{children}</span>
</a>
</span>
);
};
@@ -11,7 +11,7 @@ import {
Transforms, Transforms,
} from 'slate'; } from 'slate';
import { ReactEditor } from 'slate-react'; import { ReactEditor } from 'slate-react';
import type { CustomElement } from '..';
import { import {
fontBgColorPalette, fontBgColorPalette,
fontColorPalette, fontColorPalette,
@@ -21,6 +21,7 @@ import {
import { import {
getCommentsIdsOnTextNode, getCommentsIdsOnTextNode,
getEditorMarkForCommentId, getEditorMarkForCommentId,
getRandomString,
MARKDOWN_STYLE_MAP, MARKDOWN_STYLE_MAP,
MatchRes, MatchRes,
} from './utils'; } from './utils';
@@ -247,7 +248,11 @@ Editor.insertText = function (
const { path, offset } = at; const { path, offset } = at;
if (text.length > 0) { if (text.length > 0) {
const marks = Editor.marks(editor); const marks = Editor.marks(editor);
if (text === '\u0020' && Object.keys(marks).length) { if (
text === '\u0020' &&
Object.keys(marks).length &&
!(marks as any).doubleLinkSearch
) {
// If the input is a space and the mark has content, remove the mark // If the input is a space and the mark has content, remove the mark
const newPath = [path[0], path[1] + 1]; const newPath = [path[0], path[1] + 1];
const newPoint = { const newPoint = {
@@ -594,8 +599,13 @@ class SlateUtils {
const fragmentChildren = firstFragment.children; const fragmentChildren = firstFragment.children;
const textChildren: Text[] = []; const textChildren: CustomElement[] = [];
for (const child of fragmentChildren) { for (let i = 0; i < fragmentChildren.length; i++) {
const child = fragmentChildren[i];
if ('type' in child && child.type === 'link') {
i !== fragmentChildren.length - 1 && textChildren.push(child);
continue;
}
if (!('text' in child)) { if (!('text' in child)) {
console.error('Debug information:', point1, point2, fragment); console.error('Debug information:', point1, point2, fragment);
throw new Error('Fragment exists nested!'); throw new Error('Fragment exists nested!');
@@ -1119,32 +1129,128 @@ class SlateUtils {
}); });
} }
public insertReference(reference: string) { public setDoubleLinkSearchSlash(point: Point) {
try { const str = Editor.string(this.editor, {
// Transforms.setSelection(this.editor, this.getEndSelection()); anchor: this.getStart(),
const { anchor, focus } = this.getEndSelection(); focus: point,
Transforms.insertNodes( });
if (str.endsWith('[[')) {
Transforms.select(this.editor, {
anchor: point,
focus: Object.assign({}, point, {
offset: point.offset - 2,
}),
});
Editor.addMark(this.editor, 'doubleLinkSearch', true);
Transforms.select(this.editor, {
anchor: this.editor.selection.anchor,
focus: this.editor.selection.anchor,
});
}
}
public getDoubleLinkSearchSlashText() {
const nodes = Editor.nodes(this.editor, {
at: [],
//@ts-ignore
match: node => !!node.doubleLinkSearch,
});
const searchNode = nodes.next().value;
if (searchNode && (searchNode[0] as { text?: string }).text) {
return (searchNode[0] as { text?: string }).text;
}
return '';
}
public setSelectDoubleLinkSearchSlash() {
const nodes = Editor.nodes(this.editor, {
at: [],
//@ts-ignore
match: node => !!node.doubleLinkSearch,
});
const searchNode = nodes.next().value;
if (searchNode) {
const text = (searchNode[0] as { text?: string })?.text || '';
const path = searchNode[1];
const anchor = Editor.before(
this.editor, this.editor,
{ {
type: 'reflink', path,
reference, offset: 1,
children: [],
}, },
{ at: focus || anchor } {
); unit: 'offset',
// requestAnimationFrame(() => {
// console.log(this.editor.selection, this.editor.insertNode);
// this.editor.insertNode({
// type: 'reflink',
// reference,
// children: [{ text: '' }]
// });
// // Transforms.select();
// });
} catch (e) {
console.log(e);
} }
);
const focus = Editor.after(
this.editor,
{
path,
offset: text.length - 1,
},
{
unit: 'offset',
}
);
Transforms.select(this.editor, { anchor, focus });
}
}
public removeDoubleLinkSearchSlash(isRemoveSlash?: boolean) {
if (isRemoveSlash) {
const nodes = Editor.nodes(this.editor, {
at: [],
//@ts-ignore
match: node => !!node.doubleLinkSearch,
});
const searchNode = nodes.next().value;
if (searchNode) {
const text = (searchNode[0] as { text?: string })?.text || '';
if (text.startsWith('[[')) {
const path = searchNode[1];
Transforms.delete(this.editor, {
at: {
path,
offset: 0,
},
distance: text.length,
unit: 'character',
});
}
}
}
Transforms.setNodes(
this.editor,
{ doubleLinkSearch: null } as Partial<SlateNode>,
{
at: [],
match: node => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
return !!node.doubleLinkSearch;
},
}
);
this.editor.removeMark('doubleLinkSearch');
}
public insertDoubleLink(
workspaceId: string,
linkBlockId: string,
children: any[]
) {
const link = {
type: 'link',
linkType: 'doubleLink',
workspaceId: workspaceId,
blockId: linkBlockId,
children: children,
id: getRandomString('link'),
};
Transforms.insertNodes(this.editor, link);
requestAnimationFrame(() => {
ReactEditor.focus(this.editor);
});
} }
/** todo improve if selection is collapsed */ /** todo improve if selection is collapsed */
@@ -1,29 +1,28 @@
import type { TextProps } from '@toeverything/components/common'; import type { TextProps } from '@toeverything/components/common';
import { import {
ContentColumnValue, ContentColumnValue,
services,
Protocol, Protocol,
services,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { type CreateView } from '@toeverything/framework/virgo'; import { type CreateView } from '@toeverything/framework/virgo';
import { useEffect, useRef, useState } from 'react'; 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 { import {
TextManage, TextManage,
type ExtendedTextUtils, type ExtendedTextUtils,
} from '../../components/text-manage'; } from '../../components/text-manage';
import { tabBlock } from '../../utils/indent'; import { tabBlock } from '../../utils/indent';
import { BulletIcon, getChildrenType, NumberType } from './data';
import { BulletBlock, BulletProperties } from './types'; 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 = { export const defaultBulletProps: BulletProperties = {
text: { value: [{ text: '' }] }, text: { value: [{ text: '' }] },
@@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
</div> </div>
</List> </List>
</BlockPendantProvider> </BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} /> <RenderBlockChildren block={block} />
</IndentWrapper>
</BlockContainer> </BlockContainer>
); );
}; };
@@ -1,16 +1,18 @@
import { Protocol } from '@toeverything/datasource/db-service';
import { import {
AsyncBlock, AsyncBlock,
BaseView, BaseView,
BlockEditor, BlockEditor,
HTML2BlockResult, HTML2BlockResult,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { defaultBulletProps, BulletView } from './BulletView';
import { import {
Block2HtmlProps, Block2HtmlProps,
commonBlock2HtmlContent, commonBlock2HtmlContent,
commonHTML2block, commonHTML2block,
} from '../../utils/commonBlockClip'; } from '../../utils/commonBlockClip';
import { BulletView, defaultBulletProps } from './BulletView';
export class BulletBlock extends BaseView { export class BulletBlock extends BaseView {
public type = Protocol.Block.Type.bullet; public type = Protocol.Block.Type.bullet;
@@ -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 { 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 { lezer } from '@codemirror/lang-lezer';
import { markdown } from '@codemirror/lang-markdown';
import { php } from '@codemirror/lang-php'; 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 { 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 { 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 { clojure } from '@codemirror/legacy-modes/mode/clojure';
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript'; import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'; 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 { 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 { 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 { Extension } from '@codemirror/state';
import { Option, Select } from '@toeverything/components/ui';
import { import {
useOnSelect,
BlockPendantProvider, BlockPendantProvider,
useOnSelect,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { copyToClipboard } from '@toeverything/utils';
import { DuplicateIcon } from '@toeverything/components/icons'; 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 { interface CreateCodeView extends CreateView {
style9?: StyleWithAtRules; style9?: StyleWithAtRules;
@@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
borderRadius: theme.affine.shape.borderRadius, borderRadius: theme.affine.shape.borderRadius,
'&:hover': { '&:hover': {
'.operation': { '.operation': {
display: 'flex', opacity: 1,
}, },
}, },
'.operation': { '.operation': {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-between', justifyContent: 'space-between',
opacity: 0,
transition: 'opacity 1.5s',
}, },
'.copy-block': { '.copy-block': {
padding: '0px 10px', padding: '0px 10px',
@@ -15,7 +15,7 @@ import {
export class CodeBlock extends BaseView { export class CodeBlock extends BaseView {
type = Protocol.Block.Type.code; type = Protocol.Block.Type.code;
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
// View = CodeView; // View = CodeView;
View = CodeView; View = CodeView;
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> { override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
@@ -1,12 +1,12 @@
import { useState } from 'react';
import { CreateView } from '@toeverything/framework/virgo';
import { import {
BlockPendantProvider, BlockPendantProvider,
useOnSelect, useOnSelect,
} from '@toeverything/components/editor-core'; } 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 { SourceView } from '../../components/source-view';
import { LinkContainer } from '../../components/style-container'; import { LinkContainer } from '../../components/style-container';
import { Upload } from '../../components/upload/upload';
const MESSAGES = { const MESSAGES = {
ADD_EMBED_LINK: 'Add embed link', ADD_EMBED_LINK: 'Add embed link',
@@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
{embedLinkUrl ? ( {embedLinkUrl ? (
<SourceView <SourceView
block={block} block={block}
editorElement={props.editorElement}
isSelected={isSelect} isSelected={isSelect}
viewType="embedLink" viewType="embedLink"
link={embedLinkUrl} link={embedLinkUrl}
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class EmbedLinkBlock extends BaseView { export class EmbedLinkBlock extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
type = Protocol.Block.Type.embedLink; type = Protocol.Block.Type.embedLink;
View = EmbedLinkView; View = EmbedLinkView;
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class FigmaBlock extends BaseView { export class FigmaBlock extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
type = Protocol.Block.Type.figma; type = Protocol.Block.Type.figma;
View = FigmaView; View = FigmaView;
@@ -13,7 +13,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
export class FileBlock extends BaseView { export class FileBlock extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
type = Protocol.Block.Type.file; type = Protocol.Block.Type.file;
View = FileView; View = FileView;
override async block2html({ block }: Block2HtmlProps) { override async block2html({ block }: Block2HtmlProps) {
@@ -1,4 +1,4 @@
import { RenderBlock } from '@toeverything/components/editor-core'; import { useBlockRender } from '@toeverything/components/editor-core';
import { ChildrenView, CreateView } from '@toeverything/framework/virgo'; import { ChildrenView, CreateView } from '@toeverything/framework/virgo';
export const GridItemRender = function ( export const GridItemRender = function (
@@ -6,10 +6,11 @@ export const GridItemRender = function (
) { ) {
const GridItem = function (props: CreateView) { const GridItem = function (props: CreateView) {
const { block } = props; const { block } = props;
const { BlockRender } = useBlockRender();
const children = ( const children = (
<> <>
{block.childrenIds.map(id => { {block.childrenIds.map(id => {
return <RenderBlock key={id} blockId={id} />; return <BlockRender key={id} blockId={id} />;
})} })}
</> </>
); );
@@ -6,7 +6,7 @@ import { GridItemRender } from './GridItemRender';
export class GridItemBlock extends BaseView { export class GridItemBlock extends BaseView {
public override selectable = false; public override selectable = false;
public override activatable = false; public override editable = false;
public override allowPendant = false; public override allowPendant = false;
public override layoutOnly = true; public override layoutOnly = true;
@@ -1,16 +1,16 @@
import { RenderBlock } from '@toeverything/components/editor-core'; import { useBlockRender } from '@toeverything/components/editor-core';
import { CreateView } from '@toeverything/framework/virgo';
import React, { useEffect, useRef, useState } from 'react';
import { GridHandle } from './GirdHandle';
import { styled } from '@toeverything/components/ui'; 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 ReactDOM from 'react-dom';
import { import {
GRID_ITEM_CLASS_NAME, GRID_ITEM_CLASS_NAME,
GRID_ITEM_CONTENT_CLASS_NAME, GRID_ITEM_CONTENT_CLASS_NAME,
} from '../grid-item/GridItem'; } from '../grid-item/GridItem';
import { debounce, domToRect, Point } from '@toeverything/utils'; import { GridHandle } from './GirdHandle';
import clsx from 'clsx';
import { Protocol } from '@toeverything/datasource/db-service';
const DB_UPDATE_DELAY = 50; const DB_UPDATE_DELAY = 50;
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag'; const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
@@ -31,6 +31,7 @@ export const Grid = function (props: CreateView) {
const originalLeftWidth = useRef<number>(gridItemMinWidth); const originalLeftWidth = useRef<number>(gridItemMinWidth);
const originalRightWidth = useRef<number>(gridItemMinWidth); const originalRightWidth = useRef<number>(gridItemMinWidth);
const [alertHandleId, setAlertHandleId] = useState<string>(null); const [alertHandleId, setAlertHandleId] = useState<string>(null);
const { BlockRender } = useBlockRender();
const getLeftRightGridItemDomByIndex = (index: number) => { const getLeftRightGridItemDomByIndex = (index: number) => {
const gridItems = Array.from(gridContainerRef.current?.children).filter( const gridItems = Array.from(gridContainerRef.current?.children).filter(
@@ -226,7 +227,7 @@ export const Grid = function (props: CreateView) {
key={id} key={id}
className={GRID_ITEM_CLASS_NAME} className={GRID_ITEM_CLASS_NAME}
> >
<RenderBlock hasContainer={false} blockId={id} /> <BlockRender hasContainer={false} blockId={id} />
<GridHandle <GridHandle
onDrag={event => handleDragGrid(event, i)} onDrag={event => handleDragGrid(event, i)}
editor={editor} editor={editor}
@@ -7,7 +7,7 @@ export { GRID_PROPERTY_KEY, removePercent } from './Grid';
export class GridBlock extends BaseView { export class GridBlock extends BaseView {
public override selectable = false; public override selectable = false;
public override activatable = false; public override editable = false;
public override allowPendant = false; public override allowPendant = false;
public override layoutOnly = true; public override layoutOnly = true;
@@ -13,7 +13,7 @@ import {
export class Group extends BaseView { export class Group extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
public override allowPendant = false; public override allowPendant = false;
type = Protocol.Block.Type.group; type = Protocol.Block.Type.group;
@@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
import type { CreateView } from '@toeverything/framework/virgo'; import type { CreateView } from '@toeverything/framework/virgo';
export const ScenePage = ({ block }: CreateView) => { export const ScenePage = ({ block }: CreateView) => {
return <RenderBlockChildren block={block} />; return <RenderBlockChildren block={block} indent={false} />;
}; };
@@ -1,11 +1,18 @@
import type { KanbanCard } from '@toeverything/components/editor-core';
import { import {
RenderBlock, KanbanCard,
useBlockRender,
useEditor,
useKanban, useKanban,
useRefPage,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { PenIcon } from '@toeverything/components/icons';
import {
IconButton,
MuiClickAwayListener,
styled,
} from '@toeverything/components/ui';
import { useFlag } from '@toeverything/datasource/feature-flags'; import { useFlag } from '@toeverything/datasource/feature-flags';
import { useState, type MouseEvent } from 'react';
import { useRefPage } from './RefPage';
const CardContent = styled('div')({ const CardContent = styled('div')({
margin: '20px', margin: '20px',
@@ -23,6 +30,7 @@ const CardActions = styled('div')({
fontWeight: '300', fontWeight: '300',
color: '#98ACBD', color: '#98ACBD',
transition: 'all ease-in 0.2s', transition: 'all ease-in 0.2s',
zIndex: 1,
':hover': { ':hover': {
background: '#F5F7F8', background: '#F5F7F8',
@@ -39,11 +47,13 @@ const PlusIcon = styled('div')({
}); });
const CardContainer = styled('div')({ const CardContainer = styled('div')({
position: 'relative',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: '#fff', backgroundColor: '#fff',
border: '1px solid #E2E7ED', border: '1px solid #E2E7ED',
borderRadius: '5px', borderRadius: '5px',
overflow: 'hidden',
[CardActions.toString()]: { [CardActions.toString()]: {
opacity: '0', opacity: '0',
@@ -55,6 +65,23 @@ const CardContainer = styled('div')({
}, },
}); });
const Overlay = styled('div')({
position: 'absolute',
width: '100%',
height: '100%',
background: 'transparent',
'& > *': {
visibility: 'hidden',
position: 'absolute',
right: '24px',
top: '16px',
},
'&:hover > *': {
visibility: 'visible',
},
});
export const CardItem = ({ export const CardItem = ({
id, id,
block, block,
@@ -64,24 +91,44 @@ export const CardItem = ({
}) => { }) => {
const { addSubItem } = useKanban(); const { addSubItem } = useKanban();
const { openSubPage } = useRefPage(); const { openSubPage } = useRefPage();
const [editable, setEditable] = useState(false);
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false); const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
const { editor } = useEditor();
const { BlockRender } = useBlockRender();
const onAddItem = async () => { const onAddItem = async () => {
setEditable(true);
await addSubItem(block); await addSubItem(block);
}; };
const onClickCard = async () => { const onClickCard = async () => {
showKanbanRefPageFlag && openSubPage(id); openSubPage(id);
};
const onClickPen = (e: MouseEvent<Element>) => {
e.stopPropagation();
setEditable(true);
editor.selectionManager.activeNodeByNodeId(block.id);
}; };
return ( return (
<CardContainer onClick={onClickCard}> <MuiClickAwayListener onClickAway={() => setEditable(false)}>
<CardContainer>
<CardContent> <CardContent>
<RenderBlock blockId={id} /> <BlockRender blockId={id} />
</CardContent> </CardContent>
{showKanbanRefPageFlag && !editable && (
<Overlay onClick={onClickCard}>
<IconButton backgroundColor="#fff" onClick={onClickPen}>
<PenIcon />
</IconButton>
</Overlay>
)}
<CardActions onClick={onAddItem}> <CardActions onClick={onAddItem}>
<PlusIcon /> <PlusIcon />
<span>Add a sub-block</span> <span>Add a sub-block</span>
</CardActions> </CardActions>
</CardContainer> </CardContainer>
</MuiClickAwayListener>
); );
}; };
@@ -1,7 +1,7 @@
import { useEditor } from '@toeverything/components/editor-core';
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui'; import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
import { createContext, ReactNode, useContext, useState } from 'react'; import { createContext, ReactNode, useContext, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { RenderBlock } from '../render-block';
const Dialog = styled('div')({ const Dialog = styled('div')({
flex: 1, flex: 1,
@@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
onClick={closeSubPage} onClick={closeSubPage}
> >
<Dialog <Dialog
onClick={e => { onClick={(e: { stopPropagation: () => void }) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
@@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
}; };
const ModalPage = ({ blockId }: { blockId: string | null }) => { const ModalPage = ({ blockId }: { blockId: string | null }) => {
const { editor, editorElement } = useEditor();
const AffineEditor = editorElement as any;
return ( return (
<Modal open={!!blockId}> <Modal open={!!blockId}>
{blockId && <RenderBlock blockId={blockId} />} {blockId && (
<AffineEditor
workspace={editor.workspace}
rootBlockId={blockId}
scrollBlank={false}
// use edgeless mode prevent padding and blank bottom
isEdgeless
/>
)}
</Modal> </Modal>
); );
}; };
@@ -4,12 +4,14 @@ import { SceneKanbanContext } from './context';
import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper'; import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import type { CreateView } from '@toeverything/framework/virgo'; import type { CreateView } from '@toeverything/framework/virgo';
import { RefPageProvider } from './RefPage';
export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>( export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
({ editor, block }) => { ({ editor, block }) => {
const { kanban } = useKanban(); const { kanban } = useKanban();
return ( return (
<RefPageProvider>
<SceneKanbanContext.Provider value={{ editor, block }}> <SceneKanbanContext.Provider value={{ editor, block }}>
<CardContainerWrapper <CardContainerWrapper
dataSource={kanban} dataSource={kanban}
@@ -28,6 +30,7 @@ export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
)} )}
/> />
</SceneKanbanContext.Provider> </SceneKanbanContext.Provider>
</RefPageProvider>
); );
} }
); );
@@ -10,7 +10,7 @@ import { getRandomString } from '@toeverything/components/common';
export class ImageBlock extends BaseView { export class ImageBlock extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
type = Protocol.Block.Type.image; type = Protocol.Block.Type.image;
View = ImageView; View = ImageView;
@@ -1,8 +1,8 @@
import { TextProps } from '@toeverything/components/common'; import { TextProps } from '@toeverything/components/common';
import { import {
ContentColumnValue, ContentColumnValue,
services,
Protocol, Protocol,
services,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { type CreateView } from '@toeverything/framework/virgo'; import { type CreateView } from '@toeverything/framework/virgo';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@@ -11,18 +11,17 @@ import {
type ExtendedTextUtils, type ExtendedTextUtils,
} from '../../components/text-manage'; } from '../../components/text-manage';
import { tabBlock } from '../../utils/indent'; import { tabBlock } from '../../utils/indent';
import { IndentWrapper } from '../../components/IndentWrapper';
import type { Numbered, NumberedAsyncBlock } from './types'; import type { Numbered, NumberedAsyncBlock } from './types';
import { getChildrenType, getNumber } from './data';
import { import {
supportChildren,
RenderBlockChildren,
useOnSelect,
BlockPendantProvider, BlockPendantProvider,
RenderBlockChildren,
supportChildren,
useOnSelect,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { List } from '../../components/style-container';
import { BlockContainer } from '../../components/BlockContainer'; import { BlockContainer } from '../../components/BlockContainer';
import { List } from '../../components/style-container';
import { getChildrenType, getNumber } from './data';
export const defaultTodoProps: Numbered = { export const defaultTodoProps: Numbered = {
text: { value: [{ text: '' }] }, text: { value: [{ text: '' }] },
@@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
</List> </List>
</BlockPendantProvider> </BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} /> <RenderBlockChildren block={block} />
</IndentWrapper>
</BlockContainer> </BlockContainer>
); );
}; };
@@ -1,16 +1,16 @@
import { Protocol } from '@toeverything/datasource/db-service';
import { import {
AsyncBlock, AsyncBlock,
BaseView, BaseView,
BlockEditor, BlockEditor,
HTML2BlockResult, HTML2BlockResult,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service';
import { defaultTodoProps, NumberedView } from './NumberedView';
import { import {
Block2HtmlProps, Block2HtmlProps,
commonBlock2HtmlContent, commonBlock2HtmlContent,
commonHTML2block, commonHTML2block,
} from '../../utils/commonBlockClip'; } from '../../utils/commonBlockClip';
import { defaultTodoProps, NumberedView } from './NumberedView';
export class NumberedBlock extends BaseView { export class NumberedBlock extends BaseView {
public type = Protocol.Block.Type.numbered; public type = Protocol.Block.Type.numbered;
@@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
import { Protocol } from '@toeverything/datasource/db-service'; import { Protocol } from '@toeverything/datasource/db-service';
import { CreateView } from '@toeverything/framework/virgo'; import { CreateView } from '@toeverything/framework/virgo';
import { BlockContainer } from '../../components/BlockContainer'; import { BlockContainer } from '../../components/BlockContainer';
import { IndentWrapper } from '../../components/IndentWrapper';
import { TextManage } from '../../components/text-manage'; import { TextManage } from '../../components/text-manage';
import { dedentBlock, tabBlock } from '../../utils/indent'; import { dedentBlock, tabBlock } from '../../utils/indent';
interface CreateTextView extends CreateView { interface CreateTextView extends CreateView {
@@ -255,9 +254,7 @@ export const TextView = ({
handleTab={onTab} handleTab={onTab}
/> />
</BlockPendantProvider> </BlockPendantProvider>
<IndentWrapper>
<RenderBlockChildren block={block} /> <RenderBlockChildren block={block} />
</IndentWrapper>
</BlockContainer> </BlockContainer>
); );
}; };
@@ -1,11 +1,17 @@
import { TextProps } from '@toeverything/components/common'; import { TextProps } from '@toeverything/components/common';
import {
AsyncBlock,
BlockPendantProvider,
CreateView,
useOnSelect,
} from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import { import {
ContentColumnValue, ContentColumnValue,
Protocol, Protocol,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo'; import { useRef, useState } from 'react';
import { useRef } from 'react'; import { BlockContainer } from '../../components/BlockContainer';
import { import {
TextManage, TextManage,
type ExtendedTextUtils, type ExtendedTextUtils,
@@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
export const TodoView = ({ block, editor }: CreateView) => { export const TodoView = ({ block, editor }: CreateView) => {
const properties = { ...defaultTodoProps, ...block.getProperties() }; const properties = { ...defaultTodoProps, ...block.getProperties() };
const text_ref = useRef<ExtendedTextUtils>(null); 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 () => { const turn_into_text_block = async () => {
// Convert to text block // Convert to text block
@@ -121,6 +131,8 @@ export const TodoView = ({ block, editor }: CreateView) => {
}; };
return ( return (
<BlockContainer editor={editor} block={block} selected={isSelect}>
<BlockPendantProvider block={block}>
<TodoBlock> <TodoBlock>
<div className={'checkBoxContainer'}> <div className={'checkBoxContainer'}>
<CheckBox <CheckBox
@@ -131,7 +143,9 @@ export const TodoView = ({ block, editor }: CreateView) => {
<div className={'textContainer'}> <div className={'textContainer'}>
<TextManage <TextManage
className={properties.checked?.value ? 'checked' : ''} className={
properties.checked?.value ? 'checked' : ''
}
ref={text_ref} ref={text_ref}
editor={editor} editor={editor}
block={block} block={block}
@@ -143,6 +157,8 @@ export const TodoView = ({ block, editor }: CreateView) => {
/> />
</div> </div>
</TodoBlock> </TodoBlock>
</BlockPendantProvider>
</BlockContainer>
); );
}; };
@@ -1,17 +1,18 @@
import { Protocol } from '@toeverything/datasource/db-service';
import { import {
BaseView, BaseView,
BlockEditor, BlockEditor,
HTML2BlockResult, HTML2BlockResult,
withTreeViewChildren,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
import { Protocol } from '@toeverything/datasource/db-service'; import { defaultTodoProps, TodoView } from './TodoView';
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
import { TodoView, defaultTodoProps } from './TodoView';
import type { TodoAsyncBlock } from './types';
import { import {
Block2HtmlProps, Block2HtmlProps,
commonBlock2HtmlContent, commonBlock2HtmlContent,
commonHTML2block, commonHTML2block,
} from '../../utils/commonBlockClip'; } from '../../utils/commonBlockClip';
import type { TodoAsyncBlock } from './types';
export class TodoBlock extends BaseView { export class TodoBlock extends BaseView {
type = Protocol.Block.Type.todo; type = Protocol.Block.Type.todo;
@@ -12,7 +12,7 @@ import {
export class YoutubeBlock extends BaseView { export class YoutubeBlock extends BaseView {
public override selectable = true; public override selectable = true;
public override activatable = false; public override editable = false;
type = Protocol.Block.Type.youtube; type = Protocol.Block.Type.youtube;
View = YoutubeView; View = YoutubeView;
@@ -23,7 +23,7 @@ export const BlockContainer = function ({
); );
}; };
export const Container = styled('div')<{ selected: boolean }>( export const Container = styled('div')<{ selected?: boolean }>(
({ selected, theme }) => ({ ({ selected, theme }) => ({
backgroundColor: selected ? theme.affine.palette.textSelected : '', backgroundColor: selected ? theme.affine.palette.textSelected : '',
marginBottom: '2px', 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,9 +1,9 @@
import { memo, useEffect, useRef, useState } from 'react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { memo, useEffect, useRef, useState } from 'react';
import { StyledBlockPreview } from '@toeverything/components/common'; import { StyledBlockPreview } from '@toeverything/components/common';
import { AsyncBlock, useEditor } from '@toeverything/components/editor-core';
import { services } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service';
import { AsyncBlock } from '@toeverything/framework/virgo';
import { debounce, sleep } from '@toeverything/utils'; import { debounce, sleep } from '@toeverything/utils';
const updateTitle = async ( const updateTitle = async (
@@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => {
type BlockPreviewProps = { type BlockPreviewProps = {
block: AsyncBlock; block: AsyncBlock;
blockId: string; blockId: string;
editorElement?: () => JSX.Element;
}; };
const InternalBlockPreview = (props: BlockPreviewProps) => { const InternalBlockPreview = (props: BlockPreviewProps) => {
const container = useRef<HTMLDivElement>(); const container = useRef<HTMLDivElement>();
const [preview, setPreview] = useState(true); const [preview, setPreview] = useState(true);
const title = useBlockTitle(props.block, props.blockId); const title = useBlockTitle(props.block, props.blockId);
const { editorElement } = useEditor();
const AffineEditor = props.editorElement as any; const AffineEditor = editorElement as any;
useEffect(() => { useEffect(() => {
if (container?.current) { if (container?.current) {
@@ -1,17 +1,15 @@
import { import {
AsyncBlock, AsyncBlock,
useCurrentView, useCurrentView,
useLazyIframe,
} from '@toeverything/components/editor-core'; } from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import { ReactElement, ReactNode, useEffect, useRef, useState } from 'react'; import { ReactNode, useEffect, useRef, useState } from 'react';
import { SCENE_CONFIG } from '../../blocks/group/config'; import { SCENE_CONFIG } from '../../blocks/group/config';
import { BlockPreview } from './BlockView'; import { BlockPreview } from './BlockView';
import { formatUrl } from './format-url'; import { formatUrl } from './format-url';
export interface Props { export interface Props {
block: AsyncBlock; block: AsyncBlock;
editorElement?: () => JSX.Element;
viewType?: string; viewType?: string;
link: string; link: string;
// onResizeEnd: (data: any) => void; // onResizeEnd: (data: any) => void;
@@ -150,7 +148,7 @@ const LoadingContiner = () => {
}; };
export const SourceView = (props: Props) => { export const SourceView = (props: Props) => {
const { link, isSelected, block, editorElement } = props; const { link, isSelected, block } = props;
const src = formatUrl(link); const src = formatUrl(link);
// let iframeShow = useLazyIframe(src, 3000, iframeContainer); // let iframeShow = useLazyIframe(src, 3000, iframeContainer);
const [currentView] = useCurrentView(); const [currentView] = useCurrentView();
@@ -161,10 +159,7 @@ export const SourceView = (props: Props) => {
<SourceViewContainer isSelected={isSelected} scene={type}> <SourceViewContainer isSelected={isSelected} scene={type}>
<MouseMaskContainer /> <MouseMaskContainer />
<LazyIframe <LazyIframe src={src} fallback={LoadingContiner()} />
src={src}
fallback={LoadingContiner()}
></LazyIframe>
</SourceViewContainer> </SourceViewContainer>
</div> </div>
); );
@@ -175,11 +170,7 @@ export const SourceView = (props: Props) => {
style={{ padding: '0' }} style={{ padding: '0' }}
scene={type} scene={type}
> >
<BlockPreview <BlockPreview block={block} blockId={src} />
block={block}
editorElement={editorElement}
blockId={src}
/>
</SourceViewContainer> </SourceViewContainer>
); );
} }
@@ -1,4 +1,5 @@
export const isYoutubeUrl = (url?: string): boolean => { export const isYoutubeUrl = (url?: string): boolean => {
if (!url) return false;
const allowedHosts = ['www.youtu.be', 'www.youtube.com']; const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
const host = new URL(url).host; const host = new URL(url).host;
return allowedHosts.includes(host); return allowedHosts.includes(host);
+20 -8
View File
@@ -1,22 +1,34 @@
import { createContext, useContext } from 'react';
import type { BlockEditor, AsyncBlock } from './editor';
import { genErrorObj } from '@toeverything/utils'; import { genErrorObj } from '@toeverything/utils';
import { createContext, PropsWithChildren, useContext } from 'react';
import type { AsyncBlock, BlockEditor } from './editor';
const RootContext = createContext<{ type EditorProps = {
editor: BlockEditor; editor: BlockEditor;
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top // 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; editorElement: () => JSX.Element;
}>( };
const EditorContext = createContext<EditorProps>(
genErrorObj( 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any ) as any
); );
export const EditorProvider = RootContext.Provider;
export const useEditor = () => { 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>
);
}; };
/** /**
+11 -11
View File
@@ -4,12 +4,12 @@ import {
services, services,
type ReturnUnobserve, type ReturnUnobserve,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import type { PropsWithChildren } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { EditorProvider } from './Contexts'; import { EditorProvider } from './Contexts';
import type { BlockEditor } from './editor'; import type { BlockEditor } from './editor';
import { useIsOnDrag } from './hooks'; import { useIsOnDrag } from './hooks';
import { addNewGroup, appendNewGroup } from './recast-block'; import { addNewGroup, appendNewGroup } from './recast-block';
import { BlockRenderProvider, RenderBlock } from './render-block';
import { SelectionRect, SelectionRef } from './Selection'; import { SelectionRect, SelectionRef } from './Selection';
interface RenderRootProps { interface RenderRootProps {
@@ -24,11 +24,7 @@ interface RenderRootProps {
const MAX_PAGE_WIDTH = 5000; const MAX_PAGE_WIDTH = 5000;
export const MIN_PAGE_WIDTH = 1480; export const MIN_PAGE_WIDTH = 1480;
export const RenderRoot = ({ export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
editor,
editorElement,
children,
}: PropsWithChildren<RenderRootProps>) => {
const selectionRef = useRef<SelectionRef>(null); const selectionRef = useRef<SelectionRef>(null);
const triggeredBySelect = useRef(false); const triggeredBySelect = useRef(false);
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH); const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
@@ -158,7 +154,8 @@ export const RenderRoot = ({
}; };
return ( return (
<EditorProvider value={{ editor, editorElement }}> <EditorProvider editor={editor} editorElement={editorElement}>
<BlockRenderProvider blockRender={RenderBlock}>
<Container <Container
isEdgeless={editor.isEdgeless} isEdgeless={editor.isEdgeless}
ref={ref => { ref={ref => {
@@ -184,13 +181,16 @@ export const RenderRoot = ({
isOnDrag={isOnDrag} isOnDrag={isOnDrag}
> >
<Content style={{ maxWidth: pageWidth + 'px' }}> <Content style={{ maxWidth: pageWidth + 'px' }}>
{children} <RenderBlock blockId={editor.getRootBlockId()} />
</Content> </Content>
{/** TODO: remove selectionManager insert */} {/** TODO: remove selectionManager insert */}
{editor && <SelectionRect ref={selectionRef} editor={editor} />} {editor && (
<SelectionRect ref={selectionRef} editor={editor} />
)}
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />} {editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
{patchedNodes} {patchedNodes}
</Container> </Container>
</BlockRenderProvider>
</EditorProvider> </EditorProvider>
); );
}; };
@@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
); );
return ( return (
<ScrollBlankContainter <ScrollBlankContainer
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onClick={onClick} onClick={onClick}
@@ -283,7 +283,7 @@ const Content = styled('div')({
transitionTimingFunction: 'ease-in', transitionTimingFunction: 'ease-in',
}); });
const ScrollBlankContainter = styled('div')({ const ScrollBlankContainer = styled('div')({
paddingBottom: '30vh', paddingBottom: '30vh',
margin: `0 -${PADDING_X}px`, margin: `0 -${PADDING_X}px`,
}); });
@@ -1,19 +1,19 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import EventEmitter from 'eventemitter3';
import { import {
ReturnEditorBlock,
UpdateEditorBlock,
DefaultColumnsValue, DefaultColumnsValue,
Protocol, Protocol,
ReturnEditorBlock,
UpdateEditorBlock,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { import {
isDev,
createNoopWithMessage, createNoopWithMessage,
lowerFirst, isDev,
last, last,
lowerFirst,
} from '@toeverything/utils'; } from '@toeverything/utils';
import { BlockProvider } from './block-provider'; import EventEmitter from 'eventemitter3';
import { BaseView, BaseView as BlockView } from './../views/base-view'; import { BaseView, BaseView as BlockView } from './../views/base-view';
import { BlockProvider } from './block-provider';
type EventType = 'update'; type EventType = 'update';
export interface EventData { export interface EventData {
@@ -154,6 +154,7 @@ export class AsyncBlock {
} }
this.initialized = true; this.initialized = true;
this.raw_data = await this.filterPageInvalidChildren(this.raw_data); this.raw_data = await this.filterPageInvalidChildren(this.raw_data);
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
const { workspace, id } = this.raw_data; const { workspace, id } = this.raw_data;
this.unobserve = await this.services.observe( this.unobserve = await this.services.observe(
{ workspace, id }, { workspace, id },
@@ -161,6 +162,7 @@ export class AsyncBlock {
const oldData = this.raw_data; const oldData = this.raw_data;
this.raw_data = blockData; this.raw_data = blockData;
this.raw_data = await this.filterPageInvalidChildren(blockData); this.raw_data = await this.filterPageInvalidChildren(blockData);
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
this.emit('update', { block: this, oldData }); this.emit('update', { block: this, oldData });
} }
); );
@@ -495,4 +497,33 @@ export class AsyncBlock {
getBoundingClientRect() { getBoundingClientRect() {
return this.dom?.getBoundingClientRect(); return this.dom?.getBoundingClientRect();
} }
async updateDoubleLinkBlock(rawData: ReturnEditorBlock) {
const values = rawData.properties?.text?.value || [];
for (let i = 0; i < values.length; i++) {
const item = values[i] as any;
if (item.linkType === 'doubleLink') {
const linkBlock = await this.services.load({
workspace: item.workspaceId,
id: item.blockId,
});
let children = linkBlock?.getProperties().text?.value || [];
if (children.length === 1 && !children[0].text) {
children = [{ text: 'Untitled' }];
}
if (
children.map(v => v.text).join('') !==
(item.children || []).map((v: any) => v.text).join('')
) {
const newItem = {
...item,
children: children,
};
values.splice(i, 1, newItem);
}
}
}
return rawData;
}
} }
@@ -24,6 +24,11 @@ type TextUtilsFunctions =
| 'setSearchSlash' | 'setSearchSlash'
| 'removeSearchSlash' | 'removeSearchSlash'
| 'getSearchSlashText' | 'getSearchSlashText'
| 'setDoubleLinkSearchSlash'
| 'getDoubleLinkSearchSlashText'
| 'setSelectDoubleLinkSearchSlash'
| 'removeDoubleLinkSearchSlash'
| 'insertDoubleLink'
| 'selectionToSlateRange' | 'selectionToSlateRange'
| 'transformPoint' | 'transformPoint'
| 'toggleTextFormatBySelection' | 'toggleTextFormatBySelection'
@@ -37,7 +42,6 @@ type TextUtilsFunctions =
| 'getCommentsIdsBySelection' | 'getCommentsIdsBySelection'
| 'getCurrentSelection' | 'getCurrentSelection'
| 'removeSelection' | 'removeSelection'
| 'insertReference'
| 'isCollapsed' | 'isCollapsed'
| 'blur' | 'blur'
| 'setSelection' | 'setSelection'
@@ -257,27 +261,60 @@ export class BlockHelper {
} }
} }
public insertReference( public setDoubleLinkSearchSlash(blockId: string, point: Point) {
reference: string, const textUtils = this._blockTextUtilsMap[blockId];
if (textUtils) {
textUtils.setDoubleLinkSearchSlash(point);
} else {
console.warn('Could find the block text utils');
}
}
public getDoubleLinkSearchSlashText(blockId: string) {
const textUtils = this._blockTextUtilsMap[blockId];
if (textUtils) {
return textUtils.getDoubleLinkSearchSlashText();
}
console.warn('Could find the block text utils');
return '';
}
public setSelectDoubleLinkSearchSlash(blockId: string) {
const textUtils = this._blockTextUtilsMap[blockId];
if (textUtils) {
return textUtils.setSelectDoubleLinkSearchSlash();
}
console.warn('Could find the block text utils');
return '';
}
public removeDoubleLinkSearchSlash(
blockId: string, blockId: string,
selection: Selection, isRemoveSlash?: boolean
offset: number
) { ) {
const text_utils = this._blockTextUtilsMap[blockId]; const textUtils = this._blockTextUtilsMap[blockId];
if (text_utils) { if (textUtils) {
const offsetSelection = window.getSelection(); textUtils.removeDoubleLinkSearchSlash(isRemoveSlash);
offsetSelection.setBaseAndExtent( } else {
selection.anchorNode, console.warn('Could find the block text utils');
selection.anchorOffset, }
selection.focusNode, }
selection.focusOffset + offset
);
text_utils.removeSelection(offsetSelection); public async insertDoubleLink(
text_utils.insertReference(reference); workspaceId: string,
linkBlockId: string,
// range. blockId: string
// text_utils.toggleTextFormatBySelection(format, range); ) {
const textUtils = this._blockTextUtilsMap[blockId];
if (textUtils) {
const linkBlock = await this._editor.getBlock({
workspace: workspaceId,
id: linkBlockId,
});
let children = linkBlock.getProperties().text?.value || [];
if (children.length === 1 && !children[0].text) {
children = [{ text: 'Untitled' }];
}
textUtils.insertDoubleLink(workspaceId, linkBlockId, children);
} }
console.warn('Could find the block text utils'); console.warn('Could find the block text utils');
} }
@@ -1,17 +1,15 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import HotKeys from 'hotkeys-js';
import type { PatchNode } from '@toeverything/components/ui'; import type { PatchNode } from '@toeverything/components/ui';
import { Commands } from '@toeverything/datasource/commands';
import type { import type {
BlockFlavors, BlockFlavors,
ReturnEditorBlock, ReturnEditorBlock,
UpdateEditorBlock, UpdateEditorBlock,
} from '@toeverything/datasource/db-service'; } from '@toeverything/datasource/db-service';
import { services } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service';
import { Commands } from '@toeverything/datasource/commands';
import { domToRect, last, Point, sleep } from '@toeverything/utils'; import { domToRect, last, Point, sleep } from '@toeverything/utils';
import assert from 'assert'; import assert from 'assert';
import HotKeys from 'hotkeys-js';
import type { WorkspaceAndBlockId } from './block'; import type { WorkspaceAndBlockId } from './block';
import { AsyncBlock } from './block'; import { AsyncBlock } from './block';
import { BlockHelper } from './block/block-helper'; import { BlockHelper } from './block/block-helper';
@@ -295,7 +293,7 @@ export class Editor implements Virgo {
return await blockView.onCreate(block); return await blockView.onCreate(block);
} }
private async getBlock({ public async getBlock({
workspace, workspace,
id, id,
}: WorkspaceAndBlockId): Promise<AsyncBlock | null> { }: WorkspaceAndBlockId): Promise<AsyncBlock | null> {
@@ -408,13 +408,13 @@ export class SelectionManager implements VirgoSelection {
/** /**
* *
* get previous activatable blockNode * get previous editable blockNode
* @private * @private
* @param {AsyncBlock} node * @param {AsyncBlock} node
* @return {*} {(Promise<AsyncBlock | null>)} * @return {*} {(Promise<AsyncBlock | null>)}
* @memberof SelectionManager * @memberof SelectionManager
*/ */
private async _getPreviousActivatableBlockNode( private async _getPreviousEditableBlockNode(
node: AsyncBlock node: AsyncBlock
): Promise<AsyncBlock | null> { ): Promise<AsyncBlock | null> {
const preNode = await node.physicallyPerviousSibling(); const preNode = await node.physicallyPerviousSibling();
@@ -422,8 +422,8 @@ export class SelectionManager implements VirgoSelection {
if (!preNode) { if (!preNode) {
return null; return null;
} }
const { activatable, selectable } = this._editor.getView(preNode.type); const { editable, selectable } = this._editor.getView(preNode.type);
if (activatable) { if (editable) {
this.setSelectedNodesIds([]); this.setSelectedNodesIds([]);
return preNode; return preNode;
} }
@@ -432,18 +432,18 @@ export class SelectionManager implements VirgoSelection {
(document.activeElement as HTMLInputElement)?.blur(); (document.activeElement as HTMLInputElement)?.blur();
return null; return null;
} }
return this._getPreviousActivatableBlockNode(preNode); return this._getPreviousEditableBlockNode(preNode);
} }
/** /**
* *
* get next activatable blockNode * get next editable blockNode
* @private * @private
* @param {AsyncBlock} node * @param {AsyncBlock} node
* @return {*} {(Promise<AsyncBlock | null>)} * @return {*} {(Promise<AsyncBlock | null>)}
* @memberof SelectionManager * @memberof SelectionManager
*/ */
private async _getNextActivatableBlockNode( private async _getNextEditableBlockNode(
node: AsyncBlock, node: AsyncBlock,
ignoreSelf = true ignoreSelf = true
): Promise<AsyncBlock | null> { ): Promise<AsyncBlock | null> {
@@ -482,8 +482,8 @@ export class SelectionManager implements VirgoSelection {
} }
} }
const { activatable, selectable } = this._editor.getView(nextNode.type); const { editable, selectable } = this._editor.getView(nextNode.type);
if (activatable) { if (editable) {
this.setSelectedNodesIds([]); this.setSelectedNodesIds([]);
return nextNode; return nextNode;
} }
@@ -492,7 +492,7 @@ export class SelectionManager implements VirgoSelection {
(document.activeElement as HTMLInputElement)?.blur(); (document.activeElement as HTMLInputElement)?.blur();
return null; return null;
} }
return this._getNextActivatableBlockNode(nextNode); return this._getNextEditableBlockNode(nextNode);
} }
/** /**
@@ -513,7 +513,7 @@ export class SelectionManager implements VirgoSelection {
return; return;
} }
} }
preNode = await this._getPreviousActivatableBlockNode(node); preNode = await this._getPreviousEditableBlockNode(node);
if (preNode) { if (preNode) {
this.activeNodeByNodeId(preNode.id, position); this.activeNodeByNodeId(preNode.id, position);
} }
@@ -622,7 +622,7 @@ export class SelectionManager implements VirgoSelection {
} }
} }
nextNode = await this._getNextActivatableBlockNode(node); nextNode = await this._getNextEditableBlockNode(node);
if (nextNode) { if (nextNode) {
this.activeNodeByNodeId(nextNode.id, position); this.activeNodeByNodeId(nextNode.id, position);
} }
@@ -13,7 +13,6 @@ import { HTML2BlockResult } from '../clipboard/types';
export interface CreateView { export interface CreateView {
block: AsyncBlock; block: AsyncBlock;
editor: Editor; editor: Editor;
editorElement: () => JSX.Element;
/** /**
* @deprecated Use recast table instead * @deprecated Use recast table instead
*/ */
@@ -32,10 +31,10 @@ export abstract class BaseView {
abstract type: string; abstract type: string;
/** /**
* activatable means can be focused * editable means can be focused
* @memberof BaseView * @memberof BaseView
*/ */
public activatable = true; public editable = true;
public selectable = true; public selectable = true;
+12 -15
View File
@@ -1,19 +1,16 @@
export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot';
export * from './render-block';
export * from './hooks';
export { RenderBlock } from './render-block';
export * from './recast-block';
export * from './recast-block/types';
export * from './block-pendant'; export * from './block-pendant';
export { useEditor } from './Contexts';
export * from './editor';
export * from './hooks';
export * from './kanban'; export * from './kanban';
export * from './kanban/types'; export * from './kanban/types';
export * from './recast-block';
export * from './recast-block/types';
export {
BlockRenderProvider,
RenderBlockChildren,
useBlockRender,
withTreeViewChildren,
} from './render-block';
export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot';
export * from './utils'; export * from './utils';
export * from './editor';
export { RefPageProvider, useRefPage } from './ref-page';
@@ -10,6 +10,8 @@ import {
RecastMetaProperty, RecastMetaProperty,
RecastPropertyId, RecastPropertyId,
} from '../recast-block/types'; } from '../recast-block/types';
import { BlockRenderProvider } from '../render-block';
import { KanbanBlockRender } from '../render-block/RenderKanbanBlock';
import { useInitKanbanEffect, useRecastKanban } from './kanban'; import { useInitKanbanEffect, useRecastKanban } from './kanban';
import { KanbanGroup } from './types'; import { KanbanGroup } from './types';
@@ -54,9 +56,11 @@ export const KanbanProvider = ({
}; };
return ( return (
<BlockRenderProvider blockRender={KanbanBlockRender}>
<KanbanContext.Provider value={value}> <KanbanContext.Provider value={value}>
{children} {children}
</KanbanContext.Provider> </KanbanContext.Provider>
</BlockRenderProvider>
); );
}; };
@@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service';
import { AsyncBlock } from '../editor'; import { AsyncBlock } from '../editor';
import { ComponentType, createContext, ReactNode, useContext } from 'react'; import { ComponentType, createContext, ReactNode, useContext } from 'react';
import { RecastBlock } from './types'; import { RecastBlock } from './types';
import { RefPageProvider } from '../ref-page';
/** /**
* Determine whether the block supports RecastBlock * Determine whether the block supports RecastBlock
@@ -48,7 +47,7 @@ export const RecastBlockProvider = ({
return ( return (
<RecastBlockContext.Provider value={block}> <RecastBlockContext.Provider value={block}>
<RefPageProvider>{children}</RefPageProvider> {children}
</RecastBlockContext.Provider> </RecastBlockContext.Provider>
); );
}; };
@@ -1 +0,0 @@
export { useRefPage, RefPageProvider } from './ModalPage';
@@ -0,0 +1,35 @@
import { genErrorObj } from '@toeverything/utils';
import { createContext, PropsWithChildren, useContext } from 'react';
import { RenderBlockProps } from './RenderBlock';
type BlockRenderProps = {
blockRender: (args: RenderBlockProps) => JSX.Element | null;
};
export const BlockRenderContext = createContext<BlockRenderProps>(
genErrorObj(
'Failed to get BlockChildrenContext! The context only can use under the "render-root"'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
);
/**
* CAUTION! DO NOT PROVIDE A DYNAMIC BLOCK RENDER!
*/
export const BlockRenderProvider = ({
blockRender,
children,
}: PropsWithChildren<BlockRenderProps>) => {
return (
<BlockRenderContext.Provider value={{ blockRender }}>
{children}
</BlockRenderContext.Provider>
);
};
export const useBlockRender = () => {
const { blockRender } = useContext(BlockRenderContext);
return {
BlockRender: blockRender,
};
};
@@ -4,7 +4,12 @@ import { useCallback, useMemo } from 'react';
import { useEditor } from '../Contexts'; import { useEditor } from '../Contexts';
import { useBlock } from '../hooks'; import { useBlock } from '../hooks';
interface RenderBlockProps { /**
* Render nothing
*/
export const NullBlockRender = (): null => null;
export interface RenderBlockProps {
blockId: string; blockId: string;
hasContainer?: boolean; hasContainer?: boolean;
} }
@@ -13,7 +18,7 @@ export function RenderBlock({
blockId, blockId,
hasContainer = true, hasContainer = true,
}: RenderBlockProps) { }: RenderBlockProps) {
const { editor, editorElement } = useEditor(); const { editor } = useEditor();
const { block } = useBlock(blockId); const { block } = useBlock(blockId);
const setRef = useCallback( const setRef = useCallback(
@@ -29,7 +34,7 @@ export function RenderBlock({
if (block?.type) { if (block?.type) {
return editor.getView(block.type).View; return editor.getView(block.type).View;
} }
return () => null; return (): null => null;
}, [editor, block?.type]); }, [editor, block?.type]);
if (!block) { if (!block) {
@@ -50,7 +55,6 @@ export function RenderBlock({
block={block} block={block}
columns={columns.columns} columns={columns.columns}
columnsFromId={columns.fromId} columnsFromId={columns.fromId}
editorElement={editorElement}
/> />
); );
@@ -65,4 +69,5 @@ export function RenderBlock({
const BlockContainer = styled('div')(({ theme }) => ({ const BlockContainer = styled('div')(({ theme }) => ({
fontSize: theme.typography.body1.fontSize, fontSize: theme.typography.body1.fontSize,
flex: 1,
})); }));
@@ -1,16 +1,39 @@
import { styled } from '@toeverything/components/ui';
import type { AsyncBlock } from '../editor'; import type { AsyncBlock } from '../editor';
import { RenderBlock } from './RenderBlock'; import { useBlockRender } from './Context';
import { NullBlockRender } from './RenderBlock';
interface RenderChildrenProps { export interface RenderChildrenProps {
block: AsyncBlock; block: AsyncBlock;
indent?: boolean;
} }
export const RenderBlockChildren = ({ block }: RenderChildrenProps) => { export const RenderBlockChildren = ({
block,
indent = true,
}: RenderChildrenProps) => {
const { BlockRender } = useBlockRender();
if (BlockRender === NullBlockRender) {
return null;
}
return block.childrenIds.length ? ( return block.childrenIds.length ? (
<> <StyledIdentWrapper indent={indent}>
{block.childrenIds.map(childId => { {block.childrenIds.map(childId => {
return <RenderBlock key={childId} blockId={childId} />; return <BlockRender key={childId} blockId={childId} />;
})} })}
</> </StyledIdentWrapper>
) : null; ) : null;
}; };
/**
* Indent rendering child nodes
*/
const StyledIdentWrapper = styled('div')<{ indent?: boolean }>(
({ indent }) => ({
display: 'flex',
flexDirection: 'column',
// TODO: marginLeft should use theme provided by styled
...(indent && { marginLeft: '30px' }),
})
);
@@ -0,0 +1,56 @@
import { styled } from '@toeverything/components/ui';
import { useBlock } from '../hooks';
import { BlockRenderProvider } from './Context';
import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock';
/**
* Render block without children.
*/
const BlockWithoutChildrenRender = ({ blockId }: RenderBlockProps) => {
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
};
/**
* Render a block, but only one level of children.
*/
const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => {
return (
<BlockRenderProvider blockRender={BlockWithoutChildrenRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
};
export const KanbanBlockRender = ({ blockId }: RenderBlockProps) => {
const { block } = useBlock(blockId);
if (!block) {
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
</BlockRenderProvider>
);
}
return (
<BlockRenderProvider blockRender={NullBlockRender}>
<RenderBlock blockId={blockId} />
{block?.childrenIds.map(childId => (
<StyledBorder key={childId}>
<RenderBlock blockId={childId} />
</StyledBorder>
))}
</BlockRenderProvider>
);
};
const StyledBorder = styled('div')({
border: '1px solid #E0E6EB',
borderRadius: '5px',
margin: '4px',
padding: '0 4px',
});
@@ -1,10 +1,3 @@
import {
BlockPendantProvider,
CreateView,
RenderBlock,
useCurrentView,
useOnSelect,
} from '@toeverything/components/editor-core';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import type { import type {
ComponentPropsWithoutRef, ComponentPropsWithoutRef,
@@ -12,9 +5,10 @@ import type {
CSSProperties, CSSProperties,
ReactElement, ReactElement,
} from 'react'; } from 'react';
import { forwardRef, useState } from 'react'; import { forwardRef } from 'react';
import { SCENE_CONFIG } from '../blocks/group/config'; import { CreateView } from '../editor';
import { BlockContainer } from '../components/BlockContainer'; import { useBlockRender } from './Context';
import { NullBlockRender } from './RenderBlock';
type WithChildrenConfig = { type WithChildrenConfig = {
indent: CSSProperties['marginLeft']; indent: CSSProperties['marginLeft'];
@@ -41,45 +35,6 @@ const TreeView = forwardRef<
); );
}); });
interface ChildrenViewProp {
childrenIds: string[];
handleCollapse: () => void;
indent?: string | number;
}
const ChildrenView = ({
childrenIds,
handleCollapse,
indent,
}: ChildrenViewProp) => {
const [currentView] = useCurrentView();
const isKanbanScene = currentView.type === SCENE_CONFIG.KANBAN;
return (
<Children style={{ ...(!isKanbanScene && { marginLeft: indent }) }}>
{childrenIds.map((childId, idx) => {
if (isKanbanScene) {
return (
<StyledBorder key={childId}>
<RenderBlock blockId={childId} />
</StyledBorder>
);
}
return (
<TreeView
key={childId}
lastItem={idx === childrenIds.length - 1}
onClick={handleCollapse}
>
<RenderBlock key={childId} blockId={childId} />
</TreeView>
);
})}
</Children>
);
};
const CollapsedNode = forwardRef< const CollapsedNode = forwardRef<
HTMLDivElement, HTMLDivElement,
ComponentPropsWithoutRef<'div'> ComponentPropsWithoutRef<'div'>
@@ -104,15 +59,15 @@ export const withTreeViewChildren = (
}; };
return (props: CreateView) => { return (props: CreateView) => {
const { block, editor } = props; const { block } = props;
const { BlockRender } = useBlockRender();
const collapsed = block.getProperty('collapsed')?.value; const collapsed = block.getProperty('collapsed')?.value;
const childrenIds = block.childrenIds; const childrenIds = block.childrenIds;
const showChildren = !collapsed && childrenIds.length > 0; const showChildren =
!collapsed &&
childrenIds.length > 0 &&
BlockRender !== NullBlockRender;
const [isSelect, setIsSelect] = useState<boolean>(false);
useOnSelect(block.id, (isSelect: boolean) => {
setIsSelect(isSelect);
});
const handleCollapse = () => { const handleCollapse = () => {
block.setProperty('collapsed', { value: true }); block.setProperty('collapsed', { value: true });
}; };
@@ -122,15 +77,8 @@ export const withTreeViewChildren = (
}; };
return ( return (
<BlockContainer <>
editor={props.editor} {creator(props)}
block={block}
selected={isSelect}
className={Wrapper.toString()}
>
<BlockPendantProvider block={block}>
<div>{creator(props)}</div>
</BlockPendantProvider>
{collapsed && ( {collapsed && (
<CollapsedNode <CollapsedNode
@@ -138,22 +86,24 @@ export const withTreeViewChildren = (
style={{ marginLeft: config.indent }} style={{ marginLeft: config.indent }}
/> />
)} )}
{showChildren && ( {showChildren &&
<ChildrenView childrenIds.map((childId, idx) => {
childrenIds={childrenIds} return (
handleCollapse={handleCollapse} <TreeView
indent={config.indent} key={childId}
/> lastItem={idx === childrenIds.length - 1}
)} onClick={handleCollapse}
</BlockContainer> style={{ marginLeft: config.indent }}
>
<BlockRender key={childId} blockId={childId} />
</TreeView>
);
})}
</>
); );
}; };
}; };
const Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' });
const Children = Wrapper;
const TREE_COLOR = '#D5DFE6'; const TREE_COLOR = '#D5DFE6';
// adjust left and right margins of the the tree line // adjust left and right margins of the the tree line
const TREE_LINE_LEFT_OFFSET = '-16px'; const TREE_LINE_LEFT_OFFSET = '-16px';
@@ -163,6 +113,7 @@ const TREE_LINE_WIDTH = '12px';
const TreeWrapper = styled('div')({ const TreeWrapper = styled('div')({
position: 'relative', position: 'relative',
display: 'flex',
}); });
const StyledTreeView = styled('div')({ const StyledTreeView = styled('div')({
@@ -228,9 +179,3 @@ const LastItemRadius = styled('div')({
borderRadius: '0 0 0 3px', borderRadius: '0 0 0 3px',
pointerEvents: 'none', pointerEvents: 'none',
}); });
const StyledBorder = styled('div')({
border: '1px solid #E0E6EB',
borderRadius: '5px',
margin: '4px',
});
@@ -1,2 +1,4 @@
export { BlockRenderProvider, useBlockRender } from './Context';
export * from './RenderBlock'; export * from './RenderBlock';
export * from './RenderBlockChildren'; export * from './RenderBlockChildren';
export { withTreeViewChildren } from './WithTreeViewChildren';
+12 -12
View File
@@ -1,15 +1,15 @@
import type { PluginCreator } from '@toeverything/framework/virgo'; import type { PluginCreator } from '@toeverything/framework/virgo';
import {
LeftMenuPlugin,
InlineMenuPlugin,
CommandMenuPlugin,
ReferenceMenuPlugin,
SelectionGroupPlugin,
GroupMenuPlugin,
} from './menu';
import { TemplatePlugin } from './template';
import { FullTextSearchPlugin } from './search';
import { AddCommentPlugin } from './comment'; import { AddCommentPlugin } from './comment';
import {
CommandMenuPlugin,
DoubleLinkMenuPlugin,
GroupMenuPlugin,
InlineMenuPlugin,
LeftMenuPlugin,
SelectionGroupPlugin,
} from './menu';
import { FullTextSearchPlugin } from './search';
import { TemplatePlugin } from './template';
// import { PlaceholderPlugin } from './placeholder'; // import { PlaceholderPlugin } from './placeholder';
// import { BlockPropertyPlugin } from './block-property'; // import { BlockPropertyPlugin } from './block-property';
@@ -19,9 +19,9 @@ export const plugins: PluginCreator[] = [
LeftMenuPlugin, LeftMenuPlugin,
InlineMenuPlugin, InlineMenuPlugin,
CommandMenuPlugin, CommandMenuPlugin,
ReferenceMenuPlugin, DoubleLinkMenuPlugin,
TemplatePlugin, TemplatePlugin,
SelectionGroupPlugin, // SelectionGroupPlugin,
AddCommentPlugin, AddCommentPlugin,
GroupMenuPlugin, GroupMenuPlugin,
]; ];
@@ -0,0 +1,121 @@
import { CommonList, CommonListItem } from '@toeverything/components/common';
import { styled } from '@toeverything/components/ui';
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export type DoubleLinkMenuContainerProps = {
editor: Virgo;
hooks: PluginHooks;
style?: React.CSSProperties;
blockId: string;
onSelected?: (item: string) => void;
onClose?: () => void;
types?: Array<string>;
items?: CommonListItem[];
};
export const DoubleLinkMenuContainer = (
props: DoubleLinkMenuContainerProps
) => {
const { hooks, onSelected, onClose, types, style, items } = props;
const menuRef = useRef<HTMLDivElement>(null);
const [currentItem, setCurrentItem] = useState<string | undefined>();
useEffect(() => {
if (types && !currentItem) {
setCurrentItem(types[0]);
}
}, [currentItem, onClose, types]);
const handleUpDownKey = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) {
event.preventDefault();
const isUpkey = event.code === 'ArrowUp';
if (!currentItem && types.length) {
setCurrentItem(types[isUpkey ? types.length - 1 : 0]);
}
if (currentItem) {
const idx = types.indexOf(currentItem);
if (isUpkey ? idx > 0 : idx < types.length - 1) {
setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]);
}
}
}
},
[currentItem, types]
);
const handleClickUp = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handleUpDownKey(event);
},
[handleUpDownKey]
);
const handleClickDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handleUpDownKey(event);
},
[handleUpDownKey]
);
const handleClickEnter = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Enter' && currentItem) {
event.preventDefault();
onSelected && onSelected(currentItem);
}
},
[currentItem, onSelected]
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handleClickUp(event);
handleClickDown(event);
handleClickEnter(event);
},
[handleClickUp, handleClickDown, handleClickEnter]
);
useEffect(() => {
const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handleKeyDown);
return () => {
sub.unsubscribe();
};
}, [hooks, handleKeyDown]);
return (
<RootContainer
ref={menuRef}
onKeyDownCapture={handleKeyDown}
style={style}
>
<ContentContainer>
<CommonList
items={items}
onSelected={type => onSelected?.(type)}
currentItem={currentItem}
setCurrentItem={setCurrentItem}
/>
</ContentContainer>
</RootContainer>
);
};
const RootContainer = styled('div')(({ theme }) => ({
zIndex: 1,
borderRadius: '10px',
boxShadow: theme.affine.shadows.shadow1,
backgroundColor: '#fff',
padding: '8px 4px',
}));
const ContentContainer = styled('div')(({ theme }) => ({
display: 'flex',
overflow: 'hidden',
maxHeight: '300px',
}));
@@ -0,0 +1,376 @@
import { CommonListItem } from '@toeverything/components/common';
import { AddIcon } from '@toeverything/components/icons';
import {
Input,
ListButton,
MuiClickAwayListener,
MuiGrow as Grow,
MuiPaper as Paper,
MuiPopper as Popper,
styled,
} from '@toeverything/components/ui';
import { services } from '@toeverything/datasource/db-service';
import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo';
import React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useParams } from 'react-router-dom';
import { QueryBlocks, QueryResult } from '../../search';
import { DoubleLinkMenuContainer } from './Container';
const ADD_NEW_SUB_PAGE = 'AddNewSubPage';
const ADD_NEW_PAGE = 'AddNewPage';
const ARRAY_CODES = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'];
export type DoubleLinkMenuProps = {
editor: Virgo;
hooks: PluginHooks;
style?: { left: number; top: number };
};
type DoubleLinkMenuStyle = {
left: number;
top: number;
height: number;
};
export const DoubleLinkMenu = ({
editor,
hooks,
style,
}: DoubleLinkMenuProps) => {
const { page_id: curPageId } = useParams();
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const dialogRef = useRef<HTMLDivElement>();
const newPageSearchRef = useRef<HTMLInputElement>();
const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] =
useState<DoubleLinkMenuStyle>({
left: 0,
top: 0,
height: 0,
});
const [curBlockId, setCurBlockId] = useState<string>();
const [searchText, setSearchText] = useState<string>();
const [inAddNewPage, setInAddNewPage] = useState(false);
const [filterText, setFilterText] = useState<string>('');
const [searchResultBlocks, setSearchResultBlocks] = useState<QueryResult>(
[]
);
const menuTypes = useMemo(() => {
return Object.values(searchResultBlocks)
.map(({ id }) => id)
.concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]);
}, [searchResultBlocks]);
const menuItems: CommonListItem[] = useMemo(() => {
const items: CommonListItem[] = [];
if (searchResultBlocks?.length > 0) {
items.push({
renderCustom: () => {
return (
<ListButton
content={
inAddNewPage ? 'SUGGESTED' : 'LINK TO PAGE'
}
/>
);
},
});
items.push(
...(searchResultBlocks?.map(
block =>
({
block: {
...block,
content: block.content || 'Untitled',
},
} as CommonListItem)
) || [])
);
}
if (items.length > 0) {
items.push({ divider: 'newPage' });
}
items.push({
content: {
id: ADD_NEW_SUB_PAGE,
content: 'Add new sub-page',
icon: AddIcon,
},
});
!inAddNewPage &&
items.push({
content: {
id: ADD_NEW_PAGE,
content: 'Add new page in...',
icon: AddIcon,
},
});
return items;
}, [searchResultBlocks, inAddNewPage]);
useEffect(() => {
const text = inAddNewPage ? filterText : searchText;
QueryBlocks(editor, text, result => {
if (!inAddNewPage) {
result = result.filter(item => item.id !== curPageId);
}
setSearchResultBlocks(result);
});
}, [editor, searchText, filterText, inAddNewPage, curPageId]);
const hideMenu = useCallback(() => {
setIsOpen(false);
setInAddNewPage(false);
editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId);
editor.scrollManager.unLock();
}, [curBlockId, editor]);
const resetState = useCallback(
(preNodeId: string, nextNodeId: string) => {
editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId);
setCurBlockId(nextNodeId);
setSearchText('');
setIsOpen(true);
editor.scrollManager.lock();
const clientRect =
editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0)
?.getBoundingClientRect();
if (clientRect) {
const rectTop = clientRect.top;
const { top, left } = editor.container.getBoundingClientRect();
setDoubleLinkMenuStyle({
top: rectTop - top,
left: clientRect.left - left,
height: clientRect.height,
});
setAnchorEl(dialogRef.current);
}
const textSelection = editor.blockHelper.selectionToSlateRange(
nextNodeId,
editor.selection.currentSelectInfo?.browserSelection
);
if (textSelection) {
const { anchor } = textSelection;
editor.blockHelper.setDoubleLinkSearchSlash(nextNodeId, anchor);
}
},
[editor]
);
const searchChange = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (ARRAY_CODES.includes(event.code)) {
return;
}
if (event.code === 'Backspace') {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (!searchText || searchText === '[[') {
hideMenu();
event.preventDefault();
event.stopPropagation();
return;
}
}
const { type, anchorNode } = editor.selection.currentSelectInfo;
if (
!isOpen ||
(type === 'Range' &&
anchorNode &&
anchorNode.id !== curBlockId &&
editor.blockHelper.isSelectionCollapsed(anchorNode.id))
) {
const text = editor.blockHelper.getBlockTextBeforeSelection(
anchorNode.id
);
if (text.endsWith('[[')) {
resetState(curBlockId, anchorNode.id);
}
}
if (isOpen) {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (searchText && searchText.startsWith('[[')) {
setSearchText(searchText.slice(2).trim());
}
}
},
[editor, isOpen, curBlockId, hideMenu]
);
const handleKeyup = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => searchChange(event),
[searchChange]
);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isOpen) {
return;
}
if (event.code === 'Escape') {
hideMenu();
}
if (event.code === 'Backspace') {
const searchText =
editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId);
if (!searchText || searchText === '[[') {
event.preventDefault();
event.stopPropagation();
return;
}
}
},
[hideMenu, editor, isOpen, curBlockId]
);
useEffect(() => {
const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYUP)
.subscribe(handleKeyup);
sub.add(
hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handleKeyDown)
);
return () => {
sub.unsubscribe();
};
}, [handleKeyup, handleKeyDown, hooks]);
const insertDoubleLink = useCallback(
async (pageId: string) => {
editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId);
await editor.blockHelper.insertDoubleLink(
editor.workspace,
pageId,
curBlockId
);
hideMenu();
},
[editor, curBlockId, hideMenu]
);
const addSubPage = useCallback(
async (parentPageId: string) => {
const newPage = await services.api.editorBlock.create({
workspace: editor.workspace,
type: 'page' as const,
});
services.api.editorBlock.update({
id: newPage.id,
workspace: editor.workspace,
properties: {
text: { value: [{ text: searchText }] },
},
});
await services.api.pageTree.addChildPageToWorkspace(
editor.workspace,
parentPageId,
newPage.id
);
return newPage.id;
},
[searchText, editor]
);
const handleSelected = async (id: string) => {
if (curBlockId) {
if (id === ADD_NEW_PAGE) {
setInAddNewPage(true);
setTimeout(() => {
newPageSearchRef.current?.focus();
});
return;
}
if (id === ADD_NEW_SUB_PAGE) {
const pageId = await addSubPage(curPageId);
insertDoubleLink(pageId);
return;
}
if (inAddNewPage) {
const pageId = await addSubPage(id);
insertDoubleLink(pageId);
} else {
insertDoubleLink(id);
}
}
};
const handleFilterChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
await setFilterText(text);
},
[]
);
return (
<div
ref={dialogRef}
style={{
position: 'absolute',
width: '10px',
...doubleLinkMenuStyle,
}}
>
<MuiClickAwayListener onClickAway={() => hideMenu()}>
<Popper
open={isOpen}
anchorEl={anchorEl}
transition
placement="bottom-start"
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: 'left bottom',
}}
>
<Paper>
{inAddNewPage && (
<NewPageSearchContainer>
<Input
ref={newPageSearchRef}
value={filterText}
onChange={handleFilterChange}
placeholder="Search page to add to..."
/>
</NewPageSearchContainer>
)}
<DoubleLinkMenuContainer
editor={editor}
hooks={hooks}
style={style}
blockId={curBlockId}
onSelected={handleSelected}
onClose={hideMenu}
items={menuItems}
types={menuTypes}
/>
</Paper>
</Grow>
)}
</Popper>
</MuiClickAwayListener>
</div>
);
};
const NewPageSearchContainer = styled('div')({
padding: '8px 8px 0px 8px',
});
@@ -1,12 +1,11 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin'; import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils'; import { PluginRenderRoot } from '../../utils';
import { ReferenceMenu } from './ReferenceMenu'; import { DoubleLinkMenu } from './DoubleLinkMenu';
const PLUGIN_NAME = 'reference-menu'; const PLUGIN_NAME = 'reference-menu';
export class ReferenceMenuPlugin extends BasePlugin { export class DoubleLinkMenuPlugin extends BasePlugin {
private _root?: PluginRenderRoot; private _root?: PluginRenderRoot;
public static override get pluginName(): string { public static override get pluginName(): string {
@@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin {
this._root?.render( this._root?.render(
<StrictMode> <StrictMode>
<ReferenceMenu editor={this.editor} hooks={this.hooks} /> <DoubleLinkMenu editor={this.editor} hooks={this.hooks} />
</StrictMode> </StrictMode>
); );
} }
@@ -0,0 +1 @@
export { DoubleLinkMenuPlugin } from './Plugin';
@@ -1,9 +1,7 @@
export { CommandMenuPlugin } from './command-menu';
export { DoubleLinkMenuPlugin } from './double-link-menu';
export { GroupMenuPlugin } from './group-menu';
export { InlineMenuPlugin } from './inline-menu'; export { InlineMenuPlugin } from './inline-menu';
export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin'; export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin';
export { CommandMenuPlugin } from './command-menu';
export { ReferenceMenuPlugin } from './reference-menu';
export { SelectionGroupPlugin } from './selection-group-menu';
export { MENU_WIDTH as menuWidth } from './left-menu/menu-config'; export { MENU_WIDTH as menuWidth } from './left-menu/menu-config';
export { SelectionGroupPlugin } from './selection-group-menu';
export { GroupMenuPlugin } from './group-menu';
@@ -1,190 +0,0 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo';
import {
CommonList,
CommonListItem,
commonListContainer,
} from '@toeverything/components/common';
import { domToRect } from '@toeverything/utils';
import { styled } from '@toeverything/components/ui';
import { QueryResult } from '../../search';
export type ReferenceMenuContainerProps = {
editor: Virgo;
hooks: PluginHooks;
style?: React.CSSProperties;
isShow?: boolean;
blockId: string;
onSelected?: (item: string) => void;
onClose?: () => void;
searchBlocks?: QueryResult;
types?: Array<string>;
};
export const ReferenceMenuContainer = ({
hooks,
isShow = false,
onSelected,
onClose,
types,
searchBlocks,
style,
}: ReferenceMenuContainerProps) => {
const menu_ref = useRef<HTMLDivElement>(null);
const [current_item, set_current_item] = useState<string | undefined>();
const [need_check_into_view, set_need_check_into_view] =
useState<boolean>(false);
useEffect(() => {
if (need_check_into_view) {
if (current_item && menu_ref.current) {
const item_ele =
menu_ref.current.querySelector<HTMLButtonElement>(
`.item-${current_item}`
);
const scroll_ele =
menu_ref.current.querySelector<HTMLButtonElement>(
`.${commonListContainer}`
);
if (item_ele) {
const itemRect = domToRect(item_ele);
const scrollRect = domToRect(scroll_ele);
if (
itemRect.top < scrollRect.top ||
itemRect.bottom > scrollRect.bottom
) {
// IMP: may be do it with self function
item_ele.scrollIntoView({
block: 'nearest',
});
}
}
}
set_need_check_into_view(false);
}
}, [need_check_into_view, current_item]);
useEffect(() => {
if (isShow && types && !current_item) set_current_item(types[0]);
if (!isShow) onClose?.();
}, [current_item, isShow, onClose, types]);
useEffect(() => {
if (isShow && types) {
if (!types.includes(current_item)) {
set_need_check_into_view(true);
if (types.length) {
set_current_item(types[0]);
} else {
set_current_item(undefined);
}
}
}
}, [isShow, types, current_item]);
const handle_click_up = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && types && event.code === 'ArrowUp') {
event.preventDefault();
if (!current_item && types.length) {
set_current_item(types[types.length - 1]);
}
if (current_item) {
const idx = types.indexOf(current_item);
if (idx > 0) {
set_need_check_into_view(true);
set_current_item(types[idx - 1]);
}
}
}
},
[isShow, types, current_item]
);
const handle_click_down = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && types && event.code === 'ArrowDown') {
event.preventDefault();
if (!current_item && types.length) {
set_current_item(types[0]);
}
if (current_item) {
const idx = types.indexOf(current_item);
if (idx < types.length - 1) {
set_need_check_into_view(true);
set_current_item(types[idx + 1]);
}
}
}
},
[isShow, types, current_item]
);
const handle_click_enter = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
if (isShow && event.code === 'Enter' && current_item) {
event.preventDefault();
onSelected && onSelected(current_item);
}
},
[isShow, current_item, onSelected]
);
const handle_key_down = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
handle_click_up(event);
handle_click_down(event);
handle_click_enter(event);
},
[handle_click_up, handle_click_down, handle_click_enter]
);
useEffect(() => {
const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down);
return () => {
sub.unsubscribe();
};
}, [hooks, handle_key_down]);
return isShow ? (
<RootContainer
ref={menu_ref}
onKeyDownCapture={handle_key_down}
style={style}
>
<ContentContainer>
<CommonList
items={
searchBlocks?.map(
block => ({ block } as CommonListItem)
) || []
}
onSelected={type => onSelected?.(type)}
currentItem={current_item}
setCurrentItem={set_current_item}
/>
</ContentContainer>
</RootContainer>
) : null;
};
const RootContainer = styled('div')(({ theme }) => ({
position: 'fixed',
zIndex: 1,
maxHeight: '525px',
borderRadius: '10px',
boxShadow: theme.affine.shadows.shadow1,
backgroundColor: '#fff',
padding: '8px 4px',
}));
const ContentContainer = styled('div')(({ theme }) => ({
display: 'flex',
overflow: 'hidden',
maxHeight: '493px',
}));
@@ -1,148 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { MuiClickAwayListener, styled } from '@toeverything/components/ui';
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
import { Point } from '@toeverything/utils';
import { ReferenceMenuContainer } from './Container';
import { QueryBlocks, QueryResult } from '../../search';
export type ReferenceMenuProps = {
editor: Virgo;
hooks: PluginHooks;
style?: { left: number; top: number };
};
export type RefLinkComponent = {
type: 'reflink';
reference: string;
};
const BEFORE_REGEX = /\[\[(.*)$/;
export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
const [is_show, set_is_show] = useState(false);
const [block_id, set_block_id] = useState<string>();
const [position, set_position] = useState<Point>(new Point(0, 0));
const [search_text, set_search_text] = useState<string>('');
const [search_blocks, set_search_blocks] = useState<QueryResult>([]);
useEffect(() => {
QueryBlocks(editor, search_text, result => set_search_blocks(result));
}, [editor, search_text]);
const search_block_ids = useMemo(
() => Object.values(search_blocks).map(({ id }) => id),
[search_blocks]
);
const handle_search = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => {
const { type, anchorNode } = editor.selection.currentSelectInfo;
if (
type === 'Range' &&
anchorNode &&
editor.blockHelper.isSelectionCollapsed(anchorNode.id)
) {
const text = editor.blockHelper.getBlockTextBeforeSelection(
anchorNode.id
);
const matched = BEFORE_REGEX.exec(text)?.[1];
if (typeof matched === 'string') {
if (event.key === '[') set_is_show(true);
set_block_id(anchorNode.id);
set_search_text(matched);
const rect =
editor.selection.currentSelectInfo?.browserSelection
?.getRangeAt(0)
?.getBoundingClientRect();
if (rect) {
set_position(new Point(rect.left, rect.top + 24));
}
} else if (is_show) {
set_is_show(false);
}
}
},
[editor, is_show]
);
const handle_keyup = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => handle_search(event),
[handle_search]
);
const handle_key_down = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Escape') {
set_is_show(false);
}
},
[]
);
useEffect(() => {
const sub = hooks
.get(HookType.ON_ROOT_NODE_KEYUP)
.subscribe(handle_keyup);
sub.add(
hooks
.get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE)
.subscribe(handle_key_down)
);
return () => {
sub.unsubscribe();
};
}, [handle_keyup, handle_key_down, hooks]);
const handle_selected = async (reference: string) => {
if (block_id) {
const { anchorNode } = editor.selection.currentSelectInfo;
editor.blockHelper.insertReference(
reference,
anchorNode.id,
editor.selection.currentSelectInfo?.browserSelection,
-search_text.length - 2
);
}
set_is_show(false);
};
const handle_close = () => {
block_id && editor.blockHelper.removeSearchSlash(block_id);
};
return (
<ReferenceMenuWrapper
style={{ top: position.y, left: position.x }}
onKeyUp={handle_keyup}
>
<MuiClickAwayListener onClickAway={() => set_is_show(false)}>
<div>
<ReferenceMenuContainer
editor={editor}
hooks={hooks}
style={style}
isShow={is_show && !!search_text}
blockId={block_id}
onSelected={handle_selected}
onClose={handle_close}
searchBlocks={search_blocks}
types={search_block_ids}
/>
</div>
</MuiClickAwayListener>
</ReferenceMenuWrapper>
);
};
const ReferenceMenuWrapper = styled('div')({
position: 'absolute',
zIndex: 1,
});
@@ -1 +0,0 @@
export { ReferenceMenuPlugin } from './Plugin';
@@ -1,16 +1,15 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import style9 from 'style9';
import { BlockPreview } from '@toeverything/components/common'; import { BlockPreview } from '@toeverything/components/common';
import { import {
TransitionsModal,
MuiBox as Box, MuiBox as Box,
MuiBox, MuiBox,
styled, styled,
TransitionsModal,
} from '@toeverything/components/ui'; } from '@toeverything/components/ui';
import { Virgo, BlockEditor } from '@toeverything/framework/virgo'; import { BlockEditor, Virgo } from '@toeverything/framework/virgo';
import { throttle } from '@toeverything/utils'; import { throttle } from '@toeverything/utils';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router';
import style9 from 'style9';
const styles = style9.create({ const styles = style9.create({
wrapper: { wrapper: {
@@ -37,9 +36,7 @@ const query_blocks = (
search: string, search: string,
callback: (result: QueryResult) => void callback: (result: QueryResult) => void
) => { ) => {
(editor as BlockEditor) (editor as BlockEditor).search(search).then(pages => callback(pages));
.search(search)
.then(pages => callback(pages.filter(b => !!b.content)));
}; };
export const QueryBlocks = throttle(query_blocks, 500); export const QueryBlocks = throttle(query_blocks, 500);
+1 -2
View File
@@ -1,6 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import style9 from 'style9'; import style9 from 'style9';
import { SvgIconProps } from '../svg-icon'; import { SvgIconProps } from '../svg-icon';
import { BaseButton } from './base-button'; import { BaseButton } from './base-button';
@@ -35,7 +34,7 @@ const styles = style9.create({
type ListButtonProps = { type ListButtonProps = {
className?: string; className?: string;
onClick: () => void; onClick?: () => void;
onMouseOver?: () => void; onMouseOver?: () => void;
content?: string; content?: string;
children?: () => JSX.Element; children?: () => JSX.Element;
+1 -1
View File
@@ -1,5 +1,5 @@
import { ArrowRightIcon } from '@toeverything/components/icons'; import { ArrowRightIcon } from '@toeverything/components/icons';
import { ReactElement, useRef, useState } from 'react'; import { useRef, useState, type ReactElement } from 'react';
import { Divider } from '../divider'; import { Divider } from '../divider';
import { import {
MuiGrow as Grow, MuiGrow as Grow,
+8 -1
View File
@@ -35,6 +35,8 @@ export const Popper = ({
offset = [0, 5], offset = [0, 5],
showArrow = false, showArrow = false,
popperHandlerRef, popperHandlerRef,
onClick,
onClickAway,
...popperProps ...popperProps
}: PopperProps) => { }: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null); const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
@@ -97,15 +99,20 @@ export const Popper = ({
return ( return (
<ClickAwayListener <ClickAwayListener
onClickAway={() => { onClickAway={() => {
if (visibleControlledByParent) {
onClickAway?.();
} else {
setVisible(false); setVisible(false);
}
}} }}
> >
<Container> <Container>
{isAnchorCustom ? null : ( {isAnchorCustom ? null : (
<div <div
ref={(dom: HTMLDivElement) => setAnchorEl(dom)} ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
onClick={() => { onClick={e => {
if (!hasClickTrigger || visibleControlledByParent) { if (!hasClickTrigger || visibleControlledByParent) {
onClick?.(e);
return; return;
} }
setVisible(!visible); setVisible(!visible);
@@ -62,4 +62,6 @@ export type PopperProps = {
showArrow?: boolean; showArrow?: boolean;
popperHandlerRef?: Ref<PopperHandler>; popperHandlerRef?: Ref<PopperHandler>;
onClickAway?: () => void;
} & Omit<PopperUnstyledProps, 'open' | 'ref'>; } & Omit<PopperUnstyledProps, 'open' | 'ref'>;
+4 -1
View File
@@ -233,7 +233,10 @@ export const Theme = {
}, },
shadows: { shadows: {
none: 'none', none: 'none',
shadow1: '0px 1px 5px rgba(152, 172, 189, 0.2)', shadow1:
'0px 1px 10px -6px rgba(24, 39, 75, 0.08), 0px 3px 16px -6px rgba(24, 39, 75, 0.04)',
shadow2:
'0px 6px 16px -8px rgba(0,0,0,0.08), 0px 9px 14px 0px rgba(0,0,0,0.05), 0px 12px 24px 16px rgba(0,0,0,0.03)',
}, },
border: ['none'], border: ['none'],
spacing: { spacing: {
@@ -168,6 +168,7 @@ export class Database {
block.on('children', observer_name, listener); block.on('children', observer_name, listener);
block.on('content', observer_name, listener); block.on('content', observer_name, listener);
block.on('parent', observer_name, listener); block.on('parent', observer_name, listener);
block.on('cascade', observer_name, listener);
} }
this.#observers.setStatus(observer_name, 'observing'); this.#observers.setStatus(observer_name, 'observing');
@@ -188,6 +189,7 @@ export class Database {
block.off('children', observer_name); block.off('children', observer_name);
block.off('content', observer_name); block.off('content', observer_name);
block.off('parent', observer_name); block.off('parent', observer_name);
block.off('cascade', observer_name);
} }
} }
} }
@@ -29,7 +29,7 @@ const groupTemplateMap = {
const defaultTemplateList: Array<TemplateMeta> = [ const defaultTemplateList: Array<TemplateMeta> = [
{ {
name: 'New From Quick Start', name: 'New From Quick Start',
groupKeys: ['todolist'], groupKeys: ['getStartedGroup0', 'getStartedGroup1', 'getStartedGroup2'],
}, },
{ name: 'New From Grid System', groupKeys: ['grid'] }, { name: 'New From Grid System', groupKeys: ['grid'] },
{ name: 'New From Blog', groupKeys: ['blog'] }, { name: 'New From Blog', groupKeys: ['blog'] },
@@ -53,6 +53,7 @@ export type DefaultColumnsValue = {
filterConstraint: FilterConstraint; filterConstraint: FilterConstraint;
filterWeakSqlConstraint: string; filterWeakSqlConstraint: string;
sorterConstraint: SorterConstraint; sorterConstraint: SorterConstraint;
bindings: StringColumnValue;
}; };
export const DEFAULT_COLUMN_KEYS = { export const DEFAULT_COLUMN_KEYS = {
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more