Compare commits

..

2 Commits

Author SHA1 Message Date
Chen
5526696357 fix: update toggle switch active class name 2025-04-11 10:31:02 +08:00
cuikaipeng
5be0292536 fix(editor): the switch button style set in the TOC does not effect 2025-04-10 21:59:03 +08:00
560 changed files with 7034 additions and 11658 deletions

View File

@@ -5,7 +5,7 @@ rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"]
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup,-no_fixup_chains", "-C", "link-args=-all_load", "-C", "link-args=-weak_framework ScreenCaptureKit"]
rustflags = ["-C", "link-args=-all_load", "-C", "link-args=-weak_framework ScreenCaptureKit"]
# https://sourceware.org/bugzilla/show_bug.cgi?id=21032
# https://sourceware.org/bugzilla/show_bug.cgi?id=21031
# https://github.com/rust-lang/rust/issues/134820

View File

@@ -44,7 +44,7 @@ services:
redis:
image: redis
container_name: affine_redis
container_name: redis
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
@@ -54,7 +54,7 @@ services:
postgres:
image: postgres:16
container_name: affine_postgres
container_name: postgres
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
environment:

View File

@@ -200,6 +200,11 @@
"type": "object",
"description": "Configuration for mailer module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether enabled mail service.\n@default false",
"default": false
},
"SMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"\n@environment `MAILER_HOST`",

View File

@@ -218,43 +218,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-bs-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
e2e-blocksuite-cross-browser-test:
name: E2E BlockSuite Cross Browser Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
shard: [1, 2]
browser: ['chromium', 'firefox', 'webkit']
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
playwright-platform: ${{ matrix.browser }}
electron-install: false
full-cache: true
- name: Run playground build
run: yarn workspace @blocksuite/playground build
- name: Run playwright tests
env:
BROWSER: ${{ matrix.browser }}
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
name: test-results-e2e-legacy-bs-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
@@ -1177,7 +1141,6 @@ jobs:
- check-yarn-binary
- e2e-test
- e2e-blocksuite-test
- e2e-blocksuite-cross-browser-test
- e2e-mobile-test
- unit-test
- build-native

3
.gitignore vendored
View File

@@ -85,6 +85,3 @@ packages/frontend/core/public/static/templates
af
af.cmd
*.resolved
# playwright
storageState.json

View File

@@ -29,7 +29,10 @@
"type": "chrome",
"request": "launch",
"name": "Debug AFFiNE Web",
"url": "http://localhost:8080"
"url": "http://localhost:8080",
"sourceMapPathOverrides": {
"webpack://affine/blocksuite/*": "${workspaceFolder}/blocksuite/*"
}
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@ npmPublishAccess: public
npmPublishRegistry: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.9.1.cjs
yarnPath: .yarn/releases/yarn-4.9.0.cjs

113
Cargo.lock generated
View File

