Compare commits

..

1 Commits

Author SHA1 Message Date
fengmk2 b1d7011047 chore(server): use jemalloc to reduce RSS 2025-07-10 11:22:37 +08:00
593 changed files with 4658 additions and 17075 deletions
+2 -11
View File
@@ -18,19 +18,11 @@ services:
ports: ports:
- 6379:6379 - 6379:6379
# https://mailpit.axllent.org/docs/install/docker/ mailhog:
mailpit: image: mailhog/mailhog:latest
image: axllent/mailpit:latest
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
- mailpit_data:/data
# https://manual.manticoresearch.com/Starting_the_server/Docker # https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch: manticoresearch:
@@ -95,5 +87,4 @@ networks:
volumes: volumes:
postgres_data: postgres_data:
manticoresearch_data: manticoresearch_data:
mailpit_data:
elasticsearch_data: elasticsearch_data:
+7 -64
View File
@@ -219,41 +219,6 @@
"type": "boolean", "type": "boolean",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`", "description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"default": false "default": false
},
"fallbackDomains": {
"type": "array",
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
"default": ""
},
"fallbackSMTP.port": {
"type": "number",
"description": "Port of the email server (they commonly are 25, 465 or 587)\n@default 465",
"default": 465
},
"fallbackSMTP.username": {
"type": "string",
"description": "Username used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.password": {
"type": "string",
"description": "Password used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"",
"default": ""
},
"fallbackSMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false",
"default": false
} }
} }
}, },
@@ -664,34 +629,14 @@
"properties": { "properties": {
"enabled": { "enabled": {
"type": "boolean", "type": "boolean",
"description": "Whether to enable the copilot plugin. <br> Document: <a href=\"https://docs.affine.pro/self-host-affine/administer/ai\" target=\"_blank\">https://docs.affine.pro/self-host-affine/administer/ai</a>\n@default false", "description": "Whether to enable the copilot plugin.\n@default false",
"default": false "default": false
}, },
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "claude-sonnet-4@20250514",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4@20250514",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
"polish_and_summarize": "gemini-2.5-flash"
}
}
},
"providers.openai": { "providers.openai": {
"type": "object", "type": "object",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node", "description": "The config for the openai provider.\n@default {\"apiKey\":\"\"}\n@link https://github.com/openai/openai-node",
"default": { "default": {
"apiKey": "", "apiKey": ""
"baseURL": "https://api.openai.com/v1"
} }
}, },
"providers.fal": { "providers.fal": {
@@ -703,10 +648,9 @@
}, },
"providers.gemini": { "providers.gemini": {
"type": "object", "type": "object",
"description": "The config for the gemini provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://generativelanguage.googleapis.com/v1beta\"}", "description": "The config for the gemini provider.\n@default {\"apiKey\":\"\"}",
"default": { "default": {
"apiKey": "", "apiKey": ""
"baseURL": "https://generativelanguage.googleapis.com/v1beta"
} }
}, },
"providers.geminiVertex": { "providers.geminiVertex": {
@@ -753,10 +697,9 @@
}, },
"providers.anthropic": { "providers.anthropic": {
"type": "object", "type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.anthropic.com/v1\"}", "description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\"}",
"default": { "default": {
"apiKey": "", "apiKey": ""
"baseURL": "https://api.anthropic.com/v1"
} }
}, },
"providers.anthropicVertex": { "providers.anthropicVertex": {
+15 -15
View File
@@ -29,25 +29,25 @@ const isInternal = buildType === 'internal';
const replicaConfig = { const replicaConfig = {
stable: { stable: {
web: 2, web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2, graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2, sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2, renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2, doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 3,
}, },
beta: { beta: {
web: 1, web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1, graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1, sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1, renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
doc: Number(process.env.BETA_DOC_REPLICA) || 1, doc: Number(process.env.BETA_DOC_REPLICA) || 2,
}, },
canary: { canary: {
web: 1, web: 2,
graphql: 1, graphql: 2,
sync: 1, sync: 2,
renderer: 1, renderer: 2,
doc: 1, doc: 2,
}, },
}; };
-6
View File
@@ -4,15 +4,9 @@ inputs:
app-version: app-version:
description: 'App Version' description: 'App Version'
required: true required: true
ios-app-version:
description: 'iOS App Store Version (Optional, use App version if empty)'
required: false
type: string
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- name: 'Write Version' - name: 'Write Version'
shell: bash shell: bash
env:
IOS_APP_VERSION: ${{ inputs.ios-app-version }}
run: ./scripts/set-version.sh ${{ inputs.app-version }} run: ./scripts/set-version.sh ${{ inputs.app-version }}
@@ -1,4 +1,4 @@
replicaCount: 2 replicaCount: 3
enabled: false enabled: false
database: database:
connectionName: "" connectionName: ""
@@ -33,11 +33,8 @@ service:
resources: resources:
limits: limits:
memory: "1Gi" memory: "4Gi"
cpu: "1" cpu: "2"
requests:
memory: "512Mi"
cpu: "100m"
volumes: [] volumes: []
volumeMounts: [] volumeMounts: []
+1 -1
View File
@@ -465,7 +465,7 @@ jobs:
name: ${{ env.RELEASE_VERSION }} name: ${{ env.RELEASE_VERSION }}
draft: ${{ inputs.build-type == 'stable' }} draft: ${{ inputs.build-type == 'stable' }}
prerelease: ${{ inputs.build-type != 'stable' }} prerelease: ${{ inputs.build-type != 'stable' }}
tag_name: v${{ env.RELEASE_VERSION}} tag_name: ${{ env.RELEASE_VERSION}}
files: | files: |
./release/* ./release/*
./release/.env.example ./release/.env.example
+2 -6
View File
@@ -12,9 +12,6 @@ on:
build-type: build-type:
type: string type: string
required: true required: true
ios-app-version:
type: string
required: false
env: env:
BUILD_TYPE: ${{ inputs.build-type }} BUILD_TYPE: ${{ inputs.build-type }}
@@ -81,7 +78,7 @@ jobs:
path: packages/frontend/apps/android/dist path: packages/frontend/apps/android/dist
ios: ios:
runs-on: 'macos-15' runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
needs: needs:
- build-ios-web - build-ios-web
steps: steps:
@@ -90,7 +87,6 @@ jobs:
uses: ./.github/actions/setup-version uses: ./.github/actions/setup-version
with: with:
app-version: ${{ inputs.app-version }} app-version: ${{ inputs.app-version }}
ios-app-version: ${{ inputs.ios-app-version }}
- name: 'Update Code Sign Identity' - name: 'Update Code Sign Identity'
shell: bash shell: bash
run: ./packages/frontend/apps/ios/update_code_sign_identity.sh run: ./packages/frontend/apps/ios/update_code_sign_identity.sh
@@ -110,7 +106,7 @@ jobs:
enableScripts: false enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1
with: with:
xcode-version: 16.4 xcode-version: 16.2
- name: Install Swiftformat - name: Install Swiftformat
run: brew install swiftformat run: brew install swiftformat
- name: Cap sync - name: Cap sync
+2 -9
View File
@@ -21,10 +21,6 @@ on:
required: true required: true
type: boolean type: boolean
default: false default: false
ios-app-version:
description: 'iOS App Store Version (Optional, use tag version if empty)'
required: false
type: string
permissions: permissions:
contents: write contents: write
@@ -34,7 +30,6 @@ permissions:
packages: write packages: write
security-events: write security-events: write
attestations: write attestations: write
issues: write
jobs: jobs:
prepare: prepare:
@@ -74,8 +69,7 @@ jobs:
name: Wait for approval name: Wait for approval
with: with:
secret: ${{ secrets.GITHUB_TOKEN }} secret: ${{ secrets.GITHUB_TOKEN }}
approvers: forehalo,fengmk2,darkskygit approvers: forehalo,fengmk2
minimum-approvals: 1
fail-on-denial: true fail-on-denial: true
issue-title: Please confirm to release docker image issue-title: Please confirm to release docker image
issue-body: | issue-body: |
@@ -84,7 +78,7 @@ jobs:
Tag: ghcr.io/toeverything/affine:${{ needs.prepare.outputs.BUILD_TYPE }} Tag: ghcr.io/toeverything/affine:${{ needs.prepare.outputs.BUILD_TYPE }}
> comment with "approve", "approved", "lgtm", "yes" to approve > comment with "approve", "approved", "lgtm", "yes" to approve
> comment with "deny", "denied", "no" to deny > comment with "deny", "deny", "no" to deny
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -123,4 +117,3 @@ jobs:
build-type: ${{ needs.prepare.outputs.BUILD_TYPE }} build-type: ${{ needs.prepare.outputs.BUILD_TYPE }}
app-version: ${{ needs.prepare.outputs.APP_VERSION }} app-version: ${{ needs.prepare.outputs.APP_VERSION }}
git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }} git-short-hash: ${{ needs.prepare.outputs.GIT_SHORT_HASH }}
ios-app-version: ${{ inputs.ios-app-version }}
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
shell: cmd shell: cmd
run: | run: |
cd ${{ env.ARCHIVE_DIR }}/out cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }} signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file - name: zip file
shell: cmd shell: cmd
run: | run: |
-1
View File
@@ -2,7 +2,6 @@
**/node_modules **/node_modules
.yarn .yarn
.github/helm .github/helm
.git
.vscode .vscode
.yarnrc.yml .yarnrc.yml
.docker .docker
Generated
+12 -12
View File
@@ -93,7 +93,7 @@ dependencies = [
"symphonia", "symphonia",
"thiserror 2.0.12", "thiserror 2.0.12",
"uuid", "uuid",
"windows 0.61.3", "windows 0.61.1",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@@ -1691,7 +1691,7 @@ dependencies = [
"libc", "libc",
"log", "log",
"rustversion", "rustversion",
"windows 0.61.3", "windows 0.61.1",
] ]
[[package]] [[package]]
@@ -2284,7 +2284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.48.5",
] ]
[[package]] [[package]]
@@ -4732,9 +4732,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.25.8" version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2" checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",
@@ -4842,9 +4842,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-scala" name = "tree-sitter-scala"
version = "0.24.0" version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7516aeb3d1f40ede8e3045b163e86993b3434514dd06c34c0b75e782d9a0b251" checksum = "efde5e68b4736e9eac17bfa296c6f104a26bffab363b365eb898c40a63c15d2f"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter-language", "tree-sitter-language",
@@ -5334,7 +5334,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -5365,9 +5365,9 @@ dependencies = [
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [ dependencies = [
"windows-collections", "windows-collections",
"windows-core 0.61.2", "windows-core 0.61.2",
@@ -5477,9 +5477,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.1.3" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]] [[package]]
name = "windows-numerics" name = "windows-numerics"
+1 -1
View File
@@ -93,7 +93,7 @@ tree-sitter-javascript = { version = "0.23" }
tree-sitter-kotlin-ng = { version = "1.1" } tree-sitter-kotlin-ng = { version = "1.1" }
tree-sitter-python = { version = "0.23" } tree-sitter-python = { version = "0.23" }
tree-sitter-rust = { version = "0.24" } tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.24" } tree-sitter-scala = { version = "0.23" }
tree-sitter-typescript = { version = "0.23" } tree-sitter-typescript = { version = "0.23" }
uniffi = "0.29" uniffi = "0.29"
url = { version = "2.5" } url = { version = "2.5" }
-1
View File
@@ -266,7 +266,6 @@
"./components/toggle-button": "./src/components/toggle-button.ts", "./components/toggle-button": "./src/components/toggle-button.ts",
"./components/toggle-switch": "./src/components/toggle-switch.ts", "./components/toggle-switch": "./src/components/toggle-switch.ts",
"./components/toolbar": "./src/components/toolbar.ts", "./components/toolbar": "./src/components/toolbar.ts",
"./components/tooltip": "./src/components/tooltip.ts",
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts", "./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts", "./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
"./components/resource": "./src/components/resource.ts", "./components/resource": "./src/components/resource.ts",
@@ -1 +0,0 @@
export * from '@blocksuite/affine-components/tooltip';
@@ -39,13 +39,6 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
private readonly _loadTheme = async ( private readonly _loadTheme = async (
highlighter: HighlighterCore highlighter: HighlighterCore
): Promise<void> => { ): Promise<void> => {
// It is possible that by the time the highlighter is ready all instances
// have already been unmounted. In that case there is no need to load
// themes or update state.
if (CodeBlockHighlighter._refCount === 0) {
return;
}
const config = this.std.getOptional(CodeBlockConfigExtension.identifier); const config = this.std.getOptional(CodeBlockConfigExtension.identifier);
const darkTheme = config?.theme?.dark ?? CODE_BLOCK_DEFAULT_DARK_THEME; const darkTheme = config?.theme?.dark ?? CODE_BLOCK_DEFAULT_DARK_THEME;
const lightTheme = config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME; const lightTheme = config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME;
@@ -85,27 +78,14 @@ export class CodeBlockHighlighter extends LifeCycleWatcher {
override unmounted(): void { override unmounted(): void {
CodeBlockHighlighter._refCount--; CodeBlockHighlighter._refCount--;
// Dispose the shared highlighter **after** any in-flight creation finishes. // Only dispose the shared highlighter when no instances are using it
if (CodeBlockHighlighter._refCount !== 0) { if (
return; CodeBlockHighlighter._refCount === 0 &&
} CodeBlockHighlighter._sharedHighlighter
) {
const doDispose = (highlighter: HighlighterCore | null) => { CodeBlockHighlighter._sharedHighlighter.dispose();
if (highlighter) {
highlighter.dispose();
}
CodeBlockHighlighter._sharedHighlighter = null; CodeBlockHighlighter._sharedHighlighter = null;
CodeBlockHighlighter._highlighterPromise = null; CodeBlockHighlighter._highlighterPromise = null;
};
if (CodeBlockHighlighter._sharedHighlighter) {
// Highlighter already created dispose immediately.
doDispose(CodeBlockHighlighter._sharedHighlighter);
} else if (CodeBlockHighlighter._highlighterPromise) {
// Highlighter still being created wait for it, then dispose.
CodeBlockHighlighter._highlighterPromise
.then(doDispose)
.catch(console.error);
} }
} }
} }
@@ -164,10 +164,8 @@ export class DatabaseBlockDataSource extends DataSourceBase {
readonly$: ReadonlySignal<boolean> = computed(() => { readonly$: ReadonlySignal<boolean> = computed(() => {
return ( return (
this._model.store.readonly || this._model.store.readonly ||
(IS_MOBILE && // TODO(@L-Sun): use block level readonly
!this._model.store.provider IS_MOBILE
.get(FeatureFlagService)
.getFlag('enable_mobile_database_editing'))
); );
}); });
@@ -13,7 +13,6 @@ import {
BlockElementCommentManager, BlockElementCommentManager,
CommentProviderIdentifier, CommentProviderIdentifier,
DocModeProvider, DocModeProvider,
FeatureFlagService,
NotificationProvider, NotificationProvider,
type TelemetryEventMap, type TelemetryEventMap,
TelemetryProvider, TelemetryProvider,
@@ -35,7 +34,6 @@ import {
uniMap, uniMap,
} from '@blocksuite/data-view'; } from '@blocksuite/data-view';
import { widgetPresets } from '@blocksuite/data-view/widget-presets'; import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { IS_MOBILE } from '@blocksuite/global/env';
import { Rect } from '@blocksuite/global/gfx'; import { Rect } from '@blocksuite/global/gfx';
import { import {
CommentIcon, CommentIcon,
@@ -50,7 +48,6 @@ import { autoUpdate } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core'; import { computed, signal } from '@preact/signals-core';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { popSideDetail } from './components/layout.js'; import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js'; import { DatabaseConfigExtension } from './config.js';
@@ -352,7 +349,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true'); this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.classList.add(databaseBlockStyles); this.classList.add(databaseBlockStyles);
this.listenFullWidthChange(); this.listenFullWidthChange();
this.handleMobileEditing();
} }
listenFullWidthChange() { listenFullWidthChange() {
@@ -368,41 +364,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}) })
); );
} }
handleMobileEditing() {
if (!IS_MOBILE) return;
let notifyClosed = true;
const handler = () => {
if (
!this.std
.get(FeatureFlagService)
.getFlag('enable_mobile_database_editing')
) {
const notification = this.std.getOptional(NotificationProvider);
if (notification && notifyClosed) {
notifyClosed = false;
notification.notify({
title: html`<div
style=${styleMap({
whiteSpace: 'wrap',
})}
>
Mobile database editing is not supported yet. You can open it in
experimental features, or edit it in desktop mode.
</div>`,
accent: 'warning',
onClose: () => {
notifyClosed = true;
},
});
}
}
};
this.disposables.addFromEvent(this, 'click', handler);
}
private readonly dataViewRootLogic = lazy( private readonly dataViewRootLogic = lazy(
() => () =>
new DataViewRootUILogic({ new DataViewRootUILogic({
@@ -1,7 +1,6 @@
import { ImageBlockModel } from '@blocksuite/affine-model'; import { ImageBlockModel } from '@blocksuite/affine-model';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
ToolbarModuleExtension, ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
@@ -50,10 +49,6 @@ const builtinToolbarConfig = {
}); });
}, },
}, },
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{ {
placement: ActionPlacement.More, placement: ActionPlacement.More,
id: 'a.clipboard', id: 'a.clipboard',
@@ -24,7 +24,6 @@ import {
getPrevContentBlock, getPrevContentBlock,
matchModels, matchModels,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
import { IS_ANDROID, IS_MOBILE } from '@blocksuite/global/env';
import { BlockSelection, type EditorHost } from '@blocksuite/std'; import { BlockSelection, type EditorHost } from '@blocksuite/std';
import type { BlockModel, Text } from '@blocksuite/store'; import type { BlockModel, Text } from '@blocksuite/store';
@@ -79,28 +78,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
index: lengthBeforeJoin, index: lengthBeforeJoin,
length: 0, length: 0,
}).catch(console.error); }).catch(console.error);
// due to some IME like Microsoft Swift IME on Android will reset range after join text,
// for example:
//
// $ZERO_WIDTH_FOR_EMPTY_LINE <--- p1
// |aaa <--- p2
//
// after pressing backspace, during beforeinput event, the native range is (p1, 1) -> (p2, 0)
// and after browser and IME handle the event, the native range is (p1, 1) -> (p1, 1)
//
// a|aa <--- p1
//
// so we need to set range again after join text.
if (IS_ANDROID) {
setTimeout(() => {
asyncSetInlineRange(editorHost.std, prevBlock, {
index: lengthBeforeJoin,
length: 0,
}).catch(console.error);
});
}
return true; return true;
} }
@@ -114,17 +91,10 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
...EMBED_BLOCK_MODEL_LIST, ...EMBED_BLOCK_MODEL_LIST,
]) ])
) { ) {
// due to create a block selection will clear text selection, which lead const selection = editorHost.selection.create(BlockSelection, {
// the virtual keyboard to be auto closed on mobile. This behavior breaks blockId: prevBlock.id,
// the user experience. });
if (!IS_MOBILE) { editorHost.selection.setGroup('note', [selection]);
const selection = editorHost.selection.create(BlockSelection, {
blockId: prevBlock.id,
});
editorHost.selection.setGroup('note', [selection]);
} else {
doc.deleteBlock(prevBlock);
}
if (model.text?.length === 0) { if (model.text?.length === 0) {
doc.deleteBlock(model, { doc.deleteBlock(model, {
@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const movedElements = new Set([ const movedElements = new Set([
...selectedElements, ...selectedElements,
...selectedElements.flatMap(el => ...selectedElements
isGfxGroupCompatibleModel(el) ? el.descendantElements : [] .map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
), .flat(),
]); ]);
movedElements.forEach(element => { movedElements.forEach(element => {
@@ -4,6 +4,6 @@ export * from './clipboard/command';
export * from './edgeless-root-block.js'; export * from './edgeless-root-block.js';
export { EdgelessRootService } from './edgeless-root-service.js'; export { EdgelessRootService } from './edgeless-root-service.js';
export * from './utils/clipboard-utils.js'; export * from './utils/clipboard-utils.js';
export { getElementProps, sortEdgelessElements } from './utils/clone-utils.js'; export { sortEdgelessElements } from './utils/clone-utils.js';
export { isCanvasElement } from './utils/query.js'; export { isCanvasElement } from './utils/query.js';
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts'; export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
@@ -5,7 +5,6 @@ import {
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { import {
ActionPlacement, ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig, type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit'; import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -62,10 +61,6 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show(); surfaceRefBlock.captionElement.show();
}, },
}, },
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{ {
id: 'a.clipboard', id: 'a.clipboard',
placement: ActionPlacement.More, placement: ActionPlacement.More,
@@ -11,7 +11,7 @@ import {
getBoundWithRotation, getBoundWithRotation,
intersects, intersects,
} from '@blocksuite/global/gfx'; } from '@blocksuite/global/gfx';
import { type BlockStdScope, SurfaceSelection } from '@blocksuite/std'; import type { BlockStdScope } from '@blocksuite/std';
import type { import type {
GfxCompatibleInterface, GfxCompatibleInterface,
GridManager, GridManager,
@@ -298,10 +298,7 @@ export class DomRenderer {
viewportBounds, viewportBounds,
zoom zoom
); );
const zIndexStyle = { Object.assign(domElement.style, geometricStyles);
'z-index': this.layerManager.getZIndex(elementModel),
};
Object.assign(domElement.style, geometricStyles, zIndexStyle);
Object.assign(domElement.style, PLACEHOLDER_RESET_STYLES); Object.assign(domElement.style, PLACEHOLDER_RESET_STYLES);
// Clear classes specific to shapes, if applicable // Clear classes specific to shapes, if applicable
@@ -338,10 +335,7 @@ export class DomRenderer {
zoom zoom
); );
const opacityStyle = getOpacity(elementModel); const opacityStyle = getOpacity(elementModel);
const zIndexStyle = { Object.assign(domElement.style, geometricStyles, opacityStyle);
'z-index': this.layerManager.getZIndex(elementModel),
};
Object.assign(domElement.style, geometricStyles, opacityStyle, zIndexStyle);
this._renderElement(elementModel, domElement); this._renderElement(elementModel, domElement);
} }
@@ -390,36 +384,6 @@ export class DomRenderer {
this.refresh(); this.refresh();
}) })
); );
// Workaround for the group rendering reactive update when selection changed
let lastSet = new Set<string>();
this._disposables.add(
this.std.selection.filter$(SurfaceSelection).subscribe(selections => {
const groupRelatedSelection = new Set(
selections.flatMap(s =>
s.elements.flatMap(e => {
const element = surfaceModel.getElementById(e);
if (
element &&
(element.type === 'group' || element.groups.length !== 0)
) {
return [element.id, ...element.groups.map(g => g.id)];
}
return [];
})
)
);
if (lastSet.symmetricDifference(groupRelatedSelection).size !== 0) {
lastSet.union(groupRelatedSelection).forEach(g => {
this._markElementDirty(g, UpdateType.ELEMENT_UPDATED);
});
this.refresh();
}
lastSet = groupRelatedSelection;
})
);
} }
addOverlay = (overlay: Overlay) => { addOverlay = (overlay: Overlay) => {
+1 -2
View File
@@ -73,8 +73,7 @@
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts", "./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts",
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts", "./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts",
"./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts", "./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts",
"./slider": "./src/slider/index.ts", "./slider": "./src/slider/index.ts"
"./tooltip": "./src/tooltip/index.ts"
}, },
"files": [ "files": [
"src", "src",
@@ -85,8 +85,6 @@ export class MenuSubMenu extends MenuFocusable {
.catch(err => console.error(err)); .catch(err => console.error(err));
}); });
this.menu.openSubMenu(menu); this.menu.openSubMenu(menu);
// in case that the menu is not closed, but the component is removed,
this.disposables.add(unsub);
} }
protected override render(): unknown { protected override render(): unknown {
@@ -18,7 +18,6 @@ export const LoadingIcon = ({
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
style="fill: none;"
> >
<style> <style>
.spinner { .spinner {
@@ -1,4 +1,3 @@
import { effects as tooltipEffects } from '../tooltip/effect.js';
import { EditorIconButton } from './icon-button.js'; import { EditorIconButton } from './icon-button.js';
import { import {
EditorMenuAction, EditorMenuAction,
@@ -7,6 +6,7 @@ import {
} from './menu-button.js'; } from './menu-button.js';
import { EditorToolbarSeparator } from './separator.js'; import { EditorToolbarSeparator } from './separator.js';
import { EditorToolbar } from './toolbar.js'; import { EditorToolbar } from './toolbar.js';
import { Tooltip } from './tooltip.js';
export { EditorChevronDown } from './chevron-down.js'; export { EditorChevronDown } from './chevron-down.js';
export { ToolbarMoreMenuConfigExtension } from './config.js'; export { ToolbarMoreMenuConfigExtension } from './config.js';
@@ -20,6 +20,7 @@ export { MenuContext } from './menu-context.js';
export { EditorToolbarSeparator } from './separator.js'; export { EditorToolbarSeparator } from './separator.js';
export { darkToolbarStyles, lightToolbarStyles } from './styles.js'; export { darkToolbarStyles, lightToolbarStyles } from './styles.js';
export { EditorToolbar } from './toolbar.js'; export { EditorToolbar } from './toolbar.js';
export { Tooltip } from './tooltip.js';
export type { export type {
AdvancedMenuItem, AdvancedMenuItem,
FatMenuItems, FatMenuItems,
@@ -37,12 +38,11 @@ export {
} from './utils.js'; } from './utils.js';
export function effects() { export function effects() {
tooltipEffects();
customElements.define('editor-toolbar-separator', EditorToolbarSeparator); customElements.define('editor-toolbar-separator', EditorToolbarSeparator);
customElements.define('editor-toolbar', EditorToolbar); customElements.define('editor-toolbar', EditorToolbar);
customElements.define('editor-icon-button', EditorIconButton); customElements.define('editor-icon-button', EditorIconButton);
customElements.define('editor-menu-button', EditorMenuButton); customElements.define('editor-menu-button', EditorMenuButton);
customElements.define('editor-menu-content', EditorMenuContent); customElements.define('editor-menu-content', EditorMenuContent);
customElements.define('editor-menu-action', EditorMenuAction); customElements.define('editor-menu-action', EditorMenuAction);
customElements.define('affine-tooltip', Tooltip);
} }
@@ -1,7 +0,0 @@
import { Tooltip } from './tooltip.js';
export function effects() {
if (!customElements.get('affine-tooltip')) {
customElements.define('affine-tooltip', Tooltip);
}
}
@@ -1,2 +0,0 @@
export { effects } from './effect.js';
export { Tooltip } from './tooltip.js';
@@ -65,7 +65,7 @@ export abstract class DataViewUILogicBase<
return handler(context); return handler(context);
}); });
} }
setSelection(selection?: Selection) { setSelection(selection?: Selection): void {
this.root.setSelection(selection); this.root.setSelection(selection);
} }
@@ -73,9 +73,7 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.view.readonly$.value) { if (this.view.readonly$.value) {
return; return;
} }
const setSelection = this.kanbanViewLogic.setSelection.bind( const setSelection = this.kanbanViewLogic.setSelection;
this.kanbanViewLogic
);
const viewId = this.kanbanViewLogic.view.id; const viewId = this.kanbanViewLogic.view.id;
if (setSelection && viewId) { if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) { if (editing && this.cell?.beforeEnterEditMode() === false) {
@@ -103,12 +101,12 @@ export class MobileKanbanCell extends SignalWatcher(
this.disposables.add( this.disposables.add(
effect(() => { effect(() => {
const isEditing = this.isSelectionEditing$.value; const isEditing = this.isSelectionEditing$.value;
if (isEditing && !this.isEditing$.peek()) { if (isEditing) {
this.isEditing$.value = true; this.isEditing$.value = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
this._cell.value?.afterEnterEditingMode(); this._cell.value?.afterEnterEditingMode();
}); });
} else if (!isEditing && this.isEditing$.peek()) { } else {
this._cell.value?.beforeExitEditingMode(); this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false; this.isEditing$.value = false;
} }
@@ -86,9 +86,6 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
} }
renderAddGroup = () => { renderAddGroup = () => {
if (this.readonly) {
return;
}
const addGroup = this.groupManager.addGroup; const addGroup = this.groupManager.addGroup;
if (!addGroup) { if (!addGroup) {
return; return;
@@ -68,9 +68,7 @@ export class MobileTableCell extends SignalWatcher(
if (this.view.readonly$.value) { if (this.view.readonly$.value) {
return; return;
} }
const setSelection = this.tableViewLogic.setSelection.bind( const setSelection = this.tableViewLogic.setSelection;
this.tableViewLogic
);
const viewId = this.tableViewLogic.view.id; const viewId = this.tableViewLogic.view.id;
if (setSelection && viewId) { if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) { if (editing && this.cell?.beforeEnterEditMode() === false) {
@@ -105,13 +103,13 @@ export class MobileTableCell extends SignalWatcher(
this.disposables.add( this.disposables.add(
effect(() => { effect(() => {
const isEditing = this.isSelectionEditing$.value; const isEditing = this.isSelectionEditing$.value;
if (isEditing && !this.isEditing$.peek()) { if (isEditing) {
this.isEditing$.value = true; this.isEditing$.value = true;
const cell = this._cell.value; const cell = this._cell.value;
requestAnimationFrame(() => { requestAnimationFrame(() => {
cell?.afterEnterEditingMode(); cell?.afterEnterEditingMode();
}); });
} else if (!isEditing && this.isEditing$.peek()) { } else {
this._cell.value?.beforeExitEditingMode(); this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false; this.isEditing$.value = false;
} }
@@ -5,6 +5,12 @@ export const mobileTableViewWrapper = css({
position: 'relative', position: 'relative',
width: '100%', width: '100%',
paddingBottom: '4px', paddingBottom: '4px',
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflowX: 'hidden',
overflowY: 'hidden', overflowY: 'hidden',
}); });
@@ -88,9 +88,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
}; };
private readonly addFilter = (e: MouseEvent) => { private readonly addFilter = (e: MouseEvent) => {
if (this.dataViewLogic.root.config.dataSource.readonly$.peek()) {
return;
}
const element = popupTargetFromElement(e.target as HTMLElement); const element = popupTargetFromElement(e.target as HTMLElement);
popCreateFilter(element, { popCreateFilter(element, {
vars: this.vars, vars: this.vars,
@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
ignoreEmpty = false ignoreEmpty = false
) { ) {
const notes = getNotesFromStore(store, modes); const notes = getNotesFromStore(store, modes);
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty)); return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
} }
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './adapter'; export * from './adapter';
export * from './brush-tool'; export * from './brush-tool';
export * from './element-renderer';
export * from './eraser-tool'; export * from './eraser-tool';
export * from './highlighter-tool'; export * from './highlighter-tool';
export * from './renderer';
export * from './toolbar/configs'; export * from './toolbar/configs';
export * from './toolbar/senior-tool'; export * from './toolbar/senior-tool';
@@ -1,69 +0,0 @@
import {
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import type { BrushElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
export const BrushDomRendererExtension = DomElementRendererExtension(
'brush',
(
model: BrushElementModel,
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(model.color, DefaultTheme.black, true);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the brush stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);
@@ -1,73 +0,0 @@
import {
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import type { HighlighterElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
export const HighlighterDomRendererExtension = DomElementRendererExtension(
'highlighter',
(
model: HighlighterElementModel,
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(
model.color,
DefaultTheme.hightlighterColor,
true
);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the highlighter stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);
@@ -1,2 +0,0 @@
export { BrushDomRendererExtension } from './brush';
export { HighlighterDomRendererExtension } from './highlighter';
@@ -1,2 +0,0 @@
export { BrushElementRendererExtension } from './brush';
export { HighlighterElementRendererExtension } from './highlighter';
@@ -1,2 +0,0 @@
export * from './dom';
export * from './element';
+1 -9
View File
@@ -5,14 +5,9 @@ import {
import { BrushTool } from './brush-tool'; import { BrushTool } from './brush-tool';
import { effects } from './effects'; import { effects } from './effects';
import { BrushElementRendererExtension } from './element-renderer';
import { EraserTool } from './eraser-tool'; import { EraserTool } from './eraser-tool';
import { HighlighterTool } from './highlighter-tool'; import { HighlighterTool } from './highlighter-tool';
import {
BrushDomRendererExtension,
BrushElementRendererExtension,
HighlighterDomRendererExtension,
HighlighterElementRendererExtension,
} from './renderer';
import { import {
brushToolbarExtension, brushToolbarExtension,
highlighterToolbarExtension, highlighterToolbarExtension,
@@ -35,9 +30,6 @@ export class BrushViewExtension extends ViewExtensionProvider {
context.register(HighlighterTool); context.register(HighlighterTool);
context.register(BrushElementRendererExtension); context.register(BrushElementRendererExtension);
context.register(BrushDomRendererExtension);
context.register(HighlighterElementRendererExtension);
context.register(HighlighterDomRendererExtension);
context.register(brushToolbarExtension); context.register(brushToolbarExtension);
context.register(highlighterToolbarExtension); context.register(highlighterToolbarExtension);
@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { connectorDomRenderer } from './connector-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'connector' elements.
*/
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);
@@ -1,18 +1,14 @@
import { import type { DomRenderer } from '@blocksuite/affine-block-surface';
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import { import {
type ConnectorElementModel, type ConnectorElementModel,
ConnectorMode, ConnectorMode,
DefaultTheme, DefaultTheme,
type LocalConnectorElementModel,
type PointStyle, type PointStyle,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx'; import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
import { isConnectorWithLabel } from '../connector-manager'; import { isConnectorWithLabel } from '../../connector-manager.js';
import { DEFAULT_ARROW_SIZE } from './utils'; import { DEFAULT_ARROW_SIZE } from '../utils.js';
interface PathBounds { interface PathBounds {
minX: number; minX: number;
@@ -225,8 +221,8 @@ function renderConnectorLabel(
* @param element - The HTMLElement to apply the connector's styles to. * @param element - The HTMLElement to apply the connector's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities. * @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/ */
export const connectorBaseDomRenderer = ( export const connectorDomRenderer = (
model: ConnectorElementModel | LocalConnectorElementModel, model: ConnectorElementModel,
element: HTMLElement, element: HTMLElement,
renderer: DomRenderer renderer: DomRenderer
): void => { ): void => {
@@ -362,21 +358,10 @@ export const connectorBaseDomRenderer = (
element.style.height = `${model.h * zoom}px`; element.style.height = `${model.h * zoom}px`;
element.style.overflow = 'visible'; element.style.overflow = 'visible';
element.style.pointerEvents = 'none'; element.style.pointerEvents = 'none';
};
export const connectorDomRenderer = ( // Set z-index for layering
model: ConnectorElementModel, element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
element: HTMLElement,
renderer: DomRenderer
): void => {
connectorBaseDomRenderer(model, element, renderer);
renderConnectorLabel(model, element, renderer, renderer.viewport.zoom);
};
/** // Render label if present
* Extension to register the DOM-based renderer for 'connector' elements. renderConnectorLabel(model, element, renderer, zoom);
*/ };
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);
@@ -25,7 +25,7 @@ import {
} from '@blocksuite/global/gfx'; } from '@blocksuite/global/gfx';
import { deltaInsertsToChunks } from '@blocksuite/std/inline'; import { deltaInsertsToChunks } from '@blocksuite/std/inline';
import { isConnectorWithLabel } from '../connector-manager'; import { isConnectorWithLabel } from '../connector-manager.js';
import { import {
DEFAULT_ARROW_SIZE, DEFAULT_ARROW_SIZE,
getArrowOptions, getArrowOptions,
@@ -33,7 +33,7 @@ import {
renderCircle, renderCircle,
renderDiamond, renderDiamond,
renderTriangle, renderTriangle,
} from './utils'; } from './utils.js';
export const connector: ElementRenderer< export const connector: ElementRenderer<
ConnectorElementModel | LocalConnectorElementModel ConnectorElementModel | LocalConnectorElementModel
+2 -1
View File
@@ -1,8 +1,9 @@
export * from './adapter'; export * from './adapter';
export * from './connector-manager'; export * from './connector-manager';
export * from './connector-tool'; export * from './connector-tool';
export * from './element-renderer';
export { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
export * from './element-transform'; export * from './element-transform';
export * from './renderer';
export * from './text'; export * from './text';
export * from './toolbar/config'; export * from './toolbar/config';
export * from './toolbar/quick-tool'; export * from './toolbar/quick-tool';
@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';
+2 -4
View File
@@ -6,11 +6,9 @@ import {
import { ConnectionOverlay } from './connector-manager'; import { ConnectionOverlay } from './connector-manager';
import { ConnectorTool } from './connector-tool'; import { ConnectorTool } from './connector-tool';
import { effects } from './effects'; import { effects } from './effects';
import { ConnectorElementRendererExtension } from './element-renderer';
import { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
import { ConnectorFilter } from './element-transform'; import { ConnectorFilter } from './element-transform';
import {
ConnectorDomRendererExtension,
ConnectorElementRendererExtension,
} from './renderer';
import { connectorToolbarExtension } from './toolbar/config'; import { connectorToolbarExtension } from './toolbar/config';
import { connectorQuickTool } from './toolbar/quick-tool'; import { connectorQuickTool } from './toolbar/quick-tool';
import { ConnectorElementView, ConnectorInteraction } from './view/view'; import { ConnectorElementView, ConnectorInteraction } from './view/view';
@@ -6,7 +6,7 @@ import {
import type { GroupElementModel } from '@blocksuite/affine-model'; import type { GroupElementModel } from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx'; import { Bound } from '@blocksuite/global/gfx';
import { titleRenderParams } from './utils'; import { titleRenderParams } from './utils.js';
export const group: ElementRenderer<GroupElementModel> = ( export const group: ElementRenderer<GroupElementModel> = (
model, model,
@@ -13,7 +13,7 @@ import {
GROUP_TITLE_FONT_SIZE, GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_OFFSET, GROUP_TITLE_OFFSET,
GROUP_TITLE_PADDING, GROUP_TITLE_PADDING,
} from './consts'; } from './consts.js';
export function titleRenderParams(group: GroupElementModel, zoom: number) { export function titleRenderParams(group: GroupElementModel, zoom: number) {
let text = group.title.toString().trim(); let text = group.title.toString().trim();
+1 -1
View File
@@ -1,6 +1,6 @@
export * from './adapter'; export * from './adapter';
export * from './command'; export * from './command';
export * from './element-renderer';
export * from './element-view'; export * from './element-view';
export * from './renderer';
export * from './text/text'; export * from './text/text';
export * from './toolbar/config'; export * from './toolbar/config';
@@ -1,62 +0,0 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { FontWeight, type GroupElementModel } from '@blocksuite/affine-model';
import {
GROUP_TITLE_FONT,
GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_PADDING,
} from './consts';
import { titleRenderParams } from './utils';
export const GroupDomRendererExtension = DomElementRendererExtension(
'group',
(model: GroupElementModel, domElement, renderer) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
const renderParams = titleRenderParams(model, zoom);
model.externalXYWH = renderParams.titleBound.serialize();
domElement.innerHTML = '';
domElement.style.outlineColor = '';
domElement.style.outlineWidth = '';
domElement.style.outlineStyle = '';
const elements = renderer.provider.selectedElements?.() || [];
const renderTitle = () => {
const { text } = renderParams;
const titleElement = document.createElement('div');
titleElement.style.transform = `translate(0, -100%)`;
titleElement.style.fontFamily = GROUP_TITLE_FONT;
titleElement.style.fontWeight = `${FontWeight.Regular}`;
titleElement.style.fontStyle = 'normal';
titleElement.style.fontSize = `${GROUP_TITLE_FONT_SIZE}px`;
titleElement.style.color = renderer.getPropertyValue('--affine-blue');
titleElement.style.textAlign = 'left';
titleElement.style.padding = `${GROUP_TITLE_PADDING[0]}px ${GROUP_TITLE_PADDING[1]}px`;
titleElement.textContent = text;
domElement.replaceChildren(titleElement);
};
if (elements.includes(model.id)) {
if (model.showTitle) {
renderTitle();
} else {
domElement.style.outlineColor =
renderer.getPropertyValue('--affine-blue');
domElement.style.outlineWidth = '2px';
domElement.style.outlineStyle = 'solid';
}
} else if (model.childElements.some(child => elements.includes(child.id))) {
domElement.style.outlineColor = '#8FD1FF';
domElement.style.outlineWidth = '2px';
domElement.style.outlineStyle = 'solid';
}
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);
@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';
@@ -21,7 +21,7 @@ import {
GROUP_TITLE_FONT_SIZE, GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_OFFSET, GROUP_TITLE_OFFSET,
GROUP_TITLE_PADDING, GROUP_TITLE_PADDING,
} from '../renderer/consts'; } from '../element-renderer/consts';
export function mountGroupTitleEditor( export function mountGroupTitleEditor(
group: GroupElementModel, group: GroupElementModel,
+1 -5
View File
@@ -4,12 +4,9 @@ import {
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { effects } from './effects'; import { effects } from './effects';
import { GroupElementRendererExtension } from './element-renderer';
import { GroupElementView, GroupInteraction } from './element-view'; import { GroupElementView, GroupInteraction } from './element-view';
import { GroupInteractionExtension } from './interaction-ext'; import { GroupInteractionExtension } from './interaction-ext';
import {
GroupDomRendererExtension,
GroupElementRendererExtension,
} from './renderer';
import { groupToolbarExtension } from './toolbar/config'; import { groupToolbarExtension } from './toolbar/config';
export class GroupViewExtension extends ViewExtensionProvider { export class GroupViewExtension extends ViewExtensionProvider {
@@ -23,7 +20,6 @@ export class GroupViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) { override setup(context: ViewExtensionContext) {
super.setup(context); super.setup(context);
context.register(GroupElementRendererExtension); context.register(GroupElementRendererExtension);
context.register(GroupDomRendererExtension);
context.register(GroupElementView); context.register(GroupElementView);
if (this.isEdgeless(context.scope)) { if (this.isEdgeless(context.scope)) {
context.register(groupToolbarExtension); context.register(groupToolbarExtension);
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './adapter'; export * from './adapter';
export * from './element-renderer';
export * from './indicator-overlay'; export * from './indicator-overlay';
export * from './interactivity'; export * from './interactivity';
export * from './renderer';
export * from './toolbar/config'; export * from './toolbar/config';
export * from './toolbar/senior-tool'; export * from './toolbar/senior-tool';
export * from './utils'; export * from './utils';
@@ -1,65 +0,0 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import {
connectorBaseDomRenderer,
ConnectorPathGenerator,
} from '@blocksuite/affine-gfx-connector';
import type {
MindmapElementModel,
MindmapNode,
} from '@blocksuite/affine-model';
import type { GfxModel } from '@blocksuite/std/gfx';
export const MindmapDomRendererExtension = DomElementRendererExtension(
'mindmap',
(model: MindmapElementModel, domElement, renderer) => {
const bound = model.elementBound;
const { zoom } = renderer.viewport;
// Set element size and position
domElement.style.width = `${bound.w * zoom}px`;
domElement.style.height = `${bound.h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
const newChildren: HTMLDivElement[] = [];
const traverse = (node: MindmapNode) => {
const connectors = model.getConnectors(node);
if (!connectors) return;
connectors.reverse().forEach(result => {
const { connector, outdated } = result;
const elementGetter = (id: string) =>
model.surface.getElementById(id) ??
(model.surface.store.getModelById(id) as GfxModel);
if (outdated) {
ConnectorPathGenerator.updatePath(connector, null, elementGetter);
}
const connectorContainer = document.createElement('div');
connectorContainer.style.position = 'absolute';
connectorContainer.style.transformOrigin = 'top left';
const geometricStyles = {
left: `${(connector.x - bound.x) * zoom}px`,
top: `${(connector.y - bound.y) * zoom}px`,
};
const opacityStyle = { opacity: node.element.opacity };
Object.assign(connectorContainer.style, geometricStyles, opacityStyle);
connectorBaseDomRenderer(connector, connectorContainer, renderer);
newChildren.push(connectorContainer);
});
if (node.detail.collapsed) {
return;
} else {
node.children.forEach(traverse);
}
};
model.tree && traverse(model.tree);
domElement.replaceChildren(...newChildren);
}
);
@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';
+1 -5
View File
@@ -4,12 +4,9 @@ import {
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { effects } from './effects'; import { effects } from './effects';
import { MindmapElementRendererExtension } from './element-renderer';
import { MindMapIndicatorOverlay } from './indicator-overlay'; import { MindMapIndicatorOverlay } from './indicator-overlay';
import { MindMapDragExtension } from './interactivity'; import { MindMapDragExtension } from './interactivity';
import {
MindmapDomRendererExtension,
MindmapElementRendererExtension,
} from './renderer';
import { import {
mindmapToolbarExtension, mindmapToolbarExtension,
shapeMindmapToolbarExtension, shapeMindmapToolbarExtension,
@@ -28,7 +25,6 @@ export class MindmapViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) { override setup(context: ViewExtensionContext) {
super.setup(context); super.setup(context);
context.register(MindmapElementRendererExtension); context.register(MindmapElementRendererExtension);
context.register(MindmapDomRendererExtension);
context.register(mindMapSeniorTool); context.register(mindMapSeniorTool);
context.register(mindmapToolbarExtension); context.register(mindmapToolbarExtension);
context.register(shapeMindmapToolbarExtension); context.register(shapeMindmapToolbarExtension);
@@ -1,7 +1,4 @@
import { import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
DefaultTool,
EdgelessLegacySlotIdentifier,
} from '@blocksuite/affine-block-surface';
import { on } from '@blocksuite/affine-shared/utils'; import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std'; import type { PointerEventState } from '@blocksuite/std';
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx'; import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
@@ -67,15 +64,12 @@ export class PanTool extends BaseTool<PanToolOption> {
const { toolType, options: originalToolOptions } = currentTool; const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections; const selectionToRestore = this.gfx.selection.surfaceSelections;
if (!toolType) return; if (!toolType) return;
// restore to DefaultTool if previous tool is CopilotTool
if (toolType.toolName === 'copilot') {
this.controller.setTool(DefaultTool);
return;
}
let finalOptions: ToolOptions<BaseTool<any>> | undefined = let finalOptions: ToolOptions<BaseTool<any>> | undefined =
originalToolOptions; originalToolOptions;
if (toolType.toolName === 'frameNavigator') { const PRESENT_TOOL_NAME = 'frameNavigator';
if (toolType.toolName === PRESENT_TOOL_NAME) {
// When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button), // When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button),
// set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset // set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset
// and maintain the panned position. // and maintain the panned position.
@@ -99,17 +93,15 @@ export class PanTool extends BaseTool<PanToolOption> {
}); });
} }
requestAnimationFrame(() => { this.controller.setTool(PanTool, {
this.controller.setTool(PanTool, { panning: true,
panning: true,
});
}); });
const dispose = on(document, 'pointerup', evt => { const dispose = on(document, 'pointerup', evt => {
if (evt.button === MouseButton.MIDDLE) { if (evt.button === MouseButton.MIDDLE) {
restoreToPrevious(); restoreToPrevious();
dispose();
} }
dispose();
}); });
return false; return false;
@@ -1 +1,2 @@
export * from './highlighter';
export * from './shape'; export * from './shape';
@@ -1,5 +1,4 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface'; import type { DomRenderer } from '@blocksuite/affine-block-surface';
import { isRTL } from '@blocksuite/affine-gfx-text';
import type { ShapeElementModel } from '@blocksuite/affine-model'; import type { ShapeElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model'; import { DefaultTheme } from '@blocksuite/affine-model';
import { SVGShapeBuilder } from '@blocksuite/global/gfx'; import { SVGShapeBuilder } from '@blocksuite/global/gfx';
@@ -100,8 +99,6 @@ export const shapeDomRenderer = (
const unscaledWidth = model.w; const unscaledWidth = model.w;
const unscaledHeight = model.h; const unscaledHeight = model.h;
const newChildren: Element[] = [];
const fillColor = renderer.getColorValue( const fillColor = renderer.getColorValue(
model.fillColor, model.fillColor,
DefaultTheme.shapeFillColor, DefaultTheme.shapeFillColor,
@@ -173,7 +170,8 @@ export const shapeDomRenderer = (
} }
svg.append(polygon); svg.append(polygon);
newChildren.push(svg); // Replace existing children to avoid memory leaks
element.replaceChildren(svg);
} else { } else {
// Standard rendering for other shapes (e.g., rect, ellipse) // Standard rendering for other shapes (e.g., rect, ellipse)
// innerHTML was already cleared by applyShapeSpecificStyles if necessary // innerHTML was already cleared by applyShapeSpecificStyles if necessary
@@ -181,43 +179,10 @@ export const shapeDomRenderer = (
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
} }
if (model.textDisplay && model.text) {
const str = model.text.toString();
const textElement = document.createElement('div');
if (isRTL(str)) {
textElement.dir = 'rtl';
}
textElement.style.position = 'absolute';
textElement.style.inset = '0';
textElement.style.display = 'flex';
textElement.style.flexDirection = 'column';
textElement.style.justifyContent =
model.textVerticalAlign === 'center'
? 'center'
: model.textVerticalAlign === 'top'
? 'flex-start'
: 'flex-end';
textElement.style.whiteSpace = 'pre-wrap';
textElement.style.wordBreak = 'break-word';
textElement.style.textAlign = model.textAlign;
textElement.style.alignmentBaseline = 'alphabetic';
textElement.style.fontFamily = model.fontFamily;
textElement.style.fontSize = `${model.fontSize * zoom}px`;
textElement.style.fontWeight = model.fontWeight;
textElement.style.color = renderer.getColorValue(
model.color,
DefaultTheme.shapeTextColor,
true
);
textElement.textContent = str;
newChildren.push(textElement);
}
// Replace existing children to avoid memory leaks
element.replaceChildren(...newChildren);
applyTransformStyles(model, element); applyTransformStyles(model, element);
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
manageClassNames(model, element); manageClassNames(model, element);
applyShadowStyles(model, element, renderer); applyShadowStyles(model, element, renderer);
}; };
+5 -1
View File
@@ -4,7 +4,10 @@ import {
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import { effects } from './effects'; import { effects } from './effects';
import { ShapeElementRendererExtension } from './element-renderer'; import {
HighlighterElementRendererExtension,
ShapeElementRendererExtension,
} from './element-renderer';
import { ShapeDomRendererExtension } from './element-renderer/shape-dom'; import { ShapeDomRendererExtension } from './element-renderer/shape-dom';
import { ShapeElementView, ShapeViewInteraction } from './element-view'; import { ShapeElementView, ShapeViewInteraction } from './element-view';
import { ShapeTool } from './shape-tool'; import { ShapeTool } from './shape-tool';
@@ -21,6 +24,7 @@ export class ShapeViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) { override setup(context: ViewExtensionContext) {
super.setup(context); super.setup(context);
if (this.isEdgeless(context.scope)) { if (this.isEdgeless(context.scope)) {
context.register(HighlighterElementRendererExtension);
context.register(ShapeElementRendererExtension); context.register(ShapeElementRendererExtension);
context.register(ShapeDomRendererExtension); context.register(ShapeDomRendererExtension);
context.register(ShapeElementView); context.register(ShapeElementView);
@@ -116,7 +116,6 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
`; `;
private _cleanup: (() => void) | null = null; private _cleanup: (() => void) | null = null;
private _autoUpdateCleanup: (() => void) | null = null;
private _prevTool: ToolOptionWithType | null = null; private _prevTool: ToolOptionWithType | null = null;
@@ -129,11 +128,6 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]]; return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]];
} }
override connectedCallback() {
super.connectedCallback();
this.disposables.add(() => this._autoUpdateCleanup?.());
}
private _closePanel() { private _closePanel() {
if (this._openedPanel) { if (this._openedPanel) {
this._openedPanel.remove(); this._openedPanel.remove();
@@ -181,8 +175,8 @@ export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
requestAnimationFrame(() => { requestAnimationFrame(() => {
const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement; const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement;
this._autoUpdateCleanup?.();
this._autoUpdateCleanup = autoUpdate(this, panel, () => { autoUpdate(this, panel, () => {
computePosition(this, panel, { computePosition(this, panel, {
placement: 'top', placement: 'top',
middleware: [offset(20), arrow({ element: arrowEl }), shift()], middleware: [offset(20), arrow({ element: arrowEl }), shift()],
@@ -103,52 +103,54 @@ export class InlineCommentManager extends LifeCycleWatcher {
id: CommentId, id: CommentId,
selections: BaseSelection[] selections: BaseSelection[]
) => { ) => {
const needCommentTexts = selections.flatMap(selection => { const needCommentTexts = selections
if (!selection.is(TextSelection)) return []; .map(selection => {
const [_, { selectedBlocks }] = this.std.command if (!selection.is(TextSelection)) return [];
.chain() const [_, { selectedBlocks }] = this.std.command
.pipe(getSelectedBlocksCommand, { .chain()
textSelection: selection, .pipe(getSelectedBlocksCommand, {
}) textSelection: selection,
.run(); })
.run();
if (!selectedBlocks) return []; if (!selectedBlocks) return [];
type MakeRequired<T, K extends keyof T> = T & { type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>; [key in K]: NonNullable<T[key]>;
}; };
return selectedBlocks return selectedBlocks
.map( .map(
({ model }) => ({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const [model, getInlineEditorByModel(this.std, model)] as const
) )
.filter( .filter(
( (
pair pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] => ): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1] !!pair[0].text && !!pair[1]
) )
.map(([model, inlineEditor]) => { .map(([model, inlineEditor]) => {
let from: TextRangePoint; let from: TextRangePoint;
let to: TextRangePoint | null; let to: TextRangePoint | null;
if (model.id === selection.from.blockId) { if (model.id === selection.from.blockId) {
from = selection.from; from = selection.from;
to = null; to = null;
} else if (model.id === selection.to?.blockId) { } else if (model.id === selection.to?.blockId) {
from = selection.to; from = selection.to;
to = null; to = null;
} else { } else {
from = { from = {
blockId: model.id, blockId: model.id,
index: 0, index: 0,
length: model.text.yText.length, length: model.text.yText.length,
}; };
to = null; to = null;
} }
return [new TextSelection({ from, to }), inlineEditor] as const; return [new TextSelection({ from, to }), inlineEditor] as const;
}); });
}); })
.flat();
if (needCommentTexts.length === 0) return; if (needCommentTexts.length === 0) return;
@@ -22,11 +22,8 @@ import { isEqual } from 'lodash-es';
}) })
export class InlineComment extends WithDisposable(ShadowlessElement) { export class InlineComment extends WithDisposable(ShadowlessElement) {
static override styles = css` static override styles = css`
inline-comment {
display: inline;
}
inline-comment.unresolved { inline-comment.unresolved {
display: inline-block;
background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')}; background-color: ${unsafeCSSVarV2('block/comment/highlightDefault')};
border-bottom: 2px solid border-bottom: 2px solid
${unsafeCSSVarV2('block/comment/highlightUnderline')}; ${unsafeCSSVarV2('block/comment/highlightUnderline')};
@@ -150,9 +150,6 @@ export class AffineReference extends WithDisposable(ShadowlessElement) {
readonly open = (event?: Partial<DocLinkClickedEvent>) => { readonly open = (event?: Partial<DocLinkClickedEvent>) => {
if (!this.config.interactable) return; if (!this.config.interactable) return;
if (event?.event?.button === 2) {
return;
}
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({ this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
...this.referenceInfo, ...this.referenceInfo,
@@ -131,7 +131,7 @@ export class HighlighterElementModel extends GfxPrimitiveElementModel<Highlighte
instance['_local'].delete('commands'); instance['_local'].delete('commands');
}) })
@derive((lineWidth: number, instance: Instance) => { @derive((lineWidth: number, instance: Instance) => {
const oldBound = Bound.fromXYWH(instance.deserializedXYWH); const oldBound = instance.elementBound;
if ( if (
lineWidth === instance.lineWidth || lineWidth === instance.lineWidth ||
@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
// may be hover on a block or element, in this case // may be hover on a block or element, in this case
// the selection is empty, so we need to get the current model // the selection is empty, so we need to get the current model
if (model) { if (model && selections.length === 0) {
if (model instanceof BlockModel) { if (model instanceof BlockModel) {
commentProvider.addComment([ commentProvider.addComment([
new BlockSelection({ new BlockSelection({
@@ -15,7 +15,6 @@ export interface BlockSuiteFlags {
enable_shape_shadow_blur: boolean; enable_shape_shadow_blur: boolean;
enable_mobile_keyboard_toolbar: boolean; enable_mobile_keyboard_toolbar: boolean;
enable_mobile_linked_doc_menu: boolean; enable_mobile_linked_doc_menu: boolean;
enable_mobile_database_editing: boolean;
enable_block_meta: boolean; enable_block_meta: boolean;
enable_callout: boolean; enable_callout: boolean;
enable_edgeless_scribbled_style: boolean; enable_edgeless_scribbled_style: boolean;
@@ -42,7 +41,6 @@ export class FeatureFlagService extends StoreExtension {
enable_mobile_keyboard_toolbar: false, enable_mobile_keyboard_toolbar: false,
enable_mobile_linked_doc_menu: false, enable_mobile_linked_doc_menu: false,
enable_block_meta: true, enable_block_meta: true,
enable_mobile_database_editing: false,
enable_callout: false, enable_callout: false,
enable_edgeless_scribbled_style: false, enable_edgeless_scribbled_style: false,
enable_table_virtual_scroll: false, enable_table_virtual_scroll: false,
@@ -5,7 +5,6 @@ import type { Signal } from '@preact/signals-core';
import type { AffineUserInfo } from './types'; import type { AffineUserInfo } from './types';
export interface UserService { export interface UserService {
currentUserInfo$: Signal<AffineUserInfo | null>;
userInfo$(id: string): Signal<AffineUserInfo | null>; userInfo$(id: string): Signal<AffineUserInfo | null>;
isLoading$(id: string): Signal<boolean>; isLoading$(id: string): Signal<boolean>;
error$(id: string): Signal<string | null>; // user friendly error string error$(id: string): Signal<string | null>; // user friendly error string
@@ -4,14 +4,6 @@ import type { ReadonlySignal } from '@preact/signals-core';
export interface VirtualKeyboardProvider { export interface VirtualKeyboardProvider {
readonly visible$: ReadonlySignal<boolean>; readonly visible$: ReadonlySignal<boolean>;
readonly height$: ReadonlySignal<number>; readonly height$: ReadonlySignal<number>;
/**
* The static height of the keyboard, it should record the last non-zero height of virtual keyboard
*/
readonly staticHeight$: ReadonlySignal<number>;
/**
* The safe area of the app tab, it will be used when the keyboard is open or closed
*/
readonly appTabSafeArea$: ReadonlySignal<string>;
} }
export interface VirtualKeyboardProviderWithAction export interface VirtualKeyboardProviderWithAction
@@ -11,12 +11,14 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
return new DOMRect(); return new DOMRect();
} }
const lockedElementsByFrame = selected.flatMap(selectable => { const lockedElementsByFrame = selected
if (selectable instanceof FrameBlockModel && selectable.isLocked()) { .map(selectable => {
return selectable.descendantElements; if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
} return selectable.descendantElements;
return []; }
}); return [];
})
.flat();
selected = [...new Set([...selected, ...lockedElementsByFrame])]; selected = [...new Set([...selected, ...lockedElementsByFrame])];
@@ -114,7 +114,6 @@ export class PreviewHelper {
}); });
let width: number = 500; let width: number = 500;
// oxlint-disable-next-line no-unassigned-vars
let height; let height;
const noteBlock = this.widget.host.querySelector('affine-note'); const noteBlock = this.widget.host.querySelector('affine-note');
@@ -20,7 +20,6 @@
"@blocksuite/affine-block-paragraph": "workspace:*", "@blocksuite/affine-block-paragraph": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*", "@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-block-surface-ref": "workspace:*", "@blocksuite/affine-block-surface-ref": "workspace:*",
"@blocksuite/affine-block-table": "workspace:*",
"@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*", "@blocksuite/affine-fragment-doc-title": "workspace:*",
@@ -18,7 +18,6 @@ import {
} from '@blocksuite/affine-block-paragraph'; } from '@blocksuite/affine-block-paragraph';
import { DefaultTool, getSurfaceBlock } from '@blocksuite/affine-block-surface'; import { DefaultTool, getSurfaceBlock } from '@blocksuite/affine-block-surface';
import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref'; import { insertSurfaceRefBlockCommand } from '@blocksuite/affine-block-surface-ref';
import { insertTableBlockCommand } from '@blocksuite/affine-block-table';
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal'; import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import { insertInlineLatex } from '@blocksuite/affine-inline-latex'; import { insertInlineLatex } from '@blocksuite/affine-inline-latex';
@@ -41,20 +40,14 @@ import {
deleteSelectedModelsCommand, deleteSelectedModelsCommand,
draftSelectedModelsCommand, draftSelectedModelsCommand,
duplicateSelectedModelsCommand, duplicateSelectedModelsCommand,
focusBlockEnd,
getBlockSelectionsCommand, getBlockSelectionsCommand,
getSelectedModelsCommand, getSelectedModelsCommand,
getTextSelectionCommand, getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands'; } from '@blocksuite/affine-shared/commands';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts'; import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types'; import type { AffineTextStyleAttributes } from '@blocksuite/affine-shared/types';
import { import {
createDefaultDoc, createDefaultDoc,
isInsideBlockByFlavour,
openSingleFileWith, openSingleFileWith,
type Signal, type Signal,
} from '@blocksuite/affine-shared/utils'; } from '@blocksuite/affine-shared/utils';
@@ -94,7 +87,6 @@ import {
RedoIcon, RedoIcon,
RightTabIcon, RightTabIcon,
StrikeThroughIcon, StrikeThroughIcon,
TableIcon,
TeXIcon, TeXIcon,
TextIcon, TextIcon,
TodayIcon, TodayIcon,
@@ -168,6 +160,10 @@ export type KeyboardSubToolbarConfig = {
export type KeyboardToolbarContext = { export type KeyboardToolbarContext = {
std: BlockStdScope; std: BlockStdScope;
rootComponent: BlockComponent; rootComponent: BlockComponent;
/**
* Close tool bar, and blur the focus if blur is true, default is false
*/
closeToolbar: (blur?: boolean) => void;
/** /**
* Close current tool panel and show virtual keyboard * Close current tool panel and show virtual keyboard
*/ */
@@ -262,62 +258,6 @@ const textToolActionItems: KeyboardToolbarActionItem[] = [
.run(); .run();
}, },
}, },
{
name: 'Table',
icon: TableIcon(),
showWhen: ({ std, rootComponent: { model } }) =>
std.store.schema.flavourSchemaMap.has('affine:table') &&
!isInsideBlockByFlavour(std.store, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertTableBlockCommand, {
place: 'after',
removeEmptyLine: true,
})
.pipe(({ insertedTableBlockId }) => {
if (insertedTableBlockId) {
const telemetry = std.getOptional(TelemetryProvider);
telemetry?.track('BlockCreated', {
blockType: 'affine:table',
});
}
})
.run();
},
},
{
name: 'Callout',
icon: FontIcon(),
showWhen: ({ std, rootComponent: { model } }) => {
return (
std.get(FeatureFlagService).getFlag('enable_callout') &&
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
);
},
action: ({ rootComponent: { model }, std }) => {
const { store } = model;
const parent = store.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
if (index === -1) return;
const calloutId = store.addBlock('affine:callout', {}, parent, index + 1);
if (!calloutId) return;
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
if (!paragraphId) return;
std.host.updateComplete
.then(() => {
const paragraph = std.view.getBlock(paragraphId);
if (!paragraph) return;
std.command.exec(focusBlockEnd, {
focusBlock: paragraph,
});
})
.catch(console.error);
},
},
]; ];
const listToolActionItems: KeyboardToolbarActionItem[] = [ const listToolActionItems: KeyboardToolbarActionItem[] = [
@@ -4,7 +4,7 @@ import {
requiredProperties, requiredProperties,
ShadowlessElement, ShadowlessElement,
} from '@blocksuite/std'; } from '@blocksuite/std';
import { html, nothing } from 'lit'; import { html, nothing, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
@@ -71,13 +71,22 @@ export class AffineKeyboardToolPanel extends SignalWatcher(
.map(group => (typeof group === 'function' ? group(this.context) : group)) .map(group => (typeof group === 'function' ? group(this.context) : group))
.filter((group): group is KeyboardToolPanelGroup => group !== null); .filter((group): group is KeyboardToolPanelGroup => group !== null);
return html`<div class="affine-keyboard-tool-panel-container"> return repeat(
${repeat( groups,
groups, group => group.name,
group => group.name, group => this._renderGroup(group)
group => this._renderGroup(group) );
)} }
</div>`;
protected override willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('height')) {
this.style.height = `${this.height}px`;
if (this.height === 0) {
this.style.padding = '0';
} else {
this.style.padding = '';
}
}
} }
@property({ attribute: false }) @property({ attribute: false })
@@ -85,4 +94,7 @@ export class AffineKeyboardToolPanel extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor context!: KeyboardToolbarContext; accessor context!: KeyboardToolbarContext;
@property({ attribute: false })
accessor height = 0;
} }
@@ -8,7 +8,7 @@ import {
requiredProperties, requiredProperties,
ShadowlessElement, ShadowlessElement,
} from '@blocksuite/std'; } from '@blocksuite/std';
import { effect, type Signal, signal } from '@preact/signals-core'; import { effect, type Signal, signal, untracked } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
import { property } from 'lit/decorators.js'; import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js'; import { repeat } from 'lit/directives/repeat.js';
@@ -22,6 +22,7 @@ import type {
KeyboardToolbarItem, KeyboardToolbarItem,
KeyboardToolPanelConfig, KeyboardToolPanelConfig,
} from './config'; } from './config';
import { PositionController } from './position-controller';
import { keyboardToolbarStyles } from './styles'; import { keyboardToolbarStyles } from './styles';
import { import {
isKeyboardSubToolBarConfig, isKeyboardSubToolBarConfig,
@@ -40,7 +41,10 @@ export class AffineKeyboardToolbar extends SignalWatcher(
) { ) {
static override styles = keyboardToolbarStyles; static override styles = keyboardToolbarStyles;
private readonly _expanded$ = signal(false); /** This field records the panel static height same as the virtual keyboard height */
panelHeight$ = signal(0);
positionController = new PositionController(this);
get std() { get std() {
return this.rootComponent.std; return this.rootComponent.std;
@@ -50,31 +54,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
return this._currentPanelIndex$.value !== -1; return this._currentPanelIndex$.value !== -1;
} }
private get panelHeight() {
return this._expanded$.value
? `${
this.keyboard.staticHeight$.value !== 0
? this.keyboard.staticHeight$.value
: 330
}px`
: this.keyboard.appTabSafeArea$.value;
}
/**
* Prevent flickering during keyboard opening
*/
private _resetPanelIndexTimeoutId: ReturnType<typeof setTimeout> | null =
null;
private readonly _closeToolPanel = () => { private readonly _closeToolPanel = () => {
this._currentPanelIndex$.value = -1;
if (!this.keyboard.visible$.peek()) this.keyboard.show(); if (!this.keyboard.visible$.peek()) this.keyboard.show();
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
this._resetPanelIndexTimeoutId = setTimeout(() => {
this._currentPanelIndex$.value = -1;
}, 100);
}; };
private readonly _currentPanelIndex$ = signal(-1); private readonly _currentPanelIndex$ = signal(-1);
@@ -101,10 +83,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
if (this._currentPanelIndex$.value === index) { if (this._currentPanelIndex$.value === index) {
this._closeToolPanel(); this._closeToolPanel();
} else { } else {
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
this._currentPanelIndex$.value = index; this._currentPanelIndex$.value = index;
this.keyboard.hide(); this.keyboard.hide();
this._scrollCurrentBlockIntoView(); this._scrollCurrentBlockIntoView();
@@ -145,6 +123,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
return { return {
std: this.std, std: this.std,
rootComponent: this.rootComponent, rootComponent: this.rootComponent,
closeToolbar: (blur = false) => {
this.close(blur);
},
closeToolPanel: () => { closeToolPanel: () => {
this._closeToolPanel(); this._closeToolPanel();
}, },
@@ -221,7 +202,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
} }
private _renderItems() { private _renderItems() {
if (!this.std.event.active$.value) if (document.activeElement !== this.rootComponent)
return html`<div class="item-container"></div>`; return html`<div class="item-container"></div>`;
const goPrevToolbarAction = when( const goPrevToolbarAction = when(
@@ -245,15 +226,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
<icon-button <icon-button
size="36px" size="36px"
@click=${() => { @click=${() => {
if (this.keyboard.staticHeight$.value === 0) { this.close(true);
this._closeToolPanel();
return;
}
if (this.keyboard.visible$.peek()) {
this.keyboard.hide();
} else {
this.keyboard.show();
}
}} }}
> >
${KeyboardIcon()} ${KeyboardIcon()}
@@ -264,23 +237,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// There are two cases that `_expanded$` will be true:
// 1. when virtual keyboard is opened, the panel need to be expanded and overlapped by the keyboard,
// so that the toolbar will be on the top of the keyboard.
// 2. the panel is opened, whether the keyboard is closed or not exists (e.g. a physical keyboard connected)
//
// There is one case that `_expanded$` will be false:
// 1. the panel is closed, and the keyboard is closed, the toolbar will be rendered at the bottom of the viewport
this._disposables.add(
effect(() => {
if (this.keyboard.visible$.value || this.panelOpened) {
this._expanded$.value = true;
} else {
this._expanded$.value = false;
}
})
);
// prevent editor blur when click item in toolbar // prevent editor blur when click item in toolbar
this.disposables.addFromEvent(this, 'pointerdown', e => { this.disposables.addFromEvent(this, 'pointerdown', e => {
e.preventDefault(); e.preventDefault();
@@ -304,17 +260,15 @@ export class AffineKeyboardToolbar extends SignalWatcher(
if (this.keyboard.visible$.value) { if (this.keyboard.visible$.value) {
this._closeToolPanel(); this._closeToolPanel();
} }
// when keyboard is closed and the panel is not opened, we need to close the toolbar,
// this usually happens when user close keyboard from system side
else if (this.hasUpdated && untracked(() => !this.panelOpened)) {
this.close(true);
}
}) })
); );
this._watchAutoShow(); this._watchAutoShow();
this.disposables.add(() => {
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
});
} }
private _watchAutoShow() { private _watchAutoShow() {
@@ -377,10 +331,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
<affine-keyboard-tool-panel <affine-keyboard-tool-panel
.config=${this._currentPanelConfig} .config=${this._currentPanelConfig}
.context=${this._context} .context=${this._context}
style=${styleMap({ .height=${this.panelHeight$.value}
height: this.panelHeight,
paddingBottom: this.keyboard.appTabSafeArea$.value,
})}
></affine-keyboard-tool-panel> ></affine-keyboard-tool-panel>
`; `;
} }
@@ -388,6 +339,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
@property({ attribute: false }) @property({ attribute: false })
accessor keyboard!: VirtualKeyboardProviderWithAction; accessor keyboard!: VirtualKeyboardProviderWithAction;
@property({ attribute: false })
accessor close: (blur: boolean) => void = () => {};
@property({ attribute: false }) @property({ attribute: false })
accessor config!: KeyboardToolbarConfig; accessor config!: KeyboardToolbarConfig;
@@ -0,0 +1,42 @@
import { type VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { BlockStdScope, ShadowlessElement } from '@blocksuite/std';
import { effect, type Signal } from '@preact/signals-core';
import type { ReactiveController, ReactiveControllerHost } from 'lit';
/**
* This controller is used to control the keyboard toolbar position
*/
export class PositionController implements ReactiveController {
private readonly _disposables = new DisposableGroup();
host: ReactiveControllerHost &
ShadowlessElement & {
std: BlockStdScope;
panelHeight$: Signal<number>;
keyboard: VirtualKeyboardProvider;
panelOpened: boolean;
};
constructor(host: PositionController['host']) {
(this.host = host).addController(this);
}
hostConnected() {
const { keyboard } = this.host;
this._disposables.add(
effect(() => {
if (keyboard.visible$.value) {
this.host.panelHeight$.value = keyboard.height$.value;
}
})
);
this.host.style.bottom = '0px';
}
hostDisconnected() {
this._disposables.dispose();
}
}
@@ -7,7 +7,6 @@ export const keyboardToolbarStyles = css`
position: fixed; position: fixed;
display: block; display: block;
width: 100vw; width: 100vw;
bottom: 0;
} }
.keyboard-toolbar { .keyboard-toolbar {
@@ -61,18 +60,14 @@ export const keyboardToolbarStyles = css`
export const keyboardToolPanelStyles = css` export const keyboardToolPanelStyles = css`
affine-keyboard-tool-panel { affine-keyboard-tool-panel {
display: block;
overflow-y: auto;
box-sizing: border-box;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
}
.affine-keyboard-tool-panel-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: 24px;
width: 100%; width: 100%;
padding: 16px 4px 8px 8px; padding: 16px 4px 8px 8px;
overflow-y: auto;
box-sizing: border-box;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
} }
${scrollbarStyle('affine-keyboard-tool-panel')} ${scrollbarStyle('affine-keyboard-tool-panel')}
@@ -20,6 +20,18 @@ import {
export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget'; export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget';
export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel> { export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel> {
private readonly _close = (blur: boolean) => {
if (blur) {
if (document.activeElement === this._docTitle?.inlineEditorContainer) {
this._docTitle?.inlineEditor?.setInlineRange(null);
this._docTitle?.inlineEditor?.eventSource?.blur();
} else if (document.activeElement === this.block?.rootComponent) {
this.std.selection.clear();
}
}
this._show$.value = false;
};
private readonly _show$ = signal(false); private readonly _show$ = signal(false);
private _initialInputMode: string = ''; private _initialInputMode: string = '';
@@ -61,26 +73,29 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
override connectedCallback(): void { override connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
this.disposables.add(
effect(() => {
this._show$.value = this.std.event.active$.value;
})
);
const rootComponent = this.block?.rootComponent; const rootComponent = this.block?.rootComponent;
if (rootComponent && this.keyboard.fallback) { if (rootComponent) {
this._initialInputMode = rootComponent.inputMode; this.disposables.addFromEvent(rootComponent, 'focus', () => {
this.disposables.add(() => { this._show$.value = true;
rootComponent.inputMode = this._initialInputMode;
}); });
this.disposables.add( this.disposables.addFromEvent(rootComponent, 'blur', () => {
effect(() => { this._show$.value = false;
// recover input mode when keyboard toolbar is hidden });
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode; 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;
}
})
);
}
} }
if (this._docTitle) { if (this._docTitle) {
@@ -114,6 +129,7 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
.keyboard=${this.keyboard} .keyboard=${this.keyboard}
.config=${this.config} .config=${this.config}
.rootComponent=${this.block.rootComponent} .rootComponent=${this.block.rootComponent}
.close=${this._close}
></affine-keyboard-toolbar>`} ></affine-keyboard-toolbar>`}
></blocksuite-portal>`; ></blocksuite-portal>`;
} }
@@ -17,7 +17,6 @@
{ "path": "../../blocks/paragraph" }, { "path": "../../blocks/paragraph" },
{ "path": "../../blocks/surface" }, { "path": "../../blocks/surface" },
{ "path": "../../blocks/surface-ref" }, { "path": "../../blocks/surface-ref" },
{ "path": "../../blocks/table" },
{ "path": "../../components" }, { "path": "../../components" },
{ "path": "../../ext-loader" }, { "path": "../../ext-loader" },
{ "path": "../../fragments/doc-title" }, { "path": "../../fragments/doc-title" },
@@ -113,9 +113,11 @@ export class LinkedDocPopover extends SignalWatcher(
} }
private get _flattenActionList() { private get _flattenActionList() {
return this._actionGroup.flatMap(group => return this._actionGroup
group.items.map(item => ({ ...item, groupName: group.name })) .map(group =>
); group.items.map(item => ({ ...item, groupName: group.name }))
)
.flat();
} }
private get _query() { private get _query() {
@@ -341,18 +343,7 @@ export class LinkedDocPopover extends SignalWatcher(
override willUpdate() { override willUpdate() {
if (!this.hasUpdated) { if (!this.hasUpdated) {
const updatePosition = throttle(() => { const updatePosition = throttle(() => {
this._position = getPopperPosition( this._position = getPopperPosition(this, this.context.startNativeRange);
{
getBoundingClientRect: () => {
return {
...this.getBoundingClientRect(),
// Workaround: the width of the popover is zero when it is not rendered
width: 280,
};
},
},
this.context.startNativeRange
);
}, 10); }, 10);
this.disposables.addFromEvent(window, 'resize', updatePosition); this.disposables.addFromEvent(window, 'resize', updatePosition);
@@ -65,98 +65,6 @@ export class Unzip {
this.unzipped = fflate.unzipSync(new Uint8Array(await blob.arrayBuffer())); this.unzipped = fflate.unzipSync(new Uint8Array(await blob.arrayBuffer()));
} }
private fixFileNameEncoding(fileName: string): string {
try {
// check if contains non-ASCII characters
if (fileName.split('').some(char => char.charCodeAt(0) > 127)) {
// try different encodings
const fixedName = this.tryDifferentEncodings(fileName);
if (fixedName && fixedName !== fileName) {
return fixedName;
}
}
return fileName;
} catch {
return fileName;
}
}
// try different encodings
private tryDifferentEncodings(fileName: string): string | null {
try {
// convert string to bytes
const bytes = new Uint8Array(fileName.length);
for (let i = 0; i < fileName.length; i++) {
bytes[i] = fileName.charCodeAt(i);
}
// try different encodings
// The macOS system zip tool creates archives with UTF-8 encoded filenames.
// However, this implementation doesn't strictly adhere to the ZIP specification.
// Simply forcing UTF-8 encoding when unzipping should resolve filename corruption issues.
const encodings = ['utf-8'];
for (const encoding of encodings) {
try {
const decoder = new TextDecoder(encoding);
const result = decoder.decode(bytes);
// check if decoded result is valid
if (result && this.isValidDecodedString(result)) {
return result;
}
} catch {
// ignore encoding error, try next encoding
}
}
} catch {
// ignore conversion error
}
return null;
}
// check if decoded string is valid
private isValidDecodedString(str: string): boolean {
// check if contains control characters
const controlCharCodes = new Set([
0x00,
0x01,
0x02,
0x03,
0x04,
0x05,
0x06,
0x07,
0x08, // \x00-\x08
0x0b,
0x0c, // \x0B, \x0C
0x0e,
0x0f,
0x10,
0x11,
0x12,
0x13,
0x14,
0x15,
0x16,
0x17,
0x18,
0x19,
0x1a,
0x1b,
0x1c,
0x1d,
0x1e,
0x1f, // \x0E-\x1F
0x7f, // \x7F
]);
return !str
.split('')
.some(char => controlCharCodes.has(char.charCodeAt(0)));
}
*[Symbol.iterator]() { *[Symbol.iterator]() {
const keys = Object.keys(this.unzipped ?? {}); const keys = Object.keys(this.unzipped ?? {});
let index = 0; let index = 0;
@@ -173,10 +81,7 @@ export class Unzip {
const content = new File([this.unzipped![path]], fileName, { const content = new File([this.unzipped![path]], fileName, {
type: mime ?? '', type: mime ?? '',
}) as Blob; }) as Blob;
yield { path, content, index };
const fixedPath = this.fixFileNameEncoding(path);
yield { path: fixedPath, content, index };
index++; index++;
} }
} }
@@ -142,13 +142,15 @@ export class SlashMenu extends WithDisposable(LitElement) {
// We search first and second layer // We search first and second layer
if (this._filteredItems.length !== 0 && depth >= 1) break; if (this._filteredItems.length !== 0 && depth >= 1) break;
queue = queue.flatMap(item => { queue = queue
if (isSubMenuItem(item)) { .map<typeof queue>(item => {
return item.subMenu; if (isSubMenuItem(item)) {
} else { return item.subMenu;
return []; } else {
} return [];
}); }
})
.flat();
depth++; depth++;
} }
@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
return; return;
} }
const elementIds = selections.flatMap(s => const elementIds = selections
s.editing || s.inoperable ? [] : s.elements .map(s => (s.editing || s.inoperable ? [] : s.elements))
); .flat();
const count = elementIds.length; const count = elementIds.length;
const activated = context.activated && Boolean(count); const activated = context.activated && Boolean(count);
@@ -229,7 +229,8 @@ export function renderToolbar(
? module.config.when(context) ? module.config.when(context)
: (module.config.when ?? true) : (module.config.when ?? true)
) )
.flatMap(module => module.config.actions); .map<ToolbarActions>(module => module.config.actions)
.flat();
const combined = combine(actions, context); const combined = combine(actions, context);
@@ -91,11 +91,15 @@ export class KeyboardControl {
const disposables = new DisposableGroup(); const disposables = new DisposableGroup();
if (IS_ANDROID) { if (IS_ANDROID) {
disposables.add( disposables.add(
this._dispatcher.add('beforeInput', ctx => { this._dispatcher.add(
if (this.composition) return false; 'beforeInput',
const binding = androidBindKeymapPatch(keymap); ctx => {
return binding(ctx); if (this.composition) return false;
}) const binding = androidBindKeymapPatch(keymap);
return binding(ctx);
},
options
)
); );
} }
@@ -226,18 +226,6 @@ export class UIEventDispatcher extends LifeCycleWatcher {
this._setActive(false); this._setActive(false);
} }
}); });
// When the selection is outside the host, the event dispatcher should be inactive
this.disposables.addFromEvent(document, 'selectionchange', () => {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return;
const { anchorNode, focusNode } = sel;
if (
(anchorNode && !this.host.contains(anchorNode)) ||
(focusNode && !this.host.contains(focusNode))
) {
this._setActive(false);
}
});
} }
private _buildEventScopeBySelection(name: EventName) { private _buildEventScopeBySelection(name: EventName) {
+1 -1
View File
@@ -104,7 +104,7 @@ export function bindKeymap(
}; };
} }
// In some IME of Android like, the keypress event dose not contain // In Android, the keypress event dose not contain
// the information about what key is pressed. See // the information about what key is pressed. See
// https://stackoverflow.com/a/68188679 // https://stackoverflow.com/a/68188679
// https://stackoverflow.com/a/66724830 // https://stackoverflow.com/a/66724830
+3 -2
View File
@@ -57,7 +57,7 @@ export type CanvasLayer = BaseLayer<GfxPrimitiveElementModel> & {
type: 'canvas'; type: 'canvas';
/** /**
* The z-index of the first element in this canvas layer. * The z-index of canvas layer.
* *
* A canvas layer renders all the elements in a single canvas, * A canvas layer renders all the elements in a single canvas,
* this property is used to render the canvas with correct z-index. * this property is used to render the canvas with correct z-index.
@@ -165,7 +165,8 @@ export class LayerManager extends GfxExtension {
]; ];
curLayer.zIndex = currentCSSZindex; curLayer.zIndex = currentCSSZindex;
layers.push(curLayer as LayerManager['layers'][number]); layers.push(curLayer as LayerManager['layers'][number]);
currentCSSZindex += curLayer.elements.length; currentCSSZindex +=
curLayer.type === 'block' ? curLayer.elements.length : 1;
} }
}; };
const addLayer = (type: 'canvas' | 'block') => { const addLayer = (type: 'canvas' | 'block') => {
@@ -1,4 +1,3 @@
import { IS_ANDROID } from '@blocksuite/global/env';
import type { BaseTextAttributes } from '@blocksuite/store'; import type { BaseTextAttributes } from '@blocksuite/store';
import type { InlineEditor } from '../inline-editor.js'; import type { InlineEditor } from '../inline-editor.js';
@@ -42,10 +41,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
} }
}; };
private readonly _onBeforeInput = async (event: InputEvent) => { private readonly _onBeforeInput = (event: InputEvent) => {
const range = this.editor.rangeService.getNativeRange(); const range = this.editor.rangeService.getNativeRange();
if ( if (
this.editor.isReadonly || this.editor.isReadonly ||
this._isComposing ||
!range || !range ||
!this._isRangeCompletelyInRoot(range) !this._isRangeCompletelyInRoot(range)
) )
@@ -54,29 +54,33 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
let inlineRange = this.editor.toInlineRange(range); let inlineRange = this.editor.toInlineRange(range);
if (!inlineRange) return; if (!inlineRange) return;
if (this._isComposing) {
if (IS_ANDROID && event.inputType === 'insertCompositionText') {
this._compositionInlineRange = inlineRange;
}
return;
}
let ifHandleTargetRange = true; let ifHandleTargetRange = true;
if ( if (event.inputType.startsWith('delete')) {
event.inputType.startsWith('delete') && if (
(isInEmbedGap(range.commonAncestorContainer) || isInEmbedGap(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
) {
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
} else if (
isInEmptyLine(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
// do not use target range when deleting across lines
// https://github.com/toeverything/blocksuite/issues/5381 // https://github.com/toeverything/blocksuite/issues/5381
isInEmptyLine(range.commonAncestorContainer)) && inlineRange = {
inlineRange.length === 0 && index: inlineRange.index - 1,
inlineRange.index > 0 length: 1,
) { };
// do not use target range when deleting across lines ifHandleTargetRange = false;
inlineRange = { }
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
} }
if (ifHandleTargetRange) { if (ifHandleTargetRange) {
@@ -93,24 +97,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
} }
} }
} }
if (!inlineRange) return; if (!inlineRange) return;
event.preventDefault(); event.preventDefault();
if (IS_ANDROID) {
this.editor.rerenderWholeEditor();
await this.editor.waitForUpdate();
if (
event.inputType === 'deleteContentBackward' &&
!(inlineRange.index === 0 && inlineRange.length === 0)
) {
// when press backspace at offset 1, double characters will be removed.
// because we mock backspace key event `androidBindKeymapPatch` in blocksuite/framework/std/src/event/keymap.ts
// so we need to stop the event propagation to prevent the double characters removal.
event.stopPropagation();
}
}
const ctx: BeforeinputHookCtx<TextAttributes> = { const ctx: BeforeinputHookCtx<TextAttributes> = {
inlineEditor: this.editor, inlineEditor: this.editor,
raw: event, raw: event,
@@ -355,9 +346,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
return; return;
} }
this.editor.disposables.addFromEvent(eventSource, 'beforeinput', e => { this.editor.disposables.addFromEvent(
this._onBeforeInput(e).catch(console.error); eventSource,
}); 'beforeinput',
this._onBeforeInput
);
this.editor.disposables.addFromEvent( this.editor.disposables.addFromEvent(
eventSource, eventSource,
'compositionstart', 'compositionstart',
@@ -30,9 +30,9 @@ function inlineTextStyles(
} }
return styleMap({ return styleMap({
'font-weight': props.bold ? 'bold' : 'inherit', 'font-weight': props.bold ? 'bold' : 'normal',
'font-style': props.italic ? 'italic' : 'inherit', 'font-style': props.italic ? 'italic' : 'normal',
'text-decoration': textDecorations.length > 0 ? textDecorations : 'inherit', 'text-decoration': textDecorations.length > 0 ? textDecorations : 'none',
...inlineCodeStyle, ...inlineCodeStyle,
}); });
} }

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