mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 15:46:29 +08:00
Merge remote-tracking branch 'origin/develop' into fix/experience
This commit is contained in:
@@ -266,6 +266,43 @@
|
||||
"contributions": [
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
FROM node:16-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
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 g++ make python3 git libpng-dev
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
FROM node:16-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
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 g++ make python3 git libpng-dev
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
<b>
|
||||
<a href="https://affine.pro">AFFiNE.PRO</a><br>
|
||||
</b>
|
||||
The Next-Gen Knowledge Base to Replace Notion & Miro.
|
||||
The Next-Gen Collaborative Knowledge Base
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
Planning, Sorting and Creating all Together. Open-source, Privacy-First, and Free to use.
|
||||
Open-source and privacy-first. <br />
|
||||
A free replacement for Notion & Miro.
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
@@ -18,13 +19,14 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
-->
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-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 -->
|
||||
|
||||
[](https://affine.pro)
|
||||
[](https://livedemo.affine.pro)
|
||||
[](https://github.com/toeverything/AFFiNE)
|
||||
[![All Contributors][all-contributors-badge]](#contributors)
|
||||
<br/>
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://reactjs.org/)
|
||||
@@ -35,11 +37,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
<p align="center">
|
||||
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
|
||||
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
<a href="https://discord.com/invite/yz6tGVsf5p"><img src="https://img.shields.io/badge/-Discord-424549?style=social&logo=discord" height=25></a>
|
||||
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
<a href="https://t.me/affineworkos"><img src="https://img.shields.io/badge/-Telegram-red?style=social&logo=telegram" height=25></a>
|
||||
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
<a href="https://twitter.com/AffineOfficial"><img src="https://img.shields.io/badge/-Twitter-red?style=social&logo=twitter" height=25></a>
|
||||
|
||||
<a href="https://medium.com/@affineworkos"><img src="https://img.shields.io/badge/-Medium-red?style=social&logo=medium" height=25></a>
|
||||
</p>
|
||||
@@ -48,96 +50,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/>
|
||||
|
||||
# Stay Up-to-Date and Support Us
|
||||
# :star: Support Us and Keep Updated :star:
|
||||
|
||||

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

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

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

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

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

|
||||
|
||||
# Roadmap
|
||||
### Sort your knowledge
|
||||
|
||||
Coming Soon...
|
||||

|
||||
|
||||
# Releases
|
||||
# Useful Links
|
||||
|
||||
Get our latest [release notes](https://github.com/toeverything/AFFiNE/wiki) from here.
|
||||
- [AFFiNE Documentation](https://docs.affine.pro/affine/) - More detailed documentation on how to use and develop with AFFiNE
|
||||
|
||||
# Feature requests
|
||||
- [Feature Roadmap](https://github.com/toeverything/AFFiNE/issues/293) - Looking for a feature? It might already be planned for release - you can check here
|
||||
- [Release Notes](https://github.com/toeverything/AFFiNE/wiki) - Find out what changes we are making and how we are improving AFFiNE
|
||||
|
||||
Please go to [feature requests](https://github.com/toeverything/AFFiNE/issues).
|
||||
- [Contributing Guide](/docs/CONTRIBUTING.md) - Want to help improve AFFiNE? You might not even need to write a line of code. Find out how you can contribute.
|
||||
- [Code of Conduct](/docs/CODE_OF_CONDUCT.md) - How we promote and maintain a harassment-free experience for everyone in our community.
|
||||
|
||||
# FAQ
|
||||
- AFFiNE Communities: [Discord](https://discord.gg/yz6tGVsf5p) | [Telegram](https://t.me/affineworkos) | [Twitter](https://twitter.com/AffineOfficial) |
|
||||
[Medium](https://medium.com/@affineworkos) | [AFFiNE Blog](https://blog.affine.pro)
|
||||
|
||||
Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors.
|
||||
# Contact Us
|
||||
|
||||
Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/).
|
||||
Feel free to send us an email: contact@toeverything.info
|
||||
|
||||
# The Philosophy of AFFiNE
|
||||
|
||||
@@ -160,7 +119,7 @@ That's why we are making AFFiNE. Some of the most important features are:
|
||||
- Data is always stored locally by default
|
||||
- CRDTs are applied so that peer-to-peer collaboration is possible.
|
||||
|
||||
We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth.
|
||||
We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a single source of truth.
|
||||
|
||||
We would like to give special thanks to the innovators and pioneers who greatly inspired us:
|
||||
|
||||
@@ -182,12 +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.
|
||||
|
||||
# Community
|
||||
|
||||
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
||||
|
||||
[Discuss AFFiNE on GitHub](https://github.com/toeverything/AFFiNE/discussions)
|
||||
|
||||
# Contributors
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
@@ -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/githbq"><img src="https://avatars.githubusercontent.com/u/10009709?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Bob Hu</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=githbq" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://quavo.vercel.app/"><img src="https://avatars.githubusercontent.com/u/67266933?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Quavo</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=lucky-chap" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/LuciNyan"><img src="https://avatars.githubusercontent.com/u/22126563?v=4?s=50" width="50px;" alt=""/><br /><sub><b>子瞻 Luci</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=LuciNyan" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="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>
|
||||
</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.
|
||||
|
||||
See LICENSE for details.
|
||||
See [LICENSE](/LICENSE) for details.
|
||||
|
||||
@@ -175,7 +175,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({
|
||||
width: 300,
|
||||
minWidth: 300,
|
||||
borderRadius: '0px 10px 10px 0px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
boxShadow: theme.affine.shadows.shadow2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
transitionProperty: 'left',
|
||||
transitionDuration: '0.35s',
|
||||
|
||||
@@ -7,16 +7,18 @@
|
||||
"keywords": [],
|
||||
"author": "DarkSky <darksky2048@gmail.com>",
|
||||
"dependencies": {
|
||||
"@mui/joy": "^5.0.0-alpha.39",
|
||||
"@emotion/react": "^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": {
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"image-minimizer-webpack-plugin": "^3.2.3",
|
||||
"imagemin": "^8.0.1",
|
||||
"imagemin-optipng": "^8.0.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"webpack": "^5.74.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "加入我们的社区"
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,15 @@ import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import RedditIcon from '@mui/icons-material/Reddit';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import { Box, Button, Container, Grid, SvgIcon, Typography } from '@mui/joy';
|
||||
import Option from '@mui/joy/Option';
|
||||
import Select from '@mui/joy/Select';
|
||||
import { CssVarsProvider, styled } from '@mui/joy/styles';
|
||||
import { LogoIcon } from '@toeverything/components/icons';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useMediaQuery } from '@mui/material';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CollaborationImage from './collaboration.png';
|
||||
import { options } from './i18n';
|
||||
import LogoImage from './logo.png';
|
||||
import PageImage from './page.png';
|
||||
import ShapeImage from './shape.png';
|
||||
@@ -199,6 +202,7 @@ const AffineImage = styled('img')({
|
||||
|
||||
const GitHub = (props: { center?: boolean; flat?: boolean }) => {
|
||||
const matches = useMediaQuery('(max-width: 1024px)');
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -234,14 +238,14 @@ const GitHub = (props: { center?: boolean; flat?: boolean }) => {
|
||||
startIcon={<GitHubIcon />}
|
||||
size="lg"
|
||||
>
|
||||
{props.center ? 'Check ' : ''}GitHub
|
||||
{props.center ? t('Check GitHub') : t('GitHub')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => {
|
||||
const matches = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -277,14 +281,18 @@ const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => {
|
||||
startIcon={<LogoIcon />}
|
||||
size="lg"
|
||||
>
|
||||
Try it Online
|
||||
{t('Try it Online')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export function App() {
|
||||
const matches = useMediaQuery('(max-width: 1024px)');
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const changeLanguage = (event: any) => {
|
||||
i18n.changeLanguage(event);
|
||||
};
|
||||
return (
|
||||
<CssVarsProvider>
|
||||
<VenusContainer
|
||||
@@ -342,6 +350,13 @@ export function App() {
|
||||
>
|
||||
Blog
|
||||
</Button>
|
||||
<Select defaultValue="en" onChange={changeLanguage}>
|
||||
{options.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
@@ -369,7 +384,7 @@ export function App() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Open Source,
|
||||
{t('Open Source')},
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="96px"
|
||||
@@ -380,7 +395,7 @@ export function App() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Privacy First
|
||||
{t('Privacy First')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -413,7 +428,7 @@ export function App() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Alternative
|
||||
{t('Alternative')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -432,8 +447,7 @@ export function App() {
|
||||
fontWeight={'400'}
|
||||
sx={{ color: '#888' }}
|
||||
>
|
||||
Affine is the next-generation collaborative
|
||||
knowledge base for professionals.
|
||||
{t('description1.part1')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -488,8 +502,7 @@ export function App() {
|
||||
level={matches ? 'h2' : 'h1'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
It’s not just a collection of Docs, whiteboard, and
|
||||
tables.
|
||||
{t('description1.part2')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -506,11 +519,10 @@ export function App() {
|
||||
}}
|
||||
>
|
||||
<Typography fontSize="1.2em">
|
||||
Transform any building block as you like.
|
||||
{t('description1.part3')}
|
||||
</Typography>
|
||||
<Typography fontSize="1.2em">
|
||||
Say goodbye to redundancy. Store your data once, and
|
||||
keep your data as you like it.
|
||||
{t('description1.part4')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -547,23 +559,19 @@ export function App() {
|
||||
fontWeight={'bold'}
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
>
|
||||
Shape Your Page
|
||||
{t('description2.part1')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
Docs, Kanbans, and Databases are all fully
|
||||
functional anywhere, anytime. A truly
|
||||
what-you-see-is-what-you-get environment for
|
||||
your data.
|
||||
{t('description2.part2')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
All pages come with a document (Paper Mode) and
|
||||
whiteboard (Edgeless Mode) view.
|
||||
{t('description2.part3')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -625,27 +633,25 @@ export function App() {
|
||||
fontWeight={'bold'}
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
>
|
||||
Plan Your Task
|
||||
{t('description3.part1')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
No more chaos managing multiple views.
|
||||
{t('description3.part2')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
Set a TODO with Markdown, and seamlessly edit it
|
||||
within a Kanban.
|
||||
{t('description3.part3')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
Managing multi-dimensional tables should be this
|
||||
simple – and now it is.
|
||||
{t('description3.part4')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -697,27 +703,19 @@ export function App() {
|
||||
fontWeight={'bold'}
|
||||
style={{ marginBottom: '0.5em' }}
|
||||
>
|
||||
Privacy-first, and collaborative. No compromises
|
||||
whatsoever.
|
||||
{t('description4.part1')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
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.
|
||||
{t('description4.part2')}
|
||||
</Typography>
|
||||
<Typography
|
||||
fontSize="1.2em"
|
||||
style={{ marginBottom: '0.25em' }}
|
||||
>
|
||||
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.
|
||||
{t('description4.part3')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -762,7 +760,7 @@ export function App() {
|
||||
}}
|
||||
>
|
||||
<Typography fontSize={'1.5em'}>
|
||||
Build for an open and semantic future
|
||||
{t('BuildFor')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
@@ -778,7 +776,7 @@ export function App() {
|
||||
>
|
||||
<Typography level="h3" sx={{ display: 'flex' }}>
|
||||
<span style={{ alignSelf: 'center' }}>
|
||||
Keep Updated on
|
||||
{t('KeepUpdated')}
|
||||
</span>
|
||||
<GitHub />
|
||||
</Typography>
|
||||
@@ -795,7 +793,7 @@ export function App() {
|
||||
}}
|
||||
>
|
||||
<Typography level="h2" sx={{ display: 'flex' }}>
|
||||
Join Our Community
|
||||
{t('Join')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './app/i18n';
|
||||
|
||||
import App from './app';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Welcome to ourcontributing guide <!-- omit in toc -->
|
||||
# Welcome to our contributing guide <!-- omit in toc -->
|
||||
|
||||
Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:.
|
||||
|
||||
@@ -10,7 +10,7 @@ Use the table of contents icon on the top left corner of this document to get to
|
||||
|
||||
## New contributor guide
|
||||
|
||||
To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions:
|
||||
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:
|
||||
|
||||
- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github)
|
||||
- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git)
|
||||
@@ -19,19 +19,17 @@ To get an overview of the project, read the [README](README.md). Here are some r
|
||||
|
||||
## Getting started
|
||||
|
||||
To navigate our codebase with confidence, see [the introduction to working in the docs repository](/contributing/working-in-docs-repository.md) :confetti_ball:. For more information on how we write our markdown files, see [the GitHub Markdown reference](contributing/content-markup-reference.md).
|
||||
|
||||
Check to see what [types of contributions](/contributing/types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:.
|
||||
Check to see what [types of contributions](types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:.
|
||||
|
||||
### Issues
|
||||
|
||||
#### Create a new issue
|
||||
#### Create a new issue or feature request
|
||||
|
||||
If you spot a problem, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/toeverything/AFFiNE/issues/new/choose).
|
||||
|
||||
#### Solve an issue
|
||||
|
||||
Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See [Labels](/contributing/how-to-use-labels.md) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See our [Labels](https://github.com/toeverything/AFFiNE/labels) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
|
||||
### Make Changes
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Types of contributions :memo:
|
||||
|
||||
You can contribute to AFFiNE in several ways. This repo is a place to discuss and collaborate on AFFiNE!
|
||||
|
||||
### :mega: Discussions
|
||||
|
||||
Discussions are where we have conversations.
|
||||
|
||||
If you'd like help troubleshooting a docs PR you're working on, have a great new idea, or want to share something amazing you've learned in our docs, join us in [discussions](https://github.com/toeverything/AFFiNE/discussions).
|
||||
|
||||
### :lady_beetle: Issues
|
||||
|
||||
[Issues](https://docs.github.com/en/github/managing-your-work-on-github/about-issues) are used to track tasks that contributors can help with. If an issue has a triage label, we haven't reviewed it yet, and you shouldn't begin work on it.
|
||||
|
||||
If you've found something in the content or the website that should be updated, search open issues to see if someone else has reported the same thing. If it's something new, open an issue using a [template](https://github.com/toeverything/AFFiNE/issues/new/choose). We'll use the issue to have a conversation about the problem you want to fix.
|
||||
|
||||
### :hammer_and_wrench: Pull requests
|
||||
|
||||
A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository. When we merge those changes, they should be deployed to the live site within 24 hours. :earth_africa:
|
||||
You can [create a new pull request](https://github.com/toeverything/AFFiNE/compare) and view [current pull requests](https://github.com/toeverything/AFFiNE/pulls).
|
||||
|
||||
### :question: Support
|
||||
|
||||
We are a small team working hard to keep up with the documentation demands of a continuously changing product.
|
||||
You may be able to find additional help and information on our social media platforms and groups - the links to these can be found in our [README](../README.md).
|
||||
|
||||
### :earth_asia: Translations
|
||||
|
||||
AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content.
|
||||
|
||||
**We do not currently offer this feature**, but we hope to in the future.
|
||||
@@ -22,7 +22,6 @@ const AffineBoard = ({
|
||||
editor,
|
||||
}: AffineBoardProps & { editor: BlockEditor }) => {
|
||||
const [app, set_app] = useState<TldrawApp>();
|
||||
|
||||
const [document] = useState(() => {
|
||||
return {
|
||||
...deepCopy(TldrawApp.default_document),
|
||||
@@ -49,7 +48,7 @@ const AffineBoard = ({
|
||||
};
|
||||
});
|
||||
|
||||
const shapes = useShapes(workspace, rootBlockId);
|
||||
const { shapes } = useShapes(workspace, rootBlockId);
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
app.replacePageContent(shapes || {}, {}, {});
|
||||
@@ -66,6 +65,9 @@ const AffineBoard = ({
|
||||
onMount(app) {
|
||||
set_app(app);
|
||||
},
|
||||
async onPaste(e, data) {
|
||||
console.log('e,data: ', e, data);
|
||||
},
|
||||
async onCopy(e, groupIds) {
|
||||
const clip =
|
||||
await editor.clipboard.clipboardUtils.getClipDataOfBlocksById(
|
||||
@@ -77,7 +79,7 @@ const AffineBoard = ({
|
||||
clip.getData()
|
||||
);
|
||||
},
|
||||
onChangePage(app, shapes, bindings, assets) {
|
||||
async onChangePage(app, shapes, bindings, assets) {
|
||||
Promise.all(
|
||||
Object.entries(shapes).map(async ([id, shape]) => {
|
||||
if (shape === undefined) {
|
||||
@@ -106,7 +108,7 @@ const AffineBoard = ({
|
||||
});
|
||||
}
|
||||
shape.affineId = block.id;
|
||||
return services.api.editorBlock.update({
|
||||
return await services.api.editorBlock.update({
|
||||
workspace: shape.workspace,
|
||||
id: block.id,
|
||||
properties: {
|
||||
|
||||
@@ -9,28 +9,37 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
const [blocks, setBlocks] = useState<ReturnEditorBlock[]>();
|
||||
const [blocks, setBlocks] = useState<{
|
||||
shapes: [ReturnEditorBlock[]];
|
||||
}>();
|
||||
useEffect(() => {
|
||||
services.api.editorBlock
|
||||
.get({ workspace, ids: [rootBlockId] })
|
||||
.then(async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
(blockData?.[0]?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace,
|
||||
ids: [childId],
|
||||
})
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
setBlocks(shapes);
|
||||
Promise.all([
|
||||
services.api.editorBlock
|
||||
.get({ workspace, ids: [rootBlockId] })
|
||||
.then(async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
(blockData?.[0]?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
workspace,
|
||||
ids: [childId],
|
||||
})
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
return shapes;
|
||||
}),
|
||||
]).then(shapes => {
|
||||
setBlocks({
|
||||
shapes: shapes,
|
||||
});
|
||||
});
|
||||
|
||||
let unobserve: () => void;
|
||||
services.api.editorBlock
|
||||
.observe({ workspace, id: rootBlockId }, async blockData => {
|
||||
const shapes = await Promise.all(
|
||||
Promise.all(
|
||||
(blockData?.children || []).map(async childId => {
|
||||
const childBlock = (
|
||||
await services.api.editorBlock.get({
|
||||
@@ -40,8 +49,11 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
)?.[0];
|
||||
return childBlock;
|
||||
})
|
||||
);
|
||||
setBlocks(shapes);
|
||||
).then(shapes => {
|
||||
setBlocks({
|
||||
shapes: [shapes],
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(cb => {
|
||||
unobserve = cb;
|
||||
@@ -53,8 +65,7 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
}, [workspace, rootBlockId]);
|
||||
|
||||
let groupCount = 0;
|
||||
|
||||
return blocks?.reduce((acc, block) => {
|
||||
let blocksShapes = blocks?.shapes[0]?.reduce((acc, block) => {
|
||||
const shapeProps = block.properties.shapeProps?.value
|
||||
? JSON.parse(block.properties.shapeProps.value)
|
||||
: {};
|
||||
@@ -75,4 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => {
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, TDShape>);
|
||||
|
||||
return {
|
||||
shapes: blocksShapes,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import {
|
||||
RenderBlock,
|
||||
RenderRoot,
|
||||
type BlockEditor,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -88,9 +87,7 @@ export const AffineEditor = forwardRef<BlockEditor, AffineEditorProps>(
|
||||
editor={editor}
|
||||
editorElement={AffineEditor as any}
|
||||
scrollBlank={scrollBlank}
|
||||
>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</RenderRoot>
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
TDStatus,
|
||||
} from '@toeverything/components/board-types';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
// import { FocusButton } from '~components/FocusButton';
|
||||
import { usePageClientWidth } from '@toeverything/datasource/state';
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
@@ -22,22 +24,20 @@ import {
|
||||
useState,
|
||||
type RefObject,
|
||||
} 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 { ZoomBar } from './components/zoom-bar';
|
||||
import {
|
||||
TldrawContext,
|
||||
useKeyboardShortcuts,
|
||||
useStylesheet,
|
||||
useTldrawApp,
|
||||
} 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 {
|
||||
/**
|
||||
@@ -295,7 +295,6 @@ const InnerTldraw = memo(function InnerTldraw({
|
||||
const pageState = document.pageStates[page.id];
|
||||
const assets = document.assets;
|
||||
const { selectedIds } = pageState;
|
||||
|
||||
const isHideBoundsShape =
|
||||
selectedIds.length === 1 &&
|
||||
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
|
||||
const hideBounds =
|
||||
(isInSession && app.session?.constructor.name !== 'BrushSession') ||
|
||||
(isInSession && app.session?.constructor.name === 'BrushSession') ||
|
||||
!isSelecting ||
|
||||
isHideBoundsShape ||
|
||||
!!pageState.editingId;
|
||||
|
||||
@@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>(
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
margin: '5px',
|
||||
padding: '3px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
cursor: 'pointer',
|
||||
boxSizing: 'border-box',
|
||||
'&:hover': {
|
||||
|
||||
@@ -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}
|
||||
></AlignOperation>
|
||||
) : null,
|
||||
// toNextShap: (
|
||||
// <ArrowTo
|
||||
// app={app}
|
||||
// shapes={config.deleteShapes.selectedShapes}
|
||||
// ></ArrowTo>
|
||||
// ),
|
||||
};
|
||||
|
||||
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',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
@@ -100,7 +100,7 @@ const ListItemContainer = styled('div')(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ListItemTitle = styled('span')(({ theme }) => ({
|
||||
export const ListItemTitle = styled('span')(({ theme }) => ({
|
||||
marginLeft: '12px',
|
||||
color: theme.affine.palette.primaryText,
|
||||
}));
|
||||
|
||||
@@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Line
|
||||
@@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool;
|
||||
|
||||
export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [lastActiveTool, setLastActiveTool] = useState<ShapeTypes>(
|
||||
TDShapeType.Rectangle
|
||||
@@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<ShapesContainer>
|
||||
{shapes.map(({ type, label, tooltip, icon: Icon }) => (
|
||||
@@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => {
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
app.selectTool(type);
|
||||
setVisible(false);
|
||||
setLastActiveTool(type);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,12 +12,12 @@ import {
|
||||
import {
|
||||
IconButton,
|
||||
PopoverContainer,
|
||||
styled,
|
||||
// MuiIconButton as IconButton,
|
||||
// MuiTooltip as Tooltip,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { TldrawApp } from '@toeverything/components/board-state';
|
||||
import {
|
||||
@@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
}}
|
||||
direction="none"
|
||||
>
|
||||
<div className={styles('container')}>
|
||||
<div className={styles('toolBar')}>
|
||||
<Container>
|
||||
<ToolBar>
|
||||
{tools.map(
|
||||
({
|
||||
type,
|
||||
@@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => {
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ToolBar>
|
||||
</Container>
|
||||
</PopoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = style9.create({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
},
|
||||
toolBar: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
},
|
||||
const Container = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
});
|
||||
const ToolBar = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '10px',
|
||||
padding: '4px 4px',
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
styled,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { ReactElement, type CSSProperties } from 'react';
|
||||
import { useState, type CSSProperties, type ReactElement } from 'react';
|
||||
import { Palette } from '../../palette';
|
||||
import { Pen } from './Pen';
|
||||
|
||||
@@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce<
|
||||
|
||||
export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
const appCurrentTool = app.useStore(state => state.appState.activeTool);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const chosenPen =
|
||||
PENCIL_CONFIGS.find(config => config.key === appCurrentTool) ||
|
||||
PENCIL_CONFIGS[0];
|
||||
@@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
placement="right-start"
|
||||
trigger="click"
|
||||
onClick={() => setVisible(prev => !prev)}
|
||||
onClickAway={() => setVisible(false)}
|
||||
content={
|
||||
<Container>
|
||||
<PensContainer>
|
||||
@@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
name={title}
|
||||
primaryColor={color_vars['--color-0']}
|
||||
secondaryColor={color_vars['--color-1']}
|
||||
onClick={() => setPen(key)}
|
||||
onClick={() => {
|
||||
setVisible(false);
|
||||
setPen(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => {
|
||||
<Palette
|
||||
selected={chosenColor}
|
||||
colors={PENCIL_CONFIGS_MAP[chosenPenKey].colors}
|
||||
onSelect={color => setPenColor(color)}
|
||||
onSelect={color => {
|
||||
setVisible(false);
|
||||
setPenColor(color);
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
</Container>
|
||||
|
||||
@@ -42,7 +42,7 @@ type E = HTMLDivElement;
|
||||
export class ArrowUtil extends TDShapeUtil<T, E> {
|
||||
type = TDShapeType.Arrow as const;
|
||||
|
||||
override hideBounds = true;
|
||||
override hideBounds = false;
|
||||
|
||||
override canEdit = true;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TDShapeType,
|
||||
TransformInfo,
|
||||
} from '@toeverything/components/board-types';
|
||||
import type { BlockEditor } from '@toeverything/components/editor-core';
|
||||
import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { MouseEvent, SyntheticEvent } from 'react';
|
||||
@@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
Component = TDShapeUtil.Component<T, E, TDMeta>(
|
||||
({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => {
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const editorRef = useRef<BlockEditor>();
|
||||
const {
|
||||
workspace,
|
||||
rootBlockId,
|
||||
@@ -135,6 +137,18 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
}
|
||||
}, [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(
|
||||
(e: MouseEvent) => {
|
||||
if (e.detail === 2) {
|
||||
@@ -159,6 +173,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
|
||||
rootBlockId={rootBlockId}
|
||||
scrollBlank={false}
|
||||
isEdgeless
|
||||
ref={editorRef}
|
||||
/>
|
||||
{editingText ? null : <Mask />}
|
||||
</Container>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Utils } from '@tldraw/core';
|
||||
import type { ShapeStyles } from '@toeverything/components/board-types';
|
||||
import { BINDING_DISTANCE } from '@toeverything/components/board-types';
|
||||
import {
|
||||
BINDING_DISTANCE,
|
||||
ShapeStyles,
|
||||
} from '@toeverything/components/board-types';
|
||||
import * as React from 'react';
|
||||
import { getShapeStyle } from '../../shared';
|
||||
|
||||
@@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({
|
||||
return (
|
||||
<>
|
||||
<rect
|
||||
className={
|
||||
isSelected || style.isFilled
|
||||
? 'tl-fill-hitarea'
|
||||
: 'tl-stroke-hitarea'
|
||||
}
|
||||
className={'tl-fill-hitarea'}
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={1}
|
||||
strokeWidth={BINDING_DISTANCE}
|
||||
/>
|
||||
{style.isFilled && (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
width={w}
|
||||
height={h}
|
||||
width={w - sw / 2}
|
||||
height={h - sw / 2}
|
||||
fill={fill}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
|
||||
@@ -175,6 +175,10 @@ interface TDCallbacks {
|
||||
* (optional) A callback to run when the shape is copied.
|
||||
*/
|
||||
onCopy?: (e: ClipboardEvent, ids: string[]) => void;
|
||||
/**
|
||||
* (optional) A callback to run when the shape is paste.
|
||||
*/
|
||||
onPaste?: (e: ClipboardEvent, data: any) => void;
|
||||
}
|
||||
|
||||
export interface TldrawAppCtorProps {
|
||||
@@ -444,7 +448,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
if (visitedShapes.has(fromShape)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only need to update the binding's "from" shape (an arrow)
|
||||
const fromDelta = TLDR.update_arrow_bindings(
|
||||
page,
|
||||
@@ -627,7 +630,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
private prev_bindings = this.page.bindings;
|
||||
private prev_assets = this.document.assets;
|
||||
|
||||
private _broadcastPageChanges = () => {
|
||||
private _broadcastPageChanges = async () => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
const changedShapes: Record<string, TDShape | undefined> = {};
|
||||
@@ -684,7 +687,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
Object.keys(changedAssets).length > 0
|
||||
) {
|
||||
this.just_sent = true;
|
||||
this.callbacks.onChangePage?.(
|
||||
await this.callbacks.onChangePage?.(
|
||||
this,
|
||||
changedShapes,
|
||||
changedBindings,
|
||||
@@ -860,7 +863,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
if (!page.bindings[binding.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromShape = page.shapes[binding.fromId] as ArrowShape;
|
||||
|
||||
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)
|
||||
|
||||
const fromDelta = TLDR.update_arrow_bindings(page, fromShape);
|
||||
visitedShapes.add(fromShape);
|
||||
|
||||
@@ -1957,6 +1960,53 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
paste = async (point?: number[], e?: ClipboardEvent) => {
|
||||
if (this.readOnly) return;
|
||||
|
||||
if (e) {
|
||||
const data = e.clipboardData?.getData('text/html');
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
return json;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
this.callbacks.onPaste?.(e, paste_as_html(data));
|
||||
return this;
|
||||
}
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const pasteInCurrentPage = (
|
||||
shapes: TDShape[],
|
||||
bindings: TDBinding[],
|
||||
@@ -2108,31 +2158,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
// this.select(shapeId);
|
||||
};
|
||||
|
||||
const paste_as_html = (html: string) => {
|
||||
try {
|
||||
const maybeJson = html.match(/<tldraw>(.*)<\/tldraw>/)?.[1];
|
||||
|
||||
if (!maybeJson) return;
|
||||
|
||||
const json: {
|
||||
type: string;
|
||||
shapes: TDShape[];
|
||||
bindings: TDBinding[];
|
||||
assets: TDAsset[];
|
||||
} = JSON.parse(maybeJson);
|
||||
|
||||
if (json.type === 'tldr/clipboard') {
|
||||
pasteInCurrentPage(json.shapes, json.bindings, json.assets);
|
||||
return;
|
||||
} else {
|
||||
throw Error('Not tldraw data!');
|
||||
}
|
||||
} catch (e) {
|
||||
pasteTextAsShape(html);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if (e !== undefined) {
|
||||
const items: DataTransferItemList =
|
||||
e.clipboardData?.items ?? ({} as DataTransferItemList);
|
||||
@@ -3854,7 +3879,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -4138,7 +4163,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
};
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info, e) => {
|
||||
if (e.buttons === 4) {
|
||||
if (e.button === 1) {
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: true,
|
||||
@@ -4155,6 +4180,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
};
|
||||
|
||||
onPointerUp: TLPointerEventHandler = (info, e) => {
|
||||
if (e.button === 1) {
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.isPointing = false;
|
||||
this.updateInputs(info, e);
|
||||
this.currentTool.onPointerUp?.(info, e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseEditor } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { DoubleLinkElement } from './text/plugins/DoubleLink';
|
||||
import { LinkElement } from './text/plugins/link';
|
||||
import { RefLinkElement } from './text/plugins/reflink';
|
||||
|
||||
@@ -8,7 +9,8 @@ export type CustomElement =
|
||||
| { type: string; children: CustomElement[] }
|
||||
| CustomText
|
||||
| LinkElement
|
||||
| RefLinkElement;
|
||||
| RefLinkElement
|
||||
| DoubleLinkElement;
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: BaseEditor & ReactEditor;
|
||||
@@ -19,35 +21,32 @@ declare module 'slate' {
|
||||
|
||||
export { BlockPreview, StyledBlockPreview } from './block-preview';
|
||||
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 * from './text';
|
||||
|
||||
export * from './comming-soon/CommingSoon';
|
||||
export {
|
||||
NewpageIcon,
|
||||
AddIcon,
|
||||
ClockIcon,
|
||||
ViewSidebarIcon,
|
||||
CloseIcon,
|
||||
CodeBlockInlineIcon,
|
||||
DocumentIcon,
|
||||
FilterIcon,
|
||||
FullScreenIcon,
|
||||
HighlighterDuotoneIcon,
|
||||
KanbanIcon,
|
||||
ListIcon,
|
||||
SpaceIcon,
|
||||
NewpageIcon,
|
||||
PagesIcon,
|
||||
PencilDotDuotoneIcon,
|
||||
PencilDuotoneIcon,
|
||||
HighlighterDuotoneIcon,
|
||||
CodeBlockInlineIcon,
|
||||
PagesIcon,
|
||||
CloseIcon,
|
||||
DocumentIcon,
|
||||
TodoListIcon,
|
||||
KanbanIcon,
|
||||
TableIcon,
|
||||
AddIcon,
|
||||
FilterIcon,
|
||||
SorterIcon,
|
||||
FullScreenIcon,
|
||||
SpaceIcon,
|
||||
TableIcon,
|
||||
TodoListIcon,
|
||||
UnGroupIcon,
|
||||
ViewSidebarIcon,
|
||||
} from './icon';
|
||||
|
||||
export * from './comming-soon/CommingSoon';
|
||||
export { BackLink, CommonList, commonListContainer } from './list';
|
||||
export type { CommonListItem } from './list';
|
||||
export * from './Logo';
|
||||
export * from './text';
|
||||
export { default as Toolbar } from './toolbar';
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import clsx from 'clsx';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { BackwardUndoIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
BaseButton,
|
||||
ListButton,
|
||||
@@ -12,9 +8,11 @@ import {
|
||||
} from '@toeverything/components/ui';
|
||||
// eslint-disable-next-line @nrwl/nx/enforce-module-boundaries
|
||||
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 { BackwardUndoIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const commonListContainer = 'commonListContainer';
|
||||
|
||||
@@ -28,6 +26,7 @@ export type CommonListItem = {
|
||||
divider?: string;
|
||||
content?: Content;
|
||||
block?: BlockSearchItem;
|
||||
renderCustom?: (props: CommonListItem) => JSX.Element;
|
||||
};
|
||||
|
||||
type MenuItemsProps = {
|
||||
@@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => {
|
||||
// ]);
|
||||
// TODO Insert bidirectional link to be developed
|
||||
const JSONUnsupportedBlockTypes = ['page'];
|
||||
let usedItems = items.filter(item => {
|
||||
const usedItems = items.filter(item => {
|
||||
return !JSONUnsupportedBlockTypes.includes(item?.content?.id);
|
||||
});
|
||||
return (
|
||||
@@ -58,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => {
|
||||
>
|
||||
{usedItems?.length ? (
|
||||
usedItems.map((item, idx) => {
|
||||
if (item.block) {
|
||||
if (item.renderCustom) {
|
||||
return item.renderCustom(item);
|
||||
} else if (item.block) {
|
||||
return (
|
||||
<BlockPreview
|
||||
className={clsx(
|
||||
|
||||
@@ -1,54 +1,53 @@
|
||||
/* eslint-disable max-lines */
|
||||
import { ErrorBoundary, isEqual } from '@toeverything/utils';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import isUrl from 'is-url';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
DragEvent,
|
||||
forwardRef,
|
||||
KeyboardEvent,
|
||||
KeyboardEventHandler,
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
forwardRef,
|
||||
MouseEventHandler,
|
||||
useLayoutEffect,
|
||||
CSSProperties,
|
||||
MouseEvent,
|
||||
DragEvent,
|
||||
} from 'react';
|
||||
import isHotkey from 'is-hotkey';
|
||||
import {
|
||||
createEditor,
|
||||
Descendant,
|
||||
Range,
|
||||
Element as SlateElement,
|
||||
Editor,
|
||||
Transforms,
|
||||
Element as SlateElement,
|
||||
Node,
|
||||
Path,
|
||||
Range,
|
||||
Transforms,
|
||||
} from 'slate';
|
||||
import {
|
||||
Editable,
|
||||
withReact,
|
||||
Slate,
|
||||
ReactEditor,
|
||||
Slate,
|
||||
useSlateStatic,
|
||||
withReact,
|
||||
} from 'slate-react';
|
||||
|
||||
import { ErrorBoundary, isEqual, uaHelper } from '@toeverything/utils';
|
||||
|
||||
import { Contents, SlateUtils, isSelectAll } from './slate-utils';
|
||||
import { CustomElement } from '..';
|
||||
import { HOTKEYS, INLINE_STYLES } from './constants';
|
||||
import { TextWithComments } from './element-leaf/TextWithComments';
|
||||
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 {
|
||||
getCommentsIdsOnTextNode,
|
||||
getExtraPropertiesFromEditorOutmostNode,
|
||||
isInterceptCharacter,
|
||||
matchMarkdown,
|
||||
} 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 {
|
||||
/** read only */
|
||||
@@ -739,6 +738,9 @@ const EditorElement = (props: any) => {
|
||||
|
||||
switch (element.type) {
|
||||
case 'link': {
|
||||
if (element.linkType === 'doubleLink') {
|
||||
return <DoubleLinkComponent {...props} editor={editor} />;
|
||||
}
|
||||
return (
|
||||
<LinkComponent
|
||||
{...props}
|
||||
@@ -802,7 +804,7 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => {
|
||||
backgroundColor: '#F2F5F9',
|
||||
borderRadius: '5px',
|
||||
color: '#3A4C5C',
|
||||
padding: '3px 8px',
|
||||
padding: '1px 8px',
|
||||
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 = (
|
||||
<TextWithComments commentsIds={commentsIds}>
|
||||
{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,
|
||||
} from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
import type { CustomElement } from '..';
|
||||
import {
|
||||
fontBgColorPalette,
|
||||
fontColorPalette,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
getCommentsIdsOnTextNode,
|
||||
getEditorMarkForCommentId,
|
||||
getRandomString,
|
||||
MARKDOWN_STYLE_MAP,
|
||||
MatchRes,
|
||||
} from './utils';
|
||||
@@ -247,7 +248,11 @@ Editor.insertText = function (
|
||||
const { path, offset } = at;
|
||||
if (text.length > 0) {
|
||||
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
|
||||
const newPath = [path[0], path[1] + 1];
|
||||
const newPoint = {
|
||||
@@ -594,8 +599,13 @@ class SlateUtils {
|
||||
|
||||
const fragmentChildren = firstFragment.children;
|
||||
|
||||
const textChildren: Text[] = [];
|
||||
for (const child of fragmentChildren) {
|
||||
const textChildren: CustomElement[] = [];
|
||||
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)) {
|
||||
console.error('Debug information:', point1, point2, fragment);
|
||||
throw new Error('Fragment exists nested!');
|
||||
@@ -1119,34 +1129,130 @@ class SlateUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public insertReference(reference: string) {
|
||||
try {
|
||||
// Transforms.setSelection(this.editor, this.getEndSelection());
|
||||
const { anchor, focus } = this.getEndSelection();
|
||||
Transforms.insertNodes(
|
||||
public setDoubleLinkSearchSlash(point: Point) {
|
||||
const str = Editor.string(this.editor, {
|
||||
anchor: this.getStart(),
|
||||
focus: point,
|
||||
});
|
||||
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,
|
||||
{
|
||||
type: 'reflink',
|
||||
reference,
|
||||
children: [],
|
||||
path,
|
||||
offset: 1,
|
||||
},
|
||||
{ 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 */
|
||||
public getCommentsIdsBySelection() {
|
||||
const commentedTextNodes = Editor.nodes(this.editor, {
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import type { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { BulletIcon, getChildrenType, NumberType } from './data';
|
||||
import { BulletBlock, BulletProperties } from './types';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, BulletIcon, NumberType } from './data';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
export const defaultBulletProps: BulletProperties = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
|
||||
</div>
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { defaultBulletProps, BulletView } from './BulletView';
|
||||
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { BulletView, defaultBulletProps } from './BulletView';
|
||||
|
||||
export class BulletBlock extends BaseView {
|
||||
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 { css } from '@codemirror/lang-css';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { lezer } from '@codemirror/lang-lezer';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { rust } from '@codemirror/lang-rust';
|
||||
import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript';
|
||||
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile';
|
||||
import { erlang } from '@codemirror/legacy-modes/mode/erlang';
|
||||
import { go } from '@codemirror/legacy-modes/mode/go';
|
||||
import { julia } from '@codemirror/legacy-modes/mode/julia';
|
||||
import { liveScript } from '@codemirror/legacy-modes/mode/livescript';
|
||||
import { lua } from '@codemirror/legacy-modes/mode/lua';
|
||||
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
|
||||
import { pascal } from '@codemirror/legacy-modes/mode/pascal';
|
||||
import { perl } from '@codemirror/legacy-modes/mode/perl';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { scheme } from '@codemirror/legacy-modes/mode/scheme';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { stylus } from '@codemirror/legacy-modes/mode/stylus';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { tcl } from '@codemirror/legacy-modes/mode/tcl';
|
||||
import { toml } from '@codemirror/legacy-modes/mode/toml';
|
||||
import { vb } from '@codemirror/legacy-modes/mode/vb';
|
||||
import { vbScript } from '@codemirror/legacy-modes/mode/vbscript';
|
||||
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { Option, Select } from '@toeverything/components/ui';
|
||||
|
||||
import {
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { DuplicateIcon } from '@toeverything/components/icons';
|
||||
import { Option, Select, styled } from '@toeverything/components/ui';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import { elixir } from 'codemirror-lang-elixir';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { StyleWithAtRules } from 'style9';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror';
|
||||
|
||||
interface CreateCodeView extends CreateView {
|
||||
style9?: StyleWithAtRules;
|
||||
@@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({
|
||||
borderRadius: theme.affine.shape.borderRadius,
|
||||
'&:hover': {
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
'.operation': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
opacity: 0,
|
||||
transition: 'opacity 1.5s',
|
||||
},
|
||||
'.copy-block': {
|
||||
padding: '0px 10px',
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
export class CodeBlock extends BaseView {
|
||||
type = Protocol.Block.Type.code;
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
// View = CodeView;
|
||||
View = CodeView;
|
||||
override async onCreate(block: AsyncBlock): Promise<AsyncBlock> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { useState } from 'react';
|
||||
import { SourceView } from '../../components/source-view';
|
||||
import { LinkContainer } from '../../components/style-container';
|
||||
import { Upload } from '../../components/upload/upload';
|
||||
|
||||
const MESSAGES = {
|
||||
ADD_EMBED_LINK: 'Add embed link',
|
||||
@@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => {
|
||||
{embedLinkUrl ? (
|
||||
<SourceView
|
||||
block={block}
|
||||
editorElement={props.editorElement}
|
||||
isSelected={isSelect}
|
||||
viewType="embedLink"
|
||||
link={embedLinkUrl}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class EmbedLinkBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.embedLink;
|
||||
View = EmbedLinkView;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FigmaBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.figma;
|
||||
View = FigmaView;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Block2HtmlProps } from '../../utils/commonBlockClip';
|
||||
|
||||
export class FileBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.file;
|
||||
View = FileView;
|
||||
override async block2html({ block }: Block2HtmlProps) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
export const GridItemRender = function (
|
||||
@@ -6,10 +6,11 @@ export const GridItemRender = function (
|
||||
) {
|
||||
const GridItem = function (props: CreateView) {
|
||||
const { block } = props;
|
||||
const { BlockRender } = useBlockRender();
|
||||
const children = (
|
||||
<>
|
||||
{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 {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { RenderBlock } from '@toeverything/components/editor-core';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
import { useBlockRender } from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
GRID_ITEM_CLASS_NAME,
|
||||
GRID_ITEM_CONTENT_CLASS_NAME,
|
||||
} from '../grid-item/GridItem';
|
||||
import { debounce, domToRect, Point } from '@toeverything/utils';
|
||||
import clsx from 'clsx';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { GridHandle } from './GirdHandle';
|
||||
|
||||
const DB_UPDATE_DELAY = 50;
|
||||
const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag';
|
||||
@@ -31,6 +31,7 @@ export const Grid = function (props: CreateView) {
|
||||
const originalLeftWidth = useRef<number>(gridItemMinWidth);
|
||||
const originalRightWidth = useRef<number>(gridItemMinWidth);
|
||||
const [alertHandleId, setAlertHandleId] = useState<string>(null);
|
||||
const { BlockRender } = useBlockRender();
|
||||
|
||||
const getLeftRightGridItemDomByIndex = (index: number) => {
|
||||
const gridItems = Array.from(gridContainerRef.current?.children).filter(
|
||||
@@ -226,7 +227,7 @@ export const Grid = function (props: CreateView) {
|
||||
key={id}
|
||||
className={GRID_ITEM_CLASS_NAME}
|
||||
>
|
||||
<RenderBlock hasContainer={false} blockId={id} />
|
||||
<BlockRender hasContainer={false} blockId={id} />
|
||||
<GridHandle
|
||||
onDrag={event => handleDragGrid(event, i)}
|
||||
editor={editor}
|
||||
|
||||
@@ -7,7 +7,7 @@ export { GRID_PROPERTY_KEY, removePercent } from './Grid';
|
||||
|
||||
export class GridBlock extends BaseView {
|
||||
public override selectable = false;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
public override layoutOnly = true;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export class Group extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
public override allowPendant = false;
|
||||
|
||||
type = Protocol.Block.Type.group;
|
||||
|
||||
@@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
|
||||
export const ScenePage = ({ block }: CreateView) => {
|
||||
return <RenderBlockChildren block={block} />;
|
||||
return <RenderBlockChildren block={block} indent={false} />;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import type { KanbanCard } from '@toeverything/components/editor-core';
|
||||
import {
|
||||
RenderBlock,
|
||||
KanbanCard,
|
||||
useBlockRender,
|
||||
useEditor,
|
||||
useKanban,
|
||||
useRefPage,
|
||||
} 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 { useState, type MouseEvent } from 'react';
|
||||
import { useRefPage } from './RefPage';
|
||||
|
||||
const CardContent = styled('div')({
|
||||
margin: '20px',
|
||||
@@ -23,6 +30,7 @@ const CardActions = styled('div')({
|
||||
fontWeight: '300',
|
||||
color: '#98ACBD',
|
||||
transition: 'all ease-in 0.2s',
|
||||
zIndex: 1,
|
||||
|
||||
':hover': {
|
||||
background: '#F5F7F8',
|
||||
@@ -39,11 +47,13 @@ const PlusIcon = styled('div')({
|
||||
});
|
||||
|
||||
const CardContainer = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E2E7ED',
|
||||
borderRadius: '5px',
|
||||
overflow: 'hidden',
|
||||
|
||||
[CardActions.toString()]: {
|
||||
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 = ({
|
||||
id,
|
||||
block,
|
||||
@@ -64,24 +91,44 @@ export const CardItem = ({
|
||||
}) => {
|
||||
const { addSubItem } = useKanban();
|
||||
const { openSubPage } = useRefPage();
|
||||
const [editable, setEditable] = useState(false);
|
||||
const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false);
|
||||
const { editor } = useEditor();
|
||||
const { BlockRender } = useBlockRender();
|
||||
|
||||
const onAddItem = async () => {
|
||||
setEditable(true);
|
||||
await addSubItem(block);
|
||||
};
|
||||
|
||||
const onClickCard = async () => {
|
||||
showKanbanRefPageFlag && openSubPage(id);
|
||||
openSubPage(id);
|
||||
};
|
||||
|
||||
const onClickPen = (e: MouseEvent<Element>) => {
|
||||
e.stopPropagation();
|
||||
setEditable(true);
|
||||
editor.selectionManager.activeNodeByNodeId(block.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardContainer onClick={onClickCard}>
|
||||
<CardContent>
|
||||
<RenderBlock blockId={id} />
|
||||
</CardContent>
|
||||
<CardActions onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<span>Add a sub-block</span>
|
||||
</CardActions>
|
||||
</CardContainer>
|
||||
<MuiClickAwayListener onClickAway={() => setEditable(false)}>
|
||||
<CardContainer>
|
||||
<CardContent>
|
||||
<BlockRender blockId={id} />
|
||||
</CardContent>
|
||||
{showKanbanRefPageFlag && !editable && (
|
||||
<Overlay onClick={onClickCard}>
|
||||
<IconButton backgroundColor="#fff" onClick={onClickPen}>
|
||||
<PenIcon />
|
||||
</IconButton>
|
||||
</Overlay>
|
||||
)}
|
||||
<CardActions onClick={onAddItem}>
|
||||
<PlusIcon />
|
||||
<span>Add a sub-block</span>
|
||||
</CardActions>
|
||||
</CardContainer>
|
||||
</MuiClickAwayListener>
|
||||
);
|
||||
};
|
||||
|
||||
+15
-3
@@ -1,7 +1,7 @@
|
||||
import { useEditor } from '@toeverything/components/editor-core';
|
||||
import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui';
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { RenderBlock } from '../render-block';
|
||||
|
||||
const Dialog = styled('div')({
|
||||
flex: 1,
|
||||
@@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
onClick={closeSubPage}
|
||||
>
|
||||
<Dialog
|
||||
onClick={e => {
|
||||
onClick={(e: { stopPropagation: () => void }) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
@@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => {
|
||||
};
|
||||
|
||||
const ModalPage = ({ blockId }: { blockId: string | null }) => {
|
||||
const { editor, editorElement } = useEditor();
|
||||
|
||||
const AffineEditor = editorElement as any;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -4,30 +4,33 @@ import { SceneKanbanContext } from './context';
|
||||
import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper';
|
||||
import type { ComponentType } from 'react';
|
||||
import type { CreateView } from '@toeverything/framework/virgo';
|
||||
import { RefPageProvider } from './RefPage';
|
||||
|
||||
export const SceneKanban: ComponentType<CreateView> = withKanban<CreateView>(
|
||||
({ editor, block }) => {
|
||||
const { kanban } = useKanban();
|
||||
|
||||
return (
|
||||
<SceneKanbanContext.Provider value={{ editor, block }}>
|
||||
<CardContainerWrapper
|
||||
dataSource={kanban}
|
||||
render={({
|
||||
activeId,
|
||||
items,
|
||||
containerIds,
|
||||
isSortingContainer,
|
||||
}) => (
|
||||
<CardContainer
|
||||
activeId={activeId}
|
||||
items={items}
|
||||
isSortingContainer={isSortingContainer}
|
||||
containerIds={containerIds}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SceneKanbanContext.Provider>
|
||||
<RefPageProvider>
|
||||
<SceneKanbanContext.Provider value={{ editor, block }}>
|
||||
<CardContainerWrapper
|
||||
dataSource={kanban}
|
||||
render={({
|
||||
activeId,
|
||||
items,
|
||||
containerIds,
|
||||
isSortingContainer,
|
||||
}) => (
|
||||
<CardContainer
|
||||
activeId={activeId}
|
||||
items={items}
|
||||
isSortingContainer={isSortingContainer}
|
||||
containerIds={containerIds}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SceneKanbanContext.Provider>
|
||||
</RefPageProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getRandomString } from '@toeverything/components/common';
|
||||
|
||||
export class ImageBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.image;
|
||||
View = ImageView;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
services,
|
||||
Protocol,
|
||||
services,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
@@ -11,18 +11,17 @@ import {
|
||||
type ExtendedTextUtils,
|
||||
} from '../../components/text-manage';
|
||||
import { tabBlock } from '../../utils/indent';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import type { Numbered, NumberedAsyncBlock } from './types';
|
||||
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
import {
|
||||
supportChildren,
|
||||
RenderBlockChildren,
|
||||
useOnSelect,
|
||||
BlockPendantProvider,
|
||||
RenderBlockChildren,
|
||||
supportChildren,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { List } from '../../components/style-container';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { List } from '../../components/style-container';
|
||||
import { getChildrenType, getNumber } from './data';
|
||||
|
||||
export const defaultTodoProps: Numbered = {
|
||||
text: { value: [{ text: '' }] },
|
||||
@@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => {
|
||||
</List>
|
||||
</BlockPendantProvider>
|
||||
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import { defaultTodoProps, NumberedView } from './NumberedView';
|
||||
|
||||
export class NumberedBlock extends BaseView {
|
||||
public type = Protocol.Block.Type.numbered;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { CreateView } from '@toeverything/framework/virgo';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import { IndentWrapper } from '../../components/IndentWrapper';
|
||||
import { TextManage } from '../../components/text-manage';
|
||||
import { dedentBlock, tabBlock } from '../../utils/indent';
|
||||
interface CreateTextView extends CreateView {
|
||||
@@ -255,9 +254,7 @@ export const TextView = ({
|
||||
handleTab={onTab}
|
||||
/>
|
||||
</BlockPendantProvider>
|
||||
<IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</IndentWrapper>
|
||||
<RenderBlockChildren block={block} />
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { TextProps } from '@toeverything/components/common';
|
||||
import {
|
||||
AsyncBlock,
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
ContentColumnValue,
|
||||
Protocol,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo';
|
||||
import { useRef } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { BlockContainer } from '../../components/BlockContainer';
|
||||
import {
|
||||
TextManage,
|
||||
type ExtendedTextUtils,
|
||||
@@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => {
|
||||
export const TodoView = ({ block, editor }: CreateView) => {
|
||||
const properties = { ...defaultTodoProps, ...block.getProperties() };
|
||||
const text_ref = useRef<ExtendedTextUtils>(null);
|
||||
const [isSelect, setIsSelect] = useState<boolean>(false);
|
||||
useOnSelect(block.id, (isSelect: boolean) => {
|
||||
setIsSelect(isSelect);
|
||||
});
|
||||
|
||||
const turn_into_text_block = async () => {
|
||||
// Convert to text block
|
||||
@@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
<BlockContainer editor={editor} block={block} selected={isSelect}>
|
||||
<BlockPendantProvider block={block}>
|
||||
<TodoBlock>
|
||||
<div className={'checkBoxContainer'}>
|
||||
<CheckBox
|
||||
checked={properties.checked?.value}
|
||||
onChange={on_checked_change}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={properties.checked?.value ? 'checked' : ''}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
<div className={'textContainer'}>
|
||||
<TextManage
|
||||
className={
|
||||
properties.checked?.value ? 'checked' : ''
|
||||
}
|
||||
ref={text_ref}
|
||||
editor={editor}
|
||||
block={block}
|
||||
supportMarkdown
|
||||
placeholder="To-do"
|
||||
handleEnter={on_text_enter}
|
||||
handleBackSpace={on_backspace}
|
||||
handleTab={on_tab}
|
||||
/>
|
||||
</div>
|
||||
</TodoBlock>
|
||||
</BlockPendantProvider>
|
||||
</BlockContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
BaseView,
|
||||
BlockEditor,
|
||||
HTML2BlockResult,
|
||||
withTreeViewChildren,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { withTreeViewChildren } from '../../utils/WithTreeViewChildren';
|
||||
import { TodoView, defaultTodoProps } from './TodoView';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
import { defaultTodoProps, TodoView } from './TodoView';
|
||||
|
||||
import {
|
||||
Block2HtmlProps,
|
||||
commonBlock2HtmlContent,
|
||||
commonHTML2block,
|
||||
} from '../../utils/commonBlockClip';
|
||||
import type { TodoAsyncBlock } from './types';
|
||||
|
||||
export class TodoBlock extends BaseView {
|
||||
type = Protocol.Block.Type.todo;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
|
||||
export class YoutubeBlock extends BaseView {
|
||||
public override selectable = true;
|
||||
public override activatable = false;
|
||||
public override editable = false;
|
||||
type = Protocol.Block.Type.youtube;
|
||||
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 }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { ChildrenView } from '@toeverything/framework/virgo';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
|
||||
/**
|
||||
* Indent rendering child nodes
|
||||
*/
|
||||
export const IndentWrapper = (props: PropsWithChildren) => {
|
||||
return <StyledIdentWrapper>{props.children}</StyledIdentWrapper>;
|
||||
};
|
||||
|
||||
const StyledIdentWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// TODO: marginLeft should use theme provided by styled
|
||||
marginLeft: '30px',
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from './IndentWrapper';
|
||||
@@ -1,9 +1,9 @@
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { StyledBlockPreview } from '@toeverything/components/common';
|
||||
import { AsyncBlock, useEditor } from '@toeverything/components/editor-core';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '@toeverything/framework/virgo';
|
||||
import { debounce, sleep } from '@toeverything/utils';
|
||||
|
||||
const updateTitle = async (
|
||||
@@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => {
|
||||
type BlockPreviewProps = {
|
||||
block: AsyncBlock;
|
||||
blockId: string;
|
||||
editorElement?: () => JSX.Element;
|
||||
};
|
||||
|
||||
const InternalBlockPreview = (props: BlockPreviewProps) => {
|
||||
const container = useRef<HTMLDivElement>();
|
||||
const [preview, setPreview] = useState(true);
|
||||
const title = useBlockTitle(props.block, props.blockId);
|
||||
const { editorElement } = useEditor();
|
||||
|
||||
const AffineEditor = props.editorElement as any;
|
||||
const AffineEditor = editorElement as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (container?.current) {
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import {
|
||||
AsyncBlock,
|
||||
useCurrentView,
|
||||
useLazyIframe,
|
||||
} from '@toeverything/components/editor-core';
|
||||
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 { BlockPreview } from './BlockView';
|
||||
import { formatUrl } from './format-url';
|
||||
|
||||
export interface Props {
|
||||
block: AsyncBlock;
|
||||
editorElement?: () => JSX.Element;
|
||||
viewType?: string;
|
||||
link: string;
|
||||
// onResizeEnd: (data: any) => void;
|
||||
@@ -150,7 +148,7 @@ const LoadingContiner = () => {
|
||||
};
|
||||
|
||||
export const SourceView = (props: Props) => {
|
||||
const { link, isSelected, block, editorElement } = props;
|
||||
const { link, isSelected, block } = props;
|
||||
const src = formatUrl(link);
|
||||
// let iframeShow = useLazyIframe(src, 3000, iframeContainer);
|
||||
const [currentView] = useCurrentView();
|
||||
@@ -161,10 +159,7 @@ export const SourceView = (props: Props) => {
|
||||
<SourceViewContainer isSelected={isSelected} scene={type}>
|
||||
<MouseMaskContainer />
|
||||
|
||||
<LazyIframe
|
||||
src={src}
|
||||
fallback={LoadingContiner()}
|
||||
></LazyIframe>
|
||||
<LazyIframe src={src} fallback={LoadingContiner()} />
|
||||
</SourceViewContainer>
|
||||
</div>
|
||||
);
|
||||
@@ -175,11 +170,7 @@ export const SourceView = (props: Props) => {
|
||||
style={{ padding: '0' }}
|
||||
scene={type}
|
||||
>
|
||||
<BlockPreview
|
||||
block={block}
|
||||
editorElement={editorElement}
|
||||
blockId={src}
|
||||
/>
|
||||
<BlockPreview block={block} blockId={src} />
|
||||
</SourceViewContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const isYoutubeUrl = (url?: string): boolean => {
|
||||
if (!url) return false;
|
||||
const allowedHosts = ['www.youtu.be', 'www.youtube.com'];
|
||||
const host = new URL(url).host;
|
||||
return allowedHosts.includes(host);
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BlockEditor, AsyncBlock } from './editor';
|
||||
import { genErrorObj } from '@toeverything/utils';
|
||||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
import type { AsyncBlock, BlockEditor } from './editor';
|
||||
|
||||
const RootContext = createContext<{
|
||||
type EditorProps = {
|
||||
editor: BlockEditor;
|
||||
// TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top
|
||||
editorElement: () => JSX.Element;
|
||||
}>(
|
||||
};
|
||||
|
||||
const EditorContext = createContext<EditorProps>(
|
||||
genErrorObj(
|
||||
'Failed to get context! The context only can use under the "render-root"'
|
||||
'Failed to get EditorContext! The context only can use under the "render-root"'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any
|
||||
);
|
||||
|
||||
export const EditorProvider = RootContext.Provider;
|
||||
|
||||
export const useEditor = () => {
|
||||
return useContext(RootContext);
|
||||
return useContext(EditorContext);
|
||||
};
|
||||
|
||||
export const EditorProvider = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<EditorProps>) => {
|
||||
return (
|
||||
<EditorContext.Provider value={{ editor, editorElement }}>
|
||||
{children}
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,12 +4,12 @@ import {
|
||||
services,
|
||||
type ReturnUnobserve,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { EditorProvider } from './Contexts';
|
||||
import type { BlockEditor } from './editor';
|
||||
import { useIsOnDrag } from './hooks';
|
||||
import { addNewGroup, appendNewGroup } from './recast-block';
|
||||
import { BlockRenderProvider, RenderBlock } from './render-block';
|
||||
import { SelectionRect, SelectionRef } from './Selection';
|
||||
|
||||
interface RenderRootProps {
|
||||
@@ -24,11 +24,7 @@ interface RenderRootProps {
|
||||
const MAX_PAGE_WIDTH = 5000;
|
||||
export const MIN_PAGE_WIDTH = 1480;
|
||||
|
||||
export const RenderRoot = ({
|
||||
editor,
|
||||
editorElement,
|
||||
children,
|
||||
}: PropsWithChildren<RenderRootProps>) => {
|
||||
export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => {
|
||||
const selectionRef = useRef<SelectionRef>(null);
|
||||
const triggeredBySelect = useRef(false);
|
||||
const [pageWidth, setPageWidth] = useState<number>(MIN_PAGE_WIDTH);
|
||||
@@ -158,39 +154,43 @@ export const RenderRoot = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorProvider value={{ editor, editorElement }}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
{children}
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && <SelectionRect ref={selectionRef} editor={editor} />}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
<EditorProvider editor={editor} editorElement={editorElement}>
|
||||
<BlockRenderProvider blockRender={RenderBlock}>
|
||||
<Container
|
||||
isEdgeless={editor.isEdgeless}
|
||||
ref={ref => {
|
||||
if (ref != null && ref !== editor.container) {
|
||||
editor.container = ref;
|
||||
editor.getHooks().render();
|
||||
}
|
||||
}}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOverCapture={onDragOverCapture}
|
||||
onDragEnd={onDragEnd}
|
||||
onDrop={onDrop}
|
||||
isOnDrag={isOnDrag}
|
||||
>
|
||||
<Content style={{ maxWidth: pageWidth + 'px' }}>
|
||||
<RenderBlock blockId={editor.getRootBlockId()} />
|
||||
</Content>
|
||||
{/** TODO: remove selectionManager insert */}
|
||||
{editor && (
|
||||
<SelectionRect ref={selectionRef} editor={editor} />
|
||||
)}
|
||||
{editor.isEdgeless ? null : <ScrollBlank editor={editor} />}
|
||||
{patchedNodes}
|
||||
</Container>
|
||||
</BlockRenderProvider>
|
||||
</EditorProvider>
|
||||
);
|
||||
};
|
||||
@@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBlankContainter
|
||||
<ScrollBlankContainer
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onClick={onClick}
|
||||
@@ -283,7 +283,7 @@ const Content = styled('div')({
|
||||
transitionTimingFunction: 'ease-in',
|
||||
});
|
||||
|
||||
const ScrollBlankContainter = styled('div')({
|
||||
const ScrollBlankContainer = styled('div')({
|
||||
paddingBottom: '30vh',
|
||||
margin: `0 -${PADDING_X}px`,
|
||||
});
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/* eslint-disable max-lines */
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import {
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
DefaultColumnsValue,
|
||||
Protocol,
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
} from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
isDev,
|
||||
createNoopWithMessage,
|
||||
lowerFirst,
|
||||
isDev,
|
||||
last,
|
||||
lowerFirst,
|
||||
} from '@toeverything/utils';
|
||||
import { BlockProvider } from './block-provider';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { BaseView, BaseView as BlockView } from './../views/base-view';
|
||||
import { BlockProvider } from './block-provider';
|
||||
|
||||
type EventType = 'update';
|
||||
export interface EventData {
|
||||
@@ -154,6 +154,7 @@ export class AsyncBlock {
|
||||
}
|
||||
this.initialized = true;
|
||||
this.raw_data = await this.filterPageInvalidChildren(this.raw_data);
|
||||
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
|
||||
const { workspace, id } = this.raw_data;
|
||||
this.unobserve = await this.services.observe(
|
||||
{ workspace, id },
|
||||
@@ -161,6 +162,7 @@ export class AsyncBlock {
|
||||
const oldData = this.raw_data;
|
||||
this.raw_data = blockData;
|
||||
this.raw_data = await this.filterPageInvalidChildren(blockData);
|
||||
this.raw_data = await this.updateDoubleLinkBlock(this.raw_data);
|
||||
this.emit('update', { block: this, oldData });
|
||||
}
|
||||
);
|
||||
@@ -495,4 +497,33 @@ export class AsyncBlock {
|
||||
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'
|
||||
| 'removeSearchSlash'
|
||||
| 'getSearchSlashText'
|
||||
| 'setDoubleLinkSearchSlash'
|
||||
| 'getDoubleLinkSearchSlashText'
|
||||
| 'setSelectDoubleLinkSearchSlash'
|
||||
| 'removeDoubleLinkSearchSlash'
|
||||
| 'insertDoubleLink'
|
||||
| 'selectionToSlateRange'
|
||||
| 'transformPoint'
|
||||
| 'toggleTextFormatBySelection'
|
||||
@@ -37,7 +42,6 @@ type TextUtilsFunctions =
|
||||
| 'getCommentsIdsBySelection'
|
||||
| 'getCurrentSelection'
|
||||
| 'removeSelection'
|
||||
| 'insertReference'
|
||||
| 'isCollapsed'
|
||||
| 'blur'
|
||||
| 'setSelection'
|
||||
@@ -257,27 +261,60 @@ export class BlockHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public insertReference(
|
||||
reference: string,
|
||||
public setDoubleLinkSearchSlash(blockId: string, point: Point) {
|
||||
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,
|
||||
selection: Selection,
|
||||
offset: number
|
||||
isRemoveSlash?: boolean
|
||||
) {
|
||||
const text_utils = this._blockTextUtilsMap[blockId];
|
||||
if (text_utils) {
|
||||
const offsetSelection = window.getSelection();
|
||||
offsetSelection.setBaseAndExtent(
|
||||
selection.anchorNode,
|
||||
selection.anchorOffset,
|
||||
selection.focusNode,
|
||||
selection.focusOffset + offset
|
||||
);
|
||||
const textUtils = this._blockTextUtilsMap[blockId];
|
||||
if (textUtils) {
|
||||
textUtils.removeDoubleLinkSearchSlash(isRemoveSlash);
|
||||
} else {
|
||||
console.warn('Could find the block text utils');
|
||||
}
|
||||
}
|
||||
|
||||
text_utils.removeSelection(offsetSelection);
|
||||
text_utils.insertReference(reference);
|
||||
|
||||
// range.
|
||||
// text_utils.toggleTextFormatBySelection(format, range);
|
||||
public async insertDoubleLink(
|
||||
workspaceId: string,
|
||||
linkBlockId: string,
|
||||
blockId: string
|
||||
) {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
import HotKeys from 'hotkeys-js';
|
||||
|
||||
import type { PatchNode } from '@toeverything/components/ui';
|
||||
import { Commands } from '@toeverything/datasource/commands';
|
||||
import type {
|
||||
BlockFlavors,
|
||||
ReturnEditorBlock,
|
||||
UpdateEditorBlock,
|
||||
} 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 assert from 'assert';
|
||||
import HotKeys from 'hotkeys-js';
|
||||
import type { WorkspaceAndBlockId } from './block';
|
||||
import { AsyncBlock } from './block';
|
||||
import { BlockHelper } from './block/block-helper';
|
||||
@@ -295,7 +293,7 @@ export class Editor implements Virgo {
|
||||
return await blockView.onCreate(block);
|
||||
}
|
||||
|
||||
private async getBlock({
|
||||
public async getBlock({
|
||||
workspace,
|
||||
id,
|
||||
}: WorkspaceAndBlockId): Promise<AsyncBlock | null> {
|
||||
|
||||
@@ -408,13 +408,13 @@ export class SelectionManager implements VirgoSelection {
|
||||
|
||||
/**
|
||||
*
|
||||
* get previous activatable blockNode
|
||||
* get previous editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getPreviousActivatableBlockNode(
|
||||
private async _getPreviousEditableBlockNode(
|
||||
node: AsyncBlock
|
||||
): Promise<AsyncBlock | null> {
|
||||
const preNode = await node.physicallyPerviousSibling();
|
||||
@@ -422,8 +422,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
if (!preNode) {
|
||||
return null;
|
||||
}
|
||||
const { activatable, selectable } = this._editor.getView(preNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(preNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return preNode;
|
||||
}
|
||||
@@ -432,18 +432,18 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getPreviousActivatableBlockNode(preNode);
|
||||
return this._getPreviousEditableBlockNode(preNode);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* get next activatable blockNode
|
||||
* get next editable blockNode
|
||||
* @private
|
||||
* @param {AsyncBlock} node
|
||||
* @return {*} {(Promise<AsyncBlock | null>)}
|
||||
* @memberof SelectionManager
|
||||
*/
|
||||
private async _getNextActivatableBlockNode(
|
||||
private async _getNextEditableBlockNode(
|
||||
node: AsyncBlock,
|
||||
ignoreSelf = true
|
||||
): Promise<AsyncBlock | null> {
|
||||
@@ -482,8 +482,8 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
const { activatable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (activatable) {
|
||||
const { editable, selectable } = this._editor.getView(nextNode.type);
|
||||
if (editable) {
|
||||
this.setSelectedNodesIds([]);
|
||||
return nextNode;
|
||||
}
|
||||
@@ -492,7 +492,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
(document.activeElement as HTMLInputElement)?.blur();
|
||||
return null;
|
||||
}
|
||||
return this._getNextActivatableBlockNode(nextNode);
|
||||
return this._getNextEditableBlockNode(nextNode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +513,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
return;
|
||||
}
|
||||
}
|
||||
preNode = await this._getPreviousActivatableBlockNode(node);
|
||||
preNode = await this._getPreviousEditableBlockNode(node);
|
||||
if (preNode) {
|
||||
this.activeNodeByNodeId(preNode.id, position);
|
||||
}
|
||||
@@ -622,7 +622,7 @@ export class SelectionManager implements VirgoSelection {
|
||||
}
|
||||
}
|
||||
|
||||
nextNode = await this._getNextActivatableBlockNode(node);
|
||||
nextNode = await this._getNextEditableBlockNode(node);
|
||||
if (nextNode) {
|
||||
this.activeNodeByNodeId(nextNode.id, position);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { HTML2BlockResult } from '../clipboard/types';
|
||||
export interface CreateView {
|
||||
block: AsyncBlock;
|
||||
editor: Editor;
|
||||
editorElement: () => JSX.Element;
|
||||
/**
|
||||
* @deprecated Use recast table instead
|
||||
*/
|
||||
@@ -32,10 +31,10 @@ export abstract class BaseView {
|
||||
abstract type: string;
|
||||
|
||||
/**
|
||||
* activatable means can be focused
|
||||
* editable means can be focused
|
||||
* @memberof BaseView
|
||||
*/
|
||||
public activatable = true;
|
||||
public editable = true;
|
||||
|
||||
public selectable = true;
|
||||
|
||||
|
||||
@@ -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 { useEditor } from './Contexts';
|
||||
export * from './editor';
|
||||
export * from './hooks';
|
||||
export * from './kanban';
|
||||
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 './editor';
|
||||
|
||||
export { RefPageProvider, useRefPage } from './ref-page';
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
RecastMetaProperty,
|
||||
RecastPropertyId,
|
||||
} from '../recast-block/types';
|
||||
import { BlockRenderProvider } from '../render-block';
|
||||
import { KanbanBlockRender } from '../render-block/RenderKanbanBlock';
|
||||
import { useInitKanbanEffect, useRecastKanban } from './kanban';
|
||||
import { KanbanGroup } from './types';
|
||||
|
||||
@@ -54,9 +56,11 @@ export const KanbanProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<KanbanContext.Provider value={value}>
|
||||
{children}
|
||||
</KanbanContext.Provider>
|
||||
<BlockRenderProvider blockRender={KanbanBlockRender}>
|
||||
<KanbanContext.Provider value={value}>
|
||||
{children}
|
||||
</KanbanContext.Provider>
|
||||
</BlockRenderProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service';
|
||||
import { AsyncBlock } from '../editor';
|
||||
import { ComponentType, createContext, ReactNode, useContext } from 'react';
|
||||
import { RecastBlock } from './types';
|
||||
import { RefPageProvider } from '../ref-page';
|
||||
|
||||
/**
|
||||
* Determine whether the block supports RecastBlock
|
||||
@@ -48,7 +47,7 @@ export const RecastBlockProvider = ({
|
||||
|
||||
return (
|
||||
<RecastBlockContext.Provider value={block}>
|
||||
<RefPageProvider>{children}</RefPageProvider>
|
||||
{children}
|
||||
</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 { useBlock } from '../hooks';
|
||||
|
||||
interface RenderBlockProps {
|
||||
/**
|
||||
* Render nothing
|
||||
*/
|
||||
export const NullBlockRender = (): null => null;
|
||||
|
||||
export interface RenderBlockProps {
|
||||
blockId: string;
|
||||
hasContainer?: boolean;
|
||||
}
|
||||
@@ -13,7 +18,7 @@ export function RenderBlock({
|
||||
blockId,
|
||||
hasContainer = true,
|
||||
}: RenderBlockProps) {
|
||||
const { editor, editorElement } = useEditor();
|
||||
const { editor } = useEditor();
|
||||
const { block } = useBlock(blockId);
|
||||
|
||||
const setRef = useCallback(
|
||||
@@ -29,7 +34,7 @@ export function RenderBlock({
|
||||
if (block?.type) {
|
||||
return editor.getView(block.type).View;
|
||||
}
|
||||
return () => null;
|
||||
return (): null => null;
|
||||
}, [editor, block?.type]);
|
||||
|
||||
if (!block) {
|
||||
@@ -50,7 +55,6 @@ export function RenderBlock({
|
||||
block={block}
|
||||
columns={columns.columns}
|
||||
columnsFromId={columns.fromId}
|
||||
editorElement={editorElement}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -65,4 +69,5 @@ export function RenderBlock({
|
||||
|
||||
const BlockContainer = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.typography.body1.fontSize,
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { RenderBlock } from './RenderBlock';
|
||||
import { useBlockRender } from './Context';
|
||||
import { NullBlockRender } from './RenderBlock';
|
||||
|
||||
interface RenderChildrenProps {
|
||||
export interface RenderChildrenProps {
|
||||
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 ? (
|
||||
<>
|
||||
<StyledIdentWrapper indent={indent}>
|
||||
{block.childrenIds.map(childId => {
|
||||
return <RenderBlock key={childId} blockId={childId} />;
|
||||
return <BlockRender key={childId} blockId={childId} />;
|
||||
})}
|
||||
</>
|
||||
</StyledIdentWrapper>
|
||||
) : 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',
|
||||
});
|
||||
+27
-82
@@ -1,10 +1,3 @@
|
||||
import {
|
||||
BlockPendantProvider,
|
||||
CreateView,
|
||||
RenderBlock,
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import type {
|
||||
ComponentPropsWithoutRef,
|
||||
@@ -12,9 +5,10 @@ import type {
|
||||
CSSProperties,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { SCENE_CONFIG } from '../blocks/group/config';
|
||||
import { BlockContainer } from '../components/BlockContainer';
|
||||
import { forwardRef } from 'react';
|
||||
import { CreateView } from '../editor';
|
||||
import { useBlockRender } from './Context';
|
||||
import { NullBlockRender } from './RenderBlock';
|
||||
|
||||
type WithChildrenConfig = {
|
||||
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<
|
||||
HTMLDivElement,
|
||||
ComponentPropsWithoutRef<'div'>
|
||||
@@ -104,15 +59,15 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (props: CreateView) => {
|
||||
const { block, editor } = props;
|
||||
const { block } = props;
|
||||
const { BlockRender } = useBlockRender();
|
||||
const collapsed = block.getProperty('collapsed')?.value;
|
||||
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 = () => {
|
||||
block.setProperty('collapsed', { value: true });
|
||||
};
|
||||
@@ -122,15 +77,8 @@ export const withTreeViewChildren = (
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockContainer
|
||||
editor={props.editor}
|
||||
block={block}
|
||||
selected={isSelect}
|
||||
className={Wrapper.toString()}
|
||||
>
|
||||
<BlockPendantProvider block={block}>
|
||||
<div>{creator(props)}</div>
|
||||
</BlockPendantProvider>
|
||||
<>
|
||||
{creator(props)}
|
||||
|
||||
{collapsed && (
|
||||
<CollapsedNode
|
||||
@@ -138,22 +86,24 @@ export const withTreeViewChildren = (
|
||||
style={{ marginLeft: config.indent }}
|
||||
/>
|
||||
)}
|
||||
{showChildren && (
|
||||
<ChildrenView
|
||||
childrenIds={childrenIds}
|
||||
handleCollapse={handleCollapse}
|
||||
indent={config.indent}
|
||||
/>
|
||||
)}
|
||||
</BlockContainer>
|
||||
{showChildren &&
|
||||
childrenIds.map((childId, idx) => {
|
||||
return (
|
||||
<TreeView
|
||||
key={childId}
|
||||
lastItem={idx === childrenIds.length - 1}
|
||||
onClick={handleCollapse}
|
||||
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';
|
||||
// adjust left and right margins of the the tree line
|
||||
const TREE_LINE_LEFT_OFFSET = '-16px';
|
||||
@@ -163,6 +113,7 @@ const TREE_LINE_WIDTH = '12px';
|
||||
|
||||
const TreeWrapper = styled('div')({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
const StyledTreeView = styled('div')({
|
||||
@@ -228,9 +179,3 @@ const LastItemRadius = styled('div')({
|
||||
borderRadius: '0 0 0 3px',
|
||||
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 './RenderBlockChildren';
|
||||
export { withTreeViewChildren } from './WithTreeViewChildren';
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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 {
|
||||
CommandMenuPlugin,
|
||||
DoubleLinkMenuPlugin,
|
||||
GroupMenuPlugin,
|
||||
InlineMenuPlugin,
|
||||
LeftMenuPlugin,
|
||||
SelectionGroupPlugin,
|
||||
} from './menu';
|
||||
import { FullTextSearchPlugin } from './search';
|
||||
import { TemplatePlugin } from './template';
|
||||
// import { PlaceholderPlugin } from './placeholder';
|
||||
|
||||
// import { BlockPropertyPlugin } from './block-property';
|
||||
@@ -19,9 +19,9 @@ export const plugins: PluginCreator[] = [
|
||||
LeftMenuPlugin,
|
||||
InlineMenuPlugin,
|
||||
CommandMenuPlugin,
|
||||
ReferenceMenuPlugin,
|
||||
DoubleLinkMenuPlugin,
|
||||
TemplatePlugin,
|
||||
SelectionGroupPlugin,
|
||||
// SelectionGroupPlugin,
|
||||
AddCommentPlugin,
|
||||
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',
|
||||
});
|
||||
+3
-4
@@ -1,12 +1,11 @@
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { BasePlugin } from '../../base-plugin';
|
||||
import { PluginRenderRoot } from '../../utils';
|
||||
import { ReferenceMenu } from './ReferenceMenu';
|
||||
import { DoubleLinkMenu } from './DoubleLinkMenu';
|
||||
|
||||
const PLUGIN_NAME = 'reference-menu';
|
||||
|
||||
export class ReferenceMenuPlugin extends BasePlugin {
|
||||
export class DoubleLinkMenuPlugin extends BasePlugin {
|
||||
private _root?: PluginRenderRoot;
|
||||
|
||||
public static override get pluginName(): string {
|
||||
@@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin {
|
||||
|
||||
this._root?.render(
|
||||
<StrictMode>
|
||||
<ReferenceMenu editor={this.editor} hooks={this.hooks} />
|
||||
<DoubleLinkMenu editor={this.editor} hooks={this.hooks} />
|
||||
</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 { 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 { GroupMenuPlugin } from './group-menu';
|
||||
export { SelectionGroupPlugin } from './selection-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 {
|
||||
TransitionsModal,
|
||||
MuiBox as Box,
|
||||
MuiBox,
|
||||
styled,
|
||||
TransitionsModal,
|
||||
} from '@toeverything/components/ui';
|
||||
import { Virgo, BlockEditor } from '@toeverything/framework/virgo';
|
||||
import { BlockEditor, Virgo } from '@toeverything/framework/virgo';
|
||||
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({
|
||||
wrapper: {
|
||||
@@ -37,9 +36,7 @@ const query_blocks = (
|
||||
search: string,
|
||||
callback: (result: QueryResult) => void
|
||||
) => {
|
||||
(editor as BlockEditor)
|
||||
.search(search)
|
||||
.then(pages => callback(pages.filter(b => !!b.content)));
|
||||
(editor as BlockEditor).search(search).then(pages => callback(pages));
|
||||
};
|
||||
|
||||
export const QueryBlocks = throttle(query_blocks, 500);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import style9 from 'style9';
|
||||
|
||||
import { SvgIconProps } from '../svg-icon';
|
||||
import { BaseButton } from './base-button';
|
||||
|
||||
@@ -35,7 +34,7 @@ const styles = style9.create({
|
||||
|
||||
type ListButtonProps = {
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
onMouseOver?: () => void;
|
||||
content?: string;
|
||||
children?: () => JSX.Element;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowRightIcon } from '@toeverything/components/icons';
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import { useRef, useState, type ReactElement } from 'react';
|
||||
import { Divider } from '../divider';
|
||||
import {
|
||||
MuiGrow as Grow,
|
||||
|
||||
@@ -35,6 +35,8 @@ export const Popper = ({
|
||||
offset = [0, 5],
|
||||
showArrow = false,
|
||||
popperHandlerRef,
|
||||
onClick,
|
||||
onClickAway,
|
||||
...popperProps
|
||||
}: PopperProps) => {
|
||||
const [anchorEl, setAnchorEl] = useState<VirtualElement>(null);
|
||||
@@ -97,15 +99,20 @@ export const Popper = ({
|
||||
return (
|
||||
<ClickAwayListener
|
||||
onClickAway={() => {
|
||||
setVisible(false);
|
||||
if (visibleControlledByParent) {
|
||||
onClickAway?.();
|
||||
} else {
|
||||
setVisible(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
{isAnchorCustom ? null : (
|
||||
<div
|
||||
ref={(dom: HTMLDivElement) => setAnchorEl(dom)}
|
||||
onClick={() => {
|
||||
onClick={e => {
|
||||
if (!hasClickTrigger || visibleControlledByParent) {
|
||||
onClick?.(e);
|
||||
return;
|
||||
}
|
||||
setVisible(!visible);
|
||||
|
||||
@@ -62,4 +62,6 @@ export type PopperProps = {
|
||||
showArrow?: boolean;
|
||||
|
||||
popperHandlerRef?: Ref<PopperHandler>;
|
||||
|
||||
onClickAway?: () => void;
|
||||
} & Omit<PopperUnstyledProps, 'open' | 'ref'>;
|
||||
|
||||
@@ -233,7 +233,10 @@ export const Theme = {
|
||||
},
|
||||
shadows: {
|
||||
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'],
|
||||
spacing: {
|
||||
|
||||
@@ -168,6 +168,7 @@ export class Database {
|
||||
block.on('children', observer_name, listener);
|
||||
block.on('content', observer_name, listener);
|
||||
block.on('parent', observer_name, listener);
|
||||
block.on('cascade', observer_name, listener);
|
||||
}
|
||||
this.#observers.setStatus(observer_name, 'observing');
|
||||
|
||||
@@ -188,6 +189,7 @@ export class Database {
|
||||
block.off('children', observer_name);
|
||||
block.off('content', observer_name);
|
||||
block.off('parent', observer_name);
|
||||
block.off('cascade', observer_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const groupTemplateMap = {
|
||||
const defaultTemplateList: Array<TemplateMeta> = [
|
||||
{
|
||||
name: 'New From Quick Start',
|
||||
groupKeys: ['todolist'],
|
||||
groupKeys: ['getStartedGroup0', 'getStartedGroup1', 'getStartedGroup2'],
|
||||
},
|
||||
{ name: 'New From Grid System', groupKeys: ['grid'] },
|
||||
{ name: 'New From Blog', groupKeys: ['blog'] },
|
||||
|
||||
@@ -53,6 +53,7 @@ export type DefaultColumnsValue = {
|
||||
filterConstraint: FilterConstraint;
|
||||
filterWeakSqlConstraint: string;
|
||||
sorterConstraint: SorterConstraint;
|
||||
bindings: StringColumnValue;
|
||||
};
|
||||
|
||||
export const DEFAULT_COLUMN_KEYS = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user