@@ -79,7 +79,6 @@ dependencies = [
"block2",
"core-foundation",
"coreaudio-rs",
"criterion2",
"dispatch2",
"libc",
"napi",
@@ -287,9 +286,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "arbitrary"
@@ -514,9 +513,9 @@ checksum = "4848ed5727d39a7573551c205bcb1ccd88c8cad4ed2c80f62e2316f208196b8d"
[[package]]
name = "bstr"
version = "1.12.0"
version = "1.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
dependencies = [
"memchr",
"regex-automata 0.4.9",
@@ -602,9 +601,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.19"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
@@ -720,9 +719,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.36"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
"clap_derive",
@@ -730,9 +729,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.36"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstream",
"anstyle",
@@ -1208,9 +1207,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.11"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -1358,9 +1357,9 @@ dependencies = [
[[package]]
name = "four-char-code"
version = "2.3.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42da99970737c0150e3c5cd1cdc510735a2511739f5c3aa3c6bfc9f31441488d"
checksum = "c661315fd366b2a1f970df7b7cb1a28d2678d49ef4872f7dcc19b4a83150f20b"
[[package]]
name = "fs-err"
@@ -1530,9 +1529,9 @@ dependencies = [
[[package]]
name = "half"
version = "2.6.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
dependencies = [
"cfg-if",
"crunchy",
@@ -1845,9 +1844,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.9.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -1976,9 +1975,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "libloading"
@@ -1987,7 +1986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -2019,9 +2018,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "litemap"
@@ -2185,9 +2184,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.8"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
dependencies = [
"adler2",
]
@@ -2561,7 +2560,7 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pdf-extract"
version = "0.8.2"
source = "git+https://github.com/toeverything/pdf-extract?branch=darksky%2Fimprove-font-decoding#e74beed894e1b8dc228c2bf078ed92814b27759f"
source = "git+https://github.com/toeverything/pdf-extract#49ef7d2aec5bb495467a40082cd9717e849ee29a"
dependencies = [
"adobe-cmap-parser",
"cff-parser",
@@ -2897,9 +2896,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.11"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
dependencies = [
"bitflags 2.9.0",
]
@@ -3090,9 +3089,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.26"
version = "0.23.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0"
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
dependencies = [
"once_cell",
"ring",
@@ -3102,6 +3101,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.11.0"
@@ -3179,9 +3187,9 @@ dependencies = [
[[package]]
name = "scroll_derive"
version = "0.12.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932"
dependencies = [
"proc-macro2",
"quote",
@@ -3331,9 +3339,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.15.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
dependencies = [
"serde",
]
@@ -3385,9 +3393,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14e22987355fbf8cfb813a0cf8cd97b1b4ec834b94dbd759a9e8679d41fabe83"
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -3398,11 +3406,10 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55c4720d7d4cd3d5b00f61d03751c685ad09c33ae8290c8a2c11335e0604300b"
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
dependencies = [
"base64 0.22.1",
"bytes",
"chrono",
"crc",
@@ -3421,6 +3428,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"sha2",
@@ -3435,9 +3443,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175147fcb75f353ac7675509bc58abb2cb291caf0fd24a3623b8f7e3eb0a754b"
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
dependencies = [
"proc-macro2",
"quote",
@@ -3448,9 +3456,9 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cde983058e53bfa75998e1982086c5efe3c370f3250bf0357e344fa3352e32b"
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
dependencies = [
"dotenvy",
"either",
@@ -3474,9 +3482,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "847d2e5393a4f39e47e4f36cab419709bc2b83cbe4223c60e86e1471655be333"
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -3517,9 +3525,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc35947a541b9e0a2e3d85da444f1c4137c13040267141b208395a0d0ca4659f"
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
dependencies = [
"atoi",
"base64 0.22.1",
@@ -3555,9 +3563,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c48291dac4e5ed32da0927a0b981788be65674aeb62666d19873ab4289febde"
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
dependencies = [
"atoi",
"chrono",
@@ -3573,7 +3581,6 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.12",
"tracing",
"url",
]
@@ -5051,9 +5058,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]

View File

@@ -40,7 +40,7 @@ objc2-foundation = "0.3"
once_cell = "1"
parking_lot = "0.12"
path-ext = "0.1.1"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
pdf-extract = { git = "https://github.com/toeverything/pdf-extract" }
rand = "0.9"
rayon = "1.10"
readability = { version = "0.3.0", default-features = false }

View File

@@ -55,8 +55,6 @@
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
@@ -117,12 +115,10 @@
"./widgets/edgeless-auto-connect": "./src/widgets/edgeless-auto-connect.ts",
"./widgets/edgeless-toolbar": "./src/widgets/edgeless-toolbar.ts",
"./widgets/frame-title": "./src/widgets/frame-title.ts",
"./widgets/linked-doc": "./src/widgets/linked-doc.ts",
"./widgets/remote-selection": "./src/widgets/remote-selection.ts",
"./widgets/scroll-anchoring": "./src/widgets/scroll-anchoring.ts",
"./widgets/slash-menu": "./src/widgets/slash-menu.ts",
"./widgets/toolbar": "./src/widgets/toolbar.ts",
"./widgets/keyboard-toolbar": "./src/widgets/keyboard-toolbar.ts",
"./fragments/doc-title": "./src/fragments/doc-title.ts",
"./fragments/frame-panel": "./src/fragments/frame-panel.ts",
"./fragments/outline": "./src/fragments/outline.ts",

View File

@@ -2448,262 +2448,203 @@ World!
});
describe('markdown to snapshot', () => {
describe('code', () => {
test('markdown code block', async () => {
const markdown = '```python\nimport this\n```\n';
test('code', async () => {
const markdown = '```python\nimport this\n```\n';
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'import this',
},
],
},
},
children: [],
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'import this',
},
],
},
},
children: [],
},
],
};
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('code with indentation 1 - slice', async () => {
const markdown = '```python\n import this\n```';
test('code with indentation 1 - slice', async () => {
const markdown = '```python\n import this\n```';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: ' import this',
},
],
},
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: ' import this',
},
],
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('code with indentation 2 - slice', async () => {
const markdown = '````python\n```python\n import this\n```\n````';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
children: [],
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: '```python\n import this\n```',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('code with indentation 3 - slice', async () => {
const markdown = '~~~~python\n````python\n import this\n````\n~~~~';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: '````python\n import this\n````',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('html block import as code block', async () => {
const markdown = `<div class="container">
<header>
<h1>Welcome to My Page</h1>
<nav>
<ul>
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
</ul>
</nav>
</header>
<main>
<p>This is a sample HTML content</p>
</main>
</div>`;
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
],
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'html',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert:
'<div class="container">\n <header>\n <h1>Welcome to My Page</h1>\n <nav>\n <ul>\n <li><a href="#home">Home</a></li>\n <li><a href="#about">About</a></li>\n </ul>\n </nav>\n </header>\n <main>\n <p>This is a sample HTML content</p>\n </main>\n</div>',
},
],
},
},
children: [],
},
],
};
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('code with indentation 2 - slice', async () => {
const markdown = '````python\n```python\n import this\n```\n````';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: '```python\n import this\n```',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('code with indentation 3 - slice', async () => {
const markdown = '~~~~python\n````python\n import this\n````\n~~~~';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:code',
props: {
language: 'python',
wrap: false,
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: '````python\n import this\n````',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
test('paragraph', async () => {
@@ -3697,6 +3638,48 @@ bbb
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('html tag', async () => {
const markdown = `<aaa>\n`;
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: '<aaa>',
},
],
},
type: 'text',
},
children: [],
},
],
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
describe('inline latex', () => {
test.each([
['dollar sign syntax', 'inline $E=mc^2$ latex\n'],
@@ -4108,55 +4091,4 @@ hhh
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should not wrap url with angle brackets if it is not a url', async () => {
const markdown = 'prompt: How many people will live in the world in 2040?';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert:
'prompt: How many people will live in the world in 2040?',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawSliceSnapshot = await mdAdapter.toSliceSnapshot({
file: markdown,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(rawSliceSnapshot!)).toEqual(sliceSnapshot);
});
});

View File

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

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-linked-doc';

View File

@@ -52,8 +52,6 @@
{ "path": "../widgets/edgeless-auto-connect" },
{ "path": "../widgets/edgeless-toolbar" },
{ "path": "../widgets/frame-title" },
{ "path": "../widgets/keyboard-toolbar" },
{ "path": "../widgets/linked-doc" },
{ "path": "../widgets/remote-selection" },
{ "path": "../widgets/scroll-anchoring" },
{ "path": "../widgets/slash-menu" },

View File

@@ -28,7 +28,7 @@ import { checkAttachmentBlob, downloadAttachmentBlob } from './utils';
@Peekable({
enableOn: ({ model }: AttachmentBlockComponent) => {
return !model.doc.readonly && model.props.type.endsWith('pdf');
return model.props.type.endsWith('pdf');
},
})
export class AttachmentBlockComponent extends CaptionedBlockComponent<AttachmentBlockModel> {

View File

@@ -146,20 +146,17 @@ const embedConfig: AttachmentEmbedConfig[] = [
// More options: https://tinytip.co/tips/html-pdf-params/
// https://chromium.googlesource.com/chromium/src/+/refs/tags/121.0.6153.1/chrome/browser/resources/pdf/open_pdf_params_parser.ts
const parameters = '#toolbar=0';
return html`
<iframe
style="width: 100%; color-scheme: auto;"
height="480"
src=${blobUrl + parameters}
loading="lazy"
scrolling="no"
frameborder="no"
allowTransparency
allowfullscreen
type="application/pdf"
></iframe>
<div class="affine-attachment-embed-event-mask"></div>
`;
return html`<iframe
style="width: 100%; color-scheme: auto;"
height="480"
src=${blobUrl + parameters}
loading="lazy"
scrolling="no"
frameborder="no"
allowTransparency
allowfullscreen
type="application/pdf"
></iframe>`;
},
},
{

View File

@@ -136,9 +136,4 @@ export const styles = css`
width: 100%;
height: 100%;
}
.affine-attachment-embed-event-mask {
position: absolute;
inset: 0;
}
`;

View File

@@ -81,10 +81,21 @@ export class BookmarkCard extends SignalWatcher(
const theme = this.bookmark.std.get(ThemeProvider).theme;
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIconType =
!icon?.split('.').pop() || icon?.split('.').pop() === 'svg'
? 'svg+xml'
: icon?.split('.').pop();
const titleIcon = this.loading
? LoadingIcon
: icon
? html`<img src=${icon} alt="icon" />`
? html`<object
type="image/${titleIconType}"
data=${icon}
draggable="false"
>
${WebIcon16}
</object>`
: WebIcon16;
const descriptionText = this.loading
@@ -97,7 +108,9 @@ export class BookmarkCard extends SignalWatcher(
const bannerImage =
!this.loading && image
? html`<img src=${image} alt="banner" />`
? html`<object type="image/webp" data=${image} draggable="false">
${EmbedCardBannerIcon}
</object>`
: EmbedCardBannerIcon;
return html`

View File

@@ -21,21 +21,16 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
.affine-callout-block-container {
display: flex;
padding: 5px 10px;
padding: 12px 16px;
border-radius: 8px;
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
}
.affine-callout-emoji-container {
margin-right: 10px;
margin-top: 14px;
margin-right: 12px;
margin-top: 10px;
user-select: none;
font-size: 1.2em;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.affine-callout-emoji:hover {
cursor: pointer;
@@ -45,7 +40,6 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
.affine-callout-children {
flex: 1;
min-width: 0;
padding-left: 10px;
}
`;

View File

@@ -11,7 +11,6 @@
"license": "MIT",
"dependencies": {
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-inline-latex": "workspace:*",
"@blocksuite/affine-inline-link": "workspace:*",
"@blocksuite/affine-inline-preset": "workspace:*",
@@ -36,8 +35,7 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/code-painter.worker.ts"
"./effects": "./src/effects.ts"
},
"files": [
"src",

View File

@@ -3,51 +3,25 @@ import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
CODE_BLOCK_WRAP_KEY,
IN_PARAGRAPH_NODE_CONTEXT_KEY,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { Code, Html } from 'mdast';
import type { Code } from 'mdast';
const isCodeNode = (node: MarkdownAST): node is Code => node.type === 'code';
const isHtmlNode = (node: MarkdownAST): node is Html => node.type === 'html';
const isCodeOrHtmlNode = (node: MarkdownAST): node is Code | Html =>
isCodeNode(node) || isHtmlNode(node);
export const codeBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
flavour: CodeBlockSchema.model.flavour,
toMatch: o => isCodeOrHtmlNode(o.node),
toMatch: o => isCodeNode(o.node),
fromMatch: o => o.node.flavour === 'affine:code',
toBlockSnapshot: {
enter: (o, context) => {
if (!isCodeOrHtmlNode(o.node)) {
if (!isCodeNode(o.node)) {
return;
}
const { walkerContext, configs } = context;
const wrap = configs.get(CODE_BLOCK_WRAP_KEY) === 'true';
let language = 'plain text';
switch (o.node.type) {
case 'code': {
if (o.node.lang) {
language = o.node.lang;
}
break;
}
case 'html': {
const inParagraphNode = !!walkerContext.getGlobalContext(
IN_PARAGRAPH_NODE_CONTEXT_KEY
);
// only handle top level html node
if (inParagraphNode) {
return;
}
language = 'html';
break;
}
}
walkerContext
.openNode(
{
@@ -55,7 +29,7 @@ export const codeBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
id: nanoid(),
flavour: 'affine:code',
props: {
language,
language: o.node.lang ?? 'Plain Text',
wrap,
text: {
'$blocksuite:internal:text$': true,

View File

@@ -2,7 +2,6 @@ import {
type MarkdownAdapterPreprocessor,
MarkdownPreprocessorExtension,
} from '@blocksuite/affine-shared/adapters';
import { isValidUrl } from '@blocksuite/affine-shared/utils';
const codePreprocessor: MarkdownAdapterPreprocessor = {
name: 'code',
@@ -54,9 +53,14 @@ const codePreprocessor: MarkdownAdapterPreprocessor = {
//
// eg. /MuawcBMT1Mzvoar09-_66?mode=page&blockIds=rL2_GXbtLU2SsJVfCSmh_
// https://www.markdownguide.org/basic-syntax/#urls-and-email-addresses
const valid = isValidUrl(trimmedLine);
if (valid) {
return `<${trimmedLine}>`;
try {
const valid =
URL.canParse?.(trimmedLine) ?? Boolean(new URL(trimmedLine));
if (valid) {
return `<${trimmedLine}>`;
}
} catch (err) {
console.log(err);
}
}

View File

@@ -1,173 +0,0 @@
import { deleteTextCommand } from '@blocksuite/affine-inline-preset';
import {
HtmlAdapter,
pasteMiddleware,
PlainTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
getBlockIndexCommand,
getBlockSelectionsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import { type Container, createIdentifier } from '@blocksuite/global/di';
import { DisposableGroup } from '@blocksuite/global/disposable';
import {
type BlockStdScope,
Clipboard,
type ClipboardAdapterConfig,
LifeCycleWatcher,
LifeCycleWatcherIdentifier,
StdIdentifier,
type UIEventHandler,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
export const CodeClipboardAdapterConfigIdentifier =
createIdentifier<ClipboardAdapterConfig>('code-clipboard-adapter-config');
export function CodeClipboardAdapterConfigExtension(
config: ClipboardAdapterConfig
): ExtensionType {
return {
setup: di => {
di.addImpl(
CodeClipboardAdapterConfigIdentifier(config.mimeType),
() => config
);
},
};
}
const PlainTextClipboardConfig = CodeClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: PlainTextAdapter,
priority: 90,
});
const HtmlClipboardConfig = CodeClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 80,
});
export class CodeBlockClipboard extends Clipboard {
static override readonly key = 'code-block-clipboard';
override get _adapters() {
const adapterConfigs = this.std.provider.getAll(
CodeClipboardAdapterConfigIdentifier
);
return Array.from(adapterConfigs.values());
}
}
export class CodeBlockClipboardController extends LifeCycleWatcher {
static override key = 'code-block-clipboard-controller';
private readonly _disposables = new DisposableGroup();
constructor(
std: BlockStdScope,
readonly clipboard: CodeBlockClipboard
) {
super(std);
}
static override setup(di: Container) {
di.add(
this as unknown as {
new (
std: BlockStdScope,
clipboard: CodeBlockClipboard
): CodeBlockClipboardController;
},
[StdIdentifier, CodeBlockClipboard]
);
di.addImpl(LifeCycleWatcherIdentifier(this.key), provider =>
provider.get(this)
);
}
protected _init = () => {
const paste = pasteMiddleware(this.std);
this.clipboard.use(paste);
this._disposables.add({
dispose: () => {
this.clipboard.unuse(paste);
},
});
};
onPaste: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this.std.store.captureSync();
this.std.command
.chain()
.try(cmd => [
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
const textSelection = ctx.currentTextSelection;
if (!textSelection) return;
const end = textSelection.to ?? textSelection.from;
next({ currentSelectionPath: end.blockId });
}),
cmd.pipe(getBlockSelectionsCommand).pipe((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
if (!currentBlockSelections) return;
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) return;
next({ currentSelectionPath: blockSelection.blockId });
}),
])
.pipe(getBlockIndexCommand)
.try(cmd => [cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand)])
.pipe((ctx, next) => {
if (!ctx.parentBlock) {
return;
}
this.clipboard
.paste(
e,
this.std.store,
ctx.parentBlock.model.id,
ctx.blockIndex ? ctx.blockIndex + 1 : 1
)
.catch(console.error);
return next();
})
.run();
return true;
};
override mounted() {
this._init();
// add paste event listener for code block
const subscription = this.std.view.viewUpdated.subscribe(
({ type, method, view }) => {
if (type !== 'block' || view.model.flavour !== 'affine:code') return;
if (method === 'add') {
view.handleEvent('paste', this.onPaste);
}
}
);
this._disposables.add(subscription);
}
override unmounted() {
this._disposables.dispose();
}
}
export function getCodeClipboardExtensions(): ExtensionType[] {
return [
PlainTextClipboardConfig,
HtmlClipboardConfig,
CodeBlockClipboard,
CodeBlockClipboardController,
];
}

View File

@@ -8,7 +8,6 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { CodeBlockAdapterExtensions } from './adapters/extension.js';
import { getCodeClipboardExtensions } from './clipboard/index.js';
import {
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
@@ -34,5 +33,4 @@ export const CodeBlockSpec: ExtensionType[] = [
CodeBlockAdapterExtensions,
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension,
...getCodeClipboardExtensions(),
].flat();

View File

@@ -1,7 +1,7 @@
import { affineTextStyles } from '@blocksuite/affine-shared/styles';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { ShadowlessElement } from '@blocksuite/std';
import { ZERO_WIDTH_FOR_EMPTY_LINE } from '@blocksuite/std/inline';
import { ZERO_WIDTH_SPACE } from '@blocksuite/std/inline';
import type { DeltaInsert } from '@blocksuite/store';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
@@ -111,7 +111,7 @@ export class AffineCodeUnit extends ShadowlessElement {
@property({ type: Object })
accessor delta: DeltaInsert<AffineTextAttributes> = {
insert: ZERO_WIDTH_FOR_EMPTY_LINE,
insert: ZERO_WIDTH_SPACE,
};
}

View File

@@ -1,8 +1,5 @@
export * from './adapters';
export * from './clipboard';
export * from './code-block';
export * from './code-block-config';
export * from './code-block-spec';
export * from './code-toolbar';
export * from './turbo/code-layout-handler';
export * from './turbo/code-painter.worker';

View File

@@ -1,71 +0,0 @@
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
import {
BlockLayoutHandlerExtension,
BlockLayoutHandlersIdentifier,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { CodeLayout } from './code-painter.worker';
export class CodeLayoutHandlerExtension extends BlockLayoutHandlerExtension<CodeLayout> {
readonly blockType = 'affine:code';
static override setup(di: Container) {
di.addImpl(
BlockLayoutHandlersIdentifier('code'),
CodeLayoutHandlerExtension
);
}
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): CodeLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
const codeBlockElement = component.querySelector(
'.affine-code-block-container'
);
if (!codeBlockElement) return null;
const { zoom, viewScale } = viewportRecord;
const codeLayout: CodeLayout = {
type: 'affine:code',
blockId: model.id,
rect: { x: 0, y: 0, w: 0, h: 0 },
};
// Get the bounding rect of the code block
const clientRect = codeBlockElement.getBoundingClientRect();
if (!clientRect) return null;
// Convert client coordinates to model coordinates
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
clientRect.x,
clientRect.y,
]);
codeLayout.rect = {
x: modelX,
y: modelY,
w: clientRect.width / zoom / viewScale,
h: clientRect.height / zoom / viewScale,
};
return codeLayout;
}
calculateBound(layout: CodeLayout) {
const rect: Rect = layout.rect;
return {
rect,
subRects: [rect],
};
}
}

View File

@@ -1,53 +0,0 @@
import type {
BlockLayout,
BlockLayoutPainter,
WorkerToHostMessage,
} from '@blocksuite/affine-gfx-turbo-renderer';
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
export interface CodeLayout extends BlockLayout {
type: 'affine:code';
}
function isCodeLayout(layout: BlockLayout): layout is CodeLayout {
return layout.type === 'affine:code';
}
class CodeLayoutPainter implements BlockLayoutPainter {
paint(
ctx: OffscreenCanvasRenderingContext2D,
layout: BlockLayout,
layoutBaseX: number,
layoutBaseY: number
): void {
if (!isCodeLayout(layout)) {
const message: WorkerToHostMessage = {
type: 'paintError',
error: 'Invalid layout format',
blockType: 'affine:code',
};
self.postMessage(message);
return;
}
// Get the layout dimensions
const x = layout.rect.x - layoutBaseX;
const y = layout.rect.y - layoutBaseY;
const width = layout.rect.w;
const height = layout.rect.h;
// Simple white rectangle for now
ctx.fillStyle = 'white';
ctx.fillRect(x, y, width, height);
// Add a border to visualize the code block
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
}
}
export const CodeLayoutPainterExtension = BlockLayoutPainterExtension(
'affine:code',
CodeLayoutPainter
);

View File

@@ -8,7 +8,6 @@
"include": ["./src"],
"references": [
{ "path": "../../components" },
{ "path": "../../gfx/turbo-renderer" },
{ "path": "../../inlines/latex" },
{ "path": "../../inlines/link" },
{ "path": "../../inlines/preset" },

View File

@@ -259,7 +259,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
};
}
override onSelected(context: SelectedContext): void | boolean {
override onSelected(context: SelectedContext) {
const { selected, multiSelect, event: e } = context;
const { editing } = this.gfx.selection;
const alreadySelected = this.gfx.selection.has(this.model.id);
@@ -318,7 +318,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
})
.catch(console.error);
} else {
return super.onSelected(context);
super.onSelected(context);
}
}

View File

@@ -4,7 +4,7 @@ import type {
EmbedGithubStyles,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { BlockSelection, isGfxBlockComponent } from '@blocksuite/std';
import { BlockSelection } from '@blocksuite/std';
import { html, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -166,16 +166,16 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
'affine-embed-github-block': true,
loading,
[style]: true,
edgeless: isGfxBlockComponent(this),
selected: this.selected$.value,
})}
style=${styleMap({
// transform: `scale(${this._scale})`,
transform: `scale(${this._scale})`,
transformOrigin: '0 0 ',
})}
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>
<div class="affine-embed-github-banner">${bannerImage}</div>
<div class="affine-embed-github-content">
<div class="affine-embed-github-content-title">
<div class="affine-embed-github-content-title-icons">
@@ -260,7 +260,6 @@ export class EmbedGithubBlockComponent extends EmbedBlockComponent<
</div>
</div>
</div>
<div class="affine-embed-github-banner">${bannerImage}</div>
</div>
`
);

View File

@@ -14,7 +14,6 @@ export const styles = css`
opacity: var(--add, 1);
background: var(--affine-background-primary-color);
user-select: none;
overflow: hidden;
}
.affine-embed-github-content {
@@ -276,7 +275,7 @@ export const styles = css`
}
.affine-embed-github-block.vertical {
flex-direction: column-reverse;
flex-direction: column;
.affine-embed-github-content {
width: 100%;
@@ -384,8 +383,7 @@ export const styles = css`
.affine-embed-github-content {
width: 100%;
}
.affine-embed-github-block:not(.edgeless) .affine-embed-github-banner {
.affine-embed-github-banner {
display: none;
}
}

View File

@@ -209,9 +209,9 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
});
};
private readonly _handleRetry = async (e: MouseEvent) => {
private readonly _handleRetry = (e: MouseEvent) => {
e.stopPropagation();
const success = await this.onRetry();
this.onRetry();
// track retry event
this.telemetryService?.track('ReloadLink', {
@@ -220,7 +220,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
segment: 'editor',
module: 'embed block',
control: 'reload button',
result: success ? 'success' : 'failure',
});
};
@@ -302,7 +301,7 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
accessor error: Error | null = null;
@property({ attribute: false })
accessor onRetry!: () => Promise<boolean>;
accessor onRetry!: () => void;
@property({ attribute: false })
accessor model!: EmbedIframeBlockModel;

View File

@@ -78,7 +78,7 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
segment: 'editor',
module: 'embed block',
control: 'edit button',
result: status,
other: status,
});
}

View File

@@ -228,7 +228,7 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
segment: this.options?.telemetrySegment ?? 'editor',
module: 'embed block',
control: 'confirm embed link',
result: status,
other: status,
});
}

