mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
Compare commits
103 Commits
v0.22.0-be
...
v0.22.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e98f035f97 | ||
|
|
1d4bc81e90 | ||
|
|
deeea3428e | ||
|
|
8b0dd3c067 | ||
|
|
8ca17864f1 | ||
|
|
d2664480f7 | ||
|
|
b986a39da3 | ||
|
|
097a63362c | ||
|
|
7284320355 | ||
|
|
b4401a8abf | ||
|
|
0351fbcb86 | ||
|
|
6eed9c686b | ||
|
|
8d2214424c | ||
|
|
d12954f8c3 | ||
|
|
83733cd828 | ||
|
|
ed56f076ed | ||
|
|
2d17c265ca | ||
|
|
2a9f7e1835 | ||
|
|
a71904e641 | ||
|
|
814364489f | ||
|
|
24448659a4 | ||
|
|
c846c57a12 | ||
|
|
e82c9d2ddc | ||
|
|
3c29f62224 | ||
|
|
b5ef361f87 | ||
|
|
4fa85416ae | ||
|
|
f69a98eb8c | ||
|
|
115496aa8e | ||
|
|
7aafbf12a5 | ||
|
|
0f9b7d4a0d | ||
|
|
2817b5aec4 | ||
|
|
72e66aca11 | ||
|
|
7d1f2adb7f | ||
|
|
512a908fd4 | ||
|
|
71be1d424a | ||
|
|
d6a26b8093 | ||
|
|
5e05952f6e | ||
|
|
c1930c5937 | ||
|
|
b7ebd33389 | ||
|
|
de9a3e1428 | ||
|
|
374eee9196 | ||
|
|
1bdccdbd57 | ||
|
|
053efb61f0 | ||
|
|
c7aebd0412 | ||
|
|
01aa6979eb | ||
|
|
c32f7c7964 | ||
|
|
d219c92e98 | ||
|
|
063072457c | ||
|
|
13fa4f922a | ||
|
|
f54bc0c047 | ||
|
|
1f0cc51462 | ||
|
|
160e4c2a38 | ||
|
|
99198e246b | ||
|
|
44e1eb503f | ||
|
|
2288cbe54d | ||
|
|
23ff398994 | ||
|
|
ee931d546e | ||
|
|
a02eed382d | ||
|
|
ab78b8e3ab | ||
|
|
3fe2ac4e46 | ||
|
|
d02aa8c7e0 | ||
|
|
cce756365a | ||
|
|
a88dcc0951 | ||
|
|
57208a3de4 | ||
|
|
d8cbeb1bb1 | ||
|
|
418b38e8de | ||
|
|
00ff373c01 | ||
|
|
39830a410a | ||
|
|
ef3be4a816 | ||
|
|
658393159b | ||
|
|
ac3f247f01 | ||
|
|
065d9c3b73 | ||
|
|
2e58c11799 | ||
|
|
10da3ad28e | ||
|
|
887a496f8b | ||
|
|
ada69c80f6 | ||
|
|
7b82dd656b | ||
|
|
5c96566dd8 | ||
|
|
a35e1b1882 | ||
|
|
756847d3cb | ||
|
|
3c3a8bb107 | ||
|
|
88eec2cdfb | ||
|
|
52777b0064 | ||
|
|
00ccd2d865 | ||
|
|
5d94bd41a4 | ||
|
|
20d8d6131a | ||
|
|
94539ac0d0 | ||
|
|
e1ce42a6fc | ||
|
|
2a7f0162cf | ||
|
|
34a5d9dec3 | ||
|
|
c68598c0e0 | ||
|
|
9c81c24fbe | ||
|
|
517aec79ba | ||
|
|
31a1841e25 | ||
|
|
625e8392a6 | ||
|
|
f616bd29d3 | ||
|
|
d6b9e9c60a | ||
|
|
bc67766bb9 | ||
|
|
9a96cfded0 | ||
|
|
77392efaa2 | ||
|
|
927b4f4430 | ||
|
|
9ec1d08d98 | ||
|
|
86cd92a878 |
@@ -886,8 +886,8 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable indexer plugin\n@default true\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": true
|
||||
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": false
|
||||
},
|
||||
"provider.type": {
|
||||
"type": "string",
|
||||
|
||||
@@ -95,11 +95,13 @@ spec:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /info
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
|
||||
1
.github/helm/affine/charts/doc/values.yaml
vendored
1
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -36,6 +36,7 @@ resources:
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
timeoutSeconds: 5
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
|
||||
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -113,6 +113,7 @@ jobs:
|
||||
build-server-native:
|
||||
name: Build Server native - ${{ matrix.targets.name }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
7
.github/workflows/build-test.yml
vendored
7
.github/workflows/build-test.yml
vendored
@@ -20,6 +20,7 @@ env:
|
||||
COVERAGE: true
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
DEPLOYMENT_TYPE: affine
|
||||
AFFINE_INDEXER_ENABLED: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -151,7 +152,8 @@ jobs:
|
||||
- name: Clippy
|
||||
run: |
|
||||
rustup component add clippy
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
|
||||
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
|
||||
|
||||
check-git-status:
|
||||
name: Check Git Status
|
||||
@@ -825,7 +827,6 @@ jobs:
|
||||
- optimize_ci
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -923,7 +924,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release --no-fail-fast
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
|
||||
25
.github/workflows/release-mobile.yml
vendored
25
.github/workflows/release-mobile.yml
vendored
@@ -117,31 +117,10 @@ jobs:
|
||||
name: android
|
||||
path: packages/frontend/apps/android/dist
|
||||
|
||||
determine-ios-runner:
|
||||
runs-on: ubuntu-latest
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
|
||||
needs:
|
||||
- build-ios-web
|
||||
outputs:
|
||||
RUNNER: ${{ steps.runner.outputs.RUNNER }}
|
||||
steps:
|
||||
- name: Determine Runner
|
||||
id: runner
|
||||
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
|
||||
# blaze/macos-14 is free but has limited concurrency
|
||||
run: |
|
||||
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
|
||||
if [ $RANDOM_NUMBER -le 20 ]; then
|
||||
echo "Selected namespace-profile-macos (20% probability)"
|
||||
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Selected blaze/macos-14 (80% probability)"
|
||||
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
|
||||
needs:
|
||||
- determine-ios-runner
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download mobile artifact
|
||||
|
||||
611
Cargo.lock
generated
611
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -47,9 +47,9 @@ log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.28" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
@@ -77,12 +77,12 @@ smol_str = "0.3"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
strum_macros = "0.27.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.25"
|
||||
text-splitter = "0.27"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.23" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
|
||||
@@ -17,11 +17,15 @@ import {
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
formatSize,
|
||||
openSingleFileWith,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
AttachmentIcon,
|
||||
ResetIcon,
|
||||
@@ -30,17 +34,18 @@ import {
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { nanoid, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { batch, computed, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
import { downloadAttachmentBlob, getFileType, refreshData } from './utils';
|
||||
|
||||
type AttachmentResolvedStateInfo = ResolvedStateInfo & {
|
||||
kind?: TemplateResult;
|
||||
@@ -79,8 +84,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
return this.std.get(FileSizeLimitProvider).maxFileSize;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return !!this.model.props.footnoteIdentifier;
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
convertTo = () => {
|
||||
@@ -123,12 +132,50 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
// Refreshes the embed component.
|
||||
reload = () => {
|
||||
if (this.model.props.embed) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
batch(() => {
|
||||
if (this.model.props.embed$.value) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
this.refreshData();
|
||||
});
|
||||
};
|
||||
|
||||
// Replaces the current attachment.
|
||||
replace = async () => {
|
||||
const state = this.resourceController.state$.peek();
|
||||
if (state.uploading) return;
|
||||
|
||||
const file = await openSingleFileWith();
|
||||
if (!file) return;
|
||||
|
||||
const sourceId = await this.std.store.blobSync.set(file);
|
||||
const type = await getFileType(file);
|
||||
const { name, size } = file;
|
||||
|
||||
let embed = this.model.props.embed$.value ?? false;
|
||||
|
||||
this.std.store.captureSync();
|
||||
this.std.store.transact(() => {
|
||||
this.std.store.updateBlock(this.blockId, {
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
sourceId,
|
||||
embed: false,
|
||||
});
|
||||
|
||||
const provider = this.std.get(AttachmentEmbedProvider);
|
||||
embed &&= provider.embedded(this.model);
|
||||
|
||||
if (embed) {
|
||||
provider.convertTo(this.model);
|
||||
}
|
||||
|
||||
// Reloads
|
||||
this.reload();
|
||||
});
|
||||
};
|
||||
|
||||
private _selectBlock() {
|
||||
@@ -139,6 +186,34 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -162,6 +237,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
@@ -367,7 +444,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
protected renderEmbedView = () => {
|
||||
const { model, blobUrl } = this;
|
||||
if (!model.props.embed || !blobUrl) return null;
|
||||
if (!model.props.embed$.value || !blobUrl) return null;
|
||||
|
||||
const { std, _maxFileSize } = this;
|
||||
const provider = std.get(AttachmentEmbedProvider);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
|
||||
import { CitationProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { EditorHost } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
@@ -33,6 +34,7 @@ export const RenameModal = ({
|
||||
|
||||
let fileName = includeExtension ? nameWithoutExtension : originalName;
|
||||
const extension = includeExtension ? originalExtension : '';
|
||||
const citationService = editorHost.std.get(CitationProvider);
|
||||
|
||||
const abort = () => abortController.abort();
|
||||
const onConfirm = () => {
|
||||
@@ -44,6 +46,9 @@ export const RenameModal = ({
|
||||
model.store.updateBlock(model, {
|
||||
name: newFileName,
|
||||
});
|
||||
if (citationService.isCitationModel(model)) {
|
||||
citationService.trackEvent('Edit');
|
||||
}
|
||||
abort();
|
||||
};
|
||||
const onInput = (e: InputEvent) => {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
EditIcon,
|
||||
ReplaceIcon,
|
||||
ResetIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
@@ -139,27 +140,42 @@ export const attachmentViewDropdownMenu = {
|
||||
});
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
)}`;
|
||||
return html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`;
|
||||
},
|
||||
} as const satisfies ToolbarActionGroup<ToolbarAction>;
|
||||
|
||||
const replaceAction = {
|
||||
id: 'c.replace',
|
||||
tooltip: 'Replace attachment',
|
||||
icon: ReplaceIcon(),
|
||||
disabled(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return true;
|
||||
|
||||
const { downloading = false, uploading = false } =
|
||||
block.resourceController.state$.value;
|
||||
return downloading || uploading;
|
||||
},
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.replace().catch(console.error);
|
||||
},
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const downloadAction = {
|
||||
id: 'c.download',
|
||||
id: 'd.download',
|
||||
tooltip: 'Download',
|
||||
icon: DownloadIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.download();
|
||||
},
|
||||
when: ctx => {
|
||||
when(ctx) {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return false;
|
||||
// Current citation attachment block does not support download
|
||||
@@ -168,7 +184,7 @@ const downloadAction = {
|
||||
} as const satisfies ToolbarAction;
|
||||
|
||||
const captionAction = {
|
||||
id: 'd.caption',
|
||||
id: 'e.caption',
|
||||
tooltip: 'Caption',
|
||||
icon: CaptionIcon(),
|
||||
run(ctx) {
|
||||
@@ -221,6 +237,7 @@ const builtinToolbarConfig = {
|
||||
},
|
||||
},
|
||||
attachmentViewDropdownMenu,
|
||||
replaceAction,
|
||||
downloadAction,
|
||||
captionAction,
|
||||
{
|
||||
@@ -354,13 +371,17 @@ const builtinSurfaceToolbarConfig = {
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
...replaceAction,
|
||||
id: 'd.replace',
|
||||
},
|
||||
{
|
||||
...downloadAction,
|
||||
id: 'd.download',
|
||||
id: 'e.download',
|
||||
},
|
||||
{
|
||||
...captionAction,
|
||||
id: 'e.caption',
|
||||
id: 'f.caption',
|
||||
},
|
||||
],
|
||||
when: ctx => ctx.getSurfaceModelsByType(AttachmentBlockModel).length === 1,
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
LinkPreviewServiceIdentifier,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
@@ -18,6 +19,7 @@ import { html } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { refreshBookmarkUrlData } from './utils.js';
|
||||
|
||||
@@ -114,11 +116,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
);
|
||||
};
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
get imageProxyService() {
|
||||
@@ -166,6 +169,31 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
></bookmark-card>`;
|
||||
};
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -203,6 +231,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
|
||||
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
@@ -482,24 +482,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'b.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => BookmarkStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -40,6 +40,16 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
|
||||
private _inlineRangeProvider: InlineRangeProvider | null = null;
|
||||
|
||||
private readonly _localPreview$ = signal<boolean | null>(null);
|
||||
|
||||
preview$: Signal<boolean> = computed(() => {
|
||||
const modelPreview = !!this.model.props.preview$.value;
|
||||
if (this.store.readonly) {
|
||||
return this._localPreview$.value ?? modelPreview;
|
||||
}
|
||||
return modelPreview;
|
||||
});
|
||||
|
||||
highlightTokens$: Signal<ThemedToken[][]> = signal([]);
|
||||
|
||||
languageName$: Signal<string> = computed(() => {
|
||||
@@ -393,7 +403,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
true) &&
|
||||
(this.model.props.lineNumber ?? true);
|
||||
|
||||
const preview = !!this.model.props.preview;
|
||||
const preview = this.preview$.value;
|
||||
const previewContext = this.std.getOptional(
|
||||
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
|
||||
);
|
||||
@@ -461,6 +471,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
override accessor useCaptionEditor = true;
|
||||
|
||||
override accessor useZeroWidth = true;
|
||||
|
||||
setPreviewState(preview: boolean) {
|
||||
if (this.store.readonly) {
|
||||
this._localPreview$.value = preview;
|
||||
} else {
|
||||
this.store.updateBlock(this.model, { preview });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -58,11 +58,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
`;
|
||||
|
||||
private readonly _toggle = (value: boolean) => {
|
||||
if (this.blockComponent.store.readonly) return;
|
||||
|
||||
this.blockComponent.store.updateBlock(this.blockComponent.model, {
|
||||
preview: value,
|
||||
});
|
||||
this.blockComponent.setPreviewState(value);
|
||||
|
||||
const std = this.blockComponent.std;
|
||||
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
@@ -77,7 +73,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
};
|
||||
|
||||
get preview() {
|
||||
return !!this.blockComponent.model.props.preview$.value;
|
||||
return this.blockComponent.preview$.value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
61
blocksuite/affine/blocks/code/src/markdown.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
type CodeBlockModel,
|
||||
CodeBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const CodeBlockMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code-block',
|
||||
pattern: /^```([a-zA-Z0-9]*)\s$/,
|
||||
action: ({ inlineEditor, inlineRange, prefixText, pattern }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const language = match[1];
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type === 'quote'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
const codeId = store.addBlock<CodeBlockModel>(
|
||||
CodeBlockSchema.model.flavour,
|
||||
{ language },
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
if (model.text && model.text.length > prefixText.length) {
|
||||
const text = model.text.clone();
|
||||
store.addBlock('affine:paragraph', { text }, parent, index + 1);
|
||||
text.delete(0, prefixText.length);
|
||||
}
|
||||
store.deleteBlock(model, { bringChildrenTo: parent });
|
||||
|
||||
focusTextModel(std, codeId);
|
||||
},
|
||||
});
|
||||
@@ -33,6 +33,10 @@ export const codeBlockStyles = css`
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.affine-code-block-container.disable-line-numbers v-line {
|
||||
grid-template-columns: unset;
|
||||
}
|
||||
|
||||
.affine-code-block-container div:has(> v-line) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { CodeKeymapExtension } from './code-keymap.js';
|
||||
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
|
||||
import { codeSlashMenuConfig } from './configs/slash-menu.js';
|
||||
import { effects } from './effects.js';
|
||||
import { CodeBlockMarkdownExtension } from './markdown.js';
|
||||
|
||||
const codeToolbarWidget = WidgetViewExtension(
|
||||
'affine:code',
|
||||
@@ -44,6 +45,7 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:code', literal`affine-code`),
|
||||
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
|
||||
CodeKeymapExtension,
|
||||
CodeBlockMarkdownExtension,
|
||||
...getCodeClipboardExtensions(),
|
||||
]);
|
||||
context.register([
|
||||
|
||||
@@ -331,7 +331,6 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
|
||||
this.inlineEditor$.value?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
|
||||
@@ -209,10 +209,19 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
this.richText.value?.updateComplete
|
||||
@@ -233,6 +242,12 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
||||
'paste',
|
||||
this._onPaste
|
||||
);
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
|
||||
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
63
blocksuite/affine/blocks/divider/src/markdown.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
type DividerBlockModel,
|
||||
DividerBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const DividerMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'divider',
|
||||
pattern: /^(-{3,}|\*{3,}|_{3,})\s$/,
|
||||
action: ({ inlineEditor, inlineRange }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== 'quote'
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.addBlock<DividerBlockModel>(
|
||||
DividerBlockSchema.model.flavour,
|
||||
{
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
const nextBlock = parent.children.at(index + 1);
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{},
|
||||
parent
|
||||
);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { DividerMarkdownExtension } from './markdown';
|
||||
|
||||
export class DividerViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-divider-block';
|
||||
@@ -19,6 +20,7 @@ export class DividerViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register([
|
||||
BlockViewExtension('affine:divider', literal`affine-divider`),
|
||||
DividerMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ActionPlacement,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingProvider,
|
||||
FeatureFlagService,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
@@ -216,12 +215,7 @@ const conversionsActionGroup = {
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedLinkedDocBlockComponent);
|
||||
|
||||
if (
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_embed_doc_with_alias') &&
|
||||
isGfxBlockComponent(block)
|
||||
) {
|
||||
if (isGfxBlockComponent(block)) {
|
||||
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
|
||||
editorSetting?.set?.(
|
||||
'docCanvasPreferView',
|
||||
@@ -265,18 +259,18 @@ const builtinToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
@@ -374,26 +368,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
REFERENCE_NODE,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocDisplayMetaProvider,
|
||||
DocModeProvider,
|
||||
OpenDocExtensionIdentifier,
|
||||
@@ -43,6 +44,7 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { renderLinkedDocInCard } from '../common/render-linked-doc';
|
||||
@@ -254,11 +256,12 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
return this.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
private readonly _handleDoubleClick = (event: MouseEvent) => {
|
||||
@@ -454,6 +457,31 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -532,6 +560,8 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
getInitialState(): {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
ActionPlacement,
|
||||
EditorSettingProvider,
|
||||
FeatureFlagService,
|
||||
type LinkEventType,
|
||||
type OpenDocMode,
|
||||
type ToolbarAction,
|
||||
@@ -163,12 +162,7 @@ const conversionsActionGroup = {
|
||||
label: 'Card view',
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(EmbedSyncedDocBlockComponent);
|
||||
if (
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_embed_doc_with_alias') &&
|
||||
isGfxBlockComponent(block)
|
||||
) {
|
||||
if (isGfxBlockComponent(block)) {
|
||||
const editorSetting = ctx.std.getOptional(EditorSettingProvider);
|
||||
editorSetting?.set?.(
|
||||
'docCanvasPreferView',
|
||||
@@ -296,8 +290,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
label: 'Insert to page',
|
||||
tooltip: 'Insert to page',
|
||||
icon: InsertIntoPageIcon(),
|
||||
when: ({ std }) =>
|
||||
std.get(FeatureFlagService).getFlag('enable_embed_doc_with_alias'),
|
||||
run: ctx => {
|
||||
const model = ctx.getCurrentModelByType(EmbedSyncedDocModel);
|
||||
if (!model) return;
|
||||
@@ -334,8 +326,6 @@ const builtinSurfaceToolbarConfig = {
|
||||
tooltip:
|
||||
'Duplicate as note to create an editable copy, the original remains unchanged.',
|
||||
icon: DuplicateIcon(),
|
||||
when: ({ std }) =>
|
||||
std.get(FeatureFlagService).getFlag('enable_embed_doc_with_alias'),
|
||||
run: ctx => {
|
||||
const { gfx } = ctx;
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (options?.viewType === 'card') {
|
||||
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style =
|
||||
@@ -441,7 +441,11 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
let { style } = model.props;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (!BookmarkStyles.includes(style)) {
|
||||
if (
|
||||
!BookmarkStyles.includes(
|
||||
style as (typeof BookmarkStyles)[number]
|
||||
)
|
||||
) {
|
||||
style = BookmarkStyles[0];
|
||||
}
|
||||
|
||||
@@ -517,26 +521,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedGithubStyles.includes(action.id)),
|
||||
when(ctx) {
|
||||
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
|
||||
},
|
||||
|
||||
@@ -107,10 +107,10 @@ export class EmbedHtmlFullscreenToolbar extends LitElement {
|
||||
if (this._copied) return;
|
||||
|
||||
this.embedHtml.std.clipboard
|
||||
.writeToClipboard(items => {
|
||||
items['text/plain'] = this.embedHtml.model.props.html ?? '';
|
||||
return items;
|
||||
})
|
||||
.writeToClipboard(items => ({
|
||||
...items,
|
||||
'text/plain': this.embedHtml.model.props.html ?? '',
|
||||
}))
|
||||
.then(() => {
|
||||
this._copied = true;
|
||||
setTimeout(() => (this._copied = false), 1500);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
@@ -87,6 +88,12 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
this.gfx.tool.currentToolName$.value === 'frameNavigator';
|
||||
const frameIndex = this.gfx.layer.getZIndex(model);
|
||||
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="affine-frame-container"
|
||||
@@ -102,6 +109,7 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
|
||||
})}
|
||||
></div>
|
||||
${widgets}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -178,11 +186,22 @@ export const FrameBlockInteraction =
|
||||
selectable(context) {
|
||||
const { model } = context;
|
||||
|
||||
const onTitle =
|
||||
model.externalBound?.containsPoint([
|
||||
context.position.x,
|
||||
context.position.y,
|
||||
]) ?? false;
|
||||
|
||||
return (
|
||||
context.default(context) &&
|
||||
(model.isLocked() || !isTransparent(model.props.background))
|
||||
(model.isLocked() ||
|
||||
!isTransparent(model.props.background) ||
|
||||
onTitle)
|
||||
);
|
||||
},
|
||||
onSelect(context) {
|
||||
return context.default(context);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,20 +241,35 @@ export class EdgelessFrameManager extends GfxExtension {
|
||||
surfaceModel.elementAdded.subscribe(({ id, local }) => {
|
||||
const element = surfaceModel.getElementById(id);
|
||||
if (element && local) {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// new element may intended to be added to other group
|
||||
// so we need to wait for the next microtask to check if the element can be added to the frame
|
||||
// The entire frame detection logic must be in microtask for timing reasons:
|
||||
//
|
||||
// 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0]
|
||||
// because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher.
|
||||
// We need to wait for that calculation to complete before frame detection.
|
||||
//
|
||||
// 2. For shapes: Although they have valid bounds immediately, processing them in microtask
|
||||
// ensures consistent timing and allows other initialization to complete first.
|
||||
//
|
||||
// 3. Group compatibility: Some elements may need to establish their group relationships
|
||||
// before being considered for frame membership.
|
||||
//
|
||||
// By embedding the entire logic in microtask, we ensure:
|
||||
// - Connectors have proper bounds calculated (not [0,0,0,0])
|
||||
// - getFrameFromPoint() works correctly with valid element centers
|
||||
// - All element initialization is complete before frame detection
|
||||
queueMicrotask(() => {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add elements that aren't already grouped and have a valid frame
|
||||
if (!element.group && frame) {
|
||||
this.addElementsToFrame(frame, [element]);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NativeClipboardProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
formatSize,
|
||||
getBlockProps,
|
||||
isInsidePageEditor,
|
||||
@@ -111,28 +112,6 @@ export async function resetImageSize(
|
||||
block.store.updateBlock(model, props);
|
||||
}
|
||||
|
||||
function convertToPng(blob: Blob): Promise<Blob | null> {
|
||||
return new Promise(resolve => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', _ => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const c = document.createElement('canvas');
|
||||
c.width = img.width;
|
||||
c.height = img.height;
|
||||
const ctx = c.getContext('2d');
|
||||
if (!ctx) return;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
c.toBlob(resolve, 'image/png');
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = reader.result as string;
|
||||
});
|
||||
reader.addEventListener('error', () => resolve(null));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyImageBlob(
|
||||
block: ImageBlockComponent | ImageEdgelessBlockComponent
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { textKeymap } from '@blocksuite/affine-inline-preset';
|
||||
import { ListBlockSchema } from '@blocksuite/affine-model';
|
||||
import { markdownInput } from '@blocksuite/affine-rich-text';
|
||||
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { KeymapExtension, TextSelection } from '@blocksuite/std';
|
||||
@@ -125,20 +124,6 @@ export const ListKeymapExtension = KeymapExtension(
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
91
blocksuite/affine/blocks/list/src/markdown.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
type ListBlockModel,
|
||||
ListBlockSchema,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ListMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'list',
|
||||
// group 2: number
|
||||
// group 3: bullet
|
||||
// group 4: bullet
|
||||
// group 5: todo
|
||||
// group 6: todo checked
|
||||
pattern: /^((\d+\.)|(-)|(\*)|(\[ ?\])|(\[x\]))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
let type: ListType;
|
||||
|
||||
if (match[2]) {
|
||||
type = 'numbered';
|
||||
} else if (match[3] || match[4]) {
|
||||
type = 'bulleted';
|
||||
} else if (match[5] || match[6]) {
|
||||
type = 'todo';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = match[6] !== undefined;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (!matchModels(model, [ParagraphBlockModel])) return;
|
||||
|
||||
if (type !== 'numbered') {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
const id = store.addBlock<ListBlockModel>(
|
||||
ListBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...(type === 'todo' ? { checked } : {}),
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
focusTextModel(std, id);
|
||||
} else {
|
||||
let order = parseInt(match[2]);
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects.js';
|
||||
import { ListKeymapExtension, ListTextKeymapExtension } from './list-keymap.js';
|
||||
import { ListMarkdownExtension } from './markdown.js';
|
||||
|
||||
export class ListViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-list-block';
|
||||
@@ -23,6 +24,7 @@ export class ListViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:list', literal`affine-list`),
|
||||
ListKeymapExtension,
|
||||
ListTextKeymapExtension,
|
||||
ListMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
74
blocksuite/affine/blocks/paragraph/src/markdown.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
ParagraphBlockSchema,
|
||||
type ParagraphType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const ParagraphMarkdownExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'heading',
|
||||
pattern: /^((#{1,6})|(>))\s$/,
|
||||
action: ({ inlineEditor, pattern, inlineRange, prefixText }) => {
|
||||
if (inlineEditor.yTextString.slice(0, inlineRange.index).includes('\n')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const type = (
|
||||
match[2] ? `h${match[2].length}` : 'quote'
|
||||
) as ParagraphType;
|
||||
|
||||
if (!inlineEditor.rootElement) return;
|
||||
const blockComponent =
|
||||
inlineEditor.rootElement.closest<BlockComponent>('[data-block-id]');
|
||||
if (!blockComponent) return;
|
||||
|
||||
const { model, std, store } = blockComponent;
|
||||
if (
|
||||
!matchModels(model, [ParagraphBlockModel]) &&
|
||||
matchModels(model, [ListBlockModel])
|
||||
) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.deleteBlock(model, { deleteChildren: false });
|
||||
const id = store.addBlock<ParagraphBlockModel>(
|
||||
ParagraphBlockSchema.model.flavour,
|
||||
{
|
||||
type: type,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
},
|
||||
parent,
|
||||
index
|
||||
);
|
||||
|
||||
focusTextModel(std, id);
|
||||
} else if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type !== type
|
||||
) {
|
||||
store.captureSync();
|
||||
inlineEditor.deleteText({
|
||||
index: 0,
|
||||
length: inlineRange.index,
|
||||
});
|
||||
store.updateBlock(model, { type });
|
||||
focusTextModel(std, model.id);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
|
||||
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
@@ -63,6 +66,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
?.getPlaceholder(this.model);
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager.getRenderer();
|
||||
}
|
||||
@@ -94,6 +101,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
return this.std.get(DefaultInlineManagerExtension.identifier);
|
||||
}
|
||||
|
||||
get hasCitationSiblings() {
|
||||
return this.collapsedSiblings.some(sibling =>
|
||||
this.citationService.isCitationModel(sibling)
|
||||
);
|
||||
}
|
||||
|
||||
override get topContenteditableElement() {
|
||||
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
||||
return this.closest<BlockComponent>(
|
||||
@@ -286,6 +299,13 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
collapsed: value,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasCitationSiblings) {
|
||||
this.citationService.trackEvent('Expand', {
|
||||
control: 'Source Button',
|
||||
type: value ? 'Hide' : 'Show',
|
||||
});
|
||||
}
|
||||
}}
|
||||
></blocksuite-toggle-button>
|
||||
`
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import {
|
||||
focusTextModel,
|
||||
getInlineEditorByModel,
|
||||
markdownInput,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
@@ -148,10 +147,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
|
||||
raw.preventDefault();
|
||||
|
||||
if (markdownInput(std, model.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model.props.type.startsWith('h') && model.props.collapsed) {
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) return true;
|
||||
@@ -199,20 +194,6 @@ export const ParagraphKeymapExtension = KeymapExtension(
|
||||
event.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Space: ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Space': ctx => {
|
||||
if (!markdownInput(std)) {
|
||||
return;
|
||||
}
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
Tab: ctx => {
|
||||
const [success] = std.command
|
||||
.chain()
|
||||
|
||||
@@ -20,6 +20,7 @@ import { EMBED_BLOCK_MODEL_LIST } from '@blocksuite/affine-shared/consts';
|
||||
import type { ExtendedModel } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
focusTitle,
|
||||
getDocTitleInlineEditor,
|
||||
getPrevContentBlock,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
@@ -45,10 +46,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [EdgelessTextBlockModel])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const prevBlock = getPrevContentBlock(editorHost, model);
|
||||
if (!prevBlock) {
|
||||
return handleNoPreviousSibling(editorHost, model);
|
||||
@@ -123,36 +120,63 @@ function handleNoPreviousSibling(editorHost: EditorHost, model: ExtendedModel) {
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return false;
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel]) && parent.isPageBlock()) {
|
||||
const focusFirstBlockStart = () => {
|
||||
const firstBlock = parent.firstChild();
|
||||
if (firstBlock) {
|
||||
focusTextModel(editorHost.std, firstBlock.id, 0);
|
||||
}
|
||||
};
|
||||
|
||||
if (matchModels(parent, [NoteBlockModel])) {
|
||||
const hasTitleEditor = getDocTitleInlineEditor(editorHost);
|
||||
const rootModel = model.store.root as RootBlockModel;
|
||||
const title = rootModel.props.title;
|
||||
|
||||
const shouldHandleTitle = parent.isPageBlock() && hasTitleEditor;
|
||||
|
||||
doc.captureSync();
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
|
||||
if (shouldHandleTitle) {
|
||||
let textLength = 0;
|
||||
if (text) {
|
||||
textLength = text.length;
|
||||
title.join(text);
|
||||
}
|
||||
if (model.children.length > 0 || doc.getNext(model)) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
}
|
||||
// no other blocks, preserve a empty line
|
||||
else {
|
||||
text?.clear();
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Preserve at least one block to be able to focus on container click
|
||||
if (doc.getNext(model) || model.children.length > 0) {
|
||||
if (
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
} else {
|
||||
text?.clear();
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
focusTitle(editorHost, title.length - textLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
matchModels(parent, [EdgelessTextBlockModel]) ||
|
||||
model.children.length > 0
|
||||
matchModels(parent, [EdgelessTextBlockModel]) &&
|
||||
text?.length === 0 &&
|
||||
(model.children.length > 0 || doc.getNext(model))
|
||||
) {
|
||||
doc.deleteBlock(model, {
|
||||
bringChildrenTo: parent,
|
||||
});
|
||||
focusFirstBlockStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@ import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { ParagraphMarkdownExtension } from './markdown.js';
|
||||
import { ParagraphBlockConfigExtension } from './paragraph-block-config.js';
|
||||
import {
|
||||
ParagraphKeymapExtension,
|
||||
@@ -22,11 +26,6 @@ const placeholders = {
|
||||
quote: '',
|
||||
};
|
||||
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
getPlaceholder: z.optional(
|
||||
z.function().args(z.instanceof(ParagraphBlockModel)).returns(z.string())
|
||||
@@ -61,6 +60,7 @@ export class ParagraphViewExtension extends ViewExtensionProvider<
|
||||
ParagraphBlockConfigExtension({
|
||||
getPlaceholder,
|
||||
}),
|
||||
ParagraphMarkdownExtension,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import type {
|
||||
EmbedCardStyle,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BookmarkStyles,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
convertToPng,
|
||||
isInsidePageEditor,
|
||||
isTopLevelBlock,
|
||||
isUrlInClipboard,
|
||||
@@ -64,7 +68,7 @@ import * as Y from 'yjs';
|
||||
|
||||
import { PageClipboard } from '../../clipboard/index.js';
|
||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||
import { isCanvasElementWithText } from '../utils/query.js';
|
||||
import { isCanvasElementWithText, isImageBlock } from '../utils/query.js';
|
||||
import { createElementsFromClipboardDataCommand } from './command.js';
|
||||
import {
|
||||
isPureFileInClipboard,
|
||||
@@ -123,6 +127,49 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only when an image is selected, it can be pasted normally to page mode.
|
||||
if (elements.length === 1 && isImageBlock(elements[0])) {
|
||||
const element = elements[0];
|
||||
const sourceId = element.props.sourceId$.peek();
|
||||
if (!sourceId) return;
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async items => {
|
||||
const job = this.std.store.getTransformer();
|
||||
await job.assetsManager.readFromBlob(sourceId);
|
||||
|
||||
let blob = job.assetsManager.getAssets().get(sourceId) ?? null;
|
||||
if (!blob) {
|
||||
return items;
|
||||
}
|
||||
|
||||
let type = blob.type;
|
||||
let supported = false;
|
||||
|
||||
try {
|
||||
supported = ClipboardItem?.supports(type) ?? false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
// TODO(@fundon): when converting jpeg to png, image may become larger and exceed the limit.
|
||||
if (!supported) {
|
||||
type = 'image/png';
|
||||
blob = await convertToPng(blob);
|
||||
}
|
||||
|
||||
if (blob) {
|
||||
return {
|
||||
...items,
|
||||
[`${type}`]: blob,
|
||||
};
|
||||
}
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.std.clipboard.writeToClipboard(async _items => {
|
||||
const data = await prepareClipboardData(elements, this.std);
|
||||
return {
|
||||
@@ -236,7 +283,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
const options: Record<string, unknown> = {};
|
||||
|
||||
let flavour = 'affine:bookmark';
|
||||
let style = BookmarkStyles[0];
|
||||
let style: EmbedCardStyle = BookmarkStyles[0];
|
||||
let isInternalLink = false;
|
||||
let isLinkedBlock = false;
|
||||
|
||||
@@ -559,6 +606,10 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
|
||||
private async _pasteTextContentAsNote(content: BlockSnapshot[] | string) {
|
||||
if (content === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = this.toolManager.lastMousePos$.peek();
|
||||
|
||||
const noteProps = {
|
||||
|
||||
@@ -69,37 +69,39 @@ export async function prepareClipboardData(
|
||||
|
||||
export function isPureFileInClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
return (
|
||||
(types.length === 1 && types[0] === 'Files') ||
|
||||
(types.length === 2 &&
|
||||
(types.includes('text/plain') || types.includes('text/html')) &&
|
||||
types.includes('Files'))
|
||||
);
|
||||
const allowedTypes = new Set([
|
||||
'Files',
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/x-moz-file',
|
||||
]);
|
||||
|
||||
return types.includes('Files') && types.every(type => allowedTypes.has(type));
|
||||
}
|
||||
|
||||
export function tryGetSvgFromClipboard(clipboardData: DataTransfer) {
|
||||
const types = clipboardData.types;
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (types.length === 1 && types[0] !== 'text/plain') {
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
return file;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(
|
||||
clipboardData.getData('text/plain'),
|
||||
'image/svg+xml'
|
||||
);
|
||||
const svg = svgDoc.documentElement;
|
||||
|
||||
if (svg.tagName !== 'svg' || !svg.hasAttribute('xmlns')) {
|
||||
return null;
|
||||
}
|
||||
const svgContent = DOMPurify.sanitize(svgDoc.documentElement, {
|
||||
USE_PROFILES: { svg: true },
|
||||
});
|
||||
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
|
||||
const file = new File([blob], 'pasted-image.svg', { type: 'image/svg+xml' });
|
||||
return file;
|
||||
}
|
||||
|
||||
export function edgelessElementsBoundFromRawData(
|
||||
|
||||
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
) as SurfaceBlockModel;
|
||||
}
|
||||
|
||||
private get _viewportElement(): HTMLElement {
|
||||
get viewportElement(): HTMLElement {
|
||||
return this.std.get(ViewportElementProvider).viewportElement;
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
this.gfx.viewport.onResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(this._viewportElement);
|
||||
resizeObserver.observe(this.viewportElement);
|
||||
this._resizeObserver = resizeObserver;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,25 @@ type RendererOptions = {
|
||||
surfaceModel: SurfaceBlockModel;
|
||||
};
|
||||
|
||||
const UpdateType = {
|
||||
ELEMENT_ADDED: 'element-added',
|
||||
ELEMENT_REMOVED: 'element-removed',
|
||||
ELEMENT_UPDATED: 'element-updated',
|
||||
VIEWPORT_CHANGED: 'viewport-changed',
|
||||
SIZE_CHANGED: 'size-changed',
|
||||
ZOOM_STATE_CHANGED: 'zoom-state-changed',
|
||||
} as const;
|
||||
|
||||
type UpdateType = (typeof UpdateType)[keyof typeof UpdateType];
|
||||
|
||||
interface IncrementalUpdateState {
|
||||
dirtyElementIds: Set<string>;
|
||||
viewportDirty: boolean;
|
||||
sizeDirty: boolean;
|
||||
usePlaceholderDirty: boolean;
|
||||
pendingUpdates: Map<string, UpdateType[]>;
|
||||
}
|
||||
|
||||
const PLACEHOLDER_RESET_STYLES = {
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
@@ -141,6 +160,18 @@ export class DomRenderer {
|
||||
|
||||
private _sizeUpdatedRafId: number | null = null;
|
||||
|
||||
private readonly _updateState: IncrementalUpdateState = {
|
||||
dirtyElementIds: new Set(),
|
||||
viewportDirty: false,
|
||||
sizeDirty: false,
|
||||
usePlaceholderDirty: false,
|
||||
pendingUpdates: new Map(),
|
||||
};
|
||||
|
||||
private _lastViewportBounds: Bound | null = null;
|
||||
private _lastZoom: number | null = null;
|
||||
private _lastUsePlaceholder: boolean = false;
|
||||
|
||||
rootElement: HTMLElement;
|
||||
|
||||
private readonly _elementsMap = new Map<string, HTMLElement>();
|
||||
@@ -186,6 +217,7 @@ export class DomRenderer {
|
||||
private _initViewport() {
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -195,6 +227,7 @@ export class DomRenderer {
|
||||
if (this._sizeUpdatedRafId) return;
|
||||
this._sizeUpdatedRafId = requestConnectedFrame(() => {
|
||||
this._sizeUpdatedRafId = null;
|
||||
this._markSizeDirty();
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
@@ -208,6 +241,7 @@ export class DomRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this._markUsePlaceholderDirty();
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
@@ -307,6 +341,292 @@ export class DomRenderer {
|
||||
}
|
||||
|
||||
private _render() {
|
||||
this._renderIncremental();
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(payload => {
|
||||
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = (overlay: Overlay) => {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
attach = (container: HTMLElement) => {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
};
|
||||
|
||||
generateColorProperty = (color: Color, fallback?: Color) => {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getColorScheme = () => {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
};
|
||||
|
||||
getColorValue = (color: Color, fallback?: Color, real?: boolean) => {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getPropertyValue = (property: string) => {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
};
|
||||
|
||||
refresh = () => {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
};
|
||||
|
||||
removeOverlay = (overlay: Overlay) => {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a specific element as dirty for incremental updates
|
||||
* @param elementId - The ID of the element to mark as dirty
|
||||
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
|
||||
*/
|
||||
markElementDirty = (
|
||||
elementId: string,
|
||||
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
|
||||
) => {
|
||||
this._markElementDirty(elementId, updateType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a full re-render of all elements
|
||||
*/
|
||||
forceFullRender = () => {
|
||||
this._updateState.viewportDirty = true;
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
private _markElementDirty(elementId: string, updateType: UpdateType) {
|
||||
this._updateState.dirtyElementIds.add(elementId);
|
||||
const currentUpdates =
|
||||
this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (!currentUpdates.includes(updateType)) {
|
||||
currentUpdates.push(updateType);
|
||||
this._updateState.pendingUpdates.set(elementId, currentUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private _markViewportDirty() {
|
||||
this._updateState.viewportDirty = true;
|
||||
}
|
||||
|
||||
private _markSizeDirty() {
|
||||
this._updateState.sizeDirty = true;
|
||||
}
|
||||
|
||||
private _markUsePlaceholderDirty() {
|
||||
this._updateState.usePlaceholderDirty = true;
|
||||
}
|
||||
|
||||
private _clearUpdateState() {
|
||||
this._updateState.dirtyElementIds.clear();
|
||||
this._updateState.viewportDirty = false;
|
||||
this._updateState.sizeDirty = false;
|
||||
this._updateState.usePlaceholderDirty = false;
|
||||
this._updateState.pendingUpdates.clear();
|
||||
}
|
||||
|
||||
private _isViewportChanged(): boolean {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
|
||||
if (!this._lastViewportBounds || !this._lastZoom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
this._lastViewportBounds.x !== viewportBounds.x ||
|
||||
this._lastViewportBounds.y !== viewportBounds.y ||
|
||||
this._lastViewportBounds.w !== viewportBounds.w ||
|
||||
this._lastViewportBounds.h !== viewportBounds.h ||
|
||||
this._lastZoom !== zoom
|
||||
);
|
||||
}
|
||||
|
||||
private _isUsePlaceholderChanged(): boolean {
|
||||
return this._lastUsePlaceholder !== this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _updateLastState() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
this._lastViewportBounds = {
|
||||
x: viewportBounds.x,
|
||||
y: viewportBounds.y,
|
||||
w: viewportBounds.w,
|
||||
h: viewportBounds.h,
|
||||
} as Bound;
|
||||
this._lastZoom = zoom;
|
||||
this._lastUsePlaceholder = this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _renderIncremental() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
|
||||
const needsFullRender =
|
||||
this._isViewportChanged() ||
|
||||
this._isUsePlaceholderChanged() ||
|
||||
this._updateState.sizeDirty ||
|
||||
this._updateState.viewportDirty ||
|
||||
this._updateState.usePlaceholderDirty;
|
||||
|
||||
if (needsFullRender) {
|
||||
this._renderFull();
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update dirty elements
|
||||
const elementsFromGrid = this.grid.search(viewportBounds, {
|
||||
filter: ['canvas', 'local'],
|
||||
}) as SurfaceElementModel[];
|
||||
|
||||
const visibleElementIds = new Set<string>();
|
||||
|
||||
// 1. Update dirty elements
|
||||
for (const elementModel of elementsFromGrid) {
|
||||
const display = (elementModel.display ?? true) && !elementModel.hidden;
|
||||
if (
|
||||
display &&
|
||||
intersects(getBoundWithRotation(elementModel), viewportBounds)
|
||||
) {
|
||||
visibleElementIds.add(elementModel.id);
|
||||
|
||||
// Only update dirty elements
|
||||
if (this._updateState.dirtyElementIds.has(elementModel.id)) {
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(elementModel as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
this._renderOrUpdatePlaceholder(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
} else {
|
||||
this._renderOrUpdateFullElement(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove elements that are no longer in the grid
|
||||
for (const elementId of this._updateState.dirtyElementIds) {
|
||||
const updateTypes = this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (
|
||||
updateTypes.includes(UpdateType.ELEMENT_REMOVED) ||
|
||||
!visibleElementIds.has(elementId)
|
||||
) {
|
||||
const domElem = this._elementsMap.get(elementId);
|
||||
if (domElem) {
|
||||
domElem.remove();
|
||||
this._elementsMap.delete(elementId);
|
||||
elementsToRemove.push(domElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Notify changes
|
||||
if (addedElements.length > 0 || elementsToRemove.length > 0) {
|
||||
this.elementsUpdated.next({
|
||||
elements: Array.from(this._elementsMap.values()),
|
||||
added: addedElements,
|
||||
removed: elementsToRemove,
|
||||
});
|
||||
}
|
||||
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
}
|
||||
|
||||
private _renderFull() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
@@ -387,100 +707,4 @@ export class DomRenderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
}
|
||||
|
||||
generateColorProperty(color: Color, fallback?: Color) {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
}
|
||||
|
||||
getColorValue(color: Color, fallback?: Color, real?: boolean) {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getPropertyValue(property: string) {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
}
|
||||
|
||||
removeOverlay(overlay: Overlay) {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +647,16 @@ export class TableCell extends SignalWatcher(
|
||||
return this.richText$.value?.inlineEditor;
|
||||
}
|
||||
|
||||
private readonly _handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.readonly) {
|
||||
@@ -659,10 +669,7 @@ export class TableCell extends SignalWatcher(
|
||||
this.inlineEditor?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.add(() => {
|
||||
this.removeEventListener('keydown', selectAll);
|
||||
});
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'click', (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
requestAnimationFrame(() => {
|
||||
@@ -679,6 +686,13 @@ export class TableCell extends SignalWatcher(
|
||||
}
|
||||
this.richText$.value?.updateComplete
|
||||
.then(() => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (inlineEditor) {
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.subscribe(this._handleKeyDown)
|
||||
);
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const richText = this.richText$.value;
|
||||
|
||||
@@ -195,8 +195,7 @@ export class EmbedCardEditModal extends SignalWatcher(
|
||||
|
||||
const description = this.description$.value.trim();
|
||||
|
||||
const props: AliasInfo = { title };
|
||||
if (description) props.description = description;
|
||||
const props: AliasInfo = { title, description };
|
||||
|
||||
this.onSave?.(std, blockComponent, props);
|
||||
|
||||
|
||||
@@ -29,12 +29,6 @@ export class OpenDocDropdownMenu extends SignalWatcher(
|
||||
gap: unset !important;
|
||||
}
|
||||
|
||||
editor-icon-button {
|
||||
.label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-orientation] {
|
||||
width: 264px;
|
||||
gap: 4px;
|
||||
|
||||
@@ -9,6 +9,7 @@ const toolbarColorKeys: Array<keyof AffineCssVariables> = [
|
||||
'--affine-background-overlay-panel-color',
|
||||
'--affine-v2-layer-background-overlayPanel' as never,
|
||||
'--affine-v2-layer-insideBorder-blackBorder' as never,
|
||||
'--affine-v2-icon-primary' as never,
|
||||
'--affine-background-error-color',
|
||||
'--affine-background-primary-color',
|
||||
'--affine-background-tertiary-color',
|
||||
|
||||
@@ -112,7 +112,10 @@ export class GroupTrait {
|
||||
return;
|
||||
}
|
||||
const { staticMap, groupInfo } = staticInfo;
|
||||
const groupMap: Record<string, Group> = { ...staticMap };
|
||||
const groupMap: Record<string, Group> = {};
|
||||
Object.entries(staticMap).forEach(([key, group]) => {
|
||||
groupMap[key] = new Group(key, group.value, groupInfo, this);
|
||||
});
|
||||
this.view.rows$.value.forEach(row => {
|
||||
const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id)
|
||||
.jsonValue$.value;
|
||||
@@ -182,6 +185,7 @@ export class GroupTrait {
|
||||
) {}
|
||||
|
||||
addToGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
const groupInfo = this.groupInfo$.value;
|
||||
if (!groupMap || !groupInfo) {
|
||||
@@ -254,6 +258,7 @@ export class GroupTrait {
|
||||
toGroupKey: string,
|
||||
position: InsertToPosition
|
||||
) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -290,6 +295,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
moveGroupTo(groupKey: string, position: InsertToPosition) {
|
||||
this.view.lockRows(false);
|
||||
const groups = this.groupsDataList$.value;
|
||||
if (!groups) {
|
||||
return;
|
||||
@@ -305,6 +311,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
removeFromGroup(rowId: string, key: string) {
|
||||
this.view.lockRows(false);
|
||||
const groupMap = this.groupDataMap$.value;
|
||||
if (!groupMap) {
|
||||
return;
|
||||
@@ -323,6 +330,7 @@ export class GroupTrait {
|
||||
}
|
||||
|
||||
updateValue(rows: string[], value: unknown) {
|
||||
this.view.lockRows(false);
|
||||
const propertyId = this.property$.value?.id;
|
||||
if (!propertyId) {
|
||||
return;
|
||||
|
||||
@@ -128,6 +128,7 @@ export abstract class SingleViewBase<
|
||||
);
|
||||
|
||||
rowsDelete(rows: string[]): void {
|
||||
this.lockRows(false);
|
||||
this.dataSource.rowDelete(rows);
|
||||
}
|
||||
|
||||
@@ -258,6 +259,7 @@ export abstract class SingleViewBase<
|
||||
abstract propertyGetOrCreate(propertyId: string): Property;
|
||||
|
||||
rowAdd(insertPosition: InsertToPosition | number): string {
|
||||
this.lockRows(false);
|
||||
return this.dataSource.rowAdd(insertPosition);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,12 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddCard = () => {
|
||||
this.view.addCard('end', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
this.view.addCard('start', this.group.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -79,12 +81,14 @@ export class MobileKanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -66,7 +66,9 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
|
||||
|
||||
addRow = (position: InsertToPosition) => {
|
||||
if (this.readonly) return;
|
||||
return this.view.rowAdd(position);
|
||||
const id = this.view.rowAdd(position);
|
||||
this.ui$.value?.requestUpdate();
|
||||
return id;
|
||||
};
|
||||
|
||||
focusFirstCell = () => {};
|
||||
|
||||
@@ -83,6 +83,7 @@ export const popCardMenu = (
|
||||
{ before: true, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
@@ -97,6 +98,7 @@ export const popCardMenu = (
|
||||
{ before: false, id: cardId },
|
||||
groupKey
|
||||
);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -111,6 +113,7 @@ export const popCardMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
kanbanViewLogic.view.rowsDelete([cardId]);
|
||||
kanbanViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -128,6 +128,7 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
if (selection.selectionType === 'card') {
|
||||
this.view.rowsDelete(selection.cards.map(v => v.cardId));
|
||||
this.selection = undefined;
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddCardInStart = () => {
|
||||
@@ -127,6 +128,7 @@ export class KanbanGroup extends SignalWatcher(
|
||||
isEditing: true,
|
||||
};
|
||||
});
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -139,12 +141,14 @@ export class KanbanGroup extends SignalWatcher(
|
||||
this.group.rows.forEach(row => {
|
||||
this.group.manager.removeFromGroup(row.rowId, this.group.key);
|
||||
});
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(this.group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -73,6 +73,7 @@ export class KanbanViewUILogic extends DataViewUILogicBase<
|
||||
rowId,
|
||||
});
|
||||
}
|
||||
this.ui$.value?.requestUpdate();
|
||||
return rowId;
|
||||
};
|
||||
|
||||
|
||||
@@ -51,10 +51,12 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
|
||||
private readonly clickAddRow = () => {
|
||||
this.view.rowAdd('end', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickAddRowInStart = () => {
|
||||
this.view.rowAdd('start', this.group?.key);
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private readonly clickGroupOptions = (e: MouseEvent) => {
|
||||
@@ -77,6 +79,7 @@ export class MobileTableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -38,6 +38,7 @@ export const popMobileRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
view.rowsDelete([rowId]);
|
||||
tableViewLogic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -44,6 +44,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -79,6 +80,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -376,6 +376,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class TableGroupFooter extends WithDisposable(ShadowlessElement) {
|
||||
private readonly clickAddRow = () => {
|
||||
const group = this.group$.value;
|
||||
const rowId = this.tableViewManager.rowAdd('end', group?.key);
|
||||
this.requestUpdate();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const rowIndex = this.selectionController.getRow(group?.key, rowId)
|
||||
|
||||
@@ -58,6 +58,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
return;
|
||||
}
|
||||
this.tableViewManager.rowAdd('start', group.key);
|
||||
this.requestUpdate();
|
||||
const selectionController = this.selectionController;
|
||||
selectionController.selection = undefined;
|
||||
requestAnimationFrame(() => {
|
||||
@@ -95,6 +96,7 @@ export class TableGroupHeader extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.tableViewManager.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -43,6 +43,7 @@ export class TableClipboardController implements ReactiveController {
|
||||
}
|
||||
if (deleteRows.length) {
|
||||
this.logic.view.rowsDelete(deleteRows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
}
|
||||
this.clipboard
|
||||
@@ -78,6 +79,14 @@ export class TableClipboardController implements ReactiveController {
|
||||
const event = _context.get('clipboardState').raw;
|
||||
event.stopPropagation();
|
||||
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (
|
||||
active &&
|
||||
(active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export class TableHotkeysController implements ReactiveController {
|
||||
const rows = TableViewRowSelection.rowsIds(selection);
|
||||
this.selectionController.selection = undefined;
|
||||
this.logic.view.rowsDelete(rows);
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
|
||||
@@ -351,6 +351,7 @@ export class TableSelectionController implements ReactiveController {
|
||||
deleteRow(rowId: string) {
|
||||
this.view.rowsDelete([rowId]);
|
||||
this.focusToCell('up');
|
||||
this.logic.ui$.value?.requestUpdate();
|
||||
}
|
||||
|
||||
focusFirstCell() {
|
||||
|
||||
@@ -83,6 +83,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -102,6 +103,7 @@ export class TableGroup extends SignalWatcher(
|
||||
},
|
||||
isEditing: true,
|
||||
});
|
||||
this.requestUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -125,6 +127,7 @@ export class TableGroup extends SignalWatcher(
|
||||
name: 'Delete Cards',
|
||||
select: () => {
|
||||
this.view.rowsDelete(group.rows.map(row => row.rowId));
|
||||
this.requestUpdate();
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -71,6 +71,7 @@ export const popRowMenu = (
|
||||
prefix: DeleteIcon(),
|
||||
select: () => {
|
||||
selectionController.view.rowsDelete(rows);
|
||||
selectionController.logic.ui$.value?.requestUpdate();
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -16,5 +16,6 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
|
||||
.vars="${filterTrait.view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
.dataViewLogic="${props.dataViewLogic}"
|
||||
></filter-bar>`;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { property } from 'lit/decorators.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import type { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
|
||||
import { popFilterGroup } from './group-panel-view.js';
|
||||
|
||||
export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
@@ -99,6 +100,7 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
requestAnimationFrame(() => {
|
||||
this.expandGroup(element, index);
|
||||
});
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -206,6 +208,9 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewLogic!: DataViewUILogicBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -26,7 +26,10 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { FilterTrait } from '../../../core/filter/trait.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import {
|
||||
type DataViewUILogicBase,
|
||||
popCreateFilter,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
type FilterGroupView,
|
||||
getDepth,
|
||||
@@ -375,6 +378,7 @@ export const popFilterRoot = (
|
||||
props: {
|
||||
filterTrait: FilterTrait;
|
||||
onBack: () => void;
|
||||
dataViewLogic: DataViewUILogicBase;
|
||||
}
|
||||
) => {
|
||||
const filterTrait = props.filterTrait;
|
||||
@@ -414,6 +418,10 @@ export const popFilterRoot = (
|
||||
...value,
|
||||
conditions: [...value.conditions, filter],
|
||||
});
|
||||
props.dataViewLogic.eventTrace(
|
||||
'CreateDatabaseFilter',
|
||||
{}
|
||||
);
|
||||
},
|
||||
},
|
||||
{ middleware: subMenuMiddleware }
|
||||
|
||||
@@ -75,6 +75,7 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
|
||||
conditions: [filter],
|
||||
};
|
||||
this.toggleShowFilter(true);
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -145,13 +145,16 @@ const createSettingMenus = (
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import {
|
||||
AutoClearSelectionService,
|
||||
CitationService,
|
||||
DefaultOpenDocExtension,
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
@@ -76,6 +77,7 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
|
||||
FileSizeLimitService,
|
||||
LinkPreviewCache,
|
||||
LinkPreviewService,
|
||||
CitationService,
|
||||
]);
|
||||
context.register(clipboardConfigs);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
|
||||
@@ -192,10 +192,14 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
||||
this._updateTitleInMeta();
|
||||
this.requestUpdate();
|
||||
};
|
||||
this._rootModel?.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
|
||||
if (this._rootModel) {
|
||||
const rootModel = this._rootModel;
|
||||
rootModel.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
rootModel.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { type Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragMoveContext,
|
||||
@@ -74,47 +74,63 @@ export class SnapExtension extends InteractivityExtension {
|
||||
return {};
|
||||
}
|
||||
|
||||
let alignBound: Bound | null = null;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||
snapOverlay.setMovingElements(context.elements);
|
||||
},
|
||||
onResizeMove(context) {
|
||||
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||
return;
|
||||
const {
|
||||
handle,
|
||||
originalBound,
|
||||
scaleX,
|
||||
scaleY,
|
||||
handleSign,
|
||||
currentHandlePos,
|
||||
elements,
|
||||
} = context;
|
||||
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
|
||||
const alignDirection: ('vertical' | 'horizontal')[] = [];
|
||||
let switchDirection = false;
|
||||
let nx = handleSign.x;
|
||||
let ny = handleSign.y;
|
||||
|
||||
if (handle.length > 6) {
|
||||
alignDirection.push('vertical', 'horizontal');
|
||||
} else if (rotate % 90 === 0) {
|
||||
nx =
|
||||
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
|
||||
handleSign.y * Math.sin((rotate / 180) * Math.PI);
|
||||
ny =
|
||||
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
|
||||
handleSign.y * Math.cos((rotate / 180) * Math.PI);
|
||||
|
||||
if (Math.abs(nx) > Math.abs(ny)) {
|
||||
alignDirection.push('horizontal');
|
||||
} else {
|
||||
alignDirection.push('vertical');
|
||||
}
|
||||
|
||||
if (rotate % 180 !== 0) {
|
||||
switchDirection = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { handle, handleSign, lockRatio } = context;
|
||||
let { dx, dy } = context;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(
|
||||
Math.abs(dx / alignBound.w),
|
||||
Math.abs(dy / alignBound.h)
|
||||
if (alignDirection.length > 0) {
|
||||
const rst = snapOverlay.alignResize(
|
||||
currentHandlePos,
|
||||
alignDirection
|
||||
);
|
||||
|
||||
dx = min * Math.sign(dx) * alignBound.w;
|
||||
dy = min * Math.sign(dy) * alignBound.h;
|
||||
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
|
||||
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
|
||||
|
||||
context.suggest({
|
||||
scaleX: scaleX + dx / originalBound.w,
|
||||
scaleY: scaleY + dy / originalBound.h,
|
||||
});
|
||||
}
|
||||
|
||||
const currentBound = new Bound(
|
||||
alignBound.x +
|
||||
(handle.includes('left') ? -dx * handleSign.xSign : 0),
|
||||
alignBound.y +
|
||||
(handle.includes('top') ? -dy * handleSign.ySign : 0),
|
||||
Math.abs(alignBound.w + dx * handleSign.xSign),
|
||||
Math.abs(alignBound.h + dy * handleSign.ySign)
|
||||
);
|
||||
const alignRst = snapOverlay.align(currentBound);
|
||||
|
||||
context.suggest({
|
||||
dx: alignRst.dx + context.dx,
|
||||
dy: alignRst.dy + context.dy,
|
||||
});
|
||||
},
|
||||
onResizeEnd() {
|
||||
alignBound = null;
|
||||
snapOverlay.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ConnectorElementModel,
|
||||
MindmapElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
|
||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
|
||||
interface Distance {
|
||||
@@ -586,6 +586,60 @@ export class SnapOverlay extends Overlay {
|
||||
);
|
||||
}
|
||||
|
||||
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
|
||||
const { viewport } = this.gfx;
|
||||
const threshold = ALIGN_THRESHOLD / viewport.zoom;
|
||||
const searchBound = new Bound(
|
||||
position[0] - threshold / 2,
|
||||
position[1] - threshold / 2,
|
||||
threshold,
|
||||
threshold
|
||||
);
|
||||
const alignBound = new Bound(position[0], position[1], 0, 0);
|
||||
|
||||
this._intraGraphicAlignLines = {
|
||||
horizontal: [],
|
||||
vertical: [],
|
||||
};
|
||||
this._distributedAlignLines = [];
|
||||
this._updateAlignCandidates(searchBound);
|
||||
|
||||
for (const other of this._referenceBounds.all) {
|
||||
const closestDistances = this._calculateClosestDistances(
|
||||
alignBound,
|
||||
other
|
||||
);
|
||||
|
||||
if (
|
||||
direction.includes('horizontal') &&
|
||||
closestDistances.horiz &&
|
||||
(!this._intraGraphicAlignLines.horizontal.length ||
|
||||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
|
||||
) {
|
||||
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
|
||||
if (
|
||||
direction.includes('vertical') &&
|
||||
closestDistances.vert &&
|
||||
(!this._intraGraphicAlignLines.vertical.length ||
|
||||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
|
||||
) {
|
||||
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
}
|
||||
|
||||
this._intraGraphicAlignLines.horizontal =
|
||||
this._intraGraphicAlignLines.horizontal.slice(0, 1);
|
||||
this._intraGraphicAlignLines.vertical =
|
||||
this._intraGraphicAlignLines.vertical.slice(0, 1);
|
||||
this._renderer?.refresh();
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
align(bound: Bound): { dx: number; dy: number } {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;
|
||||
|
||||
@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
|
||||
element: HTMLElement,
|
||||
zoom: number
|
||||
) {
|
||||
if (model.shapeType === 'rect') {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
} else if (model.shapeType === 'ellipse') {
|
||||
element.style.borderRadius = '50%';
|
||||
} else {
|
||||
element.style.borderRadius = '';
|
||||
// Reset properties that might be set by different shape types
|
||||
element.style.removeProperty('clip-path');
|
||||
element.style.removeProperty('border-radius');
|
||||
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
|
||||
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
|
||||
while (element.firstChild) element.firstChild.remove();
|
||||
}
|
||||
|
||||
switch (model.shapeType) {
|
||||
case 'rect': {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'ellipse':
|
||||
element.style.borderRadius = '50%';
|
||||
break;
|
||||
case 'diamond':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
||||
break;
|
||||
case 'triangle':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
|
||||
break;
|
||||
}
|
||||
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
|
||||
}
|
||||
|
||||
function applyBorderStyles(
|
||||
@@ -78,6 +95,9 @@ export const shapeDomRenderer = (
|
||||
renderer: DomRenderer
|
||||
): void => {
|
||||
const { zoom } = renderer.viewport;
|
||||
const unscaledWidth = model.w;
|
||||
const unscaledHeight = model.h;
|
||||
|
||||
const fillColor = renderer.getColorValue(
|
||||
model.fillColor,
|
||||
DefaultTheme.shapeFillColor,
|
||||
@@ -89,17 +109,80 @@ export const shapeDomRenderer = (
|
||||
true
|
||||
);
|
||||
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
element.style.width = `${unscaledWidth * zoom}px`;
|
||||
element.style.height = `${unscaledHeight * zoom}px`;
|
||||
element.style.boxSizing = 'border-box';
|
||||
|
||||
// Apply shape-specific clipping, border-radius, and potentially clear innerHTML
|
||||
applyShapeSpecificStyles(model, element, zoom);
|
||||
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
if (model.shapeType === 'diamond' || model.shapeType === 'triangle') {
|
||||
// For diamond and triangle, fill and border are handled by inline SVG
|
||||
element.style.border = 'none'; // Ensure no standard CSS border interferes
|
||||
element.style.backgroundColor = 'transparent'; // Host element is transparent
|
||||
|
||||
const strokeW = model.strokeWidth;
|
||||
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Adjusted points for diamond
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
|
||||
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight / 2}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
const finalStrokeColor =
|
||||
model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent';
|
||||
// Determine dash array, only if stroke is visible and style is 'dash'
|
||||
const finalStrokeDasharray =
|
||||
model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent'
|
||||
? '12, 12'
|
||||
: 'none';
|
||||
// Determine fill color
|
||||
const finalFillColor = model.filled ? fillColor : 'transparent';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
} else {
|
||||
// Standard rendering for other shapes (e.g., rect, ellipse)
|
||||
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
|
||||
}
|
||||
|
||||
applyBorderStyles(model, element, strokeColor, zoom);
|
||||
applyTransformStyles(model, element);
|
||||
|
||||
element.style.boxSizing = 'border-box';
|
||||
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
|
||||
|
||||
manageClassNames(model, element);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
|
||||
import type { FootNote } from '@blocksuite/affine-model';
|
||||
import { CitationProvider } from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
@@ -117,6 +118,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return this.std.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
onFootnoteClick = () => {
|
||||
if (!this.footnote) {
|
||||
return;
|
||||
@@ -215,6 +220,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.citationService.trackEvent('Hover', {
|
||||
control: 'Source Footnote',
|
||||
});
|
||||
|
||||
return {
|
||||
template: this._FootNotePopup(footnote, abortController),
|
||||
container: this.std.host,
|
||||
|
||||
@@ -188,6 +188,8 @@ export class AffineLatexNode extends SignalWatcher(
|
||||
this._editorAbortController?.abort();
|
||||
this._editorAbortController = new AbortController();
|
||||
|
||||
blockComponent.selection.setGroup('note', []);
|
||||
|
||||
const portal = createLitPortal({
|
||||
template: html`<latex-editor-menu
|
||||
.std=${this.std}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'latex',
|
||||
|
||||
pattern:
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)$|(?<blockPrefix>\$\$\$\$)|(?<inlinePrefix>\$\$)$/g,
|
||||
/(?:\$\$)(?<content>[^$]+)(?:\$\$)\s$|(?<blockPrefix>\$\$\$\$)\s$|(?<inlinePrefix>\$\$)\s$/g,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = pattern.exec(prefixText);
|
||||
if (!match || !match.groups) return;
|
||||
@@ -33,22 +33,10 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
const ifEdgelessText = blockComponent.closest('affine-edgeless-text');
|
||||
|
||||
if (blockPrefix === '$$$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 4,
|
||||
index: inlineRange.index - 5,
|
||||
length: 5,
|
||||
});
|
||||
|
||||
@@ -88,34 +76,22 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
}
|
||||
|
||||
if (inlinePrefix === '$$') {
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.formatText(
|
||||
{
|
||||
index: inlineRange.index - 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 1,
|
||||
},
|
||||
{
|
||||
@@ -129,7 +105,7 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
await inlineEditor.waitForUpdate();
|
||||
|
||||
const textPoint = inlineEditor.getTextPoint(
|
||||
inlineRange.index - 2 + 1
|
||||
inlineRange.index - 3 + 1
|
||||
);
|
||||
if (!textPoint) return;
|
||||
|
||||
@@ -159,21 +135,9 @@ export const LatexExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
if (!content || content.length === 0) return;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
const startIndex = inlineRange.index - 2 - content.length - 2;
|
||||
const startIndex = inlineRange.index - 1 - 2 - content.length - 2;
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2 + content.length + 2 + 1,
|
||||
|
||||
@@ -3,27 +3,18 @@ import { InlineMarkdownExtension } from '@blocksuite/std/inline';
|
||||
|
||||
export const LinkExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'link',
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)$/,
|
||||
pattern: /.*\[(.+?)\]\((.+?)\)\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const linkText = match[1];
|
||||
const linkUrl = match[2];
|
||||
const annotatedText = match[0].slice(-linkText.length - linkUrl.length - 4);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: inlineRange.index,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(linkText.length + linkUrl.length + 4 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const StrikeInlineSpecExtension =
|
||||
|
||||
export const CodeInlineSpecExtension =
|
||||
InlineSpecExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
name: 'inline-code',
|
||||
schema: z.literal(true).optional().nullable().catch(undefined),
|
||||
match: delta => {
|
||||
return !!delta.attributes?.code;
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
{
|
||||
name: 'bolditalic',
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}$|.*\*{3}([^\s*])\*{3}$/,
|
||||
pattern: /.*\*{3}([^\s*][^*]*[^\s*])\*{3}\s$|.*\*{3}([^\s*])\*{3}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -25,20 +25,11 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 3 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 3 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -54,18 +45,13 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 3,
|
||||
length: 3,
|
||||
index: inlineRange.index - 4,
|
||||
length: 4,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 3,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 6,
|
||||
length: 0,
|
||||
@@ -76,26 +62,14 @@ export const BoldItalicMarkdown = InlineMarkdownExtension<AffineTextAttributes>(
|
||||
|
||||
export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'bold',
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}$|.*\*{2}([^\s*])\*{2}$/,
|
||||
pattern: /.*\*{2}([^\s][^*]*[^\s*])\*{2}\s$|.*\*{2}([^\s*])\*{2}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 2 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -110,18 +84,13 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 4,
|
||||
length: 0,
|
||||
@@ -131,26 +100,14 @@ export const BoldMarkdown = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
|
||||
export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'italic',
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}$|.*\*{1}([^\s*])\*{1}$/,
|
||||
pattern: /.*\*{1}([^\s][^*]*[^\s*])\*{1}\s$|.*\*{1}([^\s*])\*{1}\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -165,18 +122,13 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
length: 1,
|
||||
});
|
||||
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 0,
|
||||
@@ -187,7 +139,7 @@ export const ItalicExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
export const StrikethroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'strikethrough',
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}$|.*~{2}([^\s~])~{2}$/,
|
||||
pattern: /.*~{2}([^\s][^~]*[^\s])~{2}\s$|.*~{2}([^\s~])~{2}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -199,20 +151,11 @@ export const StrikethroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 2 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-targetText.length - (2 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -227,12 +170,8 @@ export const StrikethroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 2,
|
||||
length: 2,
|
||||
index: inlineRange.index - 3,
|
||||
length: 3,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -249,7 +188,7 @@ export const StrikethroughExtension =
|
||||
export const UnderthroughExtension =
|
||||
InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'underthrough',
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}$|.*~{1}([^\s~])~{1}$/,
|
||||
pattern: /.*~{1}([^\s][^~]*[^\s~])~{1}\s$|.*~{1}([^\s~])~{1}\s$/,
|
||||
action: ({
|
||||
inlineEditor,
|
||||
prefixText,
|
||||
@@ -261,20 +200,11 @@ export const UnderthroughExtension =
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
const annotatedText = match[0].slice(
|
||||
-(targetText.length + 1 * 2 + 1),
|
||||
-1
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -289,12 +219,8 @@ export const UnderthroughExtension =
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: inlineRange.index - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
@@ -310,26 +236,14 @@ export const UnderthroughExtension =
|
||||
|
||||
export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
name: 'code',
|
||||
pattern: /.*`([^\s][^`]*[^\s])`$|.*`([^\s`])`$/,
|
||||
pattern: /.*`([^\s][^`]*[^\s])`\s$|.*`([^\s`])`\s$/,
|
||||
action: ({ inlineEditor, prefixText, inlineRange, pattern, undoManager }) => {
|
||||
const match = prefixText.match(pattern);
|
||||
if (!match) return;
|
||||
|
||||
const targetText = match[1] ?? match[2];
|
||||
const annotatedText = match[0].slice(-targetText.length - 1 * 2);
|
||||
const startIndex = inlineRange.index - annotatedText.length;
|
||||
|
||||
inlineEditor.insertText(
|
||||
{
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 0,
|
||||
},
|
||||
' '
|
||||
);
|
||||
inlineEditor.setInlineRange({
|
||||
index: startIndex + annotatedText.length + 1,
|
||||
length: 0,
|
||||
});
|
||||
const annotatedText = match[0].slice(-(targetText.length + 1 * 2 + 1), -1);
|
||||
const startIndex = inlineRange.index - annotatedText.length - 1;
|
||||
|
||||
undoManager.stopCapturing();
|
||||
|
||||
@@ -344,12 +258,8 @@ export const CodeExtension = InlineMarkdownExtension<AffineTextAttributes>({
|
||||
);
|
||||
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length,
|
||||
length: 1,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex + annotatedText.length - 1,
|
||||
length: 1,
|
||||
index: inlineRange.index - 2,
|
||||
length: 2,
|
||||
});
|
||||
inlineEditor.deleteText({
|
||||
index: startIndex,
|
||||
|
||||
@@ -30,11 +30,12 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
|
||||
*/
|
||||
type BackwardCompatibleUndefined = undefined;
|
||||
|
||||
export const AttachmentBlockStyles: EmbedCardStyle[] = [
|
||||
export const AttachmentBlockStyles = [
|
||||
'cubeThick',
|
||||
'horizontalThin',
|
||||
'pdf',
|
||||
] as const;
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type AttachmentBlockProps = {
|
||||
name: string;
|
||||
|
||||
@@ -15,13 +15,13 @@ import type {
|
||||
LinkPreviewData,
|
||||
} from '../../utils/index.js';
|
||||
|
||||
export const BookmarkStyles: EmbedCardStyle[] = [
|
||||
export const BookmarkStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'citation',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type BookmarkBlockProps = {
|
||||
style: (typeof BookmarkStyles)[number];
|
||||
|
||||
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedFigmaBlockProps = {
|
||||
style: (typeof EmbedFigmaStyles)[number];
|
||||
|
||||
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
|
||||
assignees: string[] | null;
|
||||
};
|
||||
|
||||
export const EmbedGithubStyles: EmbedCardStyle[] = [
|
||||
export const EmbedGithubStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedGithubBlockProps = {
|
||||
style: (typeof EmbedGithubStyles)[number];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
|
||||
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedHtmlBlockProps = {
|
||||
style: (typeof EmbedHtmlStyles)[number];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { type EmbedCardStyle } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedIframeBlockProps = {
|
||||
url: string; // the original url that user input
|
||||
|
||||
@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
||||
export const EmbedLinkedDocStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'horizontalThin',
|
||||
'citation',
|
||||
];
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLinkedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
style: (typeof EmbedLinkedDocStyles)[number];
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & ReferenceInfo;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLoomBlockProps = {
|
||||
style: (typeof EmbedLoomStyles)[number];
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
|
||||
export const EmbedSyncedDocStyles = [
|
||||
'syncedDoc',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedSyncedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { BookmarkBlockModel } from '../bookmark';
|
||||
import { EmbedFigmaModel } from './figma';
|
||||
import { EmbedGithubModel } from './github';
|
||||
import type { EmbedHtmlModel } from './html';
|
||||
@@ -30,7 +31,10 @@ export type EmbedCardModel = InstanceType<
|
||||
ExternalEmbedModel | InternalEmbedModel
|
||||
>;
|
||||
|
||||
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
|
||||
export type LinkableEmbedModel =
|
||||
| EmbedCardModel
|
||||
| EmbedIframeBlockModel
|
||||
| BookmarkBlockModel;
|
||||
|
||||
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
|
||||
creatorImage: string | null;
|
||||
};
|
||||
|
||||
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedYoutubeBlockProps = {
|
||||
style: (typeof EmbedYoutubeStyles)[number];
|
||||
|
||||
@@ -10,6 +10,5 @@ export {
|
||||
onModelTextUpdated,
|
||||
selectTextModel,
|
||||
} from './dom';
|
||||
export { markdownInput } from './markdown';
|
||||
export { RichText } from './rich-text';
|
||||
export * from './utils';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
DividerBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toDivider(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
prefix: string
|
||||
) {
|
||||
const { store: doc } = std;
|
||||
if (
|
||||
matchModels(model, [DividerBlockModel]) ||
|
||||
(matchModels(model, [ParagraphBlockModel]) && model.props.type === 'quote')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
beforeConvert(std, model, prefix.length);
|
||||
const blockProps = {
|
||||
children: model.children,
|
||||
};
|
||||
doc.addBlock('affine:divider', blockProps, parent, index);
|
||||
|
||||
const nextBlock = parent.children[index + 1];
|
||||
let id = nextBlock?.id;
|
||||
if (!id) {
|
||||
id = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { markdownInput } from './markdown-input.js';
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
type ListProps,
|
||||
type ListType,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels, toNumberedList } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { focusTextModel } from '../dom.js';
|
||||
import { beforeConvert } from './utils.js';
|
||||
|
||||
export function toList(
|
||||
std: BlockStdScope,
|
||||
model: BlockModel,
|
||||
listType: ListType,
|
||||
prefix: string,
|
||||
otherProperties?: Partial<ListProps>
|
||||
) {
|
||||
if (!matchModels(model, [ParagraphBlockModel])) {
|
||||
return;
|
||||
}
|
||||
const { store: doc } = std;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
beforeConvert(std, model, prefix.length);
|
||||
|
||||
if (listType !== 'numbered') {
|
||||
const index = parent.children.indexOf(model);
|
||||
const blockProps = {
|
||||
type: listType,
|
||||
text: model.text?.clone(),
|
||||
children: model.children,
|
||||
...otherProperties,
|
||||
};
|
||||
doc.deleteBlock(model, {
|
||||
deleteChildren: false,
|
||||
});
|
||||
|
||||
const id = doc.addBlock('affine:list', blockProps, parent, index);
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
let order = parseInt(prefix.slice(0, -1));
|
||||
if (!Number.isInteger(order)) order = 1;
|
||||
|
||||
const id = toNumberedList(std, model, order);
|
||||
if (!id) return;
|
||||
|
||||
focusTextModel(std, id);
|
||||
return id;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user