Compare commits

...

28 Commits

Author SHA1 Message Date
UNIDY
914e4baf82 fix: according to graphite-app's suggestions 2025-04-29 17:32:46 +08:00
UNIDY
bd268044b4 feat: implement textAlign property for paragraph blocks, image blocks, list blocks, and table blocks 2025-04-29 17:32:46 +08:00
doodlewind
b5edd7a6bb test(editor): reduce flaky test in turbo renderer (#12053)
Flaky source: https://github.com/toeverything/AFFiNE/actions/runs/14708000170/job/41272973259

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Tests**
  - Standardized test wait durations to 16ms for improved consistency across viewport turbo renderer tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 08:34:01 +00:00
doodlewind
34039bc7d8 refactor(editor): use default fallback placeholder for turbo renderer (#12059)
Based on this PR, all block types support zooming placeholder now.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/33a32735-d31e-4055-9dbf-faaed444a6d2.png)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Improvements**
  - Enhanced layout rendering accuracy for non-root nodes, leading to more precise placement and sizing.
  - Simplified placeholder painting logic for improved consistency, with all nodes now displayed using a color based on their depth.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 08:18:57 +00:00
fundon
0f87136fd7 refactor(editor): file size limit service (#12026)
Closes: [BS-3359](https://linear.app/affine-design/issue/BS-3359/重构-filesizelimitservice-支持-handle-文件超出限制)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added an "Upgrade" button to attachment blocks that appears when file size limits are exceeded, enabling users to view storage plans and upgrade.

- **Refactor**
  - Unified file size limit handling across attachments and images for consistency.
  - Redesigned file size limit service with improved integration and dependency injection.

- **Chores**
  - Updated service registrations and dependency management for file size limit enforcement.
  - Integrated a new file size limit extension that triggers storage plan dialogs and tracking events on limit exceedance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 08:05:28 +00:00
Saul-Mirone
7e4af90c03 refactor(editor): cleanup dead code (#12049)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Removed internal utilities related to connector management and tree structure traversal. No changes to user-facing features or functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 07:51:11 +00:00
congzhou09
f5f7cbb105 fix(server): read the .env file on time (#12051) 2025-04-29 15:48:17 +08:00
donteatfriedrice
83670ab335 feat(editor): add experimental feature citation (#11984)
Closes: [BS-3122](https://linear.app/affine-design/issue/BS-3122/footnote-definition-adapter-适配)
Closes: [BS-3123](https://linear.app/affine-design/issue/BS-3123/几个-block-card-view-适配-footnote-态)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new citation card component and web element for displaying citations.
  - Added support for citation-style rendering in attachment, bookmark, and linked document blocks.
  - Enabled citation parsing from footnote definitions in markdown for attachments, bookmarks, and linked docs.
  - Added a feature flag to enable or disable citation features.
  - Provided new toolbar logic to disable downloads for citation-style attachments.

- **Improvements**
  - Updated block models and properties to support citation identifiers.
  - Added localization and settings for the citation experimental feature.
  - Enhanced markdown adapters to recognize and process citation footnotes.
  - Included new constants and styles for citation card display.

- **Bug Fixes**
  - Ensured readonly state is respected in block interactions and rendering for citation blocks.

- **Documentation**
  - Added exports and effects for new citation components and features.

- **Tests**
  - Updated snapshots to include citation-related properties in block data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 06:59:27 +00:00
pengx17
a326eac1bb fix(electron): shared-worker api binding (#11991)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved security by adding cross-origin isolation headers to HTTP responses in the desktop app.

- **Bug Fixes**
  - Enhanced reliability of worker connections by ensuring workers are fully loaded before establishing communication.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 14:12:25 +08:00
L-Sun
8b402dd49a fix(editor): improve viewport of surface ref block (#12014)
Close [BS-3339](https://linear.app/affine-design/issue/BS-3339/一个frame插入到page之后大的离谱)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Improvements**
  - Enhanced the accuracy and responsiveness of viewport and reference content rendering for surface references.
  - Adjusted padding for a more consistent viewing experience.
  - Improved handling of referenced elements to ensure smoother updates and display.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 03:39:02 +00:00
Saul-Mirone
e96fcf0c35 refactor(editor): remove page root service (#12048) 2025-04-29 03:19:37 +00:00
Saul-Mirone
4c84e6bac7 feat(editor): gfx link extension (#12046)
Closes: BS-3368

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a new link tool extension, enabling enhanced link-related functionality within the edgeless workspace.
  - Added a new view extension for link tools, improving integration and usability in edgeless mode.

- **Chores**
  - Added a new package for link tool functionality with appropriate dependencies and exports.
  - Registered new custom elements for edgeless toolbars and link tools to support modular UI components.
  - Updated project configurations and workspace dependencies to include the new link tool module.

- **Refactor**
  - Removed unused quick tool exports and toolbar component registrations to streamline the edgeless extension codebase.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 03:19:37 +00:00
doodlewind
be28038e94 perf(editor): fallback to placeholder for canvas text (#12033)
### TL;DR

For canvas elements, this PR adds placeholders during zooming operations to improve performance.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/8c8daea8-1eb4-419b-a4f4-2a8847f40b7b.png)

### What changed?

- Implemented placeholder rendering during zooming operations in the canvas renderer, but not only DOM.
- Added a `forceFullRender` property to the `GfxCompatibleInterface` to allow elements to opt out of placeholder rendering
- Set `forceFullRender = true` for connectors to ensure they always render properly, even during zooming
- Connected the turbo renderer to the viewport's zooming state to automatically switch between full and placeholder rendering

### Why make this change?

Rendering complex elements during zooming operations can cause performance issues and make the UI feel sluggish. Rendering connector label also leads to high cost DOM `set font` delays.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/961fb847-24b4-4a7f-b9dc-21b0a5edaaa1.png)

The turbo renderer improves performance by displaying simple placeholders for elements during zooming, while still rendering critical elements like connectors fully. This creates a smoother user experience while maintaining essential visual information.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced a feature-flag-controlled "turbo" rendering mode that displays placeholder graphics during zooming for improved performance.
  - Added the ability to override placeholder rendering for specific elements, ensuring full rendering when required.

- **Bug Fixes**
  - Enhanced rendering logic to ensure connectors always render fully, even during zoom operations.

- **Documentation**
  - Updated API documentation to reflect new properties related to rendering behavior.

- **Tests**
  - Improved tests to verify correct rendering behavior for connectors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 03:05:17 +00:00
Saul-Mirone
d82d37b53d feat(editor): add page dragging area widget extension (#12045)
Closes: BS-3364

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new "Page Dragging Area" widget, enabling enhanced block selection and drag area detection within the user interface.
  - Added utilities for more precise block selection based on rectangular selection areas.

- **Improvements**
  - Integrated the new widget into the view extension system for consistent behavior across supported views.
  - Enhanced clipboard handling with comprehensive adapter configurations for various data types.

- **Refactor**
  - Streamlined widget registration and block selection logic for improved maintainability and modularity.
  - Removed legacy widget exports and registrations to centralize widget management.

- **Chores**
  - Updated workspace and TypeScript configurations to support the new widget module.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 01:45:00 +00:00
renovate
f177c64ca1 chore: bump up electron version to v36 (#12047)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [electron](https://redirect.github.com/electron/electron) | [`^35.0.0` -> `^36.0.0`](https://renovatebot.com/diffs/npm/electron/35.2.1/36.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/electron/36.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/electron/36.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/electron/35.2.1/36.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/electron/35.2.1/36.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>electron/electron (electron)</summary>

### [`v36.0.0`](https://redirect.github.com/electron/electron/releases/tag/v36.0.0): electron v36.0.0

[Compare Source](https://redirect.github.com/electron/electron/compare/v35.2.1...v36.0.0)

### Release Notes for v36.0.0

#### Fixes

-   Fixed `electron.shell.openExternal` and `electron.shell.openPath` to honor user-defined system defaults on Linux. [#&#8203;46788](https://redirect.github.com/electron/electron/pull/46788) <span style="font-size:small;">(Also in [33](https://redirect.github.com/electron/electron/pull/46787), [34](https://redirect.github.com/electron/electron/pull/46791), [35](https://redirect.github.com/electron/electron/pull/46789))</span>
-   Fixed `getNativeWindowHandle()` crash that affected 36 betas on macOS. [#&#8203;46750](https://redirect.github.com/electron/electron/pull/46750)
-   Fixed a possible crash when using `navigator.bluetooth.requestDevice` and the `select-bluetooth-device` event. [#&#8203;46782](https://redirect.github.com/electron/electron/pull/46782) <span style="font-size:small;">(Also in [34](https://redirect.github.com/electron/electron/pull/46783), [35](https://redirect.github.com/electron/electron/pull/46784))</span>
-   Fixed a potential crash when closing a window with child windows. [#&#8203;46773](https://redirect.github.com/electron/electron/pull/46773) <span style="font-size:small;">(Also in [34](https://redirect.github.com/electron/electron/pull/46775), [35](https://redirect.github.com/electron/electron/pull/46774))</span>
-   Fixed an issue where the backgroundMaterial feature did not work in a frameless window on initial window creation. [#&#8203;46792](https://redirect.github.com/electron/electron/pull/46792)
-   Fixed build error with `enable_electron_extensions=false`. [#&#8203;46842](https://redirect.github.com/electron/electron/pull/46842) <span style="font-size:small;">(Also in [35](https://redirect.github.com/electron/electron/pull/46840))</span>
-   Fixed crash when renderer process crashes while webview is reloading. [#&#8203;46768](https://redirect.github.com/electron/electron/pull/46768) <span style="font-size:small;">(Also in [34](https://redirect.github.com/electron/electron/pull/46770), [35](https://redirect.github.com/electron/electron/pull/46769))</span>
-   Fixed documentation to mark `Window.autoHideMenuBar` as supported on Linux and Windows. [#&#8203;46830](https://redirect.github.com/electron/electron/pull/46830) <span style="font-size:small;">(Also in [35](https://redirect.github.com/electron/electron/pull/46829))</span>
-   Fixed the border style of windows with vibrancy on macOS. [#&#8203;46771](https://redirect.github.com/electron/electron/pull/46771) <span style="font-size:small;">(Also in [35](https://redirect.github.com/electron/electron/pull/46772))</span>

#### Other Changes

-   Updated Chromium to 136.0.7103.48. [#&#8203;46756](https://redirect.github.com/electron/electron/pull/46756)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yNTcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjI1Ny4zIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-29 01:24:51 +00:00
fundon
362f89b669 feat(editor): adjust attachment block UI (#11763)
Closes: [BS-3143](https://linear.app/affine-design/issue/BS-3143/更新-attachment-错误样式)
Closes: [BS-3341](https://linear.app/affine-design/issue/BS-3341/attachment-select状态ui调整)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved attachment block with unified reactive state handling for loading, errors, and downloads.
  - Enhanced card layouts with modular rendering and dynamic buttons based on state.

- **Bug Fixes**
  - Fixed UI state for "Embed view" action, now correctly disabled when not embedded.
  - Resolved attachment card styling issues and text overflow with new utility classes.

- **Refactor**
  - Streamlined attachment block rendering and state management for better performance and maintainability.
  - Updated toolbar configuration for consistent parameter naming.
  - Simplified embedded and open methods for improved clarity.
  - Made internal functions private to reduce exported API surface.

- **Chores**
  - Removed unused icon export and legacy upload tracking functions.
  - Updated attachment utilities to use reactive state, added data refresh, and improved error handling.
  - Refined image dimension handling for consistent resizing behavior.
  - Improved test selectors to target focused attachment containers for better reliability.
  - Refactored CSS to separate container and card styling and adopt theme-based colors consistently.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-29 01:00:56 +00:00
Saul-Mirone
df565f2fbf refact(editor): clipboard config should be in foundation (#12038)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Enhanced clipboard support with multiple adapter configurations for various MIME types, improving clipboard handling and compatibility.

- **Refactor**
  - Relocated clipboard adapter configurations to a new location for better organization and maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 15:32:31 +00:00
Saul-Mirone
468db9f3eb feat(editor): edgeless zoom toolbar widget extension (#12037)
Closes: BS-3363
2025-04-28 14:38:26 +00:00
Saul-Mirone
4e201ede17 feat(editor): viewport overlay widget extension (#12035)
Closes: BS-3360

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new viewport overlay widget, making it available as part of the workspace and enabling its integration into supported pages.

- **Refactor**
  - Updated internal imports and exports to utilize the new viewport overlay widget package.
  - Streamlined widget registration and extension mechanisms for improved modularity.

- **Chores**
  - Added configuration and project references to support the new viewport overlay widget package in the build system.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 14:38:26 +00:00
akumatus
d7be1b3424 fix(core): skip onboarding in e2e tests (#12044)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Chores**
	- Updated test setup to automatically skip onboarding steps during environment initialization.
	- Simplified test utility methods by removing notification handling logic from chat panel and editor mode switching processes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 14:25:28 +00:00
L-Sun
d57b9372ae fix(editor): edgeless note duplicated from embed-doc should be above other elements (#12028)
Close [BS-3357](https://linear.app/affine-design/issue/BS-3357/duplicate-note-注意设置z轴的数据)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Duplicating an edgeless note now ensures the new note appears above other elements in the stacking order.

- **Tests**
  - Added a test to verify that duplicated edgeless notes are rendered above existing elements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 14:06:41 +00:00
akumatus
c555cca6a1 feat(core): use claude-3-7-sonnet as basic chat model (#12036)
Close [AI-59](https://linear.app/affine-design/issue/AI-59)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added support for two new providers, Anthropic and Exa, in test environments and configurations.
- **Chores**
  - Updated workflow and test environment setup to include API keys for Anthropic and Exa providers.
- **Refactor**
  - Changed the AI model used for the "Chat With AFFiNE AI" prompt to "claude-3-7-sonnet-20250219".

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 21:28:34 +08:00
forehalo
4662ee8da7 chore: add crossorigin to resource tags (#12031)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added the crossorigin attribute to all CSS and JavaScript tags in generated HTML, improving compatibility for cross-origin resource loading.

- **Tests**
  - Updated tests to verify the presence of the crossorigin attribute in script tags.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 12:25:13 +00:00
darkskygit
21dc550b9d feat(server): add doc meta for ignored docs (#12021)
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Ignored documents in workspace embedding now display additional metadata, including document title, creation and update timestamps, and the names and avatars of users who created or updated the document.
- **Enhancements**
  - The list of ignored documents provides richer information for easier identification and management within the workspace.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 11:56:09 +00:00
renovate
9d21d13a5e chore: bump up eslint-plugin-unicorn version to v59 (#12013)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [eslint-plugin-unicorn](https://redirect.github.com/sindresorhus/eslint-plugin-unicorn) | [`^58.0.0` -> `^59.0.0`](https://renovatebot.com/diffs/npm/eslint-plugin-unicorn/58.0.0/59.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-unicorn/59.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/eslint-plugin-unicorn/59.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/eslint-plugin-unicorn/58.0.0/59.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-unicorn/58.0.0/59.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>sindresorhus/eslint-plugin-unicorn (eslint-plugin-unicorn)</summary>

### [`v59.0.0`](https://redirect.github.com/sindresorhus/eslint-plugin-unicorn/compare/v58.0.0...3838ec815057154a7fb4cd8257abfb554502ba2f)

[Compare Source](https://redirect.github.com/sindresorhus/eslint-plugin-unicorn/compare/v58.0.0...v59.0.0)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yNTcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjI1Ny4zIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-28 10:56:43 +00:00
renovate
a61c5fd458 chore: bump up oxlint version to v0.16.8 (#12011)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.16.7` -> `0.16.8`](https://renovatebot.com/diffs/npm/oxlint/0.16.7/0.16.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.16.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.16.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.16.7/0.16.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.16.7/0.16.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.16.8`](https://redirect.github.com/oxc-project/oxc/releases/tag/oxlint_v0.16.8): oxlint v0.16.8

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.16.7...oxlint_v0.16.8)

#### \[0.16.8] - 2025-04-27

##### Features

-   [`53394a7`](https://redirect.github.com/oxc-project/oxc/commit/53394a7) linter: Add auto-fix for eslint/require-await ([#&#8203;10624](https://redirect.github.com/oxc-project/oxc/issues/10624)) (yefan)
-   [`6908bc3`](https://redirect.github.com/oxc-project/oxc/commit/6908bc3) linter: Add autofix for react/self-closing-comp ([#&#8203;10512](https://redirect.github.com/oxc-project/oxc/issues/10512)) (x6eull)
-   [`e228840`](https://redirect.github.com/oxc-project/oxc/commit/e228840) parser: Fast forward lexer to EOF if errors are encountered ([#&#8203;10579](https://redirect.github.com/oxc-project/oxc/issues/10579)) (Boshen)

##### Bug Fixes

-   [`966fb03`](https://redirect.github.com/oxc-project/oxc/commit/966fb03) editor: Fix memory leaks when server or watchers restarted ([#&#8203;10628](https://redirect.github.com/oxc-project/oxc/issues/10628)) (Sysix)
-   [`f3eac51`](https://redirect.github.com/oxc-project/oxc/commit/f3eac51) language_server: Fix max integer values for range position ([#&#8203;10623](https://redirect.github.com/oxc-project/oxc/issues/10623)) (Alexander S.)
-   [`d309e07`](https://redirect.github.com/oxc-project/oxc/commit/d309e07) language_server: Fix panics when paths contains specials characters like `[` or `]` ([#&#8203;10622](https://redirect.github.com/oxc-project/oxc/issues/10622)) (Alexander S.)
-   [`91ce77a`](https://redirect.github.com/oxc-project/oxc/commit/91ce77a) language_server: Temporary ignore tests that panic on Windows ([#&#8203;10583](https://redirect.github.com/oxc-project/oxc/issues/10583)) (Yuji Sugiura)
-   [`723b4c6`](https://redirect.github.com/oxc-project/oxc/commit/723b4c6) linter: Cross_module of LintService not being enabled despite enabled import plugin ([#&#8203;10597](https://redirect.github.com/oxc-project/oxc/issues/10597)) (Ulrich Stark)
-   [`39adefe`](https://redirect.github.com/oxc-project/oxc/commit/39adefe) linter: Handle re-exporting of type correctly in `import/no-cycle` ([#&#8203;10606](https://redirect.github.com/oxc-project/oxc/issues/10606)) (Ulrich Stark)
-   [`e67901b`](https://redirect.github.com/oxc-project/oxc/commit/e67901b) linter: Incorrect fix for prefer start ends with ([#&#8203;10533](https://redirect.github.com/oxc-project/oxc/issues/10533)) (camc314)
-   [`7c85ae7`](https://redirect.github.com/oxc-project/oxc/commit/7c85ae7) linter/no-empty-function: Support 'allow' option ([#&#8203;10605](https://redirect.github.com/oxc-project/oxc/issues/10605)) (Don Isaac)
-   [`9a02066`](https://redirect.github.com/oxc-project/oxc/commit/9a02066) oxlint: Current dir as arg ([#&#8203;9382](https://redirect.github.com/oxc-project/oxc/issues/9382)) (Ben Jones)
-   [`a9785e3`](https://redirect.github.com/oxc-project/oxc/commit/a9785e3) parser,linter: Consider typescript declarations for named exports ([#&#8203;10532](https://redirect.github.com/oxc-project/oxc/issues/10532)) (Ulrich Stark)

##### Performance

-   [`3c27d0d`](https://redirect.github.com/oxc-project/oxc/commit/3c27d0d) editor: Avoid sending `workspace/didChangeConfiguration` request when the server needs a restarts ([#&#8203;10550](https://redirect.github.com/oxc-project/oxc/issues/10550)) (Sysix)

##### Refactor

-   [`e903ba2`](https://redirect.github.com/oxc-project/oxc/commit/e903ba2) editor: Split Config to VSCodeConfig and WorkspaceConfig ([#&#8203;10572](https://redirect.github.com/oxc-project/oxc/issues/10572)) (Sysix)
-   [`f6c6969`](https://redirect.github.com/oxc-project/oxc/commit/f6c6969) language_server: Make linter independent of `Backend` ([#&#8203;10497](https://redirect.github.com/oxc-project/oxc/issues/10497)) (Sysix)
-   [`db05a15`](https://redirect.github.com/oxc-project/oxc/commit/db05a15) language_server: Do not request for worspace configuration when the client does not support it ([#&#8203;10507](https://redirect.github.com/oxc-project/oxc/issues/10507)) (Sysix)
-   [`9f9e0e5`](https://redirect.github.com/oxc-project/oxc/commit/9f9e0e5) language_server: Move code actions into own file ([#&#8203;10479](https://redirect.github.com/oxc-project/oxc/issues/10479)) (Sysix)

##### Testing

-   [`9f43a58`](https://redirect.github.com/oxc-project/oxc/commit/9f43a58) language_server: Fix broken tests in windows ([#&#8203;10600](https://redirect.github.com/oxc-project/oxc/issues/10600)) (Sysix)
-   [`8a2b250`](https://redirect.github.com/oxc-project/oxc/commit/8a2b250) linter: Fix incorrect test fixture for prefer-each ([#&#8203;10587](https://redirect.github.com/oxc-project/oxc/issues/10587)) (Boshen)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yNTcuMyIsInVwZGF0ZWRJblZlciI6IjM5LjI1Ny4zIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5IiwibGFiZWxzIjpbImRlcGVuZGVuY2llcyJdfQ==-->
2025-04-28 10:43:07 +00:00
akumatus
cf5574caf6 refactor(core): use the websearch parameters passed in by the front-end (#11989)
Support [AI-60](https://linear.app/affine-design/issue/AI-60).

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Updated naming conventions for the web search tool to ensure consistency across AI chat features.
  - Simplified internal handling of web search tool options for a more streamlined experience.
  - Unified parameter naming from "mustSearch" to "webSearch" across AI chat inputs and actions.
- **Chores**
  - Removed unused configuration options related to web search from prompt settings.
  - Added support for enabling or disabling web search via new parameters in chat requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-04-28 16:26:56 +08:00
Hwang
e366f69707 build(ios): remove build mac(design for iPad) destination (#11983) 2025-04-28 16:19:44 +08:00
196 changed files with 2937 additions and 1209 deletions

View File

@@ -24,8 +24,10 @@ runs:
- name: Import config
shell: bash
run: |
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"}}}' \
printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
"$COPILOT_FAL_API_KEY" \
"$COPILOT_GOOGLE_API_KEY" \
"$COPILOT_OPENAI_API_KEY" \
"$COPILOT_PERPLEXITY_API_KEY" > ./packages/backend/server/config.json
"$COPILOT_PERPLEXITY_API_KEY" \
"$COPILOT_ANTHROPIC_API_KEY" \
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json

View File

@@ -894,6 +894,8 @@ jobs:
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -991,6 +993,8 @@ jobs:
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -81,6 +81,8 @@ jobs:
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run server tests
@@ -150,6 +152,8 @@ jobs:
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -38,6 +38,7 @@
"@blocksuite/affine-gfx-brush": "workspace:*",
"@blocksuite/affine-gfx-connector": "workspace:*",
"@blocksuite/affine-gfx-group": "workspace:*",
"@blocksuite/affine-gfx-link": "workspace:*",
"@blocksuite/affine-gfx-mindmap": "workspace:*",
"@blocksuite/affine-gfx-note": "workspace:*",
"@blocksuite/affine-gfx-pointer": "workspace:*",
@@ -57,13 +58,16 @@
"@blocksuite/affine-widget-drag-handle": "workspace:*",
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-page-dragging-area": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
"@blocksuite/affine-widget-toolbar": "workspace:*",
"@blocksuite/affine-widget-viewport-overlay": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/std": "workspace:*",
@@ -184,6 +188,10 @@
"./widgets/toolbar/view": "./src/widgets/toolbar/view.ts",
"./widgets/keyboard-toolbar": "./src/widgets/keyboard-toolbar/index.ts",
"./widgets/keyboard-toolbar/view": "./src/widgets/keyboard-toolbar/view.ts",
"./widgets/viewport-overlay": "./src/widgets/viewport-overlay/index.ts",
"./widgets/viewport-overlay/view": "./src/widgets/viewport-overlay/view.ts",
"./widgets/page-dragging-area": "./src/widgets/page-dragging-area/index.ts",
"./widgets/page-dragging-area/view": "./src/widgets/page-dragging-area/view.ts",
"./fragments/doc-title": "./src/fragments/doc-title.ts",
"./fragments/frame-panel": "./src/fragments/frame-panel.ts",
"./fragments/outline": "./src/fragments/outline.ts",
@@ -198,6 +206,8 @@
"./gfx/shape": "./src/gfx/shape/index.ts",
"./gfx/shape/store": "./src/gfx/shape/store.ts",
"./gfx/shape/view": "./src/gfx/shape/view.ts",
"./gfx/link": "./src/gfx/link/index.ts",
"./gfx/link/view": "./src/gfx/link/view.ts",
"./gfx/note": "./src/gfx/note/index.ts",
"./gfx/note/view": "./src/gfx/note/view.ts",
"./gfx/mindmap": "./src/gfx/mindmap/index.ts",
@@ -216,6 +226,7 @@
"./components/block-zero-width": "./src/components/block-zero-width.ts",
"./components/caption": "./src/components/caption.ts",
"./components/card-style-dropdown-menu": "./src/components/card-style-dropdown-menu.ts",
"./components/citation": "./src/components/citation.ts",
"./components/color-picker": "./src/components/color-picker.ts",
"./components/context-menu": "./src/components/context-menu.ts",
"./components/date-picker": "./src/components/date-picker.ts",

View File

@@ -1,3 +1,4 @@
import { AttachmentBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-attachment';
import { BookmarkBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-bookmark';
import { CodeBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-code';
import { DatabaseBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-database';
@@ -38,4 +39,5 @@ export const defaultBlockMarkdownAdapterMatchers = [
DividerBlockMarkdownAdapterExtension,
ImageBlockMarkdownAdapterExtension,
LatexBlockMarkdownAdapterExtension,
AttachmentBlockMarkdownAdapterExtension,
];

View File

@@ -21,6 +21,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation';
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
@@ -154,6 +155,7 @@ export function effects() {
componentEmbedCardModalEffects();
componentLinkPreviewEffects();
componentLinkedDocTitleEffects();
componentCitationEffects();
componentCardStyleDropdownMenuEffects();
componentHighlightDropdownMenuEffects();
componentViewDropdownMenuEffects();

View File

@@ -20,6 +20,7 @@ import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
import { BrushViewExtension } from '@blocksuite/affine-gfx-brush/view';
import { ConnectorViewExtension } from '@blocksuite/affine-gfx-connector/view';
import { GroupViewExtension } from '@blocksuite/affine-gfx-group/view';
import { LinkViewExtension as GfxLinkViewExtension } from '@blocksuite/affine-gfx-link/view';
import { MindmapViewExtension } from '@blocksuite/affine-gfx-mindmap/view';
import { NoteViewExtension as GfxNoteViewExtension } from '@blocksuite/affine-gfx-note/view';
import { PointerViewExtension } from '@blocksuite/affine-gfx-pointer/view';
@@ -35,13 +36,16 @@ import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view
import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view';
import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view';
import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view';
import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view';
import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view';
import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view';
import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
import { SlashMenuViewExtension } from '@blocksuite/affine-widget-slash-menu/view';
import { ToolbarViewExtension } from '@blocksuite/affine-widget-toolbar/view';
import { ViewportOverlayViewExtension } from '@blocksuite/affine-widget-viewport-overlay/view';
import { MigratingViewExtension } from './migrating-view';
@@ -59,6 +63,7 @@ export function getInternalViewExtensions() {
GroupViewExtension,
TextViewExtension,
TemplateViewExtension,
GfxLinkViewExtension,
// Block
AttachmentViewExtension,
@@ -100,5 +105,8 @@ export function getInternalViewExtensions() {
ScrollAnchoringViewExtension,
SlashMenuViewExtension,
ToolbarViewExtension,
ViewportOverlayViewExtension,
EdgelessZoomToolbarViewExtension,
PageDraggingAreaViewExtension,
];
}

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-gfx-link';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-gfx-link/view';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-zoom-toolbar';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-page-dragging-area';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-page-dragging-area/view';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-viewport-overlay';

View File

@@ -0,0 +1 @@
export * from '@blocksuite/affine-widget-viewport-overlay/view';

View File

@@ -35,6 +35,7 @@
{ "path": "../gfx/brush" },
{ "path": "../gfx/connector" },
{ "path": "../gfx/group" },
{ "path": "../gfx/link" },
{ "path": "../gfx/mindmap" },
{ "path": "../gfx/note" },
{ "path": "../gfx/pointer" },
@@ -54,13 +55,16 @@
{ "path": "../widgets/drag-handle" },
{ "path": "../widgets/edgeless-auto-connect" },
{ "path": "../widgets/edgeless-toolbar" },
{ "path": "../widgets/edgeless-zoom-toolbar" },
{ "path": "../widgets/frame-title" },
{ "path": "../widgets/keyboard-toolbar" },
{ "path": "../widgets/linked-doc" },
{ "path": "../widgets/page-dragging-area" },
{ "path": "../widgets/remote-selection" },
{ "path": "../widgets/scroll-anchoring" },
{ "path": "../widgets/slash-menu" },
{ "path": "../widgets/toolbar" },
{ "path": "../widgets/viewport-overlay" },
{ "path": "../data-view" },
{ "path": "../../framework/global" },
{ "path": "../../framework/std" },

View File

@@ -0,0 +1,9 @@
import type { ExtensionType } from '@blocksuite/store';
import { AttachmentBlockMarkdownAdapterExtension } from './markdown.js';
import { AttachmentBlockNotionHtmlAdapterExtension } from './notion-html.js';
export const AttachmentBlockAdapterExtensions: ExtensionType[] = [
AttachmentBlockNotionHtmlAdapterExtension,
AttachmentBlockMarkdownAdapterExtension,
];

View File

@@ -0,0 +1,2 @@
export * from './markdown.js';
export * from './notion-html.js';

View File

@@ -0,0 +1,93 @@
import {
AttachmentBlockSchema,
FootNoteReferenceParamsSchema,
} from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
FOOTNOTE_DEFINITION_PREFIX,
getFootnoteDefinitionText,
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
if (!isFootnoteDefinitionNode(node)) return false;
const footnoteDefinition = getFootnoteDefinitionText(node);
try {
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
return (
footnoteDefinitionJson.type === 'attachment' &&
!!footnoteDefinitionJson.blobId
);
} catch {
return false;
}
};
export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
{
flavour: AttachmentBlockSchema.model.flavour,
toMatch: o => isAttachmentFootnoteDefinitionNode(o.node),
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return;
}
const { walkerContext, configs } = context;
const footnoteIdentifier = o.node.identifier;
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
const footnoteDefinition = configs.get(footnoteDefinitionKey);
if (!footnoteDefinition) {
return;
}
try {
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
const { blobId, fileName } = footnoteDefinitionJson;
if (!blobId || !fileName) {
return;
}
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: AttachmentBlockSchema.model.flavour,
props: {
name: fileName,
sourceId: blobId,
footnoteIdentifier,
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren();
} catch (err) {
console.warn('Failed to parse attachment footnote definition:', err);
return;
}
},
},
fromBlockSnapshot: {},
};
export const AttachmentBlockMarkdownAdapterExtension =
BlockMarkdownAdapterExtension(attachmentBlockMarkdownAdapterMatcher);

View File

@@ -1,9 +1,9 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import {
AttachmentIcon16,
getAttachmentFileIcon,
} from '@blocksuite/affine-components/icons';
CaptionedBlockComponent,
SelectedStyle,
} from '@blocksuite/affine-components/caption';
import { getAttachmentFileIcon } from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import {
@@ -11,21 +11,31 @@ import {
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
FileSizeLimitService,
FileSizeLimitProvider,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import {
AttachmentIcon,
ResetIcon,
UpgradeIcon,
WarningIcon,
} from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std';
import { Slice } from '@blocksuite/store';
import { html } from 'lit';
import { type BlobState } from '@blocksuite/sync';
import { effect, signal } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { choose } from 'lit/directives/choose.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { AttachmentEmbedProvider } from './embed';
import { styles } from './styles';
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils';
import { downloadAttachmentBlob, refreshData } from './utils';
type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
@Peekable({
enableOn: ({ model }: AttachmentBlockComponent) => {
return !model.doc.readonly && model.props.type.endsWith('pdf');
@@ -36,6 +46,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
blockDraggable = true;
blobState$ = signal<Partial<BlobState>>({});
protected containerStyleMap = styleMap({
position: 'relative',
width: '100%',
@@ -43,7 +55,11 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
});
private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize;
return this.std.get(FileSizeLimitProvider).maxFileSize;
}
get isCitation() {
return !!this.model.props.footnoteIdentifier;
}
convertTo = () => {
@@ -63,26 +79,45 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
};
embedded = () => {
return this.std
.get(AttachmentEmbedProvider)
.embedded(this.model, this._maxFileSize);
return (
Boolean(this.blobUrl) &&
this.std
.get(AttachmentEmbedProvider)
.embedded(this.model, this._maxFileSize)
);
};
open = () => {
if (!this.blobUrl) {
return;
}
window.open(this.blobUrl, '_blank');
const blobUrl = this.blobUrl;
if (!blobUrl) return;
window.open(blobUrl, '_blank');
};
refreshData = () => {
checkAttachmentBlob(this).catch(console.error);
refreshData(this.std, this).catch(console.error);
};
updateBlobState(state: Partial<BlobState>) {
this.blobState$.value = { ...this.blobState$.value, ...state };
}
determineState = (
loading: boolean,
uploading: boolean,
overSize: boolean,
error: boolean
): State => {
if (overSize) return 'oversize';
if (error) return 'warning';
if (uploading) return 'uploading';
if (loading) return 'loading';
return 'none';
};
protected get embedView() {
return this.std
.get(AttachmentEmbedProvider)
.render(this.model, this.blobUrl, this._maxFileSize);
.render(this.model, this.blobUrl ?? undefined, this._maxFileSize);
}
private _selectBlock() {
@@ -96,38 +131,44 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
override connectedCallback() {
super.connectedCallback();
this.refreshData();
this.contentEditable = 'false';
if (!this.model.props.style) {
this.refreshData();
this.disposables.add(
effect(() => {
const blobId = this.model.props.sourceId$.value;
if (!blobId) return;
const blobState$ = this.std.store.blobSync.blobState$(blobId);
if (!blobState$) return;
const subscription = blobState$.subscribe(state => {
if (state.overSize || state.errorMessage) {
state.uploading = false;
state.downloading = false;
}
this.updateBlobState(state);
});
return () => subscription.unsubscribe();
})
);
if (!this.model.props.style && !this.doc.readonly) {
this.doc.withoutTransact(() => {
this.doc.updateBlock(this.model, {
style: AttachmentBlockStyles[1],
});
});
}
this.model.propsUpdated.subscribe(({ key }) => {
if (key === 'sourceId') {
// Reset the blob url when the sourceId is changed
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
this.blobUrl = undefined;
}
this.refreshData();
}
});
// Workaround for https://github.com/toeverything/blocksuite/issues/4724
this.disposables.add(
this.std.get(ThemeProvider).theme$.subscribe(() => this.requestUpdate())
);
}
override disconnectedCallback() {
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
const blobUrl = this.blobUrl;
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
}
super.disconnectedCallback();
}
@@ -148,71 +189,207 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}
}
override renderBlock() {
protected renderUpgradeButton = () => {
if (this.std.store.readonly) return null;
const onOverFileSize = this.std.get(FileSizeLimitProvider).onOverFileSize;
return when(
onOverFileSize,
() => html`
<button
class="affine-attachment-content-button"
@click=${(event: MouseEvent) => {
event.stopPropagation();
onOverFileSize?.();
}}
>
${UpgradeIcon()} Upgrade
</button>
`
);
};
protected renderReloadButton = () => {
return html`
<button
class="affine-attachment-content-button"
@click=${(event: MouseEvent) => {
event.stopPropagation();
this.refreshData();
}}
>
${ResetIcon()} Reload
</button>
`;
};
protected renderWithHorizontal(
classInfo: ClassInfo,
icon: TemplateResult,
title: string,
description: string,
kind: TemplateResult,
state: State
) {
return html`<div class=${classMap(classInfo)}>
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
</div>
<div class="affine-attachment-content-description">
<div class="affine-attachment-content-info truncate">
${description}
</div>
${choose(state, [
['oversize', this.renderUpgradeButton],
['warning', this.renderReloadButton],
])}
</div>
</div>
<div class="affine-attachment-banner">${kind}</div>
</div>`;
}
protected renderWithVertical(
classInfo: ClassInfo,
icon: TemplateResult,
title: string,
description: string,
kind: TemplateResult,
state?: State
) {
return html`<div class=${classMap(classInfo)}>
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
</div>
<div class="affine-attachment-content-info truncate">
${description}
</div>
</div>
<div class="affine-attachment-banner">
${kind}
${choose(state, [
['oversize', this.renderUpgradeButton],
['warning', this.renderReloadButton],
])}
</div>
</div>`;
}
protected renderCard = () => {
const { name, size, style } = this.model.props;
const cardStyle = style ?? AttachmentBlockStyles[1];
const theme = this.std.get(ThemeProvider).theme;
const theme = this.std.get(ThemeProvider).theme$.value;
const { LoadingIcon } = getEmbedCardIcons(theme);
const titleIcon = this.loading ? LoadingIcon : AttachmentIcon16;
const titleText = this.loading ? 'Loading...' : name;
const infoText = this.error ? 'File loading failed.' : humanFileSize(size);
const blobState = this.blobState$.value;
const {
uploading = false,
downloading = false,
overSize = false,
errorMessage,
} = blobState;
const warning = !overSize && Boolean(errorMessage);
const error = overSize || warning;
const loading = !error && downloading;
const state = this.determineState(loading, uploading, overSize, error);
const classInfo = {
'affine-attachment-card': true,
[cardStyle]: true,
error,
loading,
};
const icon = loading
? LoadingIcon
: error
? WarningIcon()
: AttachmentIcon();
const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name;
const description = errorMessage || humanFileSize(size);
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
return when(
cardStyle === 'cubeThick',
() =>
this.renderWithVertical(
classInfo,
icon,
title,
description,
kind,
state
),
() =>
this.renderWithHorizontal(
classInfo,
icon,
title,
description,
kind,
state
)
);
};
private readonly _renderCitation = () => {
const { name, footnoteIdentifier } = this.model.props;
const fileType = name.split('.').pop() ?? '';
const FileTypeIcon = getAttachmentFileIcon(fileType);
const embedView = this.embedView;
const fileTypeIcon = getAttachmentFileIcon(fileType);
return html`<affine-citation-card
.icon=${fileTypeIcon}
.citationTitle=${name}
.citationIdentifier=${footnoteIdentifier}
.active=${this.selected$.value}
></affine-citation-card>`;
};
override renderBlock() {
return html`
<div class="affine-attachment-container" style=${this.containerStyleMap}>
${embedView
? html`<div class="affine-attachment-embed-container">
${embedView}
</div>`
: html`<div
class=${classMap({
'affine-attachment-card': true,
[cardStyle]: true,
loading: this.loading,
error: this.error,
unsynced: false,
})}
>
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">
${titleIcon}
</div>
<div class="affine-attachment-content-title-text">
${titleText}
</div>
</div>
<div class="affine-attachment-content-info">${infoText}</div>
</div>
<div class="affine-attachment-banner">${FileTypeIcon}</div>
</div>`}
<div
class=${classMap({
'affine-attachment-container': true,
focused: this.selected$.value,
})}
style=${this.containerStyleMap}
>
${when(
this.isCitation,
() => this._renderCitation(),
() =>
when(
this.embedView,
() =>
html`<div class="affine-attachment-embed-container">
${this.embedView}
</div>`,
this.renderCard
)
)}
</div>
`;
}
@property({ attribute: false })
accessor allowEmbed = false;
accessor blobUrl: string | null = null;
@property({ attribute: false })
accessor blobUrl: string | undefined = undefined;
@property({ attribute: false })
accessor downloading = false;
@property({ attribute: false })
accessor error = false;
@property({ attribute: false })
accessor loading = false;
override accessor selectedStyle = SelectedStyle.Border;
override accessor useCaptionEditor = true;
}

View File

@@ -4,7 +4,7 @@ import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
import { AttachmentDropOption } from './attachment-service.js';
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
@@ -25,7 +25,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
AttachmentDropOption,
AttachmentEmbedConfigExtension(),
AttachmentEmbedService,
AttachmentBlockNotionHtmlAdapterExtension,
AttachmentBlockAdapterExtensions,
createBuiltinToolbarConfigExtension(flavour),
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
].flat();

View File

@@ -69,6 +69,10 @@ export const attachmentViewDropdownMenu = {
{
id: 'embed',
label: 'Embed view',
disabled: ctx => {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
return block ? !block.embedded() : true;
},
run(ctx) {
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return;
@@ -135,6 +139,12 @@ const downloadAction = {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.download();
},
when: ctx => {
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return false;
// Current citation attachment block does not support download
return model.props.style !== 'citation' && !model.props.footnoteIdentifier;
},
} as const satisfies ToolbarAction;
const captionAction = {
@@ -156,24 +166,24 @@ const builtinToolbarConfig = {
actions: [
{
id: 'a.rename',
content(cx) {
const block = cx.getCurrentBlockByType(AttachmentBlockComponent);
content(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
if (!block) return null;
const abortController = new AbortController();
abortController.signal.onabort = () => cx.show();
abortController.signal.onabort = () => ctx.show();
return html`
<editor-icon-button
aria-label="Rename"
.tooltip="${'Rename'}"
@click=${() => {
cx.hide();
ctx.hide();
createLitPortal({
template: RenameModal({
model: block.model,
editorHost: cx.host,
editorHost: ctx.host,
abortController,
}),
computePosition: {
@@ -327,7 +337,6 @@ const builtinSurfaceToolbarConfig = {
id: 'e.caption',
},
],
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
} as const satisfies ToolbarModuleConfig;

View File

@@ -3,7 +3,7 @@ import {
type ImageBlockProps,
MAX_IMAGE_WIDTH,
} from '@blocksuite/affine-model';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services';
import {
readImageSize,
transformModel,
@@ -68,7 +68,7 @@ export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
export class AttachmentEmbedService extends Extension {
private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize;
return this.std.get(FileSizeLimitProvider).maxFileSize;
}
get keys() {
@@ -187,7 +187,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
/**
* Turn the attachment block into an image block.
*/
export async function turnIntoImageBlock(model: AttachmentBlockModel) {
async function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!');
return;

View File

@@ -1,4 +1,4 @@
export * from './adapters/notion-html';
export * from './adapters';
export * from './attachment-block';
export * from './attachment-service';
export * from './attachment-spec';

View File

@@ -4,7 +4,7 @@ import {
} from '@blocksuite/affine-ext-loader';
import { AttachmentBlockSchemaExtension } from '@blocksuite/affine-model';
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html';
import { AttachmentBlockAdapterExtensions } from './adapters/extension';
export class AttachmentStoreExtension extends StoreExtensionProvider {
override name = 'affine-attachment-block';
@@ -12,6 +12,6 @@ export class AttachmentStoreExtension extends StoreExtensionProvider {
override setup(context: StoreExtensionContext) {
super.setup(context);
context.register(AttachmentBlockSchemaExtension);
context.register(AttachmentBlockNotionHtmlAdapterExtension);
context.register(AttachmentBlockAdapterExtensions);
}
}

View File

@@ -1,31 +1,33 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from 'lit';
export const styles = css`
.affine-attachment-card {
margin: 0 auto;
.affine-attachment-container {
border-radius: 8px;
box-sizing: border-box;
user-select: none;
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
background: ${unsafeCSSVarV2('layer/background/primary')};
overflow: hidden;
&.focused {
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
}
}
.affine-attachment-card {
display: flex;
gap: 12px;
width: 100%;
height: 100%;
padding: 12px;
border-radius: 8px;
border: 1px solid var(--affine-background-tertiary-color);
opacity: var(--add, 1);
background: var(--affine-background-primary-color);
user-select: none;
}
.affine-attachment-content {
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
flex: 1 0 0;
min-width: 0;
}
.affine-attachment-content-title {
@@ -33,7 +35,6 @@ export const styles = css`
flex-direction: row;
gap: 8px;
align-items: center;
align-self: stretch;
}
@@ -43,24 +44,18 @@ export const styles = css`
height: 16px;
align-items: center;
justify-content: center;
color: var(--affine-text-primary-color);
}
.affine-attachment-content-title-icon svg {
width: 16px;
height: 16px;
fill: var(--affine-background-primary-color);
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.affine-attachment-content-title-text {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
font-style: normal;
@@ -68,17 +63,15 @@ export const styles = css`
line-height: 22px;
}
.affine-attachment-content-description {
display: flex;
align-items: center;
align-self: stretch;
gap: 8px;
}
.affine-attachment-content-info {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
word-break: break-all;
overflow: hidden;
color: var(--affine-text-secondary-color);
text-overflow: ellipsis;
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
@@ -86,6 +79,26 @@ export const styles = css`
line-height: 20px;
}
.affine-attachment-content-button {
display: flex;
height: 20px;
align-items: center;
align-self: stretch;
gap: 4px;
white-space: nowrap;
padding: 0 4px;
color: ${unsafeCSSVarV2('button/primary')};
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 500;
line-height: 20px;
svg {
font-size: 16px;
}
}
.affine-attachment-banner {
display: flex;
align-items: center;
@@ -93,16 +106,15 @@ export const styles = css`
}
.affine-attachment-card.loading {
background: var(--affine-background-secondary-color);
.affine-attachment-content-title-text {
color: var(--affine-placeholder-color);
}
}
.affine-attachment-card.error,
.affine-attachment-card.unsynced {
background: var(--affine-background-secondary-color);
.affine-attachment-card.error {
.affine-attachment-content-title-icon {
color: ${unsafeCSSVarV2('status/error')};
}
}
.affine-attachment-card.cubeThick {
@@ -116,7 +128,7 @@ export const styles = css`
}
.affine-attachment-banner {
justify-content: flex-start;
justify-content: space-between;
}
}

View File

@@ -10,7 +10,7 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
type AttachmentUploadedEvent,
FileSizeLimitService,
FileSizeLimitProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
@@ -21,128 +21,19 @@ import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block';
const attachmentUploads = new Set<string>();
export function setAttachmentUploading(blockId: string) {
attachmentUploads.add(blockId);
}
export function setAttachmentUploaded(blockId: string) {
attachmentUploads.delete(blockId);
}
function isAttachmentUploading(blockId: string) {
return attachmentUploads.has(blockId);
}
/**
* This function will not verify the size of the file.
*/
// TODO(@fundon): should remove
export async function uploadAttachmentBlob(
std: BlockStdScope,
blockId: string,
blob: Blob,
filetype: string,
isEdgeless?: boolean
): Promise<void> {
if (isAttachmentUploading(blockId)) return;
let sourceId: string | undefined;
try {
setAttachmentUploading(blockId);
sourceId = await std.store.blobSync.set(blob);
} catch (error) {
console.error(error);
if (error instanceof Error) {
toast(
std.host,
`Failed to upload attachment! ${error.message || error.toString()}`
);
}
} finally {
setAttachmentUploaded(blockId);
const block = std.store.getBlock(blockId);
std.store.withoutTransact(() => {
if (!block) return;
std.store.updateBlock(block.model, {
sourceId,
} satisfies Partial<AttachmentBlockProps>);
});
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type: filetype,
category: block && sourceId ? 'success' : 'failure',
});
}
}
export async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.props.sourceId;
if (!sourceId) {
return null;
}
const {
sourceId$: { value: sourceId },
type$: { value: type },
} = model.props;
if (!sourceId) return null;
const doc = model.doc;
let blob = await doc.blobSync.get(sourceId);
if (blob) {
blob = new Blob([blob], { type: model.props.type });
}
if (!blob) return null;
return blob;
}
// TODO(@fundon): should remove
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
const model = block.model;
const { id } = model;
const { sourceId } = model.props;
if (isAttachmentUploading(id)) {
block.loading = true;
block.error = false;
block.allowEmbed = false;
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
block.blobUrl = undefined;
}
return;
}
try {
if (!sourceId) {
return;
}
const blob = await getAttachmentBlob(model);
if (!blob) {
return;
}
block.loading = false;
block.error = false;
block.allowEmbed = block.embedded();
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
}
block.blobUrl = URL.createObjectURL(blob);
} catch (error) {
console.warn(error, model, sourceId);
block.loading = false;
block.error = true;
block.allowEmbed = false;
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
block.blobUrl = undefined;
}
}
return new Blob([blob], { type });
}
/**
@@ -150,26 +41,22 @@ export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
* the download process may take a long time!
*/
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
const { host, model, loading, error, downloading, blobUrl } = block;
if (downloading) {
toast(host, 'Download in progress...');
return;
}
const { host, model, blobUrl, blobState$ } = block;
if (loading) {
toast(host, 'Please wait, file is loading...');
if (blobState$.peek().downloading) {
toast(host, 'Download in progress...');
return;
}
const name = model.props.name;
const shortName = name.length < 20 ? name : name.slice(0, 20) + '...';
if (error || !blobUrl) {
if (!blobUrl) {
toast(host, `Failed to download ${shortName}!`);
return;
}
block.downloading = true;
block.updateBlobState({ downloading: true });
toast(host, `Downloading ${shortName}`);
@@ -180,7 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
tmpLink.dispatchEvent(event);
tmpLink.remove();
block.downloading = false;
block.updateBlobState({ downloading: false });
}
export async function refreshData(
std: BlockStdScope,
block: AttachmentBlockComponent
) {
const model = block.model;
const sourceId = model.props.sourceId$.peek();
if (!sourceId) return;
const blobUrl = block.blobUrl;
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
block.blobUrl = null;
}
let blob = await std.store.blobSync.get(sourceId);
if (!blob) {
block.updateBlobState({ errorMessage: 'File not found' });
return;
}
const type = model.props.type$.peek();
blob = new Blob([blob], { type });
block.blobUrl = URL.createObjectURL(blob);
}
export async function getFileType(file: File) {
@@ -196,7 +110,7 @@ export async function getFileType(file: File) {
function hasExceeded(
std: BlockStdScope,
files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
) {
const exceeded = files.some(file => file.size > maxFileSize);
@@ -219,6 +133,7 @@ async function buildPropsWith(
try {
const { name, size } = file;
// TODO(@fundon): should re-upload when upload timeout
const sourceId = await std.store.blobSync.set(file);
type = await getFileType(file);
@@ -233,6 +148,7 @@ async function buildPropsWith(
category = 'failure';
throw err;
} finally {
// TODO(@fundon): should change event name because this is just a local operation.
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`,
module: 'attachment',
@@ -303,7 +219,6 @@ export async function addAttachments(
const gap = 32;
const width = EMBED_CARD_WIDTH.cubeThick;
const height = EMBED_CARD_HEIGHT.cubeThick;
const flavour = AttachmentBlockSchema.model.flavour;
const blocks = propsArray.map((props, index) => {
@@ -312,7 +227,7 @@ export async function addAttachments(
return { flavour, blockProps: { ...props, style, xywh } };
});
const blockIds = std.store.addBlocks(blocks);
const blockIds = std.store.addBlocks(blocks, gfx.surface);
gfx.selection.set({
elements: blockIds,

View File

@@ -1,9 +1,102 @@
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
import {
BookmarkBlockSchema,
FootNoteReferenceParamsSchema,
} from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
FOOTNOTE_DEFINITION_PREFIX,
getFootnoteDefinitionText,
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
if (!isFootnoteDefinitionNode(node)) return false;
const footnoteDefinition = getFootnoteDefinitionText(node);
try {
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
return (
footnoteDefinitionJson.type === 'url' && !!footnoteDefinitionJson.url
);
} catch {
return false;
}
};
export const bookmarkBlockMarkdownAdapterMatcher =
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour);
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour, {
toMatch: o => isUrlFootnoteDefinitionNode(o.node),
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return;
}
const { walkerContext, configs } = context;
const footnoteIdentifier = o.node.identifier;
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
const footnoteDefinition = configs.get(footnoteDefinitionKey);
if (!footnoteDefinition) {
return;
}
let footnoteDefinitionJson;
try {
footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
// If the footnote definition contains url, decode it
if (footnoteDefinitionJson.url) {
footnoteDefinitionJson.url = decodeURIComponent(
footnoteDefinitionJson.url
);
}
if (footnoteDefinitionJson.favicon) {
footnoteDefinitionJson.favicon = decodeURIComponent(
footnoteDefinitionJson.favicon
);
}
} catch (err) {
console.warn('Failed to parse or decode footnote definition:', err);
return;
}
const { url, favicon, title, description } = footnoteDefinitionJson;
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: BookmarkBlockSchema.model.flavour,
props: {
url,
footnoteIdentifier,
icon: favicon,
title,
description,
style: 'citation',
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren();
},
},
});
export const BookmarkBlockMarkdownAdapterExtension =
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);

View File

@@ -4,6 +4,7 @@ import {
} from '@blocksuite/affine-components/caption';
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { BlockSelection } from '@blocksuite/std';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
@@ -27,6 +28,14 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
protected containerStyleMap!: ReturnType<typeof styleMap>;
selectBlock = () => {
const selectionManager = this.std.selection;
const blockSelection = selectionManager.create(BlockSelection, {
blockId: this.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
};
open = () => {
let link = this.model.props.url;
if (!link.match(/^[a-zA-Z]+:\/\//)) {
@@ -41,6 +50,37 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
);
};
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
}
private readonly _renderCitationView = () => {
const { title, description, url, icon, footnoteIdentifier } =
this.model.props;
return html`
<affine-citation-card
.icon=${icon}
.citationTitle=${title || url}
.citationContent=${description}
.citationIdentifier=${footnoteIdentifier}
.onClickCallback=${this.selectBlock}
.onDoubleClickCallback=${this.open}
.active=${this.selected$.value}
></affine-citation-card>
`;
};
private readonly _renderCardView = () => {
return html`<bookmark-card
.bookmark=${this}
.loading=${this.loading}
.error=${this.error}
></bookmark-card>`;
};
override connectedCallback() {
super.connectedCallback();
@@ -58,6 +98,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
this.contentEditable = 'false';
if (!this.model.props.description && !this.model.props.title) {
if (this.doc.readonly) {
return;
}
this.refreshData();
}
@@ -85,11 +128,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
})}
style=${this.containerStyleMap}
>
<bookmark-card
.bookmark=${this}
.loading=${this.loading}
.error=${this.error}
></bookmark-card>
${this.isCitation ? this._renderCitationView() : this._renderCardView()}
</div>
`;
}

View File

@@ -5,11 +5,7 @@ import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { OpenInNewIcon } from '@blocksuite/icons/lit';
import {
BlockSelection,
isGfxBlockComponent,
ShadowlessElement,
} from '@blocksuite/std';
import { isGfxBlockComponent, ShadowlessElement } from '@blocksuite/std';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -27,7 +23,7 @@ export class BookmarkCard extends SignalWatcher(
const model = this.bookmark.model;
if (model.parent?.flavour !== 'affine:surface') {
this._selectBlock();
this.bookmark.selectBlock();
}
}
@@ -36,14 +32,6 @@ export class BookmarkCard extends SignalWatcher(
this.bookmark.open();
}
private _selectBlock() {
const selectionManager = this.bookmark.host.selection;
const blockSelection = selectionManager.create(BlockSelection, {
blockId: this.bookmark.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
}
override connectedCallback(): void {
super.connectedCallback();

View File

@@ -1,16 +1,91 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import {
EmbedLinkedDocBlockSchema,
FootNoteReferenceParamsSchema,
} from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
FOOTNOTE_DEFINITION_PREFIX,
getFootnoteDefinitionText,
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store';
const isLinkedDocFootnoteDefinitionNode = (node: MarkdownAST) => {
if (!isFootnoteDefinitionNode(node)) return false;
const footnoteDefinition = getFootnoteDefinitionText(node);
try {
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
return (
footnoteDefinitionJson.type === 'doc' && !!footnoteDefinitionJson.docId
);
} catch {
return false;
}
};
export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
{
flavour: EmbedLinkedDocBlockSchema.model.flavour,
toMatch: () => false,
toMatch: o => isLinkedDocFootnoteDefinitionNode(o.node),
fromMatch: o => o.node.flavour === EmbedLinkedDocBlockSchema.model.flavour,
toBlockSnapshot: {},
toBlockSnapshot: {
enter: (o, context) => {
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return;
}
const { walkerContext, configs } = context;
const footnoteIdentifier = o.node.identifier;
const footnoteDefinitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${footnoteIdentifier}`;
const footnoteDefinition = configs.get(footnoteDefinitionKey);
if (!footnoteDefinition) {
return;
}
try {
const footnoteDefinitionJson = FootNoteReferenceParamsSchema.parse(
JSON.parse(footnoteDefinition)
);
const { docId } = footnoteDefinitionJson;
if (!docId) {
return;
}
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: EmbedLinkedDocBlockSchema.model.flavour,
props: {
pageId: docId,
footnoteIdentifier,
style: 'citation',
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren();
} catch (err) {
console.warn('Failed to parse linked doc footnote definition:', err);
return;
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { configs, walkerContext } = context;

View File

@@ -53,11 +53,11 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
doc.deleteBlock(this.model);
};
protected override _handleClick(evt: MouseEvent): void {
protected override _handleClick = (evt: MouseEvent): void => {
if (isNewTabTrigger(evt)) {
this.open({ openMode: 'open-in-new-tab', event: evt });
} else if (isNewViewTrigger(evt)) {
this.open({ openMode: 'open-in-new-view', event: evt });
}
}
};
}

View File

@@ -55,6 +55,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
static override styles = styles;
private readonly _load = async () => {
// If this is a citation linked doc block, we don't need to load the linked doc and render linked doc content in card
if (this.isCitation) {
return;
}
const {
loading = true,
isError = false,
@@ -243,6 +248,17 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
return doc?.getStore({ id: this.model.props.pageId });
}
get readonly() {
return this.doc.readonly;
}
get isCitation() {
return (
!!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
);
}
private _handleDoubleClick(event: MouseEvent) {
event.stopPropagation();
const openDocService = this.std.get(OpenDocExtensionIdentifier);
@@ -264,105 +280,42 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
return !!linkedDoc && this.isNoteContentEmpty && this.isBannerEmpty;
}
protected _handleClick(event: MouseEvent) {
protected _handleClick = (event: MouseEvent) => {
if (isNewTabTrigger(event)) {
this.open({ openMode: 'open-in-new-tab', event });
} else if (isNewViewTrigger(event)) {
this.open({ openMode: 'open-in-new-view', event });
}
this._selectBlock();
}
override connectedCallback() {
super.connectedCallback();
this._cardStyle = this.model.props.style;
this._referenceToNode = referenceToNode(this.model.props);
this._load().catch(e => {
console.error(e);
this.isError = true;
});
const linkedDoc = this.linkedDoc;
if (linkedDoc) {
this.disposables.add(
linkedDoc.workspace.slots.docListUpdated.subscribe(() => {
this._load().catch(e => {
console.error(e);
this.isError = true;
});
})
);
// Should throttle the blockUpdated event to avoid too many re-renders
// Because the blockUpdated event is triggered too frequently at some cases
this.disposables.add(
linkedDoc.slots.blockUpdated.subscribe(
throttle(payload => {
if (
payload.type === 'update' &&
['', 'caption', 'xywh'].includes(payload.props.key)
) {
return;
}
if (payload.type === 'add' && payload.init) {
return;
}
this._load().catch(e => {
console.error(e);
this.isError = true;
});
}, RENDER_CARD_THROTTLE_MS)
)
);
this._setDocUpdatedAt();
this.disposables.add(
this.doc.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
})
);
if (this._referenceToNode) {
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
} else {
const docMode = this.std.get(DocModeProvider);
this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId);
this.disposables.add(
docMode.onPrimaryModeChange(mode => {
this._linkedDocMode = mode;
}, this.model.props.pageId)
);
}
if (this.readonly) {
return;
}
this._selectBlock();
};
this.disposables.add(
this.model.propsUpdated.subscribe(({ key }) => {
if (key === 'style') {
this._cardStyle = this.model.props.style;
}
if (key === 'pageId' || key === 'style') {
this._load().catch(e => {
console.error(e);
this.isError = true;
});
}
})
);
}
private readonly _renderCitationView = () => {
const { footnoteIdentifier } = this.model.props;
return html`<div
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'embed-block-container': true,
...this.selectedStyle$?.value,
})}
style=${styleMap({
...this.embedContainerStyle,
})}
>
<affine-citation-card
.icon=${this.icon$.value}
.citationTitle=${this.title$.value}
.citationIdentifier=${footnoteIdentifier}
.active=${this.selected$.value}
.onClickCallback=${this._handleClick}
></affine-citation-card>
</div> `;
};
getInitialState(): {
loading?: boolean;
isError?: boolean;
isNoteContentEmpty?: boolean;
isBannerEmpty?: boolean;
} {
return {};
}
override renderBlock() {
private readonly _renderEmbedView = () => {
const linkedDoc = this.linkedDoc;
const isDeleted = !linkedDoc;
const isLoading = this._loading;
@@ -502,9 +455,107 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
</div>
`
);
};
override connectedCallback() {
super.connectedCallback();
this._cardStyle = this.model.props.style;
this._referenceToNode = referenceToNode(this.model.props);
this._load().catch(e => {
console.error(e);
this.isError = true;
});
const linkedDoc = this.linkedDoc;
if (linkedDoc) {
this.disposables.add(
linkedDoc.workspace.slots.docListUpdated.subscribe(() => {
this._load().catch(e => {
console.error(e);
this.isError = true;
});
})
);
// Should throttle the blockUpdated event to avoid too many re-renders
// Because the blockUpdated event is triggered too frequently at some cases
this.disposables.add(
linkedDoc.slots.blockUpdated.subscribe(
throttle(payload => {
if (
payload.type === 'update' &&
['', 'caption', 'xywh'].includes(payload.props.key)
) {
return;
}
if (payload.type === 'add' && payload.init) {
return;
}
this._load().catch(e => {
console.error(e);
this.isError = true;
});
}, RENDER_CARD_THROTTLE_MS)
)
);
this._setDocUpdatedAt();
this.disposables.add(
this.doc.workspace.slots.docListUpdated.subscribe(() => {
this._setDocUpdatedAt();
})
);
if (this._referenceToNode) {
this._linkedDocMode = this.model.props.params?.mode ?? 'page';
} else {
const docMode = this.std.get(DocModeProvider);
this._linkedDocMode = docMode.getPrimaryMode(this.model.props.pageId);
this.disposables.add(
docMode.onPrimaryModeChange(mode => {
this._linkedDocMode = mode;
}, this.model.props.pageId)
);
}
}
this.disposables.add(
this.model.propsUpdated.subscribe(({ key }) => {
if (key === 'style') {
this._cardStyle = this.model.props.style;
}
if (key === 'pageId' || key === 'style') {
this._load().catch(e => {
console.error(e);
this.isError = true;
});
}
})
);
}
getInitialState(): {
loading?: boolean;
isError?: boolean;
isNoteContentEmpty?: boolean;
isBannerEmpty?: boolean;
} {
return {};
}
override renderBlock() {
return this.isCitation
? this._renderCitationView()
: this._renderEmbedView();
}
override updated() {
if (this.readonly) {
return;
}
// update card style when linked doc deleted
const linkedDoc = this.linkedDoc;
const { xywh, style } = this.model.props;

View File

@@ -375,6 +375,7 @@ const builtinSurfaceToolbarConfig = {
DEFAULT_NOTE_WIDTH,
DEFAULT_NOTE_HEIGHT
).serialize(),
index: gfx.layer.generateIndex(),
displayMode: NoteDisplayMode.EdgelessOnly,
} satisfies Partial<NoteProps>,
ctx.store.root

View File

@@ -1,4 +1,4 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
import {
ActionPlacement,
type ToolbarModuleConfig,
@@ -11,6 +11,9 @@ import {
DeleteIcon,
DownloadIcon,
DuplicateIcon,
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
@@ -49,6 +52,45 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.1.align-left',
tooltip: 'Align left',
icon: TextAlignLeftIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.std.host.doc.updateBlock(block.model, {
textAlign: TextAlign.Left,
});
}
},
},
{
id: 'c.2.align-center',
tooltip: 'Align center',
icon: TextAlignCenterIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.std.host.doc.updateBlock(block.model, {
textAlign: TextAlign.Center,
});
}
},
},
{
id: 'c.3.align-right',
tooltip: 'Align right',
icon: TextAlignRightIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (block) {
ctx.std.host.doc.updateBlock(block.model, {
textAlign: TextAlign.Right,
});
}
},
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -112,6 +112,15 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
width: '100%',
});
const alignItemsStyleMap = styleMap({
alignItems:
this.model.props.textAlign$.value === 'left'
? 'flex-start'
: this.model.props.textAlign$.value === 'right'
? 'flex-end'
: undefined,
});
return html`
<div class="affine-image-container" style=${containerStyleMap}>
${when(
@@ -122,7 +131,11 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
.loading=${this.loading}
.mode=${'page'}
></affine-image-fallback-card>`,
() => html`<affine-page-image .block=${this}></affine-page-image>`
() =>
html`<affine-page-image
.block=${this}
style="${alignItemsStyleMap}"
></affine-page-image>`
)}
</div>

View File

@@ -78,8 +78,7 @@ export class ImageResizeManager {
const rootComponent = getClosestRootBlockComponent(this._activeComponent);
if (
rootComponent &&
rootComponent.service.std.get(DocModeProvider).getEditorMode() ===
'edgeless'
rootComponent.std.get(DocModeProvider).getEditorMode() === 'edgeless'
) {
const viewport = rootComponent.std.get(GfxControllerIdentifier).viewport;
this._zoom = viewport.zoom;

View File

@@ -7,7 +7,7 @@ import {
ImageBlockSchema,
} from '@blocksuite/affine-model';
import {
FileSizeLimitService,
FileSizeLimitProvider,
NativeClipboardProvider,
} from '@blocksuite/affine-shared/services';
import {
@@ -362,7 +362,7 @@ export function shouldResizeImage(node: Node, target: EventTarget | null) {
function hasExceeded(
std: BlockStdScope,
files: File[],
maxFileSize = std.store.get(FileSizeLimitService).maxFileSize
maxFileSize = std.get(FileSizeLimitProvider).maxFileSize
) {
const exceeded = files.some(file => file.size > maxFileSize);
@@ -481,10 +481,12 @@ export async function addImages(
// If maxWidth is provided, limit the width of the image to maxWidth
// Otherwise, use the original width
const width = maxWidth ? Math.min(props.width, maxWidth) : props.width;
const height = maxWidth
? (props.height / props.width) * width
: props.height;
if (maxWidth) {
const p = props.height / props.width;
props.width = Math.min(props.width, maxWidth);
props.height = props.width * p;
}
const { width, height } = props;
const xywh = calcBoundByOrigin(
center,

View File

@@ -144,6 +144,10 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -155,7 +159,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
</div>`;
return html`
<div class=${'affine-list-block-container'}>
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
<div
class=${classMap({
'affine-list-rich-text-wrapper': true,

View File

@@ -3,14 +3,13 @@ import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
FOOTNOTE_DEFINITION_PREFIX,
isFootnoteDefinitionNode,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import type { FootnoteDefinition, Root } from 'mdast';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import type { Root } from 'mdast';
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
const isFootnoteDefinitionNode = (
node: MarkdownAST
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
const createFootnoteDefinition = (
identifier: string,
@@ -67,10 +66,35 @@ const createNoteBlockMarkdownAdapterMatcher = (
}
});
// Remove the footnoteDefinition node from the noteAst
noteAst.children = noteAst.children.filter(
child => !isFootnoteDefinitionNode(child)
);
const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (enableCitation) {
// if there are footnoteDefinition nodes, add a heading node to the noteAst before the first footnoteDefinition node
const footnoteDefinitionIndex = noteAst.children.findIndex(child =>
isFootnoteDefinitionNode(child)
);
if (footnoteDefinitionIndex !== -1) {
noteAst.children.splice(footnoteDefinitionIndex, 0, {
type: 'heading',
depth: 6,
data: {
collapsed: true,
},
children: [{ type: 'text', value: 'Sources' }],
});
}
} else {
// Remove the footnoteDefinition node from the noteAst
noteAst.children = noteAst.children.filter(
child => !isFootnoteDefinitionNode(child)
);
}
},
},
fromBlockSnapshot: {

View File

@@ -4,9 +4,15 @@ import {
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import {
type TextAlignConfig,
textAlignConfigs,
type TextConversionConfig,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
import {
type SlashMenuActionItem,
@@ -56,6 +62,10 @@ const noteSlashMenuConfig: SlashMenuConfig = {
createConversionItem(config, `1_List@${index++}`)
),
...textAlignConfigs.map((config, index) =>
createAlignItem(config, `2_Align@${index++}`)
),
...textFormatConfigs
.filter(i => !['Code', 'Link'].includes(i.name))
.map((config, index) =>
@@ -85,6 +95,31 @@ function createConversionItem(
};
}
function createAlignItem(
config: TextAlignConfig,
group?: SlashMenuItem['group']
): SlashMenuActionItem {
const { textAlign, name, icon } = config;
return {
name,
group,
icon,
action: ({ std }) => {
std.command
.chain()
.pipe(getTextSelectionCommand)
.pipe(getSelectedModelsCommand, { types: ['text'] })
.pipe((ctx, next) => {
ctx.selectedModels.forEach(model => {
ctx.std.host.doc.updateBlock(model, { textAlign });
});
return next();
})
.run();
},
};
}
function createTextFormatItem(
config: TextFormatConfig,
group?: SlashMenuItem['group']

View File

@@ -5,13 +5,17 @@ import {
NoteBlockSchema,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
focusBlockEnd,
focusBlockStart,
getBlockSelectionsCommand,
getNextBlockCommand,
getPrevBlockCommand,
getSelectedModelsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import {
@@ -157,6 +161,48 @@ class NoteKeymap {
);
};
private readonly _bindTextAlignHotKey = () => {
return textAlignConfigs.reduce(
(acc, item) => {
const keymap = item.hotkey!.reduce(
(acc, key) => {
return {
...acc,
[key]: ctx => {
ctx.get('defaultState').event.preventDefault();
const [result] = this._std.command
.chain()
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
])
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
.pipe((ctx, next) => {
ctx.selectedModels.forEach(model => {
ctx.std.host.doc.updateBlock(model, {
textAlign: item.textAlign,
});
});
return next();
})
.run();
return result;
},
};
},
{} as Record<string, UIEventHandler>
);
return {
...acc,
...keymap,
};
},
{} as Record<string, UIEventHandler>
);
};
private _focusBlock: BlockComponent | null = null;
private readonly _getClosestNoteByBlockId = (blockId: string) => {
@@ -568,6 +614,7 @@ class NoteKeymap {
...this._bindMoveBlockHotKey(),
...this._bindQuickActionHotKey(),
...this._bindTextConversionHotKey(),
...this._bindTextAlignHotKey(),
Tab: ctx => {
const [success] = this.std.command.exec(indentBlocks);

View File

@@ -9,6 +9,15 @@ import type { DeltaInsert } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { Heading } from 'mdast';
/**
* Extend the HeadingData type to include the collapsed property
*/
declare module 'mdast' {
interface HeadingData {
collapsed?: boolean;
}
}
const PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']);
const isParagraphMDASTType = (node: MarkdownAST) =>
@@ -46,6 +55,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
break;
}
case 'heading': {
const isCollapsed = !!o.node.data?.collapsed;
walkerContext
.openNode(
{
@@ -54,6 +64,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
flavour: 'affine:paragraph',
props: {
type: `h${o.node.depth}`,
collapsed: isCollapsed,
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node),

View File

@@ -235,6 +235,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
`;
}
const textAlignStyle = styleMap({
textAlign: this.model.props.textAlign$?.value,
});
const children = html`<div
class="affine-block-children-container"
style=${styleMap({
@@ -256,6 +260,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
</style>
<div
class="affine-paragraph-block-container"
style="${textAlignStyle}"
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
>
<div

View File

@@ -1,12 +1,6 @@
import {
AttachmentAdapter,
ClipboardAdapter,
copyMiddleware,
defaultImageProxyMiddleware,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import {
@@ -15,67 +9,7 @@ import {
getSelectedModelsCommand,
} from '@blocksuite/affine-shared/commands';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
ClipboardAdapterConfigExtension,
LifeCycleWatcher,
type UIEventHandler,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: ClipboardAdapter.MIME,
adapter: ClipboardAdapter,
priority: 100,
});
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/_notion-text-production',
adapter: NotionTextAdapter,
priority: 95,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 90,
});
const imageClipboardConfigs = [
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].map(mimeType => {
return ClipboardAdapterConfigExtension({
mimeType,
adapter: ImageAdapter,
priority: 80,
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,
});
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: '*/*',
adapter: AttachmentAdapter,
priority: 60,
});
export const clipboardConfigs: ExtensionType[] = [
SnapshotClipboardConfig,
NotionClipboardConfig,
HtmlClipboardConfig,
...imageClipboardConfigs,
PlainTextClipboardConfig,
AttachmentClipboardConfig,
];
import { LifeCycleWatcher, type UIEventHandler } from '@blocksuite/std';
/**
* ReadOnlyClipboard is a class that provides a read-only clipboard for the root block.

View File

@@ -4,16 +4,12 @@ import { BlockFlavourIdentifier, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { RootBlockAdapterExtensions } from '../adapters/extension';
import { clipboardConfigs } from '../clipboard';
import { builtinToolbarConfig } from '../configs/toolbar';
import { fallbackKeymap } from '../keyboard/keymap';
import { viewportOverlayWidget } from './widgets';
export const CommonSpecs: ExtensionType[] = [
FlavourExtension('affine:page'),
...RootBlockAdapterExtensions,
...clipboardConfigs,
viewportOverlayWidget,
fallbackKeymap,
ToolbarModuleExtension({
@@ -21,5 +17,3 @@ export const CommonSpecs: ExtensionType[] = [
config: builtinToolbarConfig,
}),
];
export * from './widgets';

View File

@@ -1,10 +0,0 @@
import { WidgetViewExtension } from '@blocksuite/std';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js';
export const viewportOverlayWidget = WidgetViewExtension(
'affine:page',
AFFINE_VIEWPORT_OVERLAY_WIDGET,
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
);

View File

@@ -19,7 +19,11 @@ import {
isFormatSupported,
textFormatConfigs,
} from '@blocksuite/affine-inline-preset';
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
import type { TextAlign } from '@blocksuite/affine-model';
import {
textAlignConfigs,
textConversionConfigs,
} from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
deleteSelectedModelsCommand,
@@ -39,6 +43,7 @@ import type {
} from '@blocksuite/affine-shared/services';
import { ActionPlacement } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import {
CopyIcon,
@@ -119,6 +124,72 @@ const conversionsActionGroup = {
},
} as const satisfies ToolbarActionGenerator;
const alignActionGroup = {
id: 'b.align',
when: ({ chain }) => isFormatSupported(chain).run()[0],
generate({ chain }) {
const [ok, { selectedModels = [] }] = chain
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getBlockSelectionsCommand),
])
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
.run();
if (!ok) return null;
const alignment =
textAlignConfigs.find(
({ textAlign }) =>
textAlign ===
getMostCommonValue(
selectedModels.map(
({ props }) => props as { textAlign?: TextAlign }
),
'textAlign'
)
) ?? textAlignConfigs[0];
const update = (textAlign: TextAlign) => {
chain
.pipe((ctx, next) => {
selectedModels.forEach(model => {
ctx.std.host.doc.updateBlock(model, { textAlign });
});
return next();
})
.run();
};
return {
content: html`
<editor-menu-button
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
${alignment.icon} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${repeat(
textAlignConfigs,
item => item.name,
({ textAlign, name, icon }) => html`
<editor-menu-action
aria-label=${name}
?data-selected=${alignment.textAlign === textAlign}
@click=${() => update(textAlign)}
>
${icon}<span class="label">${name}</span>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`,
};
},
} as const satisfies ToolbarActionGenerator;
const inlineTextActionGroup = {
id: 'b.inline-text',
when: ({ chain }) => isFormatSupported(chain).run()[0],
@@ -269,6 +340,7 @@ const turnIntoLinkedDoc = {
export const builtinToolbarConfig = {
actions: [
conversionsActionGroup,
alignActionGroup,
inlineTextActionGroup,
highlightActionGroup,
turnIntoDatabase,

View File

@@ -11,7 +11,7 @@ import {
RootBlockSchema,
} from '@blocksuite/affine-model';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { BlockStdScope } from '@blocksuite/std';
import { BlockService, type BlockStdScope } from '@blocksuite/std';
import type {
GfxController,
GfxModel,
@@ -30,10 +30,12 @@ import {
import { effect } from '@preact/signals-core';
import clamp from 'lodash-es/clamp';
import { RootService } from '../root-service.js';
import { getCursorMode } from './utils/query.js';
export class EdgelessRootService extends RootService implements SurfaceContext {
export class EdgelessRootService
extends BlockService
implements SurfaceContext
{
static override readonly flavour = RootBlockSchema.model.flavour;
private readonly _surface: SurfaceBlockModel;

View File

@@ -9,19 +9,12 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { CommonSpecs } from '../common-specs/index.js';
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from '../widgets/edgeless-zoom-toolbar/index.js';
import { EdgelessClipboardController } from './clipboard/clipboard.js';
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
import { quickTools } from './components/toolbar/tools.js';
import { EdgelessRootService } from './edgeless-root-service.js';
export const edgelessZoomToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
);
export const edgelessDraggingAreaWidget = WidgetViewExtension(
'affine:page',
EDGELESS_DRAGGING_AREA_WIDGET,
@@ -51,13 +44,11 @@ const EdgelessCommonExtension: ExtensionType[] = [
CommonSpecs,
EdgelessRootService,
ViewportElementExtension('.affine-edgeless-viewport'),
...quickTools,
].flat();
export const EdgelessRootBlockSpec: ExtensionType[] = [
...EdgelessCommonExtension,
BlockViewExtension('affine:page', literal`affine-edgeless-root`),
edgelessZoomToolbarWidget,
edgelessDraggingAreaWidget,
noteSlicerWidget,
edgelessSelectedRectWidget,

View File

@@ -12,35 +12,16 @@ import {
EDGELESS_SELECTED_RECT_WIDGET,
EdgelessSelectedRectWidget,
} from './edgeless/components/rects/edgeless-selected-rect.js';
import { EdgelessSlideMenu } from './edgeless/components/toolbar/common/slide-menu.js';
import { ToolbarArrowUpIcon } from './edgeless/components/toolbar/common/toolbar-arrow-up-icon.js';
import { EdgelessLinkToolButton } from './edgeless/components/toolbar/link/link-tool-button.js';
import {
EdgelessRootBlockComponent,
EdgelessRootPreviewBlockComponent,
PageRootBlockComponent,
PreviewRootBlockComponent,
} from './index.js';
import {
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
AffineEdgelessZoomToolbarWidget,
} from './widgets/edgeless-zoom-toolbar/index.js';
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
import {
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
AffinePageDraggingAreaWidget,
} from './widgets/page-dragging-area/page-dragging-area.js';
import {
AFFINE_VIEWPORT_OVERLAY_WIDGET,
AffineViewportOverlayWidget,
} from './widgets/viewport-overlay/viewport-overlay.js';
export function effects() {
// Register components by category
registerRootComponents();
registerWidgets();
registerEdgelessToolbarComponents();
registerMiscComponents();
}
@@ -54,37 +35,7 @@ function registerRootComponents() {
);
}
function registerWidgets() {
customElements.define(
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
AffinePageDraggingAreaWidget
);
customElements.define(
AFFINE_VIEWPORT_OVERLAY_WIDGET,
AffineViewportOverlayWidget
);
customElements.define(
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
AffineEdgelessZoomToolbarWidget
);
}
function registerEdgelessToolbarComponents() {
// Tool buttons
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
// Menus
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
// Toolbar components
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
}
function registerMiscComponents() {
// Toolbar and UI components
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
// Auto-complete components
customElements.define(
'edgeless-auto-complete-panel',
@@ -115,13 +66,6 @@ declare global {
'note-slicer': NoteSlicer;
'edgeless-dragging-area-rect': EdgelessDraggingAreaRectWidget;
'edgeless-selected-rect': EdgelessSelectedRectWidget;
'edgeless-slide-menu': EdgelessSlideMenu;
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
'edgeless-link-tool-button': EdgelessLinkToolButton;
'affine-page-root': PageRootBlockComponent;
'zoom-bar-toggle-button': ZoomBarToggleButton;
'edgeless-zoom-toolbar': EdgelessZoomToolbar;
[AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET]: AffineEdgelessZoomToolbarWidget;
}
}

View File

@@ -10,13 +10,10 @@ export * from './edgeless/edgeless-builtin-spec.js';
export * from './edgeless/edgeless-root-spec.js';
export * from './edgeless/index.js';
export * from './page/page-root-block.js';
export { PageRootService } from './page/page-root-service.js';
export * from './page/page-root-spec.js';
export * from './preview/preview-root-block.js';
export { RootService } from './root-service.js';
export * from './types.js';
export * from './utils/index.js';
export * from './widgets/index.js';
declare type _GLOBAL_ =
| typeof PointerEffect

View File

@@ -28,7 +28,6 @@ import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
import type { PageRootService } from './page-root-service.js';
const DOC_BLOCK_CHILD_PADDING = 24;
const DOC_BOTTOM_PADDING = 32;
@@ -49,10 +48,7 @@ function testClickOnBlankArea(
return state.raw.clientX < blankLeft || state.raw.clientX > blankRight;
}
export class PageRootBlockComponent extends BlockComponent<
RootBlockModel,
PageRootService
> {
export class PageRootBlockComponent extends BlockComponent<RootBlockModel> {
static override styles = css`
editor-host:has(> affine-page-root, * > affine-page-root) {
display: block;

View File

@@ -1,7 +0,0 @@
import { RootBlockSchema } from '@blocksuite/affine-model';
import { RootService } from '../root-service.js';
export class PageRootService extends RootService {
static override readonly flavour = RootBlockSchema.model.flavour;
}

View File

@@ -1,23 +1,13 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/std';
import { BlockViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { literal } from 'lit/static-html.js';
import { PageClipboard } from '../clipboard/page-clipboard.js';
import { CommonSpecs } from '../common-specs/index.js';
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from '../widgets/page-dragging-area/page-dragging-area.js';
import { PageRootService } from './page-root-service.js';
export const pageDraggingAreaWidget = WidgetViewExtension(
'affine:page',
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
literal`${unsafeStatic(AFFINE_PAGE_DRAGGING_AREA_WIDGET)}`
);
const PageCommonExtension: ExtensionType[] = [
CommonSpecs,
PageRootService,
pageDraggingAreaWidget,
ViewportElementExtension('.affine-page-viewport'),
].flat();

View File

@@ -1,6 +0,0 @@
import { RootBlockSchema } from '@blocksuite/affine-model';
import { BlockService } from '@blocksuite/std';
export abstract class RootService extends BlockService {
static override readonly flavour = RootBlockSchema.model.flavour;
}

View File

@@ -1,3 +0,0 @@
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
export { AffinePageDraggingAreaWidget } from './page-dragging-area/page-dragging-area.js';
export * from './viewport-overlay/viewport-overlay.js';

View File

@@ -1,4 +1,4 @@
import { type FrameBlockComponent } from '@blocksuite/affine-block-frame';
import { FrameBlockComponent } from '@blocksuite/affine-block-frame';
import {
EdgelessCRUDIdentifier,
getSurfaceBlock,
@@ -24,12 +24,7 @@ import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import {
Bound,
deserializeXYWH,
type SerializedXYWH,
} from '@blocksuite/global/gfx';
import { assertType } from '@blocksuite/global/utils';
import { Bound, type SerializedXYWH } from '@blocksuite/global/gfx';
import {
BlockComponent,
BlockSelection,
@@ -127,6 +122,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
private _referencedModel: GfxModel | null = null;
// since the xywh of edgeless element is not a signal, we need to use a signal to store the xywh
private readonly _referenceXYWH$ = signal<SerializedXYWH | null>(null);
private get _shouldRender() {
@@ -263,6 +259,8 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
}
private _initViewport() {
this._referenceXYWH$.value = this.referenceModel?.xywh ?? null;
const refreshViewport = () => {
if (!this._referenceXYWH$.value) return;
const previewEditorHost = this.previewEditor;
@@ -270,14 +268,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
const gfx = previewEditorHost.std.get(GfxControllerIdentifier);
const viewport = gfx.viewport;
let bound = Bound.deserialize(this._referenceXYWH$.value);
const w = Math.max(this.getBoundingClientRect().width, bound.w);
const aspectRatio = bound.w / bound.h;
const h = w / aspectRatio;
bound = Bound.fromCenter(bound.center, w, h);
viewport.setViewportByBound(bound);
viewport.setViewportByBound(
Bound.deserialize(this._referenceXYWH$.value),
this.referenceModel instanceof FrameBlockModel
? undefined
: [20, 20, 20, 20]
);
};
this.disposables.add(effect(refreshViewport));
@@ -304,28 +300,15 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
referenceXYWH$.value = referenceElement.xywh;
const { _disposable } = this;
refreshViewport();
_disposable.add(viewport.sizeUpdated.subscribe(refreshViewport));
if (referenceElement instanceof FrameBlockModel) {
if (referenceElement instanceof GfxBlockElementModel) {
_disposable.add(
referenceElement.xywh$.subscribe(xywh => {
referenceXYWH$.value = xywh;
})
);
const subscription = this.std.view.viewUpdated.subscribe(
({ id, type, method, view }) => {
if (
id === referenceElement.id &&
type === 'block' &&
method === 'add'
) {
assertType<FrameBlockComponent>(view);
view.showBorder = false;
subscription.unsubscribe();
}
}
);
_disposable.add(subscription);
} else if (referenceElement instanceof GfxPrimitiveElementModel) {
_disposable.add(
surface.elementUpdated.subscribe(({ id, oldValues }) => {
@@ -338,6 +321,21 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
})
);
}
const subscription = this.std.view.viewUpdated.subscribe(
({ id, type, method, view }) => {
if (
id === referenceElement.id &&
type === 'block' &&
method === 'add' &&
view instanceof FrameBlockComponent
) {
view.showBorder = false;
subscription.unsubscribe();
}
}
);
_disposable.add(subscription);
}
override unmounted() {
@@ -371,15 +369,17 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
this._disposables.add(dispose);
}
private _renderRefContent(referencedModel: GfxModel) {
const [, , w, h] = deserializeXYWH(referencedModel.xywh);
private _renderRefContent() {
if (!this._referenceXYWH$.value) return nothing;
const { w, h } = Bound.deserialize(this._referenceXYWH$.value);
const aspectRatio = h !== 0 ? w / h : 1;
const _previewSpec = this._previewSpec.concat(this._runtimePreviewExt);
return html`<div class="ref-content">
<div
class="ref-viewport"
style=${styleMap({
aspectRatio: `${w} / ${h}`,
aspectRatio: `${aspectRatio}`,
})}
>
${guard(this._previewDoc, () => {
@@ -424,9 +424,9 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
if (!this._shouldRender) return;
this._initReferencedModel();
this._initHotkey();
this._initViewport();
this._initReferencedModel();
}
override firstUpdated() {
@@ -445,7 +445,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
.referenceModel=${_referencedModel}
.refFlavour=${model.props.refFlavour$.value}
></surface-ref-placeholder>`
: this._renderRefContent(_referencedModel);
: this._renderRefContent();
const edgelessTheme = this.std.get(ThemeProvider).edgeless$.value;
return html`
@@ -471,7 +471,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
const viewport = {
xywh: this._referenceXYWH$.value,
padding: [60, 20, 20, 20] as [number, number, number, number],
padding: [20, 20, 20, 20] as [number, number, number, number],
};
this.std.get(EditPropsStore).setStorage('viewport', viewport);

View File

@@ -1,10 +1,12 @@
import { type Color, ColorScheme } from '@blocksuite/affine-model';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { requestConnectedFrame } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { IBound } from '@blocksuite/global/gfx';
import { getBoundWithRotation, intersects } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std';
import type {
GfxCompatibleInterface,
GridManager,
LayerManager,
SurfaceBlockModel,
@@ -43,6 +45,8 @@ export class CanvasRenderer {
private readonly _disposables = new DisposableGroup();
private readonly _turboEnabled: () => boolean;
private readonly _overlays = new Set<Overlay>();
private _refreshRafId: number | null = null;
@@ -67,6 +71,8 @@ export class CanvasRenderer {
removed: HTMLCanvasElement[];
}>();
usePlaceholder = false;
viewport: Viewport;
get stackingCanvas() {
@@ -83,6 +89,12 @@ export class CanvasRenderer {
this.layerManager = options.layerManager;
this.grid = options.gridManager;
this.provider = options.provider ?? {};
this._turboEnabled = () => {
const featureFlagService = options.std.get(FeatureFlagService);
return featureFlagService.getFlag('enable_turbo_renderer');
};
this._initViewport();
options.enableStackingCanvas = options.enableStackingCanvas ?? false;
@@ -213,6 +225,19 @@ export class CanvasRenderer {
}, this._container);
})
);
this._disposables.add(
this.viewport.zooming$.subscribe(isZooming => {
const shouldRenderPlaceholders = this._turboEnabled() && isZooming;
if (this.usePlaceholder !== shouldRenderPlaceholders) {
this.usePlaceholder = shouldRenderPlaceholders;
this.refresh();
}
})
);
this.usePlaceholder = false;
}
private _render() {
@@ -279,23 +304,30 @@ export class CanvasRenderer {
for (const element of elements) {
const display = (element.display ?? true) && !element.hidden;
if (display && intersects(getBoundWithRotation(element), bound)) {
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
);
if (
this.usePlaceholder &&
!(element as GfxCompatibleInterface).forceFullRender
) {
ctx.save();
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
const drawX = element.x - bound.x;
const drawY = element.y - bound.y;
ctx.fillRect(drawX, drawY, element.w, element.h);
ctx.restore();
} else {
ctx.save();
const renderFn = this.std.getOptional<ElementRenderer>(
ElementRendererIdentifier(element.type)
);
if (!renderFn) {
console.warn(`Cannot find renderer for ${element.type}`);
continue;
if (!renderFn) continue;
ctx.globalAlpha = element.opacity ?? 1;
const dx = element.x - bound.x;
const dy = element.y - bound.y;
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
ctx.restore();
}
ctx.save();
ctx.globalAlpha = element.opacity ?? 1;
const dx = element.x - bound.x;
const dy = element.y - bound.y;
renderFn(element, ctx, matrix.translate(dx, dy), this, rc, bound);
ctx.restore();
}
}

View File

@@ -144,6 +144,14 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
style=${styleMap({
paddingLeft: `${virtualPadding}px`,
paddingRight: `${virtualPadding}px`,
marginLeft:
this.model.props.textAlign$?.value === 'left'
? undefined
: 'auto',
marginRight:
this.model.props.textAlign$?.value === 'right'
? undefined
: 'auto',
width: 'max-content',
})}
>

View File

@@ -63,6 +63,7 @@
"./linked-doc-title": "./src/linked-doc-title/index.ts",
"./view-dropdown-menu": "./src/view-dropdown-menu/index.ts",
"./card-style-dropdown-menu": "./src/card-style-dropdown-menu/index.ts",
"./citation": "./src/citation/index.ts",
"./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts",
"./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts",
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",

View File

@@ -0,0 +1,167 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { baseTheme } from '@toeverything/theme';
import {
css,
html,
LitElement,
nothing,
type TemplateResult,
unsafeCSS,
} from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
export class CitationCard extends SignalWatcher(WithDisposable(LitElement)) {
static override styles = css`
.citation-container {
width: 100%;
box-sizing: border-box;
border-radius: 8px;
display: flex;
gap: 2px;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
padding: 4px 8px;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
cursor: pointer;
}
.citation-header {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
.citation-icon {
display: flex;
align-items: center;
justify-content: center;
height: 16px;
width: 16px;
color: ${unsafeCSSVarV2('icon/primary')};
border-radius: 4px;
svg,
img {
width: 16px;
height: 16px;
fill: ${unsafeCSSVarV2('icon/primary')};
}
}
.citation-title {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
line-height: 22px;
color: ${unsafeCSSVarV2('text/primary')};
font-size: var(--affine-font-sm);
font-weight: 500;
}
.citation-identifier {
display: flex;
width: 14px;
height: 14px;
justify-content: center;
align-items: center;
border-radius: 36px;
background: ${unsafeCSSVarV2('block/footnote/numberBg')};
color: ${unsafeCSSVarV2('text/primary')};
text-align: center;
font-size: 10px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 220% */
transition: background-color 0.3s ease-in-out;
}
}
.citation-container:hover .citation-identifier,
.citation-identifier.active {
background: ${unsafeCSSVarV2('button/primary')};
color: ${unsafeCSSVarV2('button/pureWhiteText')};
}
.citation-content {
width: 100%;
box-sizing: border-box;
overflow: hidden;
color: ${unsafeCSSVarV2('text/primary')};
font-feature-settings:
'liga' off,
'clig' off;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px; /* 166.667% */
}
`;
private readonly _IconTemplate = (icon: TemplateResult | string) => {
if (typeof icon === 'string') {
return html`<img src="${icon}" alt="favicon" />`;
}
return icon;
};
override render() {
const citationIdentifierClasses = classMap({
'citation-identifier': true,
active: this.active,
});
return html`
<div
class="citation-container"
@click=${this.onClickCallback}
@dblclick=${this.onDoubleClickCallback}
>
<div class="citation-header">
${this.icon
? html`<div class="citation-icon">
${this._IconTemplate(this.icon)}
</div>`
: nothing}
<div class="citation-title">${this.citationTitle}</div>
<div class=${citationIdentifierClasses}>
${this.citationIdentifier}
</div>
</div>
${this.citationContent
? html`<div class="citation-content">${this.citationContent}</div>`
: nothing}
</div>
`;
}
@property({ attribute: false })
accessor icon: TemplateResult | string | undefined = undefined;
@property({ attribute: false })
accessor citationTitle: string = '';
@property({ attribute: false })
accessor citationContent: string | undefined = undefined;
@property({ attribute: false })
accessor citationIdentifier: string = '';
@property({ attribute: false })
accessor onClickCallback: ((e: MouseEvent) => void) | undefined = undefined;
@property({ attribute: false })
accessor onDoubleClickCallback: ((e: MouseEvent) => void) | undefined =
undefined;
@property({ attribute: false })
accessor active: boolean = false;
}

View File

@@ -0,0 +1,7 @@
import { CitationCard } from './citation';
export * from './citation';
export function effects() {
customElements.define('affine-citation-card', CitationCard);
}

View File

@@ -340,11 +340,6 @@ export const FontFamilyIcon = icons.FontIcon({
height: '20',
});
export const AttachmentIcon16 = icons.AttachmentIcon({
width: '16',
height: '16',
});
export const TextBackgroundDuotoneIcon = html` <svg
xmlns="http://www.w3.org/2000/svg"
width="20"

View File

@@ -1,3 +1,4 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { ToggleDownIcon, ToggleRightIcon } from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
@@ -43,6 +44,12 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
.with-drag-handle .affine-block-children-container .toggle-icon {
opacity: 0;
}
.toggle-icon {
svg {
color: ${unsafeCSSVarV2('icon/primary', '#77757D')};
}
}
`;
override render() {
@@ -55,7 +62,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
${ToggleDownIcon({
width: '16px',
height: '16px',
style: 'color: #77757D',
})}
</div>
`;
@@ -70,7 +76,6 @@ export class ToggleButton extends WithDisposable(ShadowlessElement) {
${ToggleRightIcon({
width: '16px',
height: '16px',
style: 'color: #77757D',
})}
</div>
`;

View File

@@ -2,6 +2,7 @@ import { BlockSelection } from '@blocksuite/affine-components/block-selection';
import { BlockZeroWidth } from '@blocksuite/affine-components/block-zero-width';
import { effects as componentCaptionEffects } from '@blocksuite/affine-components/caption';
import { effects as componentCardStyleDropdownMenuEffects } from '@blocksuite/affine-components/card-style-dropdown-menu';
import { effects as componentCitationEffects } from '@blocksuite/affine-components/citation';
import { effects as componentColorPickerEffects } from '@blocksuite/affine-components/color-picker';
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
@@ -46,6 +47,7 @@ export function effects() {
componentLinkPreviewEffects();
componentLinkedDocTitleEffects();
componentCardStyleDropdownMenuEffects();
componentCitationEffects();
componentHighlightDropdownMenuEffects();
componentViewDropdownMenuEffects();
componentTooltipContentWithShortcutEffects();

View File

@@ -15,7 +15,6 @@ import { HighlightSelectionExtension } from '@blocksuite/affine-shared/selection
import {
BlockMetaService,
FeatureFlagService,
FileSizeLimitService,
LinkPreviewerService,
} from '@blocksuite/affine-shared/services';
import {
@@ -51,7 +50,6 @@ export class FoundationStoreExtension extends StoreExtensionProvider {
BlockMetaService,
// TODO(@mirone): maybe merge these services into a file setting service
LinkPreviewerService,
FileSizeLimitService,
ImageProxyService,
]);
}

View File

@@ -3,6 +3,14 @@ import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import {
AttachmentAdapter,
ClipboardAdapter,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
AutoClearSelectionService,
DefaultOpenDocExtension,
@@ -11,15 +19,73 @@ import {
DocModeService,
EditPropsStore,
EmbedOptionService,
FileSizeLimitService,
FontLoaderService,
PageViewportServiceExtension,
ThemeService,
ToolbarRegistryExtension,
} from '@blocksuite/affine-shared/services';
import { ClipboardAdapterConfigExtension } from '@blocksuite/std';
import { InteractivityManager, ToolController } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { effects } from './effects';
const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: ClipboardAdapter.MIME,
adapter: ClipboardAdapter,
priority: 100,
});
const NotionClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/_notion-text-production',
adapter: NotionTextAdapter,
priority: 95,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 90,
});
const imageClipboardConfigs = [
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].map(mimeType => {
return ClipboardAdapterConfigExtension({
mimeType,
adapter: ImageAdapter,
priority: 80,
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,
});
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: '*/*',
adapter: AttachmentAdapter,
priority: 60,
});
export const clipboardConfigs: ExtensionType[] = [
SnapshotClipboardConfig,
NotionClipboardConfig,
HtmlClipboardConfig,
...imageClipboardConfigs,
PlainTextClipboardConfig,
AttachmentClipboardConfig,
];
export class FoundationViewExtension extends ViewExtensionProvider {
override name = 'foundation';
@@ -44,7 +110,9 @@ export class FoundationViewExtension extends ViewExtensionProvider {
FileDropExtension,
ToolbarRegistryExtension,
AutoClearSelectionService,
FileSizeLimitService,
]);
context.register(clipboardConfigs);
if (this.isEdgeless(context.scope)) {
context.register([InteractivityManager, ToolController]);
}

View File

@@ -249,13 +249,20 @@ function renderLabel(
const [, , w, h] = labelXYWH!;
const cx = w / 2;
const cy = h / 2;
ctx.setTransform(matrix);
if (renderer.usePlaceholder) {
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
ctx.fillRect(0, 0, w, h);
return; // Skip actual label rendering
}
const deltas = wrapTextDeltas(text!, font, w);
const lines = deltaInsertsToChunks(deltas);
const lineHeight = getLineHeight(fontFamily, fontSize, fontWeight);
const textHeight = (lines.length - 1) * lineHeight * 0.5;
ctx.setTransform(matrix);
ctx.font = font;
ctx.textAlign = textAlign;
ctx.textBaseline = 'middle';

View File

@@ -0,0 +1,50 @@
{
"name": "@blocksuite/affine-gfx-link",
"description": "Gfx link for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-bookmark": "workspace:*",
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-gfx-pointer": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-rich-text": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@types/lodash-es": "^4.17.12",
"lit": "^3.2.0",
"lodash-es": "^4.17.21",
"minimatch": "^10.0.1",
"rxjs": "^7.8.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.21.0"
}

View File

@@ -0,0 +1,5 @@
import { EdgelessLinkToolButton } from './toolbar/link-tool-button';
export function effects() {
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -1,9 +1,9 @@
import { QuickToolExtension } from '@blocksuite/affine-widget-edgeless-toolbar';
import { html } from 'lit';
import { buildLinkDenseMenu } from './link/link-dense-menu.js';
import { buildLinkDenseMenu } from './toolbar/link-dense-menu';
const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
export const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
return {
content: html`<edgeless-link-tool-button
.edgeless=${block}
@@ -11,5 +11,3 @@ const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
menu: buildLinkDenseMenu(block, gfx),
};
});
export const quickTools = [linkQuickTool];

View File

@@ -2,10 +2,13 @@ import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmar
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { LinkIcon } from '@blocksuite/affine-components/icons';
import type * as PointerEffect from '@blocksuite/affine-gfx-pointer';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import { QuickToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
import { css, html, LitElement } from 'lit';
declare type _GLOBAL_ = typeof PointerEffect;
export class EdgelessLinkToolButton extends QuickToolMixin(LitElement) {
static override styles = css`
.link-icon,

View File

@@ -0,0 +1,23 @@
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { linkQuickTool } from './link-tool';
export class LinkViewExtension extends ViewExtensionProvider {
override name = 'affine-link-gfx';
override effect() {
super.effect();
effects();
}
override setup(context: ViewExtensionContext) {
super.setup(context);
if (this.isEdgeless(context.scope)) {
context.register(linkQuickTool);
}
}
}

View File

@@ -0,0 +1,24 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": ["./src"],
"references": [
{ "path": "../../blocks/bookmark" },
{ "path": "../../blocks/embed" },
{ "path": "../../blocks/surface" },
{ "path": "../../components" },
{ "path": "../../ext-loader" },
{ "path": "../pointer" },
{ "path": "../../model" },
{ "path": "../../rich-text" },
{ "path": "../../shared" },
{ "path": "../../widgets/edgeless-toolbar" },
{ "path": "../../../framework/global" },
{ "path": "../../../framework/std" },
{ "path": "../../../framework/store" }
]
}

View File

@@ -47,7 +47,8 @@ export function getViewportLayoutTree(
// Recursive function to build the tree structure
const buildLayoutTreeNode = (
model: BlockModel,
ancestorViewportState?: string | null
ancestorViewportState?: string | null,
root = false
): BlockLayoutTreeNode | null => {
const baseLayout: BlockLayout = {
blockId: model.id,
@@ -92,6 +93,29 @@ export function getViewportLayoutTree(
layoutMinY = Math.min(layoutMinY, calculatedRect.y);
layoutMaxX = Math.max(layoutMaxX, calculatedRect.x + calculatedRect.w);
layoutMaxY = Math.max(layoutMaxY, calculatedRect.y + calculatedRect.h);
} else if (component && !root) {
const clientRect = component.getBoundingClientRect();
const [modelX, modelY] = viewport.toModelCoordFromClientCoord([
clientRect.x,
clientRect.y,
]);
const rect = {
x: modelX,
y: modelY,
w: clientRect.width / zoom / viewport.viewScale,
h: clientRect.height / zoom / viewport.viewScale,
};
layout = {
...baseLayout,
rect,
};
layoutMinX = Math.min(layoutMinX, rect.x);
layoutMinY = Math.min(layoutMinY, rect.y);
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
} else {
layoutMinX = Math.min(layoutMinX, baseLayout.rect.x);
layoutMinY = Math.min(layoutMinY, baseLayout.rect.y);
@@ -116,7 +140,7 @@ export function getViewportLayoutTree(
};
const roots: BlockLayoutTreeNode[] = [];
const rootNode = buildLayoutTreeNode(rootModel);
const rootNode = buildLayoutTreeNode(rootModel, null, true);
if (rootNode) {
roots.push(rootNode);
}
@@ -155,7 +179,6 @@ export function debugLog(message: string, state: RenderingState) {
}
export function paintPlaceholder(
host: EditorHost,
canvas: HTMLCanvasElement,
layout: ViewportLayoutTree | null,
viewport: Viewport
@@ -175,27 +198,19 @@ export function paintPlaceholder(
'rgba(160, 160, 160, 0.7)',
];
const layoutHandlers = host.std.provider.getAll(
BlockLayoutHandlersIdentifier
);
const handlersArray = Array.from(layoutHandlers.values());
const paintNode = (node: BlockLayoutTreeNode, depth: number = 0) => {
const { layout: nodeLayout, type } = node;
const handler = handlersArray.find(h => h.blockType === type);
if (handler) {
ctx.fillStyle = colors[depth % colors.length];
const rect = nodeLayout.rect;
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
const width = rect.w * viewport.zoom * dpr;
const height = rect.h * viewport.zoom * dpr;
const { layout: nodeLayout } = node;
ctx.fillStyle = colors[depth % colors.length];
const rect = nodeLayout.rect;
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
const width = rect.w * viewport.zoom * dpr;
const height = rect.h * viewport.zoom * dpr;
ctx.fillRect(x, y, width, height);
if (width > 10 && height > 5) {
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
ctx.strokeRect(x, y, width, height);
}
ctx.fillRect(x, y, width, height);
if (width > 10 && height > 5) {
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
ctx.strokeRect(x, y, width, height);
}
if (node.children.length > 0) {

View File

@@ -422,12 +422,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
}
private paintPlaceholder() {
paintPlaceholder(
this.std.host,
this.canvas,
this.layoutCache,
this.viewport
);
paintPlaceholder(this.canvas, this.layoutCache, this.viewport);
}
}

View File

@@ -54,6 +54,7 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
text-overflow: ellipsis;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
transition: background 0.3s ease-in-out;
transform: translateY(-0.2em);
}
}
@@ -137,10 +138,6 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
};
private readonly _handleDocReference = (docId: string) => {
if (this.readonly) {
return;
}
this.std
.getOptional(PeekViewProvider)
?.peek({

View File

@@ -55,6 +55,8 @@ export type AttachmentBlockProps = {
embed: boolean | BackwardCompatibleUndefined;
style?: (typeof AttachmentBlockStyles)[number];
footnoteIdentifier: string | null;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -74,6 +76,7 @@ export const defaultAttachmentProps: AttachmentBlockProps = {
'meta:updatedAt': undefined,
'meta:createdBy': undefined,
'meta:updatedBy': undefined,
footnoteIdentifier: null,
};
export const AttachmentBlockSchema = defineBlockSchema({

View File

@@ -20,12 +20,14 @@ export const BookmarkStyles: EmbedCardStyle[] = [
'horizontal',
'list',
'cube',
'citation',
] as const;
export type BookmarkBlockProps = {
style: (typeof BookmarkStyles)[number];
url: string;
caption: string | null;
footnoteIdentifier: string | null;
} & LinkPreviewData &
Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -48,6 +50,8 @@ const defaultBookmarkProps: BookmarkBlockProps = {
'meta:updatedAt': undefined,
'meta:createdBy': undefined,
'meta:updatedBy': undefined,
footnoteIdentifier: null,
};
export const BookmarkBlockSchema = defineBlockSchema({

View File

@@ -10,11 +10,13 @@ export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
'list',
'cube',
'horizontalThin',
'citation',
];
export type EmbedLinkedDocBlockProps = {
style: EmbedCardStyle;
caption: string | null;
footnoteIdentifier: string | null;
} & ReferenceInfo;
export class EmbedLinkedDocModel extends defineEmbedModel<EmbedLinkedDocBlockProps>(

View File

@@ -14,6 +14,8 @@ const defaultEmbedLinkedDocBlockProps: EmbedLinkedDocBlockProps = {
// title & description aliases
title: undefined,
description: undefined,
footnoteIdentifier: null,
};
export const EmbedLinkedDocBlockSchema = createEmbedBlockSchema({

View File

@@ -9,6 +9,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types.js';
import { ImageBlockTransformer } from './image-transformer.js';
@@ -19,6 +20,7 @@ export type ImageBlockProps = {
height?: number;
rotate: number;
size?: number;
textAlign?: TextAlign;
} & Omit<GfxCommonBlockProps, 'scale'> &
BlockMeta;
@@ -32,6 +34,7 @@ const defaultImageProps: ImageBlockProps = {
lockedBySelf: false,
rotate: 0,
size: -1,
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -5,6 +5,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
// `toggle` type has been deprecated, do not use it
@@ -13,6 +14,7 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
export type ListProps = {
type: ListType;
text: Text;
textAlign?: TextAlign;
checked: boolean;
collapsed: boolean;
order: number | null;
@@ -24,6 +26,7 @@ export const ListBlockSchema = defineBlockSchema({
({
type: 'bulleted',
text: internal.Text(),
textAlign: undefined,
checked: false,
collapsed: false,

View File

@@ -5,6 +5,7 @@ import {
type Text,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type ParagraphType =
@@ -19,6 +20,7 @@ export type ParagraphType =
export type ParagraphProps = {
type: ParagraphType;
textAlign?: TextAlign;
text: Text;
collapsed: boolean;
} & BlockMeta;
@@ -28,6 +30,7 @@ export const ParagraphBlockSchema = defineBlockSchema({
props: (internal): ParagraphProps => ({
type: 'text',
text: internal.Text(),
textAlign: undefined,
collapsed: false,
'meta:createdAt': undefined,
'meta:createdBy': undefined,

View File

@@ -5,6 +5,7 @@ import {
defineBlockSchema,
} from '@blocksuite/store';
import type { TextAlign } from '../../consts';
import type { BlockMeta } from '../../utils/types';
export type TableCell = {
@@ -29,6 +30,7 @@ export interface TableBlockProps extends BlockMeta {
columns: Record<string, TableColumn>;
// key = `${rowId}:${columnId}`
cells: Record<string, TableCell>;
textAlign?: TextAlign;
}
export interface TableCellSerialized {
@@ -51,6 +53,7 @@ export const TableBlockSchema = defineBlockSchema({
rows: {},
columns: {},
cells: {},
textAlign: undefined,
'meta:createdAt': undefined,
'meta:createdBy': undefined,
'meta:updatedAt': undefined,

View File

@@ -106,6 +106,11 @@ export type ConnectorElementProps = BaseElementProps & {
export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorElementProps> {
updatingPath = false;
/**
* Connectors should always render, even during zoom.
*/
forceFullRender = true;
override get connectable() {
return false as const;
}

View File

@@ -17,7 +17,8 @@ export type EmbedCardStyle =
| 'figma'
| 'html'
| 'syncedDoc'
| 'pdf';
| 'pdf'
| 'citation';
export type LinkPreviewData = {
description: string | null;

View File

@@ -0,0 +1,35 @@
import { TextAlign } from '@blocksuite/affine-model';
import {
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@blocksuite/icons/lit';
import type { TemplateResult } from 'lit';
export interface TextAlignConfig {
textAlign: TextAlign;
name: string;
hotkey: string[] | null;
icon: TemplateResult<1>;
}
export const textAlignConfigs: TextAlignConfig[] = [
{
textAlign: TextAlign.Left,
name: 'Align left',
hotkey: [`Mod-Shift-L`],
icon: TextAlignLeftIcon(),
},
{
textAlign: TextAlign.Center,
name: 'Align center',
hotkey: [`Mod-Shift-E`],
icon: TextAlignCenterIcon(),
},
{
textAlign: TextAlign.Right,
name: 'Align right',
hotkey: [`Mod-Shift-R`],
icon: TextAlignRightIcon(),
},
];

View File

@@ -1,3 +1,4 @@
export { type TextAlignConfig, textAlignConfigs } from './align';
export { type TextConversionConfig, textConversionConfigs } from './conversion';
export {
asyncGetRichText,

View File

@@ -22,10 +22,12 @@ export {
type BlockMarkdownAdapterMatcher,
BlockMarkdownAdapterMatcherIdentifier,
FOOTNOTE_DEFINITION_PREFIX,
getFootnoteDefinitionText,
IN_PARAGRAPH_NODE_CONTEXT_KEY,
InlineDeltaToMarkdownAdapterExtension,
type InlineDeltaToMarkdownAdapterMatcher,
InlineDeltaToMarkdownAdapterMatcherIdentifier,
isFootnoteDefinitionNode,
isMarkdownAST,
type Markdown,
MarkdownAdapter,

View File

@@ -1,4 +1,4 @@
import type { Root, RootContentMap } from 'mdast';
import type { FootnoteDefinition, Root, RootContentMap } from 'mdast';
export type Markdown = string;
@@ -16,5 +16,17 @@ export const isMarkdownAST = (node: unknown): node is MarkdownAST =>
'type' in (node as object) &&
(node as MarkdownAST).type !== undefined;
export const isFootnoteDefinitionNode = (
node: MarkdownAST
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
export const getFootnoteDefinitionText = (node: FootnoteDefinition) => {
const childNode = node.children[0];
if (childNode.type !== 'paragraph') return '';
const paragraph = childNode.children[0];
if (paragraph.type !== 'text') return '';
return paragraph.value;
};
export const FOOTNOTE_DEFINITION_PREFIX = 'footnoteDefinition:';
export const IN_PARAGRAPH_NODE_CONTEXT_KEY = 'mdast:paragraph';

View File

@@ -31,6 +31,7 @@ export const EMBED_CARD_WIDTH: Record<EmbedCardStyle, number> = {
html: 752,
syncedDoc: 800,
pdf: 537 + 24 + 2,
citation: 752,
};
export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
@@ -45,6 +46,7 @@ export const EMBED_CARD_HEIGHT: Record<EmbedCardStyle, number> = {
html: 544,
syncedDoc: 455,
pdf: 759 + 46 + 24 + 2,
citation: 52,
};
export const EMBED_BLOCK_FLAVOUR_LIST = [

View File

@@ -19,6 +19,8 @@ export interface BlockSuiteFlags {
enable_callout: boolean;
enable_edgeless_scribbled_style: boolean;
enable_embed_doc_with_alias: boolean;
enable_turbo_renderer: boolean;
enable_citation: boolean;
}
export class FeatureFlagService extends StoreExtension {
@@ -42,6 +44,8 @@ export class FeatureFlagService extends StoreExtension {
enable_callout: false,
enable_edgeless_scribbled_style: false,
enable_embed_doc_with_alias: false,
enable_turbo_renderer: false,
enable_citation: false,
});
setFlag(key: keyof BlockSuiteFlags, value: boolean) {

View File

@@ -1,10 +1,23 @@
import { StoreExtension } from '@blocksuite/store';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { Extension } from '@blocksuite/store';
// bytes.parse('2GB')
const maxFileSize = 2 * 1024 * 1024 * 1024;
export class FileSizeLimitService extends StoreExtension {
static override key = 'file-size-limit';
maxFileSize = maxFileSize;
export interface IFileSizeLimitService {
maxFileSize: number;
onOverFileSize?: () => void;
}
export const FileSizeLimitProvider = createIdentifier<IFileSizeLimitService>(
'FileSizeLimitService'
);
export class FileSizeLimitService
extends Extension
implements IFileSizeLimitService
{
// 2GB
maxFileSize = 2 * 1024 * 1024 * 1024;
static override setup(di: Container) {
di.addImpl(FileSizeLimitProvider, FileSizeLimitService);
}
}

View File

@@ -4,6 +4,8 @@ import {
EDGELESS_TOOLBAR_WIDGET,
EdgelessToolbarWidget,
} from './edgeless-toolbar';
import { EdgelessSlideMenu } from './menu/slide-menu';
import { ToolbarArrowUpIcon } from './menu/toolbar-arrow-up-icon';
import { EdgelessFontFamilyPanel } from './panel/font-family-panel';
import { EdgelessFontWeightAndStylePanel } from './panel/font-weight-and-style-panel';
@@ -16,6 +18,8 @@ export function effects() {
EdgelessFontWeightAndStylePanel
);
customElements.define('edgeless-font-family-panel', EdgelessFontFamilyPanel);
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
}
declare global {
@@ -25,5 +29,7 @@ declare global {
'edgeless-toolbar-widget': EdgelessToolbarWidget;
'edgeless-font-weight-and-style-panel': EdgelessFontWeightAndStylePanel;
'edgeless-font-family-panel': EdgelessFontFamilyPanel;
'edgeless-slide-menu': EdgelessSlideMenu;
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
}
}

View File

@@ -1,7 +1,3 @@
import {
type EdgelessToolbarSlots,
edgelessToolbarSlotsContext,
} from '@blocksuite/affine-widget-edgeless-toolbar';
import { WithDisposable } from '@blocksuite/global/lit';
import { ArrowRightSmallIcon } from '@blocksuite/icons/lit';
import { consume } from '@lit/context';
@@ -9,6 +5,11 @@ import { css, html, LitElement } from 'lit';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import {
type EdgelessToolbarSlots,
edgelessToolbarSlotsContext,
} from '../index';
export class EdgelessSlideMenu extends WithDisposable(LitElement) {
static override styles = css`
:host {

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