View File

@@ -299,19 +299,12 @@ export const builtinToolbarConfig = {
icon: ResetIcon(),
run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component
?.refreshData()
.then(success => {
ctx.track('ReloadLink', {
type: 'embed iframe block',
page: 'doc editor',
segment: 'doc',
module: 'toolbar',
control: 'reload link',
result: success ? 'success' : 'failure',
});
})
.catch(console.error);
component?.refreshData().catch(console.error);
ctx.track('ReloadLink', {
...trackBaseProps,
control: 'reload link',
});
},
},
{

View File

@@ -143,7 +143,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
const { url } = this.model.props;
if (!url) {
this.status$.value = 'idle';
return false;
return;
}
// set loading status
@@ -188,13 +188,11 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
// set success status
this.status$.value = 'success';
return true;
} catch (err) {
// set error status
this.status$.value = 'error';
this.error$.value = err instanceof Error ? err : new Error(String(err));
console.error('Failed to refresh iframe data:', err);
return false;
}
};
@@ -286,7 +284,7 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
};
private readonly _handleRetry = async () => {
return await this.refreshData();
await this.refreshData();
};
private readonly _renderIframe = () => {

View File

@@ -1,5 +1,8 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import { insertContent } from '@blocksuite/affine-rich-text';
import {
getInlineEditorByModel,
insertContent,
} from '@blocksuite/affine-rich-text';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { createDefaultDoc } from '@blocksuite/affine-shared/utils';
import {
@@ -65,7 +68,22 @@ const linkedDocSlashMenuConfig: SlashMenuConfig = {
if (!linkedDocWidget) return;
// TODO(@L-Sun): make linked-doc-widget as extension
// @ts-expect-error same as above
linkedDocWidget.show({ addTriggerKey: true });
const triggerKey = linkedDocWidget.config.triggerKeys[0];
insertContent(std, model, triggerKey);
const inlineEditor = getInlineEditorByModel(std, model);
if (inlineEditor) {
// Wait for range to be updated
const subscription = inlineEditor.slots.inlineRangeSync.subscribe(
() => {
// TODO(@L-Sun): make linked-doc-widget as extension
subscription.unsubscribe();
// @ts-expect-error same as above
linkedDocWidget.show({ addTriggerKey: true });
}
);
}
},
},
],

View File

@@ -12,8 +12,6 @@ import {
import {
ActionPlacement,
DocDisplayMetaProvider,
EditorSettingProvider,
FeatureFlagService,
type LinkEventType,
type OpenDocMode,
type ToolbarAction,
@@ -35,7 +33,7 @@ import {
ExpandFullIcon,
OpenInNewIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import { type ExtensionType, Slice } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import { html } from 'lit';
@@ -215,20 +213,6 @@ const conversionsActionGroup = {
},
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
if (
ctx.std
.get(FeatureFlagService)
.getFlag('enable_embed_doc_with_alias') &&
isGfxBlockComponent(block)
) {
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
editorSetting?.set?.(
'docDropCanvasPreferView',
'affine:embed-synced-doc'
);
}
block?.convertToEmbed();
ctx.track('SelectedView', {

View File

@@ -3,8 +3,6 @@ import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import { EmbedSyncedDocModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
EditorSettingProvider,
FeatureFlagService,
type LinkEventType,
type OpenDocMode,
type ToolbarAction,
@@ -23,7 +21,7 @@ import {
ExpandFullIcon,
OpenInNewIcon,
} from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier, isGfxBlockComponent } from '@blocksuite/std';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import { type ExtensionType, Slice } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import { html } from 'lit';
@@ -32,6 +30,7 @@ import { keyed } from 'lit/directives/keyed.js';
import { repeat } from 'lit/directives/repeat.js';
import { EmbedSyncedDocBlockComponent } from '../embed-synced-doc-block';
const trackBaseProps = {
category: 'linked doc',
type: 'embed view',
@@ -143,19 +142,6 @@ const conversionsActionGroup = {
label: 'Card view',
run(ctx) {
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
if (
ctx.std
.get(FeatureFlagService)
.getFlag('enable_embed_doc_with_alias') &&
isGfxBlockComponent(block)
) {
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
editorSetting?.set?.(
'docDropCanvasPreferView',
'affine:embed-linked-doc'
);
}
block?.convertToCard();
ctx.track('SelectedView', {

View File

@@ -117,9 +117,9 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
const nextDepth = this.depth + 1;
const previewSpecBuilder = SpecProvider._.getSpec(name);
const currentDisposables = this.disposables;
const editorSetting = this.std.getOptional(EditorSettingProvider) ?? {
setting$: signal(GeneralSettingSchema.parse({})),
};
const editorSetting =
this.std.getOptional(EditorSettingProvider) ??
signal(GeneralSettingSchema.parse({}));
class EmbedSyncedDocWatcher extends LifeCycleWatcher {
static override key = 'embed-synced-doc-watcher';

View File

@@ -53,7 +53,7 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
};
}
override onSelected(context: SelectedContext): boolean | void {
override onSelected(context: SelectedContext): void {
const { x, y } = context.position;
if (
@@ -63,10 +63,10 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
// otherwise if the frame has title, then ignore it because in this case the frame cannot be selected by frame body
this.model.props.title.length)
) {
return false;
return;
}
return super.onSelected(context);
super.onSelected(context);
}
override renderGfxBlock() {

View File

@@ -80,7 +80,7 @@ const builtinSurfaceToolbarConfig = {
ctx.store.addBlock(
SurfaceRefBlockSchema.model.flavour,
{ reference: frameId, refFlavour: FrameBlockSchema.model.flavour },
{ reference: frameId, refFlavour: NoteBlockSchema.model.flavour },
lastNoteId
);

View File

@@ -13,7 +13,6 @@
"@blocksuite/affine-block-note": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",
@@ -33,8 +32,7 @@
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/image-painter.worker.ts"
"./effects": "./src/effects.ts"
},
"files": [
"src",

View File

@@ -7,7 +7,5 @@ export { ImageProxyService } from './image-proxy-service';
export * from './image-service';
export * from './image-spec';
export * from './styles';
export * from './turbo/image-layout-handler';
export * from './turbo/image-painter.worker';
export { addImages, downloadImageBlob, uploadBlobForImage } from './utils';
export { ImageSelection } from '@blocksuite/affine-shared/selection';

View File

@@ -1,69 +0,0 @@
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
import {
BlockLayoutHandlerExtension,
BlockLayoutHandlersIdentifier,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ImageLayout } from './image-painter.worker';
export class ImageLayoutHandlerExtension extends BlockLayoutHandlerExtension<ImageLayout> {
readonly blockType = 'affine:image';
static override setup(di: Container) {
di.addImpl(
BlockLayoutHandlersIdentifier('image'),
ImageLayoutHandlerExtension
);
}
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ImageLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
const imageContainer = component.querySelector('.affine-image-container');
if (!imageContainer) return null;
const resizableImg = component.querySelector(
'.resizable-img'
) as HTMLElement;
if (!resizableImg) return null;
const { zoom, viewScale } = viewportRecord;
const rect = resizableImg.getBoundingClientRect();
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
const imageLayout: ImageLayout = {
type: 'affine:image',
blockId: model.id,
rect: {
x: modelX,
y: modelY,
w: rect.width / zoom / viewScale,
h: rect.height / zoom / viewScale,
},
};
return imageLayout;
}
calculateBound(layout: ImageLayout) {
const rect: Rect = layout.rect;
return {
rect,
subRects: [rect],
};
}
}

View File

@@ -1,56 +0,0 @@
import type {
BlockLayout,
BlockLayoutPainter,
} from '@blocksuite/affine-gfx-turbo-renderer';
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
export interface ImageLayout extends BlockLayout {
type: 'affine:image';
rect: {
x: number;
y: number;
w: number;
h: number;
};
}
function isImageLayout(layout: BlockLayout): layout is ImageLayout {
return layout.type === 'affine:image';
}
class ImageLayoutPainter implements BlockLayoutPainter {
paint(
ctx: OffscreenCanvasRenderingContext2D,
layout: BlockLayout,
layoutBaseX: number,
layoutBaseY: number
): void {
if (!isImageLayout(layout)) {
console.warn(
'Expected image layout but received different format:',
layout
);
return;
}
// For now, just paint a white rectangle
const x = layout.rect.x - layoutBaseX;
const y = layout.rect.y - layoutBaseY;
const width = layout.rect.w;
const height = layout.rect.h;
// Draw a white rectangle with border
ctx.fillStyle = 'white';
ctx.fillRect(x, y, width, height);
// Add a border
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
}
}
export const ImageLayoutPainterExtension = BlockLayoutPainterExtension(
'affine:image',
ImageLayoutPainter
);

View File

@@ -10,7 +10,6 @@
{ "path": "../note" },
{ "path": "../surface" },
{ "path": "../../components" },
{ "path": "../../gfx/turbo-renderer" },
{ "path": "../../model" },
{ "path": "../../shared" },
{ "path": "../../widgets/slash-menu" },

View File

@@ -2,14 +2,18 @@ import { ParagraphBlockSchema } from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
IN_PARAGRAPH_NODE_CONTEXT_KEY,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { Heading } from 'mdast';
const PARAGRAPH_MDAST_TYPE = new Set(['paragraph', 'heading', 'blockquote']);
const PARAGRAPH_MDAST_TYPE = new Set([
'paragraph',
'html',
'heading',
'blockquote',
]);
const isParagraphMDASTType = (node: MarkdownAST) =>
PARAGRAPH_MDAST_TYPE.has(node.type);
@@ -23,8 +27,32 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
switch (o.node.type) {
case 'html': {
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: o.node.value,
},
],
},
},
children: [],
},
'children'
)
.closeNode();
break;
}
case 'paragraph': {
walkerContext.setGlobalContext(IN_PARAGRAPH_NODE_CONTEXT_KEY, true);
walkerContext
.openNode(
{
@@ -43,6 +71,7 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
'children'
)
.closeNode();
walkerContext.skipAllChildren();
break;
}
case 'heading': {
@@ -90,12 +119,6 @@ export const paragraphBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
}
}
},
leave: (o, context) => {
if (o.node.type === 'paragraph') {
const { walkerContext } = context;
walkerContext.setGlobalContext(IN_PARAGRAPH_NODE_CONTEXT_KEY, false);
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {

View File

@@ -46,8 +46,6 @@
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
"@blocksuite/affine-widget-slash-menu": "workspace:*",

View File

@@ -34,12 +34,6 @@ const NotionClipboardConfig = ClipboardAdapterConfigExtension({
priority: 95,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 90,
});
const imageClipboardConfigs = [
'image/apng',
'image/avif',
@@ -52,14 +46,20 @@ const imageClipboardConfigs = [
return ClipboardAdapterConfigExtension({
mimeType,
adapter: ImageAdapter,
priority: 80,
priority: 85,
});
});
const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/plain',
adapter: MixTextAdapter,
priority: 70,
priority: 80,
});
const HtmlClipboardConfig = ClipboardAdapterConfigExtension({
mimeType: 'text/html',
adapter: HtmlAdapter,
priority: 75,
});
const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({

View File

@@ -33,7 +33,6 @@ import {
ToolbarRegistryExtension,
} from '@blocksuite/affine-shared/services';
import { dragHandleWidget } from '@blocksuite/affine-widget-drag-handle';
import { linkedDocWidget } from '@blocksuite/affine-widget-linked-doc';
import { docRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection';
import { scrollAnchoringWidget } from '@blocksuite/affine-widget-scroll-anchoring';
import { SlashMenuExtension } from '@blocksuite/affine-widget-slash-menu';
@@ -45,7 +44,7 @@ import { RootBlockAdapterExtensions } from '../adapters/extension';
import { clipboardConfigs } from '../clipboard';
import { builtinToolbarConfig } from '../configs/toolbar';
import { fallbackKeymap } from '../keyboard/keymap';
import { viewportOverlayWidget } from './widgets';
import { linkedDocWidget, modalWidget, viewportOverlayWidget } from './widgets';
/**
* Why do we add these extensions into CommonSpecs?
@@ -84,6 +83,7 @@ export const CommonSpecs: ExtensionType[] = [
...clipboardConfigs,
...EdgelessElementViews,
...EdgelessElementRendererExtension,
modalWidget,
SlashMenuExtension,
linkedDocWidget,
dragHandleWidget,

View File

@@ -1,8 +1,20 @@
import { WidgetViewExtension } from '@blocksuite/std';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { AFFINE_LINKED_DOC_WIDGET } from '../widgets/linked-doc/config.js';
import { AFFINE_MODAL_WIDGET } from '../widgets/modal/modal.js';
import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js';
export const modalWidget = WidgetViewExtension(
'affine:page',
AFFINE_MODAL_WIDGET,
literal`${unsafeStatic(AFFINE_MODAL_WIDGET)}`
);
export const linkedDocWidget = WidgetViewExtension(
'affine:page',
AFFINE_LINKED_DOC_WIDGET,
literal`${unsafeStatic(AFFINE_LINKED_DOC_WIDGET)}`
);
export const viewportOverlayWidget = WidgetViewExtension(
'affine:page',
AFFINE_VIEWPORT_OVERLAY_WIDGET,

View File

@@ -10,7 +10,6 @@ import {
} from '@blocksuite/affine-block-embed';
import { updateBlockType } from '@blocksuite/affine-block-note';
import { toast } from '@blocksuite/affine-components/toast';
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
import {
deleteTextCommand,
formatBlockCommand,
@@ -41,6 +40,7 @@ import { ActionPlacement } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
import {
ArrowDownSmallIcon,
CopyIcon,
DatabaseTableViewIcon,
DeleteIcon,
@@ -94,7 +94,7 @@ const conversionsActionGroup = {
aria-label="Conversions"
.tooltip="${'Turn into'}"
>
${conversion.icon} ${EditorChevronDown}
${conversion.icon} ${ArrowDownSmallIcon()}
</editor-icon-button>
`}
>

View File

@@ -88,9 +88,6 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
p: () => {
this._setEdgelessTool('brush');
},
'Shift-p': () => {
this._setEdgelessTool('highlighter');
},
e: () => {
this._setEdgelessTool('eraser');
},

View File

@@ -45,6 +45,7 @@ import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { EdgelessRootBlockWidgetName } from '../types.js';
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
@@ -52,7 +53,8 @@ import { isCanvasElement } from './utils/query.js';
export class EdgelessRootBlockComponent extends BlockComponent<
RootBlockModel,
EdgelessRootService
EdgelessRootService,
EdgelessRootBlockWidgetName
> {
static override styles = css`
affine-edgeless-root {
@@ -349,7 +351,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private _initWheelEvent() {
this._disposables.add(
this.dispatcher.add('wheel', ctx => {
const config = this.std.getOptional(EditorSettingProvider)?.setting$;
const config = this.std.getOptional(EditorSettingProvider);
const state = ctx.get('defaultState');
const e = state.event as WheelEvent;
const edgelessScrollZoom = config?.peek().edgelessScrollZoom ?? false;

View File

@@ -25,12 +25,14 @@ import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockWidgetName } from '../types.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { isCanvasElement } from './utils/query.js';
export class EdgelessRootPreviewBlockComponent extends BlockComponent<
RootBlockModel,
EdgelessRootService
EdgelessRootService,
EdgelessRootBlockWidgetName
> {
static override styles = css`
affine-edgeless-root-preview {
@@ -169,7 +171,7 @@ export class EdgelessRootPreviewBlockComponent extends BlockComponent<
}
private get _disableScheduleUpdate() {
const editorSetting = this.std.getOptional(EditorSettingProvider)?.setting$;
const editorSetting = this.std.getOptional(EditorSettingProvider);
return editorSetting?.peek().edgelessDisableScheduleUpdate ?? false;
}

View File

@@ -6,6 +6,7 @@ import {
type SurfaceBlockModel,
type SurfaceContext,
} from '@blocksuite/affine-block-surface';
import { TemplateJob } from '@blocksuite/affine-gfx-template';
import {
type ConnectorElementModel,
RootBlockSchema,
@@ -38,6 +39,8 @@ export class EdgelessRootService extends RootService implements SurfaceContext {
private readonly _surface: SurfaceBlockModel;
TemplateJob = TemplateJob;
get blocks(): GfxBlockElementModel[] {
return this.layer.blocks;
}

View File

@@ -7,8 +7,6 @@ import { effects as gfxShapeEffects } from '@blocksuite/affine-gfx-shape/effects
import { effects as gfxTemplateEffects } from '@blocksuite/affine-gfx-template/effects';
import { effects as gfxCanvasTextEffects } from '@blocksuite/affine-gfx-text/effects';
import { effects as widgetEdgelessToolbarEffects } from '@blocksuite/affine-widget-edgeless-toolbar/effects';
import { effects as widgetMobileToolbarEffects } from '@blocksuite/affine-widget-keyboard-toolbar/effects';
import { effects as widgetLinkedDocEffects } from '@blocksuite/affine-widget-linked-doc/effects';
import { EdgelessAutoCompletePanel } from './edgeless/components/auto-complete/auto-complete-panel.js';
import { EdgelessAutoComplete } from './edgeless/components/auto-complete/edgeless-auto-complete.js';
@@ -29,6 +27,7 @@ import { ToolbarArrowUpIcon } from './edgeless/components/toolbar/common/toolbar
import { EdgelessDefaultToolButton } from './edgeless/components/toolbar/default/default-tool-button.js';
import { EdgelessLinkToolButton } from './edgeless/components/toolbar/link/link-tool-button.js';
import {
AffineModalWidget,
EdgelessRootBlockComponent,
EdgelessRootPreviewBlockComponent,
PageRootBlockComponent,
@@ -44,6 +43,11 @@ import {
} 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 { effects as widgetMobileToolbarEffects } from './widgets/keyboard-toolbar/effects.js';
import { effects as widgetLinkedDocEffects } from './widgets/linked-doc/effects.js';
import { Loader } from './widgets/linked-doc/import-doc/loader.js';
import { AffineCustomModal } from './widgets/modal/custom-modal.js';
import { AFFINE_MODAL_WIDGET } from './widgets/modal/modal.js';
import {
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
AffinePageDraggingAreaWidget,
@@ -89,6 +93,7 @@ function registerGfxEffects() {
}
function registerWidgets() {
customElements.define(AFFINE_MODAL_WIDGET, AffineModalWidget);
customElements.define(
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
AffinePageDraggingAreaWidget
@@ -119,6 +124,12 @@ function registerEdgelessToolbarComponents() {
}
function registerMiscComponents() {
// Modal and menu components
customElements.define('affine-custom-modal', AffineCustomModal);
// Loading and preview components
customElements.define('loader-element', Loader);
// Toolbar and UI components
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);

View File

@@ -8,7 +8,9 @@ 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 * from './root-config.js';
export { RootService } from './root-service.js';
export * from './transformers/index.js';
export * from './types.js';
export * from './utils/index.js';
export * from './widgets/index.js';

View File

@@ -27,6 +27,7 @@ import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import type { PageRootBlockWidgetName } from '../index.js';
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
import type { PageRootService } from './page-root-service.js';
@@ -51,7 +52,8 @@ function testClickOnBlankArea(
export class PageRootBlockComponent extends BlockComponent<
RootBlockModel,
PageRootService
PageRootService,
PageRootBlockWidgetName
> {
static override styles = css`
editor-host:has(> affine-page-root, * > affine-page-root) {

View File

@@ -1,15 +1,20 @@
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { keyboardToolbarWidget } from '@blocksuite/affine-widget-keyboard-toolbar';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BlockViewExtension, WidgetViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { PageClipboard } from '../clipboard/page-clipboard.js';
import { CommonSpecs } from '../common-specs/index.js';
import { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from '../widgets/keyboard-toolbar/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 keyboardToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_KEYBOARD_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_KEYBOARD_TOOLBAR_WIDGET)}`
);
export const pageDraggingAreaWidget = WidgetViewExtension(
'affine:page',
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
@@ -26,7 +31,7 @@ const PageCommonExtension: ExtensionType[] = [
export const PageRootBlockSpec: ExtensionType[] = [
...PageCommonExtension,
BlockViewExtension('affine:page', literal`affine-page-root`),
IS_MOBILE ? [keyboardToolbarWidget] : [],
keyboardToolbarWidget,
PageClipboard,
].flat();

View File

@@ -0,0 +1,12 @@
import { ConfigExtensionFactory } from '@blocksuite/std';
import type { KeyboardToolbarConfig } from './widgets/keyboard-toolbar/config.js';
import type { LinkedWidgetConfig } from './widgets/linked-doc/index.js';
export interface RootBlockConfig {
linkedWidget?: Partial<LinkedWidgetConfig>;
keyboardToolbar?: Partial<KeyboardToolbarConfig>;
}
export const RootBlockConfigExtension =
ConfigExtensionFactory<RootBlockConfig>('affine:root-block');

View File

@@ -1,6 +1,48 @@
import { RootBlockSchema } from '@blocksuite/affine-model';
import {
getBlockSelectionsCommand,
getImageSelectionsCommand,
getSelectedBlocksCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import type { BlockComponent } from '@blocksuite/std';
import { BlockService } from '@blocksuite/std';
import type { RootBlockComponent } from './types.js';
export abstract class RootService extends BlockService {
static override readonly flavour = RootBlockSchema.model.flavour;
get selectedBlocks() {
let result: BlockComponent[] = [];
this.std.command
.chain()
.tryAll(chain => [
chain.pipe(getTextSelectionCommand),
chain.pipe(getImageSelectionsCommand),
chain.pipe(getBlockSelectionsCommand),
])
.pipe(getSelectedBlocksCommand)
.pipe(({ selectedBlocks }) => {
if (!selectedBlocks) return;
result = selectedBlocks;
})
.run();
return result;
}
get selectedModels() {
return this.selectedBlocks.map(block => block.model);
}
get viewportElement() {
const rootId = this.std.store.root?.id;
if (!rootId) return null;
const rootComponent = this.std.view.getBlock(
rootId
) as RootBlockComponent | null;
if (!rootComponent) return null;
const viewportElement = rootComponent.viewportElement;
return viewportElement;
}
}

View File

@@ -6,7 +6,7 @@ import { sha } from '@blocksuite/global/utils';
import type { DocSnapshot, Schema, Store, Workspace } from '@blocksuite/store';
import { extMimeMap, getAssetName, Transformer } from '@blocksuite/store';
import { download, Unzip, Zip } from './utils.js';
import { download, Unzip, Zip } from '../transformers/utils.js';
async function exportDocs(
collection: Workspace,

View File

@@ -1,5 +1,40 @@
import type { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
import type { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title';
import type {
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET,
} from '@blocksuite/affine-widget-remote-selection';
import type { AFFINE_SLASH_MENU_WIDGET } from '@blocksuite/affine-widget-slash-menu';
import type { EdgelessRootBlockComponent } from './edgeless/edgeless-root-block.js';
import type { PageRootBlockComponent } from './page/page-root-block.js';
import type { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './widgets/edgeless-zoom-toolbar/index.js';
import type { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from './widgets/index.js';
import type { AFFINE_LINKED_DOC_WIDGET } from './widgets/linked-doc/config.js';
import type { AFFINE_MODAL_WIDGET } from './widgets/modal/modal.js';
import type { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from './widgets/page-dragging-area/page-dragging-area.js';
import type { AFFINE_VIEWPORT_OVERLAY_WIDGET } from './widgets/viewport-overlay/viewport-overlay.js';
export type PageRootBlockWidgetName =
| typeof AFFINE_KEYBOARD_TOOLBAR_WIDGET
| typeof AFFINE_MODAL_WIDGET
| typeof AFFINE_SLASH_MENU_WIDGET
| typeof AFFINE_LINKED_DOC_WIDGET
| typeof AFFINE_PAGE_DRAGGING_AREA_WIDGET
| typeof AFFINE_DRAG_HANDLE_WIDGET
| typeof AFFINE_DOC_REMOTE_SELECTION_WIDGET
| typeof AFFINE_VIEWPORT_OVERLAY_WIDGET;
export type EdgelessRootBlockWidgetName =
| typeof AFFINE_MODAL_WIDGET
| typeof AFFINE_SLASH_MENU_WIDGET
| typeof AFFINE_LINKED_DOC_WIDGET
| typeof AFFINE_DRAG_HANDLE_WIDGET
| typeof AFFINE_DOC_REMOTE_SELECTION_WIDGET
| typeof AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET
| typeof AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET
| typeof AFFINE_VIEWPORT_OVERLAY_WIDGET
| typeof AFFINE_FRAME_TITLE_WIDGET;
export type RootBlockComponent =
| PageRootBlockComponent

View File

@@ -1,4 +1,18 @@
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
export * from './keyboard-toolbar/index.js';
export {
type LinkedMenuAction,
type LinkedMenuGroup,
type LinkedMenuItem,
type LinkedWidgetConfig,
LinkedWidgetUtils,
} from './linked-doc/config.js';
export {
// It's used in the AFFiNE!
showImportModal,
} from './linked-doc/import-doc/index.js';
export { AffineLinkedDocWidget } from './linked-doc/index.js';
export { AffineModalWidget } from './modal/modal.js';
export { AffinePageDraggingAreaWidget } from './page-dragging-area/page-dragging-area.js';
export * from './viewport-overlay/viewport-overlay.js';
export { AffineFrameTitleWidget } from '@blocksuite/affine-widget-frame-title';

View File

@@ -34,7 +34,10 @@ import {
toggleUnderline,
} from '@blocksuite/affine-inline-preset';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { insertContent } from '@blocksuite/affine-rich-text';
import {
getInlineEditorByModel,
insertContent,
} from '@blocksuite/affine-rich-text';
import {
copySelectedModelsCommand,
deleteSelectedModelsCommand,
@@ -52,7 +55,6 @@ import {
openFileOrFiles,
type Signal,
} from '@blocksuite/affine-shared/utils';
import type { AffineLinkedDocWidget } from '@blocksuite/affine-widget-linked-doc';
import { viewPresets } from '@blocksuite/data-view/view-presets';
import { assertType } from '@blocksuite/global/utils';
import {
@@ -97,15 +99,13 @@ import {
YesterdayIcon,
YoutubeDuotoneIcon,
} from '@blocksuite/icons/lit';
import {
type BlockComponent,
type BlockStdScope,
ConfigExtensionFactory,
} from '@blocksuite/std';
import type { BlockStdScope } from '@blocksuite/std';
import { computed } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import type { TemplateResult } from 'lit';
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
import type { AffineLinkedDocWidget } from '../linked-doc/index.js';
import {
FigmaDuotoneIcon,
HeadingIcon,
@@ -159,7 +159,7 @@ export type KeyboardSubToolbarConfig = {
export type KeyboardToolbarContext = {
std: BlockStdScope;
rootComponent: BlockComponent;
rootComponent: PageRootBlockComponent;
/**
* Close tool bar, and blur the focus if blur is true, default is false
*/
@@ -348,11 +348,35 @@ const pageToolGroup: KeyboardToolPanelGroup = {
);
if (!linkedDocWidget) return;
assertType<AffineLinkedDocWidget>(linkedDocWidget);
linkedDocWidget.show({
mode: 'mobile',
addTriggerKey: true,
});
closeToolPanel();
const triggerKey = linkedDocWidget.config.triggerKeys[0];
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(ctx => {
const { selectedModels } = ctx;
if (!selectedModels?.length) return;
const currentModel = selectedModels[0];
insertContent(std, currentModel, triggerKey);
const inlineEditor = getInlineEditorByModel(std, currentModel);
// Wait for range to be updated
if (inlineEditor) {
const subscription = inlineEditor.slots.inlineRangeSync.subscribe(
() => {
subscription.unsubscribe();
linkedDocWidget.show({
mode: 'mobile',
addTriggerKey: true,
});
closeToolPanel();
}
);
}
})
.run();
},
},
],
@@ -404,7 +428,8 @@ const contentMediaToolGroup: KeyboardToolPanelGroup = {
{
name: 'Attachment',
icon: AttachmentIcon(),
showWhen: () => false,
showWhen: ({ std }) =>
std.store.schema.flavourSchemaMap.has('affine:attachment'),
action: async ({ std }) => {
const [_, { selectedModels }] = std.command.exec(
getSelectedModelsCommand
@@ -1004,7 +1029,8 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
{
name: 'Attachment',
icon: AttachmentIcon(),
showWhen: () => false,
showWhen: ({ std }) =>
std.store.schema.flavourSchemaMap.has('affine:attachment'),
action: async ({ std }) => {
const [_, { selectedModels }] = std.command.exec(
getSelectedModelsCommand
@@ -1129,7 +1155,3 @@ export const defaultKeyboardToolbarConfig: KeyboardToolbarConfig = {
},
],
};
export const KeyboardToolbarConfigExtension = ConfigExtensionFactory<
Partial<KeyboardToolbarConfig>
>('affine:keyboard-toolbar');

View File

@@ -2,20 +2,18 @@ import { getDocTitleByEditorHost } from '@blocksuite/affine-fragment-doc-title';
import type { RootBlockModel } from '@blocksuite/affine-model';
import {
FeatureFlagService,
isVirtualKeyboardProviderWithAction,
VirtualKeyboardProvider,
type VirtualKeyboardProviderWithAction,
} from '@blocksuite/affine-shared/services';
import { IS_MOBILE } from '@blocksuite/global/env';
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
import { WidgetComponent } from '@blocksuite/std';
import { effect, signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { literal, unsafeStatic } from 'lit/static-html.js';
import {
defaultKeyboardToolbarConfig,
KeyboardToolbarConfigExtension,
} from './config.js';
import { RootBlockConfigExtension } from '../../root-config.js';
import { defaultKeyboardToolbarConfig } from './config.js';
export * from './config.js';
export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget';
@@ -36,10 +34,7 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
private _initialInputMode: string = '';
get keyboard(): VirtualKeyboardProviderWithAction & { fallback?: boolean } {
const provider = this.std.get(VirtualKeyboardProvider);
if (isVirtualKeyboardProviderWithAction(provider)) return provider;
get keyboard(): VirtualKeyboardProviderWithAction {
return {
// fallback keyboard actions
show: () => {
@@ -54,7 +49,7 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
rootComponent.inputMode = 'none';
}
},
...provider,
...this.std.get(VirtualKeyboardProvider),
};
}
@@ -65,7 +60,8 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
get config() {
return {
...defaultKeyboardToolbarConfig,
...this.std.getOptional(KeyboardToolbarConfigExtension.identifier),
...this.std.getOptional(RootBlockConfigExtension.identifier)
?.keyboardToolbar,
};
}
@@ -74,6 +70,10 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
const rootComponent = this.block?.rootComponent;
if (rootComponent) {
this._initialInputMode = rootComponent.inputMode;
this.disposables.add(() => {
rootComponent.inputMode = this._initialInputMode;
});
this.disposables.addFromEvent(rootComponent, 'focus', () => {
this._show$.value = true;
});
@@ -81,20 +81,14 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
this._show$.value = false;
});
if (this.keyboard.fallback) {
this._initialInputMode = rootComponent.inputMode;
this.disposables.add(() => {
rootComponent.inputMode = this._initialInputMode;
});
this.disposables.add(
effect(() => {
// recover input mode when keyboard toolbar is hidden
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode;
}
})
);
}
this.disposables.add(
effect(() => {
// recover input mode when keyboard toolbar is hidden
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode;
}
})
);
}
if (this._docTitle) {
@@ -134,12 +128,6 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
}
}
export const keyboardToolbarWidget = WidgetViewExtension(
'affine:page',
AFFINE_KEYBOARD_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_KEYBOARD_TOOLBAR_WIDGET)}`
);
declare global {
interface HTMLElementTagNameMap {
[AFFINE_KEYBOARD_TOOLBAR_WIDGET]: AffineKeyboardToolbarWidget;

View File

@@ -3,7 +3,6 @@ import { type VirtualKeyboardProviderWithAction } from '@blocksuite/affine-share
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { ArrowLeftBigIcon, KeyboardIcon } from '@blocksuite/icons/lit';
import {
BlockComponent,
PropTypes,
requiredProperties,
ShadowlessElement,
@@ -15,6 +14,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js';
import { PageRootBlockComponent } from '../../page/page-root-block';
import type {
KeyboardIconType,
KeyboardToolbarConfig,
@@ -34,7 +34,7 @@ export const AFFINE_KEYBOARD_TOOLBAR = 'affine-keyboard-toolbar';
@requiredProperties({
config: PropTypes.object,
rootComponent: PropTypes.instanceOf(BlockComponent),
rootComponent: PropTypes.instanceOf(PageRootBlockComponent),
})
export class AffineKeyboardToolbar extends SignalWatcher(
WithDisposable(ShadowlessElement)
@@ -55,8 +55,10 @@ export class AffineKeyboardToolbar extends SignalWatcher(
}
private readonly _closeToolPanel = () => {
if (!this.panelOpened) return;
this._currentPanelIndex$.value = -1;
if (!this.keyboard.visible$.peek()) this.keyboard.show();
this.keyboard.show();
};
private readonly _currentPanelIndex$ = signal(-1);
@@ -253,16 +255,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
})
);
this.disposables.add(
effect(() => {
// sometime the keyboard will auto show when user click into different paragraph in Android,
// so we need to close the tool panel explicitly when the keyboard is visible
if (this.keyboard.visible$.value) {
this._closeToolPanel();
}
})
);
this._watchAutoShow();
}
@@ -338,5 +330,5 @@ export class AffineKeyboardToolbar extends SignalWatcher(
accessor config!: KeyboardToolbarConfig;
@property({ attribute: false })
accessor rootComponent!: BlockComponent;
accessor rootComponent!: PageRootBlockComponent;
}

View File

@@ -16,12 +16,7 @@ import {
isFuzzyMatch,
type Signal,
} from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import {
type BlockStdScope,
ConfigExtensionFactory,
type EditorHost,
} from '@blocksuite/std';
import type { BlockStdScope, EditorHost } from '@blocksuite/std';
import type { InlineRange } from '@blocksuite/std/inline';
import type { TemplateResult } from 'lit';
@@ -112,7 +107,6 @@ export type LinkedDocContext = {
std: BlockStdScope;
inlineEditor: AffineInlineEditor;
startRange: InlineRange;
startNativeRange: Range;
triggerKey: string;
config: LinkedWidgetConfig;
close: () => void;
@@ -177,77 +171,73 @@ export function createNewDocMenuGroup(
docName.slice(0, DISPLAY_NAME_LENGTH) +
(docName.length > DISPLAY_NAME_LENGTH ? '..' : '');
const items: LinkedMenuItem[] = [
{
key: 'create',
name: `Create "${displayDocName}" doc`,
icon: NewDocIcon,
action: () => {
abort();
const docName = query;
const newDoc = createDefaultDoc(doc.workspace, {
title: docName,
});
insertLinkedNode({
inlineEditor,
docId: newDoc.id,
});
const telemetryService = editorHost.std.getOptional(TelemetryProvider);
telemetryService?.track('LinkedDocCreated', {
control: 'new doc',
module: 'inline @',
type: 'doc',
other: 'new doc',
});
telemetryService?.track('DocCreated', {
control: 'new doc',
module: 'inline @',
type: 'doc',
});
},
},
];
if (!IS_MOBILE) {
items.push({
key: 'import',
name: 'Import',
icon: ImportIcon,
action: () => {
abort();
const onSuccess = (
docIds: string[],
options: {
importedCount: number;
}
) => {
toast(
editorHost,
`Successfully imported ${options.importedCount} Doc${options.importedCount > 1 ? 's' : ''}.`
);
for (const docId of docIds) {
insertLinkedNode({
inlineEditor,
docId,
});
}
};
const onFail = (message: string) => {
toast(editorHost, message);
};
showImportModal({
collection: doc.workspace,
schema: doc.schema,
onSuccess,
onFail,
});
},
});
}
return {
name: 'New Doc',
items,
items: [
{
key: 'create',
name: `Create "${displayDocName}" doc`,
icon: NewDocIcon,
action: () => {
abort();
const docName = query;
const newDoc = createDefaultDoc(doc.workspace, {
title: docName,
});
insertLinkedNode({
inlineEditor,
docId: newDoc.id,
});
const telemetryService =
editorHost.std.getOptional(TelemetryProvider);
telemetryService?.track('LinkedDocCreated', {
control: 'new doc',
module: 'inline @',
type: 'doc',
other: 'new doc',
});
telemetryService?.track('DocCreated', {
control: 'new doc',
module: 'inline @',
type: 'doc',
});
},
},
{
key: 'import',
name: 'Import',
icon: ImportIcon,
action: () => {
abort();
const onSuccess = (
docIds: string[],
options: {
importedCount: number;
}
) => {
toast(
editorHost,
`Successfully imported ${options.importedCount} Doc${options.importedCount > 1 ? 's' : ''}.`
);
for (const docId of docIds) {
insertLinkedNode({
inlineEditor,
docId,
});
}
};
const onFail = (message: string) => {
toast(editorHost, message);
};
showImportModal({
collection: doc.workspace,
schema: doc.schema,
onSuccess,
onFail,
});
},
},
],
};
}
@@ -270,7 +260,3 @@ export const LinkedWidgetUtils = {
};
export const AFFINE_LINKED_DOC_WIDGET = 'affine-linked-doc-widget';
export const LinkedWidgetConfigExtension = ConfigExtensionFactory<
Partial<LinkedWidgetConfig>
>('affine:widget-linked-doc');

View File

@@ -1,6 +1,5 @@
import { AFFINE_LINKED_DOC_WIDGET } from './config.js';
import { ImportDoc } from './import-doc/import-doc.js';
import { Loader } from './import-doc/loader.js';
import { AffineLinkedDocWidget } from './index.js';
import { LinkedDocPopover } from './linked-doc-popover.js';
import { AffineMobileLinkedDocMenu } from './mobile-linked-doc-menu.js';
@@ -9,9 +8,9 @@ export function effects() {
customElements.define('affine-linked-doc-popover', LinkedDocPopover);
customElements.define(AFFINE_LINKED_DOC_WIDGET, AffineLinkedDocWidget);
customElements.define('import-doc', ImportDoc);
customElements.define(
'affine-mobile-linked-doc-menu',
AffineMobileLinkedDocMenu
);
customElements.define('loader-element', Loader);
}

View File

@@ -12,9 +12,9 @@ import type { Schema, Workspace } from '@blocksuite/store';
import { html, LitElement, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
import { HtmlTransformer } from '../transformers/html.js';
import { MarkdownTransformer } from '../transformers/markdown.js';
import { NotionHtmlTransformer } from '../transformers/notion-html.js';
import { HtmlTransformer } from '../../../transformers/html.js';
import { MarkdownTransformer } from '../../../transformers/markdown.js';
import { NotionHtmlTransformer } from '../../../transformers/notion-html.js';
import { styles } from './styles.js';
export type OnSuccessHandler = (

View File

@@ -7,11 +7,7 @@ import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { getViewportElement } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
import type { BlockComponent } from '@blocksuite/std';
import {
BLOCK_ID_ATTR,
WidgetComponent,
WidgetViewExtension,
} from '@blocksuite/std';
import { BLOCK_ID_ATTR, WidgetComponent } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import {
INLINE_ROOT_ATTR,
@@ -23,18 +19,22 @@ import { html, nothing } from 'lit';
import { choose } from 'lit/directives/choose.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import type { PageRootBlockComponent } from '../../page/page-root-block.js';
import { RootBlockConfigExtension } from '../../root-config.js';
import {
AFFINE_LINKED_DOC_WIDGET,
type AFFINE_LINKED_DOC_WIDGET,
getMenus,
type LinkedDocContext,
type LinkedWidgetConfig,
LinkedWidgetConfigExtension,
} from './config.js';
import { linkedDocWidgetStyles } from './styles.js';
export { type LinkedWidgetConfig } from './config.js';
export class AffineLinkedDocWidget extends WidgetComponent<RootBlockModel> {
export class AffineLinkedDocWidget extends WidgetComponent<
RootBlockModel,
PageRootBlockComponent
> {
static override styles = linkedDocWidgetStyles;
private _context: LinkedDocContext | null = null;
@@ -43,19 +43,6 @@ export class AffineLinkedDocWidget extends WidgetComponent<RootBlockModel> {
private readonly _mode$ = signal<'desktop' | 'mobile' | 'none'>('none');
private _addTriggerKey(inlineEditor: InlineEditor, triggerKey: string) {
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
inlineEditor.insertText(
{ index: inlineRange.index, length: 0 },
triggerKey
);
inlineEditor.setInlineRange({
index: inlineRange.index + triggerKey.length,
length: 0,
});
}
private _updateInputRects() {
if (!this._context) return;
const { inlineEditor, startRange, triggerKey } = this._context;
@@ -230,7 +217,8 @@ export class AffineLinkedDocWidget extends WidgetComponent<RootBlockModel> {
scrollContainer: getViewportElement(this.std.host) ?? window,
scrollTopOffset: 46,
},
...this.std.getOptional(LinkedWidgetConfigExtension.identifier),
...this.std.getOptional(RootBlockConfigExtension.identifier)
?.linkedWidget,
};
}
@@ -271,30 +259,27 @@ export class AffineLinkedDocWidget extends WidgetComponent<RootBlockModel> {
inlineEditor = props.inlineEditor;
}
const inlineRange = inlineEditor.getInlineRange();
if (!inlineRange) return;
if (addTriggerKey) {
this._addTriggerKey(inlineEditor, primaryTriggerKey);
// we need to wait the range sync to get the correct startNativeRange
const subscription = inlineEditor.slots.inlineRangeSync.subscribe(() => {
this.show({ ...props, addTriggerKey: false });
subscription.unsubscribe();
inlineEditor.insertText(
{ index: inlineRange.index, length: 0 },
primaryTriggerKey
);
inlineEditor.setInlineRange({
index: inlineRange.index + primaryTriggerKey.length,
length: 0,
});
return;
}
const startRange = inlineEditor.getInlineRange();
if (!startRange) return;
const startNativeRange = inlineEditor.getNativeRange();
if (!startNativeRange) return;
const disposable = inlineEditor.slots.renderComplete.subscribe(() => {
this._updateInputRects();
});
this._context = {
std: this.std,
inlineEditor,
startRange,
startNativeRange,
startRange: inlineRange,
triggerKey: primaryTriggerKey,
config: this.config,
close: () => {
@@ -331,12 +316,6 @@ export class AffineLinkedDocWidget extends WidgetComponent<RootBlockModel> {
}
}
export const linkedDocWidget = WidgetViewExtension(
'affine:page',
AFFINE_LINKED_DOC_WIDGET,
literal`${unsafeStatic(AFFINE_LINKED_DOC_WIDGET)}`
);
declare global {
interface HTMLElementTagNameMap {
[AFFINE_LINKED_DOC_WIDGET]: AffineLinkedDocWidget;

View File

@@ -7,6 +7,7 @@ import {
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme';
import {
createKeydownObserver,
getCurrentNativeRange,
getPopperPosition,
getViewportElement,
} from '@blocksuite/affine-shared/utils';
@@ -159,15 +160,11 @@ export class LinkedDocPopover extends SignalWatcher(
// init
this._updateLinkedDocGroup().catch(console.error);
this._disposables.addFromEvent(this, 'pointerdown', e => {
this._disposables.addFromEvent(this, 'mousedown', e => {
// Prevent input from losing focus
e.preventDefault();
});
this._disposables.addFromEvent(this, 'mousedown', e => {
// Prevent input from losing focus in electron
e.preventDefault();
});
this._disposables.addFromEvent(window, 'pointerdown', e => {
this._disposables.addFromEvent(window, 'mousedown', e => {
if (e.target === this) return;
// We don't clear the query when clicking outside the popover
this.context.close();
@@ -249,7 +246,6 @@ export class LinkedDocPopover extends SignalWatcher(
override disconnectedCallback() {
super.disconnectedCallback();
this._menusItemsEffectCleanup();
this._updateLinkedDocGroupAbortController?.abort();
}
override render() {
@@ -284,7 +280,7 @@ export class LinkedDocPopover extends SignalWatcher(
return html`
<div class="divider" ?hidden=${idx === 0}></div>
<div class="group-title">
<div class="group-title-text">${group.name}</div>
${group.name}
${group.isLoading
? html`<span class="loading-icon">${LoadingIcon}</span>`
: nothing}
@@ -342,8 +338,11 @@ export class LinkedDocPopover extends SignalWatcher(
override willUpdate() {
if (!this.hasUpdated) {
const curRange = getCurrentNativeRange();
if (!curRange) return;
const updatePosition = throttle(() => {
this._position = getPopperPosition(this, this.context.startNativeRange);
this._position = getPopperPosition(this, curRange);
}, 10);
this.disposables.addFromEvent(window, 'resize', updatePosition);

View File

@@ -3,7 +3,10 @@ import {
getTextContentFromInlineRange,
} from '@blocksuite/affine-rich-text';
import { VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import { getViewportElement } from '@blocksuite/affine-shared/utils';
import {
createKeydownObserver,
getViewportElement,
} from '@blocksuite/affine-shared/utils';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
import { PropTypes, requiredProperties } from '@blocksuite/std';
@@ -13,6 +16,7 @@ import { property } from 'lit/decorators.js';
import { join } from 'lit/directives/join.js';
import { repeat } from 'lit/directives/repeat.js';
import { PageRootBlockComponent } from '../../index.js';
import type {
LinkedDocContext,
LinkedMenuGroup,
@@ -25,6 +29,7 @@ export const AFFINE_MOBILE_LINKED_DOC_MENU = 'affine-mobile-linked-doc-menu';
@requiredProperties({
context: PropTypes.object,
rootComponent: PropTypes.instanceOf(PageRootBlockComponent),
})
export class AffineMobileLinkedDocMenu extends SignalWatcher(
WithDisposable(LitElement)
@@ -33,6 +38,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
private readonly _expand = new Set<string>();
private _firstActionItem: LinkedMenuItem | null = null;
private readonly _linkedDocGroup$ = signal<LinkedMenuGroup[]>([]);
private readonly _renderGroup = (group: LinkedMenuGroup) => {
@@ -182,14 +189,42 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
const keydownObserverAbortController = new AbortController();
this._disposables.add(() => keydownObserverAbortController.abort());
// we need use beforeinput because the event.key of keypress event usually is `Unidentified` in Android
this.disposables.addFromEvent(eventSource, 'beforeinput', () => {
const curRange = inlineEditor.getInlineRange();
if (curRange && curRange.index < this.context.startRange.index) {
createKeydownObserver({
target: eventSource,
signal: keydownObserverAbortController.signal,
onInput: isComposition => {
if (isComposition) {
this._updateLinkedDocGroup().catch(console.error);
} else {
const subscription = inlineEditor.slots.renderComplete.subscribe(
() => {
subscription.unsubscribe();
this._updateLinkedDocGroup().catch(console.error);
}
);
}
},
onDelete: () => {
const subscription = inlineEditor.slots.renderComplete.subscribe(
() => {
subscription.unsubscribe();
const curRange = inlineEditor.getInlineRange();
if (!this.context.startRange || !curRange) return;
if (curRange.index < this.context.startRange.index) {
this.context.close();
}
this._updateLinkedDocGroup().catch(console.error);
}
);
},
onConfirm: () => {
this._firstActionItem?.action()?.catch(console.error);
},
onAbort: () => {
this.context.close();
return;
}
this._updateLinkedDocGroup().catch(console.error);
},
});
}
}
@@ -204,6 +239,8 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
return nothing;
}
this._firstActionItem = resolveSignal(groups[0].items)[0];
this.style.bottom = `${this.keyboard.height$.value}px`;
return html`
@@ -213,4 +250,7 @@ export class AffineMobileLinkedDocMenu extends SignalWatcher(
@property({ attribute: false })
accessor context!: LinkedDocContext;
@property({ attribute: false })
accessor rootComponent!: PageRootBlockComponent;
}

View File

@@ -52,13 +52,6 @@ export const linkedDocPopoverStyles = css`
flex-shrink: 0;
font-weight: 500;
justify-content: space-between;
max-width: 240px;
}
.linked-doc-popover .group-title .group-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.linked-doc-popover .group-title .loading-icon {

View File

@@ -0,0 +1,144 @@
import { css, html, LitElement, nothing } from 'lit';
import { ref } from 'lit/directives/ref.js';
import { repeat } from 'lit/directives/repeat.js';
type ModalButton = {
text: string;
type?: 'primary';
onClick: () => Promise<void> | void;
};
type ModalOptions = {
footer: null | ModalButton[];
};
export class AffineCustomModal extends LitElement {
static override styles = css`
:host {
z-index: calc(var(--affine-z-index-modal) + 3);
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.modal-background {
width: 100%;
height: 100%;
box-sizing: border-box;
align-items: center;
background-color: var(--affine-background-modal-color);
justify-content: center;
display: flex;
}
.modal-window {
width: 70%;
min-width: 500px;
height: 80%;
overflow-y: scroll;
background-color: var(--affine-background-overlay-panel-color);
border-radius: 12px;
box-shadow: var(--affine-shadow-3);
position: relative;
}
.modal-main {
height: 100%;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 20px;
padding: 24px;
position: absolute;
box-sizing: border-box;
bottom: 0;
right: 0;
}
.modal-footer .button {
align-items: center;
background: var(--affine-white);
border: 1px solid;
border-color: var(--affine-border-color);
border-radius: 8px;
color: var(--affine-text-primary-color);
cursor: pointer;
display: inline-flex;
font-size: var(--affine-font-sm);
font-weight: 500;
justify-content: center;
outline: 0;
padding: 12px 18px;
touch-action: manipulation;
transition: all 0.3s;
user-select: none;
}
.modal-footer .primary {
background: var(--affine-primary-color);
border-color: var(--affine-black-10);
box-shadow: var(--affine-button-inner-shadow);
color: var(--affine-pure-white);
}
`;
onOpen!: (div: HTMLDivElement) => void;
options!: ModalOptions;
close() {
this.remove();
}
modalRef(modal: Element | undefined) {
if (modal) this.onOpen?.(modal as HTMLDivElement);
}
override render() {
const { options } = this;
return html`<div class="modal-background">
<div class="modal-window">
<div class="modal-main" ${ref(this.modalRef)}></div>
<div class="modal-footer">
${options.footer
? repeat(
options.footer,
button => button.text,
button => html`
<button
class="button ${button.type ?? ''}"
@click=${button.onClick}
>
${button.text}
</button>
`
)
: nothing}
</div>
</div>
</div>`;
}
}
type CreateModalOption = ModalOptions & {
entry: (div: HTMLDivElement) => void;
};
export function createCustomModal(
options: CreateModalOption,
container: HTMLElement = document.body
) {
const modal = new AffineCustomModal();
modal.onOpen = options.entry;
modal.options = options;
container.append(modal);
return modal;
}

View File

@@ -0,0 +1,22 @@
import { WidgetComponent } from '@blocksuite/std';
import { nothing } from 'lit';
import { createCustomModal } from './custom-modal.js';
export const AFFINE_MODAL_WIDGET = 'affine-modal-widget';
export class AffineModalWidget extends WidgetComponent {
open(options: Parameters<typeof createCustomModal>[0]) {
return createCustomModal(options, this.ownerDocument.body);
}
override render() {
return nothing;
}
}
declare global {
interface HTMLElementTagNameMap {
[AFFINE_MODAL_WIDGET]: AffineModalWidget;
}
}

View File

@@ -43,8 +43,6 @@
{ "path": "../../widgets/edgeless-auto-connect" },
{ "path": "../../widgets/edgeless-toolbar" },
{ "path": "../../widgets/frame-title" },
{ "path": "../../widgets/keyboard-toolbar" },
{ "path": "../../widgets/linked-doc" },
{ "path": "../../widgets/remote-selection" },
{ "path": "../../widgets/scroll-anchoring" },
{ "path": "../../widgets/slash-menu" },

View File

@@ -321,7 +321,7 @@ export const calcCustomButtonStyle = (
return { '--b': b, '--c': c };
}
if (color.startsWith('--')) {
if (color.startsWith('---')) {
if (!color.endsWith('transparent')) {
b = 'var(--affine-background-overlay-panel-color)';
c = keepColor(

View File

@@ -1,6 +1,5 @@
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit-html/directives/repeat.js';
export class TooltipContentWithShortcut extends LitElement {
static override styles = css`
@@ -10,10 +9,6 @@ export class TooltipContentWithShortcut extends LitElement {
align-items: center;
gap: 10px;
}
.tooltip__shortcuts {
display: flex;
gap: 2px;
}
.tooltip__shortcut {
font-size: 12px;
position: relative;
@@ -33,30 +28,19 @@ export class TooltipContentWithShortcut extends LitElement {
opacity: 0.2;
}
.tooltip__label {
display: flex;
flex: 1;
white-space: pre;
}
`;
get shortcuts() {
let shortcut = this.shortcut;
if (!shortcut) return [];
return shortcut.split(' ');
}
override render() {
const { tip, shortcuts, postfix } = this;
const { tip, shortcut, postfix } = this;
return html`
<div class="tooltip-with-shortcut">
<span class="tooltip__label">${tip}</span>
<div class="tooltip__shortcuts">
${repeat(
shortcuts,
shortcut => html`<span class="tooltip__shortcut">${shortcut}</span>`
)}
</div>
${shortcut
? html`<span class="tooltip__shortcut">${shortcut}</span>`
: ''}
${postfix ? html`<span class="tooltip__postfix">${postfix}</span>` : ''}
</div>
`;

View File

@@ -26,10 +26,10 @@ export const cardPreview = style({
borderRadius: '4px',
cursor: 'default',
userSelect: 'none',
':hover': {
background: cssVarV2('layer/background/hoverOverlay'),
},
selectors: {
[`${outlineCard}[data-sortable="true"] &:hover`]: {
background: cssVarV2('layer/background/hoverOverlay'),
},
[`${outlineCard}[data-status="selected"] &`]: {
background: cssVarV2('layer/background/hoverOverlay'),
},

View File

@@ -1,6 +1,6 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { globalStyle, style } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const outlineBlockPreview = style({
fontFamily: cssVar('fontFamily'),
@@ -108,11 +108,6 @@ export const linkedDocPreviewUnavailable = style({
color: cssVarV2('text/disable'),
});
export const linkedDocPreviewAvailable = style({});
globalStyle(`${linkedDocPreviewAvailable} > svg`, {
marginBottom: '0.1em',
});
export const linkedDocTextUnavailable = style({
color: cssVarV2('text/disable'),
textDecoration: 'line-through',

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