mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 00:54:56 +00:00
Compare commits
6 Commits
v0.22.2
...
graphite-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d2d399796 | ||
|
|
6483f36723 | ||
|
|
df2ecf2bec | ||
|
|
148c718a12 | ||
|
|
c4af1e77d0 | ||
|
|
6a0eb80903 |
@@ -886,8 +886,8 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": false
|
||||
"description": "Enable indexer plugin\n@default true\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": true
|
||||
},
|
||||
"provider.type": {
|
||||
"type": "string",
|
||||
|
||||
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -113,7 +113,6 @@ 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:
|
||||
|
||||
6
.github/workflows/build-test.yml
vendored
6
.github/workflows/build-test.yml
vendored
@@ -20,7 +20,6 @@ env:
|
||||
COVERAGE: true
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
DEPLOYMENT_TYPE: affine
|
||||
AFFINE_INDEXER_ENABLED: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -152,8 +151,7 @@ jobs:
|
||||
- name: Clippy
|
||||
run: |
|
||||
rustup component add clippy
|
||||
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
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
check-git-status:
|
||||
name: Check Git Status
|
||||
@@ -925,7 +923,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
run: cargo nextest run --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,10 +117,31 @@ jobs:
|
||||
name: android
|
||||
path: packages/frontend/apps/android/dist
|
||||
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
|
||||
determine-ios-runner:
|
||||
runs-on: ubuntu-latest
|
||||
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-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
napi-derive = { version = "3.0.0-alpha.28" }
|
||||
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.27"
|
||||
text-splitter = "0.25"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c = { version = "0.23" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
@@ -38,7 +37,6 @@ 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';
|
||||
@@ -81,12 +79,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
return this.std.get(FileSizeLimitProvider).maxFileSize;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
return !!this.model.props.footnoteIdentifier;
|
||||
}
|
||||
|
||||
convertTo = () => {
|
||||
@@ -145,34 +139,6 @@ 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();
|
||||
|
||||
@@ -196,8 +162,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -34,7 +33,6 @@ export const RenameModal = ({
|
||||
|
||||
let fileName = includeExtension ? nameWithoutExtension : originalName;
|
||||
const extension = includeExtension ? originalExtension : '';
|
||||
const citationService = editorHost.std.get(CitationProvider);
|
||||
|
||||
const abort = () => abortController.abort();
|
||||
const onConfirm = () => {
|
||||
@@ -46,9 +44,6 @@ export const RenameModal = ({
|
||||
model.store.updateBlock(model, {
|
||||
name: newFileName,
|
||||
});
|
||||
if (citationService.isCitationModel(model)) {
|
||||
citationService.trackEvent('Edit');
|
||||
}
|
||||
abort();
|
||||
};
|
||||
const onInput = (e: InputEvent) => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
LinkPreviewServiceIdentifier,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
@@ -19,7 +18,6 @@ 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';
|
||||
|
||||
@@ -116,12 +114,11 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
);
|
||||
};
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
}
|
||||
|
||||
get imageProxyService() {
|
||||
@@ -169,31 +166,6 @@ 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();
|
||||
|
||||
@@ -231,8 +203,6 @@ 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: EmbedCardStyle = model.props.style;
|
||||
let { style } = model.props;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
@@ -482,26 +482,24 @@ 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',
|
||||
},
|
||||
] as const
|
||||
).filter(action => BookmarkStyles.includes(action.id)),
|
||||
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)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -40,16 +40,6 @@ 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(() => {
|
||||
@@ -403,7 +393,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
true) &&
|
||||
(this.model.props.lineNumber ?? true);
|
||||
|
||||
const preview = this.preview$.value;
|
||||
const preview = !!this.model.props.preview;
|
||||
const previewContext = this.std.getOptional(
|
||||
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
|
||||
);
|
||||
@@ -471,14 +461,6 @@ 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,7 +58,11 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
`;
|
||||
|
||||
private readonly _toggle = (value: boolean) => {
|
||||
this.blockComponent.setPreviewState(value);
|
||||
if (this.blockComponent.store.readonly) return;
|
||||
|
||||
this.blockComponent.store.updateBlock(this.blockComponent.model, {
|
||||
preview: value,
|
||||
});
|
||||
|
||||
const std = this.blockComponent.std;
|
||||
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
@@ -73,7 +77,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
};
|
||||
|
||||
get preview() {
|
||||
return this.blockComponent.preview$.value;
|
||||
return !!this.blockComponent.model.props.preview$.value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
@@ -368,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',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
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)
|
||||
),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
REFERENCE_NODE,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocDisplayMetaProvider,
|
||||
DocModeProvider,
|
||||
OpenDocExtensionIdentifier,
|
||||
@@ -44,7 +43,6 @@ 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';
|
||||
@@ -256,12 +254,11 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
return this.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
}
|
||||
|
||||
private readonly _handleDoubleClick = (event: MouseEvent) => {
|
||||
@@ -457,31 +454,6 @@ 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();
|
||||
|
||||
@@ -560,8 +532,6 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
getInitialState(): {
|
||||
|
||||
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
let { style } = model.props;
|
||||
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: EmbedCardStyle = model.props.style;
|
||||
let { style } = model.props;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style =
|
||||
@@ -441,11 +441,7 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
let { style } = model.props;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (
|
||||
!BookmarkStyles.includes(
|
||||
style as (typeof BookmarkStyles)[number]
|
||||
)
|
||||
) {
|
||||
if (!BookmarkStyles.includes(style)) {
|
||||
style = BookmarkStyles[0];
|
||||
}
|
||||
|
||||
@@ -521,26 +517,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',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedGithubStyles.includes(action.id)),
|
||||
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)
|
||||
),
|
||||
when(ctx) {
|
||||
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 {
|
||||
@@ -88,12 +87,6 @@ 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"
|
||||
@@ -109,7 +102,6 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
|
||||
})}
|
||||
></div>
|
||||
${widgets}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -186,22 +178,11 @@ 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) ||
|
||||
onTitle)
|
||||
(model.isLocked() || !isTransparent(model.props.background))
|
||||
);
|
||||
},
|
||||
onSelect(context) {
|
||||
return context.default(context);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,35 +241,20 @@ export class EdgelessFrameManager extends GfxExtension {
|
||||
surfaceModel.elementAdded.subscribe(({ id, local }) => {
|
||||
const element = surfaceModel.getElementById(id);
|
||||
if (element && local) {
|
||||
// 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
|
||||
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
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
|
||||
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
@@ -66,10 +63,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
?.getPlaceholder(this.model);
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager.getRenderer();
|
||||
}
|
||||
@@ -101,12 +94,6 @@ 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>(
|
||||
@@ -299,13 +286,6 @@ 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>
|
||||
`
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
EmbedCardStyle,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BookmarkStyles,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
@@ -239,7 +236,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
const options: Record<string, unknown> = {};
|
||||
|
||||
let flavour = 'affine:bookmark';
|
||||
let style: EmbedCardStyle = BookmarkStyles[0];
|
||||
let style = BookmarkStyles[0];
|
||||
let isInternalLink = false;
|
||||
let isLinkedBlock = false;
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
) as SurfaceBlockModel;
|
||||
}
|
||||
|
||||
get viewportElement(): HTMLElement {
|
||||
private 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,25 +43,6 @@ 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',
|
||||
@@ -160,18 +141,6 @@ 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>();
|
||||
@@ -217,7 +186,6 @@ export class DomRenderer {
|
||||
private _initViewport() {
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -227,7 +195,6 @@ export class DomRenderer {
|
||||
if (this._sizeUpdatedRafId) return;
|
||||
this._sizeUpdatedRafId = requestConnectedFrame(() => {
|
||||
this._sizeUpdatedRafId = null;
|
||||
this._markSizeDirty();
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
@@ -241,7 +208,6 @@ export class DomRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this._markUsePlaceholderDirty();
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
@@ -341,292 +307,6 @@ 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[] = [];
|
||||
@@ -707,4 +387,100 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ export class OpenDocDropdownMenu extends SignalWatcher(
|
||||
gap: unset !important;
|
||||
}
|
||||
|
||||
editor-icon-button {
|
||||
.label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-orientation] {
|
||||
width: 264px;
|
||||
gap: 4px;
|
||||
|
||||
@@ -9,7 +9,6 @@ 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',
|
||||
|
||||
@@ -16,6 +16,5 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
|
||||
.vars="${filterTrait.view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
.dataViewLogic="${props.dataViewLogic}"
|
||||
></filter-bar>`;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ 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) {
|
||||
@@ -100,7 +99,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
requestAnimationFrame(() => {
|
||||
this.expandGroup(element, index);
|
||||
});
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -208,9 +206,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewLogic!: DataViewUILogicBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -26,10 +26,7 @@ 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 {
|
||||
type DataViewUILogicBase,
|
||||
popCreateFilter,
|
||||
} from '../../../core/index.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import {
|
||||
type FilterGroupView,
|
||||
getDepth,
|
||||
@@ -378,7 +375,6 @@ export const popFilterRoot = (
|
||||
props: {
|
||||
filterTrait: FilterTrait;
|
||||
onBack: () => void;
|
||||
dataViewLogic: DataViewUILogicBase;
|
||||
}
|
||||
) => {
|
||||
const filterTrait = props.filterTrait;
|
||||
@@ -418,10 +414,6 @@ export const popFilterRoot = (
|
||||
...value,
|
||||
conditions: [...value.conditions, filter],
|
||||
});
|
||||
props.dataViewLogic.eventTrace(
|
||||
'CreateDatabaseFilter',
|
||||
{}
|
||||
);
|
||||
},
|
||||
},
|
||||
{ middleware: subMenuMiddleware }
|
||||
|
||||
@@ -75,7 +75,6 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
|
||||
conditions: [filter],
|
||||
};
|
||||
this.toggleShowFilter(true);
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -145,16 +145,13 @@ const createSettingMenus = (
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import {
|
||||
AutoClearSelectionService,
|
||||
CitationService,
|
||||
DefaultOpenDocExtension,
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
@@ -77,7 +76,6 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
|
||||
FileSizeLimitService,
|
||||
LinkPreviewCache,
|
||||
LinkPreviewService,
|
||||
CitationService,
|
||||
]);
|
||||
context.register(clipboardConfigs);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
|
||||
@@ -192,14 +192,10 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
||||
this._updateTitleInMeta();
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
if (this._rootModel) {
|
||||
const rootModel = this._rootModel;
|
||||
rootModel.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
rootModel.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
}
|
||||
this._rootModel?.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
|
||||
|
||||
import { connectorDomRenderer } from './connector-dom/index.js';
|
||||
|
||||
/**
|
||||
* Extension to register the DOM-based renderer for 'connector' elements.
|
||||
*/
|
||||
export const ConnectorDomRendererExtension = DomElementRendererExtension(
|
||||
'connector',
|
||||
connectorDomRenderer
|
||||
);
|
||||
@@ -0,0 +1,367 @@
|
||||
import type { DomRenderer } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
type ConnectorElementModel,
|
||||
ConnectorMode,
|
||||
DefaultTheme,
|
||||
type PointStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
|
||||
|
||||
import { isConnectorWithLabel } from '../../connector-manager.js';
|
||||
import { DEFAULT_ARROW_SIZE } from '../utils.js';
|
||||
|
||||
interface PathBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
function calculatePathBounds(path: PointLocation[]): PathBounds {
|
||||
if (path.length === 0) {
|
||||
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||
}
|
||||
|
||||
let minX = path[0][0];
|
||||
let minY = path[0][1];
|
||||
let maxX = path[0][0];
|
||||
let maxY = path[0][1];
|
||||
|
||||
for (const point of path) {
|
||||
minX = Math.min(minX, point[0]);
|
||||
minY = Math.min(minY, point[1]);
|
||||
maxX = Math.max(maxX, point[0]);
|
||||
maxY = Math.max(maxY, point[1]);
|
||||
}
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
function createConnectorPath(
|
||||
points: PointLocation[],
|
||||
mode: ConnectorMode
|
||||
): string {
|
||||
if (points.length < 2) return '';
|
||||
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
pathBuilder.moveTo(points[0][0], points[0][1]);
|
||||
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
// Use bezier curves
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
pathBuilder.curveTo(
|
||||
prev.absOut[0],
|
||||
prev.absOut[1],
|
||||
curr.absIn[0],
|
||||
curr.absIn[1],
|
||||
curr[0],
|
||||
curr[1]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use straight lines
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
pathBuilder.lineTo(points[i][0], points[i][1]);
|
||||
}
|
||||
}
|
||||
|
||||
return pathBuilder.build();
|
||||
}
|
||||
|
||||
function createArrowMarker(
|
||||
id: string,
|
||||
style: PointStyle,
|
||||
color: string,
|
||||
strokeWidth: number,
|
||||
isStart: boolean = false
|
||||
): SVGMarkerElement {
|
||||
const marker = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'marker'
|
||||
);
|
||||
const size = DEFAULT_ARROW_SIZE * (strokeWidth / 2);
|
||||
|
||||
marker.id = id;
|
||||
marker.setAttribute('viewBox', '0 0 20 20');
|
||||
marker.setAttribute('refX', isStart ? '20' : '0');
|
||||
marker.setAttribute('refY', '10');
|
||||
marker.setAttribute('markerWidth', String(size));
|
||||
marker.setAttribute('markerHeight', String(size));
|
||||
marker.setAttribute('orient', 'auto');
|
||||
marker.setAttribute('markerUnits', 'strokeWidth');
|
||||
|
||||
switch (style) {
|
||||
case 'Arrow': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute(
|
||||
'd',
|
||||
isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z'
|
||||
);
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
case 'Triangle': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute(
|
||||
'd',
|
||||
isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z'
|
||||
);
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
case 'Circle': {
|
||||
const circle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
circle.setAttribute('cx', '10');
|
||||
circle.setAttribute('cy', '10');
|
||||
circle.setAttribute('r', '4');
|
||||
circle.setAttribute('fill', color);
|
||||
circle.setAttribute('stroke', color);
|
||||
marker.append(circle);
|
||||
break;
|
||||
}
|
||||
case 'Diamond': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute('d', 'M 10 6 L 14 10 L 10 14 L 6 10 Z');
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
function renderConnectorLabel(
|
||||
model: ConnectorElementModel,
|
||||
container: HTMLElement,
|
||||
renderer: DomRenderer,
|
||||
zoom: number
|
||||
) {
|
||||
if (!isConnectorWithLabel(model) || !model.labelXYWH) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lx, ly, lw, lh] = model.labelXYWH;
|
||||
const {
|
||||
labelStyle: {
|
||||
color,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fontStyle,
|
||||
fontFamily,
|
||||
textAlign,
|
||||
},
|
||||
} = model;
|
||||
|
||||
// Create label element
|
||||
const labelElement = document.createElement('div');
|
||||
labelElement.style.position = 'absolute';
|
||||
labelElement.style.left = `${lx * zoom}px`;
|
||||
labelElement.style.top = `${ly * zoom}px`;
|
||||
labelElement.style.width = `${lw * zoom}px`;
|
||||
labelElement.style.height = `${lh * zoom}px`;
|
||||
labelElement.style.pointerEvents = 'none';
|
||||
labelElement.style.overflow = 'hidden';
|
||||
labelElement.style.display = 'flex';
|
||||
labelElement.style.alignItems = 'center';
|
||||
labelElement.style.justifyContent =
|
||||
textAlign === 'center'
|
||||
? 'center'
|
||||
: textAlign === 'right'
|
||||
? 'flex-end'
|
||||
: 'flex-start';
|
||||
|
||||
// Style the text
|
||||
labelElement.style.color = renderer.getColorValue(
|
||||
color,
|
||||
DefaultTheme.black,
|
||||
true
|
||||
);
|
||||
labelElement.style.fontSize = `${fontSize * zoom}px`;
|
||||
labelElement.style.fontWeight = fontWeight;
|
||||
labelElement.style.fontStyle = fontStyle;
|
||||
labelElement.style.fontFamily = fontFamily;
|
||||
labelElement.style.textAlign = textAlign;
|
||||
labelElement.style.lineHeight = '1.2';
|
||||
labelElement.style.whiteSpace = 'pre-wrap';
|
||||
labelElement.style.wordWrap = 'break-word';
|
||||
|
||||
// Add text content
|
||||
if (model.text) {
|
||||
labelElement.textContent = model.text.toString();
|
||||
}
|
||||
|
||||
container.append(labelElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a ConnectorElementModel to a given HTMLElement using DOM/SVG.
|
||||
* This function is intended to be registered via the DomElementRendererExtension.
|
||||
*
|
||||
* @param model - The connector element model containing rendering properties.
|
||||
* @param element - The HTMLElement to apply the connector's styles to.
|
||||
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
|
||||
*/
|
||||
export const connectorDomRenderer = (
|
||||
model: ConnectorElementModel,
|
||||
element: HTMLElement,
|
||||
renderer: DomRenderer
|
||||
): void => {
|
||||
const { zoom } = renderer.viewport;
|
||||
const {
|
||||
mode,
|
||||
path: points,
|
||||
strokeStyle,
|
||||
frontEndpointStyle,
|
||||
rearEndpointStyle,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
} = model;
|
||||
|
||||
// Clear previous content
|
||||
element.innerHTML = '';
|
||||
|
||||
// Early return if no path points
|
||||
if (!points || points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bounds for the SVG viewBox
|
||||
const pathBounds = calculatePathBounds(points);
|
||||
const padding = Math.max(strokeWidth * 2, 20); // Add padding for arrows
|
||||
const svgWidth = (pathBounds.maxX - pathBounds.minX + padding * 2) * zoom;
|
||||
const svgHeight = (pathBounds.maxY - pathBounds.minY + padding * 2) * zoom;
|
||||
const offsetX = pathBounds.minX - padding;
|
||||
const offsetY = pathBounds.minY - padding;
|
||||
|
||||
// Create SVG element
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.left = `${offsetX * zoom}px`;
|
||||
svg.style.top = `${offsetY * zoom}px`;
|
||||
svg.style.width = `${svgWidth}px`;
|
||||
svg.style.height = `${svgHeight}px`;
|
||||
svg.style.overflow = 'visible';
|
||||
svg.style.pointerEvents = 'none';
|
||||
svg.setAttribute('viewBox', `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`);
|
||||
|
||||
// Create defs for markers
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
svg.append(defs);
|
||||
|
||||
const strokeColor = renderer.getColorValue(
|
||||
stroke,
|
||||
DefaultTheme.connectorColor,
|
||||
true
|
||||
);
|
||||
|
||||
// Create markers for endpoints
|
||||
let startMarkerId = '';
|
||||
let endMarkerId = '';
|
||||
|
||||
if (frontEndpointStyle !== 'None') {
|
||||
startMarkerId = `start-marker-${model.id}`;
|
||||
const startMarker = createArrowMarker(
|
||||
startMarkerId,
|
||||
frontEndpointStyle,
|
||||
strokeColor,
|
||||
strokeWidth,
|
||||
true
|
||||
);
|
||||
defs.append(startMarker);
|
||||
}
|
||||
|
||||
if (rearEndpointStyle !== 'None') {
|
||||
endMarkerId = `end-marker-${model.id}`;
|
||||
const endMarker = createArrowMarker(
|
||||
endMarkerId,
|
||||
rearEndpointStyle,
|
||||
strokeColor,
|
||||
strokeWidth,
|
||||
false
|
||||
);
|
||||
defs.append(endMarker);
|
||||
}
|
||||
|
||||
// Create path element
|
||||
const pathElement = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
|
||||
// Adjust points relative to the SVG coordinate system
|
||||
const adjustedPoints = points.map(point => {
|
||||
const adjustedPoint = new PointLocation([
|
||||
point[0] - offsetX,
|
||||
point[1] - offsetY,
|
||||
]);
|
||||
if (point.absIn) {
|
||||
adjustedPoint.in = [
|
||||
point.absIn[0] - offsetX - adjustedPoint[0],
|
||||
point.absIn[1] - offsetY - adjustedPoint[1],
|
||||
];
|
||||
}
|
||||
if (point.absOut) {
|
||||
adjustedPoint.out = [
|
||||
point.absOut[0] - offsetX - adjustedPoint[0],
|
||||
point.absOut[1] - offsetY - adjustedPoint[1],
|
||||
];
|
||||
}
|
||||
return adjustedPoint;
|
||||
});
|
||||
|
||||
const pathData = createConnectorPath(adjustedPoints, mode);
|
||||
pathElement.setAttribute('d', pathData);
|
||||
pathElement.setAttribute('stroke', strokeColor);
|
||||
pathElement.setAttribute('stroke-width', String(strokeWidth));
|
||||
pathElement.setAttribute('fill', 'none');
|
||||
pathElement.setAttribute('stroke-linecap', 'round');
|
||||
pathElement.setAttribute('stroke-linejoin', 'round');
|
||||
|
||||
// Apply stroke style
|
||||
if (strokeStyle === 'dash') {
|
||||
pathElement.setAttribute('stroke-dasharray', '12,12');
|
||||
}
|
||||
|
||||
// Apply markers
|
||||
if (startMarkerId) {
|
||||
pathElement.setAttribute('marker-start', `url(#${startMarkerId})`);
|
||||
}
|
||||
if (endMarkerId) {
|
||||
pathElement.setAttribute('marker-end', `url(#${endMarkerId})`);
|
||||
}
|
||||
|
||||
svg.append(pathElement);
|
||||
element.append(svg);
|
||||
|
||||
// Set element size and position
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
element.style.overflow = 'visible';
|
||||
element.style.pointerEvents = 'none';
|
||||
|
||||
// Set z-index for layering
|
||||
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
|
||||
|
||||
// Render label if present
|
||||
renderConnectorLabel(model, element, renderer, zoom);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ export * from './adapter';
|
||||
export * from './connector-manager';
|
||||
export * from './connector-tool';
|
||||
export * from './element-renderer';
|
||||
export { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
|
||||
export * from './element-transform';
|
||||
export * from './text';
|
||||
export * from './toolbar/config';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ConnectionOverlay } from './connector-manager';
|
||||
import { ConnectorTool } from './connector-tool';
|
||||
import { effects } from './effects';
|
||||
import { ConnectorElementRendererExtension } from './element-renderer';
|
||||
import { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
|
||||
import { ConnectorFilter } from './element-transform';
|
||||
import { connectorToolbarExtension } from './toolbar/config';
|
||||
import { connectorQuickTool } from './toolbar/quick-tool';
|
||||
@@ -24,6 +25,7 @@ export class ConnectorViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register(ConnectorElementView);
|
||||
context.register(ConnectorElementRendererExtension);
|
||||
context.register(ConnectorDomRendererExtension);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
context.register(ConnectorTool);
|
||||
context.register(ConnectorFilter);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { type Bound } from '@blocksuite/global/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragMoveContext,
|
||||
@@ -74,63 +74,47 @@ export class SnapExtension extends InteractivityExtension {
|
||||
return {};
|
||||
}
|
||||
|
||||
let alignBound: Bound | null = null;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
snapOverlay.setMovingElements(context.elements);
|
||||
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||
},
|
||||
onResizeMove(context) {
|
||||
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;
|
||||
}
|
||||
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alignDirection.length > 0) {
|
||||
const rst = snapOverlay.alignResize(
|
||||
currentHandlePos,
|
||||
alignDirection
|
||||
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)
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
dx = min * Math.sign(dx) * alignBound.w;
|
||||
dy = min * Math.sign(dy) * alignBound.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, type IVec, Point } from '@blocksuite/global/gfx';
|
||||
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
|
||||
interface Distance {
|
||||
@@ -586,60 +586,6 @@ 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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DomRenderer } from '@blocksuite/affine-block-surface';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { DefaultTheme } from '@blocksuite/affine-model';
|
||||
import { SVGShapeBuilder } from '@blocksuite/global/gfx';
|
||||
|
||||
import { manageClassNames, setStyles } from './utils';
|
||||
|
||||
@@ -122,25 +123,22 @@ export const shapeDomRenderer = (
|
||||
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(' ');
|
||||
// Generate diamond points using shared utility
|
||||
svgPoints = SVGShapeBuilder.diamond(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
// triangle - generate triangle points using shared utility
|
||||
svgPoints = SVGShapeBuilder.triangle(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -118,10 +117,6 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return this.std.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
onFootnoteClick = () => {
|
||||
if (!this.footnote) {
|
||||
return;
|
||||
@@ -220,10 +215,6 @@ 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,8 +188,6 @@ 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}
|
||||
|
||||
@@ -30,12 +30,11 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
|
||||
*/
|
||||
type BackwardCompatibleUndefined = undefined;
|
||||
|
||||
export const AttachmentBlockStyles = [
|
||||
export const AttachmentBlockStyles: EmbedCardStyle[] = [
|
||||
'cubeThick',
|
||||
'horizontalThin',
|
||||
'pdf',
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
] as const;
|
||||
|
||||
export type AttachmentBlockProps = {
|
||||
name: string;
|
||||
|
||||
@@ -15,13 +15,13 @@ import type {
|
||||
LinkPreviewData,
|
||||
} from '../../utils/index.js';
|
||||
|
||||
export const BookmarkStyles = [
|
||||
export const BookmarkStyles: EmbedCardStyle[] = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
] as const;
|
||||
|
||||
export type BookmarkBlockProps = {
|
||||
style: (typeof BookmarkStyles)[number];
|
||||
|
||||
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
|
||||
export type EmbedFigmaBlockProps = {
|
||||
style: (typeof EmbedFigmaStyles)[number];
|
||||
|
||||
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
|
||||
assignees: string[] | null;
|
||||
};
|
||||
|
||||
export const EmbedGithubStyles = [
|
||||
export const EmbedGithubStyles: EmbedCardStyle[] = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
] as const;
|
||||
|
||||
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 = ['html'] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
|
||||
|
||||
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 = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
|
||||
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 = [
|
||||
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'horizontalThin',
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
];
|
||||
|
||||
export type EmbedLinkedDocBlockProps = {
|
||||
style: (typeof EmbedLinkedDocStyles)[number];
|
||||
style: EmbedCardStyle;
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & ReferenceInfo;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
|
||||
export type EmbedLoomBlockProps = {
|
||||
style: (typeof EmbedLoomStyles)[number];
|
||||
|
||||
@@ -5,9 +5,7 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedSyncedDocStyles = [
|
||||
'syncedDoc',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
|
||||
|
||||
export type EmbedSyncedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -31,10 +30,7 @@ export type EmbedCardModel = InstanceType<
|
||||
ExternalEmbedModel | InternalEmbedModel
|
||||
>;
|
||||
|
||||
export type LinkableEmbedModel =
|
||||
| EmbedCardModel
|
||||
| EmbedIframeBlockModel
|
||||
| BookmarkBlockModel;
|
||||
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
|
||||
|
||||
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
|
||||
creatorImage: string | null;
|
||||
};
|
||||
|
||||
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
|
||||
export type EmbedYoutubeBlockProps = {
|
||||
style: (typeof EmbedYoutubeStyles)[number];
|
||||
|
||||
@@ -80,8 +80,4 @@ describe('isValidUrl: determining whether a URL is valid is very complicated', (
|
||||
// See also https://stackoverflow.com/questions/9238640/how-long-can-a-tld-possibly-be#:~:text=Longest%20TLD%20up%20to%20date,17%20when%20decoded%20%5Bverm%C3%B6gensberatung%5D.
|
||||
expect(isValidUrl('example.xn--vermgensberatung-pwb')).toEqual(false);
|
||||
});
|
||||
|
||||
test('should allow ip address url when origin is same', () => {
|
||||
expect(isValidUrl('http://127.0.0.1', 'http://127.0.0.1')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ const handlePoint = (
|
||||
};
|
||||
|
||||
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
slots.afterExport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const snapshot = payload.snapshot;
|
||||
|
||||
@@ -53,14 +53,10 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
return sliceText(slots, std);
|
||||
sliceText(slots, std);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const fileNameMiddleware =
|
||||
(fileName?: string): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'page') {
|
||||
return;
|
||||
}
|
||||
@@ -20,8 +20,4 @@ export const fileNameMiddleware =
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -528,7 +528,7 @@ export const pasteMiddleware = (
|
||||
): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
let tr: PasteTr | undefined;
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
const { snapshot } = payload;
|
||||
flatNote(snapshot);
|
||||
@@ -543,18 +543,13 @@ export const pasteMiddleware = (
|
||||
}
|
||||
}
|
||||
});
|
||||
const afterImportSubscription = slots.afterImport.subscribe(payload => {
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (tr && payload.type === 'slice') {
|
||||
tr.pasted();
|
||||
tr.focusPasted();
|
||||
tr.convertToLinkedDoc();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
afterImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
|
||||
map(({ model }) => model)
|
||||
);
|
||||
|
||||
const afterImportBlockSubscription = afterImportBlock$
|
||||
afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
|
||||
.subscribe(model => {
|
||||
Object.keys(model.props.cells).forEach(cellId => {
|
||||
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// replace LinkedPage pageId with new id in paragraph blocks
|
||||
const replaceLinkedPageIdSubscription = afterImportBlock$
|
||||
afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [ParagraphBlockModel, ListBlockModel])
|
||||
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
|
||||
}
|
||||
});
|
||||
|
||||
const replaceSurfaceRefIdSubscription = afterImportBlock$
|
||||
afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
|
||||
.subscribe(model => {
|
||||
const original = model.props.reference;
|
||||
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// TODO(@fundon): process linked block/element
|
||||
const replaceLinkedDocIdSubscription = afterImportBlock$
|
||||
afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
|
||||
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
|
||||
|
||||
// Before Import
|
||||
|
||||
const beforeImportPageSubscription = slots.beforeImport
|
||||
slots.beforeImport
|
||||
.pipe(filter(payload => payload.type === 'page'))
|
||||
.subscribe(payload => {
|
||||
if (idMap.has(payload.snapshot.meta.id)) {
|
||||
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
|
||||
payload.snapshot.meta.id = newId;
|
||||
});
|
||||
|
||||
const beforeImportBlockSubscription = slots.beforeImport
|
||||
slots.beforeImport
|
||||
.pipe(
|
||||
filter(
|
||||
(payload): payload is BeforeImportBlockPayload =>
|
||||
@@ -244,13 +244,4 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
replaceLinkedPageIdSubscription.unsubscribe();
|
||||
replaceSurfaceRefIdSubscription.unsubscribe();
|
||||
replaceLinkedDocIdSubscription.unsubscribe();
|
||||
beforeImportPageSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,42 +5,33 @@ export const surfaceRefToEmbed =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
let pageId: string | null = null;
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
}
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
}
|
||||
);
|
||||
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
});
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
}
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,13 +3,9 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
|
||||
export const titleMiddleware =
|
||||
(metas: DocMeta[]): TransformerMiddleware =>
|
||||
({ slots, adapterConfigs }) => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
|
||||
slots.beforeExport.subscribe(() => {
|
||||
for (const meta of metas) {
|
||||
adapterConfigs.set('title:' + meta.id, meta.title);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const uploadMiddleware = (
|
||||
}
|
||||
}
|
||||
|
||||
const blockViewSubscription = blockView$
|
||||
blockView$
|
||||
.pipe(
|
||||
map(payload => {
|
||||
if (assetsManager.uploadingAssetsMap.size === 0) return null;
|
||||
@@ -110,9 +110,5 @@ export const uploadMiddleware = (
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
blockViewSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { type BlockStdScope, StdIdentifier } from '@blocksuite/std';
|
||||
import { type BlockModel, Extension } from '@blocksuite/store';
|
||||
|
||||
import { DocModeProvider } from '../doc-mode-service';
|
||||
import type {
|
||||
CitationEvents,
|
||||
CitationEventType,
|
||||
} from '../telemetry-service/citation';
|
||||
import { TelemetryProvider } from '../telemetry-service/telemetry-service';
|
||||
|
||||
const CitationEventTypeMap = {
|
||||
Hover: 'AICitationHoverSource',
|
||||
Expand: 'AICitationExpandSource',
|
||||
Delete: 'AICitationDelete',
|
||||
Edit: 'AICitationEdit',
|
||||
} as const;
|
||||
|
||||
type EventType = keyof typeof CitationEventTypeMap;
|
||||
|
||||
type EventTypeMapping = {
|
||||
[K in EventType]: CitationEventType;
|
||||
};
|
||||
|
||||
export interface CitationViewService {
|
||||
/**
|
||||
* Tracks citation-related events
|
||||
* @param type - The type of citation event to track
|
||||
* @param properties - The properties of the event
|
||||
*/
|
||||
trackEvent<T extends EventType>(
|
||||
type: T,
|
||||
properties?: CitationEvents[EventTypeMapping[T]]
|
||||
): void;
|
||||
/**
|
||||
* Checks if the model is a citation model
|
||||
* @param model - The model to check
|
||||
* @returns True if the model is a citation model, false otherwise
|
||||
*/
|
||||
isCitationModel(model: BlockModel): boolean;
|
||||
}
|
||||
|
||||
export const CitationProvider =
|
||||
createIdentifier<CitationViewService>('CitationService');
|
||||
|
||||
export class CitationService extends Extension implements CitationViewService {
|
||||
constructor(private readonly std: BlockStdScope) {
|
||||
super();
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(CitationProvider, CitationService, [StdIdentifier]);
|
||||
}
|
||||
|
||||
get docModeService() {
|
||||
return this.std.getOptional(DocModeProvider);
|
||||
}
|
||||
|
||||
get telemetryService() {
|
||||
return this.std.getOptional(TelemetryProvider);
|
||||
}
|
||||
|
||||
isCitationModel = (model: BlockModel) => {
|
||||
return (
|
||||
'footnoteIdentifier' in model.props &&
|
||||
!!model.props.footnoteIdentifier &&
|
||||
'style' in model.props &&
|
||||
model.props.style === 'citation'
|
||||
);
|
||||
};
|
||||
|
||||
trackEvent<T extends EventType>(
|
||||
type: T,
|
||||
properties?: CitationEvents[EventTypeMapping[T]]
|
||||
) {
|
||||
const editorMode = this.docModeService?.getEditorMode() ?? 'page';
|
||||
this.telemetryService?.track(CitationEventTypeMap[type], {
|
||||
page: editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
|
||||
module: 'AI Result',
|
||||
control: 'Source',
|
||||
...properties,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './citation-service';
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './auto-clear-selection-service';
|
||||
export * from './block-meta-service';
|
||||
export * from './citation-service';
|
||||
export * from './doc-display-meta-service';
|
||||
export * from './doc-mode-service';
|
||||
export * from './drag-handle-config';
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { TelemetryEvent } from './types';
|
||||
export type CitationEventType =
|
||||
| 'AICitationHoverSource'
|
||||
| 'AICitationExpandSource'
|
||||
| 'AICitationDelete'
|
||||
| 'AICitationEdit';
|
||||
|
||||
export type CitationEvents = Record<CitationEventType, TelemetryEvent>;
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './citation.js';
|
||||
export * from './database.js';
|
||||
export * from './link.js';
|
||||
export * from './telemetry-service.js';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import type { CitationEvents } from './citation.js';
|
||||
import type { CodeBlockEvents } from './code-block.js';
|
||||
import type { OutDatabaseAllEvents } from './database.js';
|
||||
import type { LinkToolbarEvents } from './link.js';
|
||||
@@ -29,8 +28,7 @@ export type TelemetryEventMap = OutDatabaseAllEvents &
|
||||
LinkToolbarEvents &
|
||||
SlashMenuEvents &
|
||||
CodeBlockEvents &
|
||||
NoteEvents &
|
||||
CitationEvents & {
|
||||
NoteEvents & {
|
||||
DocCreated: DocCreatedEvent;
|
||||
Link: TelemetryEvent;
|
||||
LinkedDocCreated: LinkedDocCreatedEvent;
|
||||
|
||||
@@ -11,9 +11,6 @@ const ALLOWED_SCHEMES = new Set([
|
||||
// https://publicsuffix.org/
|
||||
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
|
||||
|
||||
const IPV4_ADDR_REGEXP =
|
||||
/^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
|
||||
|
||||
const toURL = (str: string) => {
|
||||
try {
|
||||
if (!URL.canParse(str)) return null;
|
||||
@@ -24,20 +21,16 @@ const toURL = (str: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
function resolveURL(str: string, baseUrl: string, padded = false) {
|
||||
function resolveURL(str: string) {
|
||||
const url = toURL(str);
|
||||
if (!url) return null;
|
||||
|
||||
const protocol = url.protocol.substring(0, url.protocol.length - 1);
|
||||
const hostname = url.hostname;
|
||||
const origin = url.origin;
|
||||
|
||||
let allowed = ALLOWED_SCHEMES.has(protocol);
|
||||
if (allowed && hostname.includes('.')) {
|
||||
allowed =
|
||||
origin === baseUrl ||
|
||||
TLD_REGEXP.test(hostname) ||
|
||||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
|
||||
allowed = TLD_REGEXP.test(hostname);
|
||||
}
|
||||
|
||||
return { url, allowed };
|
||||
@@ -75,10 +68,10 @@ export function normalizeUrl(str: string) {
|
||||
*
|
||||
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
|
||||
*/
|
||||
export function isValidUrl(str: string, baseUrl = location.origin) {
|
||||
export function isValidUrl(str: string) {
|
||||
str = str.trim();
|
||||
|
||||
let result = resolveURL(str, baseUrl);
|
||||
let result = resolveURL(str);
|
||||
|
||||
if (result && !result.allowed) return false;
|
||||
|
||||
@@ -87,7 +80,7 @@ export function isValidUrl(str: string, baseUrl = location.origin) {
|
||||
if (!hasScheme) {
|
||||
const dotIdx = str.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx < str.length - 1) {
|
||||
result = resolveURL(`https://${str}`, baseUrl, true);
|
||||
result = resolveURL(`https://${str}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
|
||||
return ({ slots, transformerConfigs }) => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
|
||||
slots.beforeExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
});
|
||||
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
slots.afterExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -110,10 +110,5 @@ export const gfxBlocksFilter = (
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
EmbedGithubModel,
|
||||
EmbedLinkedDocModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const cardStyleUpdater =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'block' || !payload.parent) return;
|
||||
const parentModel = std.store.getModelById(payload.parent);
|
||||
if (!matchModels(parentModel, [NoteBlockModel])) return;
|
||||
|
||||
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
|
||||
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
|
||||
// but the instance of it is not changed.
|
||||
// So change the style of snapshot.props in the middleware is not working.
|
||||
// Instead, we can change the style of the model instance in the middleware,
|
||||
const model = std.store.getModelById(payload.snapshot.id);
|
||||
if (!model) return;
|
||||
|
||||
if (model instanceof AttachmentBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontalThin',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof BookmarkBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedGithubModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedLinkedDocModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,45 +9,36 @@ export const newIdCrossDoc =
|
||||
let samePage = false;
|
||||
const oldToNewIdMap = new Map<string, string>();
|
||||
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
}
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
}
|
||||
);
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,25 +7,19 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const reorderList =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
}
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -76,7 +76,6 @@ import last from 'lodash-es/last';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import { PreviewHelper } from '../helpers/preview-helper.js';
|
||||
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
|
||||
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
|
||||
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
|
||||
import { reorderList } from '../middleware/reorder-list';
|
||||
import {
|
||||
@@ -1434,7 +1433,6 @@ export class DragEventWatcher {
|
||||
newIdCrossDoc(std),
|
||||
reorderList(std),
|
||||
surfaceRefToEmbed(std),
|
||||
cardStyleUpdater(std),
|
||||
];
|
||||
|
||||
if (selectedIds) {
|
||||
|
||||
@@ -374,8 +374,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize' | 'rotate';
|
||||
angle: number;
|
||||
handle: ResizeHandle;
|
||||
flipX?: boolean;
|
||||
flipY?: boolean;
|
||||
pure?: boolean;
|
||||
}) => {
|
||||
if (!options) {
|
||||
@@ -383,25 +381,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const { type, angle, flipX, flipY } = options;
|
||||
const { type, angle, handle } = options;
|
||||
let cursor: CursorType = 'default';
|
||||
let handle: ResizeHandle = options.handle;
|
||||
|
||||
if (flipX) {
|
||||
handle = (
|
||||
handle.includes('left')
|
||||
? handle.replace('left', 'right')
|
||||
: handle.replace('right', 'left')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (flipY) {
|
||||
handle = (
|
||||
handle.includes('top')
|
||||
? handle.replace('top', 'bottom')
|
||||
: handle.replace('bottom', 'top')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (type === 'rotate') {
|
||||
cursor = generateCursorUrl(angle, handle);
|
||||
@@ -645,7 +626,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
onResizeStart: () => {
|
||||
this._mode = 'resize';
|
||||
},
|
||||
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
|
||||
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
|
||||
if (lockRatio) {
|
||||
this._scaleDirection = handle;
|
||||
this._scalePercent = `${Math.round(scaleX * 100)}%`;
|
||||
@@ -661,8 +642,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize',
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
handle,
|
||||
flipX: scaleX < 0,
|
||||
flipY: scaleY < 0,
|
||||
});
|
||||
},
|
||||
onResizeEnd: () => {
|
||||
@@ -673,14 +652,6 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
}
|
||||
},
|
||||
option => {
|
||||
if (
|
||||
['resize', 'rotate'].includes(
|
||||
interaction.activeInteraction$.value?.type ?? ''
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this._updateCursor({
|
||||
...option,
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
import { type FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<FrameBlockModel> {
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
|
||||
private get _frames() {
|
||||
return Object.values(this.store.blocks.value)
|
||||
.map(({ model }) => model)
|
||||
.filter(model => model instanceof FrameBlockModel);
|
||||
}
|
||||
|
||||
getFrameTitle(frame: FrameBlockModel | string) {
|
||||
const id = typeof frame === 'string' ? frame : frame.id;
|
||||
const frameTitle = this.shadowRoot?.querySelector(
|
||||
`affine-frame-title[data-id="${id}"]`
|
||||
) as AffineFrameTitle | null;
|
||||
return frameTitle;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<affine-frame-title
|
||||
.model=${this.model}
|
||||
data-id=${this.model.id}
|
||||
></affine-frame-title>`;
|
||||
return repeat(
|
||||
this._frames,
|
||||
({ id }) => id,
|
||||
frame =>
|
||||
html`<affine-frame-title
|
||||
.model=${frame}
|
||||
data-id=${frame.id}
|
||||
></affine-frame-title>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const frameTitleWidget = WidgetViewExtension(
|
||||
'affine:frame',
|
||||
'affine:page',
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
} from './affine-frame-title-widget';
|
||||
import type { AffineFrameTitle } from './frame-title';
|
||||
import { frameTitleStyleVars } from './styles';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
@@ -136,13 +135,12 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
|
||||
const frameTitleWidget = this.edgeless.std.view.getWidget(
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
this.frameModel.id
|
||||
rootBlockId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
if (!frameTitleWidget) return nothing;
|
||||
|
||||
const frameTitle =
|
||||
frameTitleWidget.querySelector<AffineFrameTitle>('affine-frame-title');
|
||||
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
|
||||
|
||||
const colors = frameTitle?.colors ?? {
|
||||
background: cssVarV2('edgeless/frame/background/white'),
|
||||
|
||||
@@ -142,10 +142,12 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
}px)`,
|
||||
];
|
||||
|
||||
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
|
||||
|
||||
this.style.display = '';
|
||||
this.style.setProperty('--bg-color', this.colors.background);
|
||||
this.style.left = '0px';
|
||||
this.style.top = '0px';
|
||||
this.style.left = `${anchor[0]}px`;
|
||||
this.style.top = `${anchor[1]}px`;
|
||||
this.style.display = hidden ? 'none' : 'flex';
|
||||
this.style.transform = transformOperation.join(' ');
|
||||
this.style.maxWidth = `${maxWidth}px`;
|
||||
@@ -203,6 +205,18 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'click', evt => {
|
||||
if (evt.shiftKey) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
|
||||
|
||||
@@ -566,29 +566,23 @@ Optional flag to insert before sibling
|
||||
|
||||
### updateBlock()
|
||||
|
||||
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
|
||||
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
|
||||
|
||||
Updates a block's properties or executes a callback in a transaction
|
||||
|
||||
#### Type Parameters
|
||||
|
||||
##### T
|
||||
|
||||
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### modelOrId
|
||||
|
||||
The block model or block ID to update
|
||||
|
||||
`string` | `T`
|
||||
`string` | `BlockModel`\<`object`\>
|
||||
|
||||
##### callBackOrProps
|
||||
|
||||
Either a callback function to execute or properties to update
|
||||
|
||||
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
|
||||
`Partial`\<`BlockProps`\> | () => `void`
|
||||
|
||||
#### Returns
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { SVGPathBuilder, SVGShapeBuilder } from '../gfx/svg-path.js';
|
||||
|
||||
describe('SVGPathBuilder', () => {
|
||||
test('should build a simple path', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder.moveTo(10, 20).lineTo(30, 40).build();
|
||||
|
||||
expect(result).toBe('M 10 20 L 30 40');
|
||||
});
|
||||
|
||||
test('should build a path with curves', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder
|
||||
.moveTo(0, 0)
|
||||
.curveTo(10, 0, 10, 10, 20, 10)
|
||||
.build();
|
||||
|
||||
expect(result).toBe('M 0 0 C 10 0 10 10 20 10');
|
||||
});
|
||||
|
||||
test('should build a closed path', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder
|
||||
.moveTo(0, 0)
|
||||
.lineTo(10, 0)
|
||||
.lineTo(5, 10)
|
||||
.closePath()
|
||||
.build();
|
||||
|
||||
expect(result).toBe('M 0 0 L 10 0 L 5 10 Z');
|
||||
});
|
||||
|
||||
test('should clear commands', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
pathBuilder.moveTo(10, 20).lineTo(30, 40);
|
||||
pathBuilder.clear();
|
||||
const result = pathBuilder.moveTo(0, 0).build();
|
||||
|
||||
expect(result).toBe('M 0 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SVGShapeBuilder', () => {
|
||||
test('should generate diamond polygon points', () => {
|
||||
const result = SVGShapeBuilder.diamond(100, 80, 2);
|
||||
expect(result).toBe('50,1 99,40 50,79 1,40');
|
||||
});
|
||||
|
||||
test('should generate triangle polygon points', () => {
|
||||
const result = SVGShapeBuilder.triangle(100, 80, 2);
|
||||
expect(result).toBe('50,1 99,79 1,79');
|
||||
});
|
||||
|
||||
test('should generate diamond path', () => {
|
||||
const result = SVGShapeBuilder.diamondPath(100, 80, 2);
|
||||
expect(result).toBe('M 50 1 L 99 40 L 50 79 L 1 40 Z');
|
||||
});
|
||||
|
||||
test('should generate triangle path', () => {
|
||||
const result = SVGShapeBuilder.trianglePath(100, 80, 2);
|
||||
expect(result).toBe('M 50 1 L 99 79 L 1 79 Z');
|
||||
});
|
||||
|
||||
test('should handle zero stroke width', () => {
|
||||
const diamondResult = SVGShapeBuilder.diamond(100, 80, 0);
|
||||
expect(diamondResult).toBe('50,0 100,40 50,80 0,40');
|
||||
|
||||
const triangleResult = SVGShapeBuilder.triangle(100, 80, 0);
|
||||
expect(triangleResult).toBe('50,0 100,80 0,80');
|
||||
});
|
||||
});
|
||||
@@ -4,4 +4,5 @@ export * from './math.js';
|
||||
export * from './model/index.js';
|
||||
export * from './perfect-freehand/index.js';
|
||||
export * from './polyline.js';
|
||||
export * from './svg-path.js';
|
||||
export * from './xywh.js';
|
||||
|
||||
160
blocksuite/framework/global/src/gfx/svg-path.ts
Normal file
160
blocksuite/framework/global/src/gfx/svg-path.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
interface PathCommand {
|
||||
command: string;
|
||||
coordinates: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility class for building SVG path strings using command-based API.
|
||||
* Supports moveTo, lineTo, curveTo operations and can build complete path strings.
|
||||
*/
|
||||
export class SVGPathBuilder {
|
||||
private commands: PathCommand[] = [];
|
||||
|
||||
/**
|
||||
* Move to a specific point without drawing
|
||||
*/
|
||||
moveTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'M',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line to a specific point
|
||||
*/
|
||||
lineTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'L',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a cubic Bézier curve
|
||||
*/
|
||||
curveTo(
|
||||
cp1x: number,
|
||||
cp1y: number,
|
||||
cp2x: number,
|
||||
cp2y: number,
|
||||
x: number,
|
||||
y: number
|
||||
): this {
|
||||
this.commands.push({
|
||||
command: 'C',
|
||||
coordinates: [cp1x, cp1y, cp2x, cp2y, x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current path
|
||||
*/
|
||||
closePath(): this {
|
||||
this.commands.push({
|
||||
command: 'Z',
|
||||
coordinates: [],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the complete SVG path string
|
||||
*/
|
||||
build(): string {
|
||||
const pathSegments = this.commands.map(cmd => {
|
||||
const coords = cmd.coordinates.join(' ');
|
||||
return coords ? `${cmd.command} ${coords}` : cmd.command;
|
||||
});
|
||||
|
||||
return pathSegments.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all commands and reset the builder
|
||||
*/
|
||||
clear(): this {
|
||||
this.commands = [];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SVG polygon points string for common shapes
|
||||
*/
|
||||
export class SVGShapeBuilder {
|
||||
/**
|
||||
* Generate diamond (rhombus) polygon points
|
||||
*/
|
||||
static diamond(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height / 2}`,
|
||||
`${width / 2},${height - halfStroke}`,
|
||||
`${halfStroke},${height / 2}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate triangle polygon points
|
||||
*/
|
||||
static triangle(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height - halfStroke}`,
|
||||
`${halfStroke},${height - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate diamond path using SVGPathBuilder
|
||||
*/
|
||||
static diamondPath(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
|
||||
return pathBuilder
|
||||
.moveTo(width / 2, halfStroke)
|
||||
.lineTo(width - halfStroke, height / 2)
|
||||
.lineTo(width / 2, height - halfStroke)
|
||||
.lineTo(halfStroke, height / 2)
|
||||
.closePath()
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate triangle path using SVGPathBuilder
|
||||
*/
|
||||
static trianglePath(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
|
||||
return pathBuilder
|
||||
.moveTo(width / 2, halfStroke)
|
||||
.lineTo(width - halfStroke, height - halfStroke)
|
||||
.lineTo(halfStroke, height - halfStroke)
|
||||
.closePath()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -220,12 +220,6 @@ export class UIEventDispatcher extends LifeCycleWatcher {
|
||||
|
||||
this._setActive(false);
|
||||
});
|
||||
// When the document is hidden, the event dispatcher should be inactive
|
||||
this.disposables.addFromEvent(document, 'visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
this._setActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildEventScopeBySelection(name: EventName) {
|
||||
|
||||
@@ -947,34 +947,23 @@ export class InteractivityManager extends GfxExtension {
|
||||
...options,
|
||||
lockRatio,
|
||||
elements,
|
||||
onResizeMove: ({
|
||||
scaleX,
|
||||
scaleY,
|
||||
originalBound,
|
||||
handleSign,
|
||||
handlePos,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
}) => {
|
||||
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
|
||||
const suggested: {
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
priority?: number;
|
||||
}[] = [];
|
||||
const suggest = (distance: { scaleX: number; scaleY: number }) => {
|
||||
const suggest = (distance: { dx: number; dy: number }) => {
|
||||
suggested.push(distance);
|
||||
};
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeMove?.({
|
||||
scaleX,
|
||||
scaleY,
|
||||
dx,
|
||||
dy,
|
||||
elements,
|
||||
handle,
|
||||
handleSign,
|
||||
handlePos,
|
||||
originalBound,
|
||||
currentHandlePos,
|
||||
handle,
|
||||
lockRatio,
|
||||
suggest,
|
||||
});
|
||||
@@ -984,9 +973,9 @@ export class InteractivityManager extends GfxExtension {
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
});
|
||||
|
||||
return last(suggested) ?? { scaleX, scaleY };
|
||||
return last(suggested) ?? { dx, dy };
|
||||
},
|
||||
onResizeStart: ({ handleSign, handlePos, data }) => {
|
||||
onResizeStart: ({ data }) => {
|
||||
this.activeInteraction$.value = {
|
||||
type: 'resize',
|
||||
elements,
|
||||
@@ -995,8 +984,6 @@ export class InteractivityManager extends GfxExtension {
|
||||
ext.onResizeStart?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1058,15 +1045,13 @@ export class InteractivityManager extends GfxExtension {
|
||||
|
||||
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
|
||||
},
|
||||
onResizeEnd: ({ handleSign, handlePos, data }) => {
|
||||
onResizeEnd: ({ data }) => {
|
||||
this.activeInteraction$.value = null;
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeEnd?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
options.onResizeEnd?.();
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Bound,
|
||||
getCommonBoundWithRotation,
|
||||
type IBound,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
@@ -30,7 +29,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
|
||||
'bottom',
|
||||
];
|
||||
|
||||
type ReadonlyIBound = Readonly<Required<IBound>>;
|
||||
type ElementInitialSnapshot = Readonly<Required<IBound>>;
|
||||
|
||||
export interface OptionResize {
|
||||
elements: GfxModel[];
|
||||
@@ -38,18 +37,16 @@ export interface OptionResize {
|
||||
lockRatio: boolean;
|
||||
event: PointerEvent;
|
||||
onResizeMove: (payload: {
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
|
||||
originalBound: IBound;
|
||||
|
||||
handleSign: IPoint;
|
||||
|
||||
handlePos: IVec;
|
||||
currentHandlePos: IVec;
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
|
||||
lockRatio: boolean;
|
||||
}) => { scaleX: number; scaleY: number };
|
||||
}) => { dx: number; dy: number };
|
||||
onResizeUpdate: (payload: {
|
||||
lockRatio: boolean;
|
||||
scaleX: number;
|
||||
@@ -62,16 +59,8 @@ export interface OptionResize {
|
||||
matrix: DOMMatrix;
|
||||
}[];
|
||||
}) => void;
|
||||
onResizeStart?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
onResizeEnd?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
}
|
||||
|
||||
export type RotateOption = {
|
||||
@@ -106,102 +95,11 @@ export class ResizeController {
|
||||
this.gfx = option.gfx;
|
||||
}
|
||||
|
||||
getCoordsTransform(originalBound: IBound, handle: ResizeHandle) {
|
||||
const { x: xSign, y: ySign } = this.getHandleSign(handle);
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(
|
||||
originalBound.w / 2 + originalBound.x,
|
||||
originalBound.h / 2 + originalBound.y
|
||||
)
|
||||
.rotate(-(originalBound.rotate ?? 0))
|
||||
.translate(
|
||||
-(originalBound.w / 2 + originalBound.x),
|
||||
-(originalBound.h / 2 + originalBound.y)
|
||||
);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean = false) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handlePos = toModel(
|
||||
new DOMPoint(originalBound.w * xSign, originalBound.h * ySign)
|
||||
);
|
||||
|
||||
return {
|
||||
xSign,
|
||||
ySign,
|
||||
originalBound,
|
||||
toLocalM,
|
||||
toLocalRotatedM,
|
||||
toLocal,
|
||||
toModel,
|
||||
handlePos: [handlePos.x, handlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleFromDelta(
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
delta: { dx: number; dy: number },
|
||||
handleStartPos: IVec,
|
||||
lockRatio: boolean
|
||||
) {
|
||||
const { originalBound, xSign, ySign, toModel, toLocal } = transform;
|
||||
const currentPos = toLocal(
|
||||
new DOMPoint(handleStartPos[0] + delta.dx, handleStartPos[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign ? currentPos.x / (originalBound.w * xSign) : 1;
|
||||
let scaleY = ySign ? currentPos.y / (originalBound.h * ySign) : 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const finalHandlePos = toModel(
|
||||
new DOMPoint(
|
||||
originalBound.w * xSign * scaleX,
|
||||
originalBound.h * ySign * scaleY
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: [finalHandlePos.x, finalHandlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleMatrix(
|
||||
{ scaleX, scaleY }: { scaleX: number; scaleY: number },
|
||||
lockRatio: boolean
|
||||
) {
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleM: new DOMMatrix().scaleSelf(scaleX, scaleY),
|
||||
};
|
||||
}
|
||||
|
||||
startResize(options: OptionResize) {
|
||||
const {
|
||||
elements,
|
||||
handle,
|
||||
lockRatio,
|
||||
onResizeStart,
|
||||
onResizeMove,
|
||||
onResizeUpdate,
|
||||
@@ -209,32 +107,19 @@ export class ResizeController {
|
||||
event,
|
||||
} = options;
|
||||
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
h: el.h,
|
||||
rotate: el.rotate,
|
||||
}));
|
||||
const originalBound: IBound =
|
||||
originals.length > 1
|
||||
? getCommonBoundWithRotation(originals)
|
||||
: {
|
||||
x: originals[0].x,
|
||||
y: originals[0].y,
|
||||
w: originals[0].w,
|
||||
h: originals[0].h,
|
||||
rotate: originals[0].rotate,
|
||||
};
|
||||
const originalBound = getCommonBoundWithRotation(originals);
|
||||
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
]);
|
||||
const transform = this.getCoordsTransform(originalBound, handle);
|
||||
const handleSign = {
|
||||
x: transform.xSign,
|
||||
y: transform.ySign,
|
||||
};
|
||||
const handleSign = this.getHandleSign(handle);
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
@@ -245,69 +130,45 @@ export class ResizeController {
|
||||
dx: currPt[0] - startPt[0],
|
||||
dy: currPt[1] - startPt[1],
|
||||
};
|
||||
const shouldLockRatio =
|
||||
options.lockRatio || e.shiftKey || elements.length > 1;
|
||||
const {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: currentHandlePos,
|
||||
} = this.getScaleFromDelta(
|
||||
transform,
|
||||
delta,
|
||||
transform.handlePos,
|
||||
shouldLockRatio
|
||||
);
|
||||
|
||||
const scale = onResizeMove({
|
||||
scaleX,
|
||||
scaleY,
|
||||
|
||||
originalBound,
|
||||
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||
|
||||
delta = onResizeMove({
|
||||
dx: delta.dx,
|
||||
dy: delta.dy,
|
||||
handleSign,
|
||||
|
||||
handlePos: transform.handlePos,
|
||||
currentHandlePos,
|
||||
|
||||
lockRatio: shouldLockRatio,
|
||||
});
|
||||
const scaleInfo = this.getScaleMatrix(scale, shouldLockRatio);
|
||||
|
||||
if (elements.length === 1) {
|
||||
this.resizeSingle(
|
||||
originals[0],
|
||||
elements[0],
|
||||
shouldLockRatio,
|
||||
transform,
|
||||
scaleInfo,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
onResizeUpdate
|
||||
);
|
||||
} else {
|
||||
this.resizeMulti(
|
||||
originalBound,
|
||||
originals,
|
||||
elements,
|
||||
transform,
|
||||
scaleInfo,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
onResizeUpdate
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStart?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
onResizeStart?.({ data: elements.map(model => ({ model })) });
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.host.removeEventListener('pointermove', onPointerMove);
|
||||
this.host.removeEventListener('pointerup', onPointerUp);
|
||||
|
||||
onResizeEnd?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
onResizeEnd?.({ data: elements.map(model => ({ model })) });
|
||||
};
|
||||
|
||||
this.host.addEventListener('pointermove', onPointerMove);
|
||||
@@ -315,15 +176,55 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeSingle(
|
||||
orig: ReadonlyIBound,
|
||||
orig: ElementInitialSnapshot,
|
||||
model: GfxModel,
|
||||
lockRatio: boolean,
|
||||
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
const { xSign, ySign } = handleSign;
|
||||
|
||||
const pivot = new DOMPoint(
|
||||
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||
orig.y + (-ySign === 1 ? orig.h : 0)
|
||||
);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
|
||||
.rotate(-orig.rotate)
|
||||
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
|
||||
: 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
|
||||
const [visualTopLeft, visualBottomRight] = [
|
||||
new DOMPoint(orig.x, orig.y),
|
||||
@@ -381,14 +282,45 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeMulti(
|
||||
originals: ReadonlyIBound[],
|
||||
originalBound: Bound,
|
||||
originals: ElementInitialSnapshot[],
|
||||
elements: GfxModel[],
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { toLocalM } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
const { xSign, ySign } = handleSign;
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy)
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
|
||||
originalBound.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
|
||||
originalBound.h
|
||||
: 1;
|
||||
|
||||
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
|
||||
const data = elements.map((model, i) => {
|
||||
const orig = originals[i];
|
||||
@@ -425,7 +357,7 @@ export class ResizeController {
|
||||
startRotate(option: RotateOption) {
|
||||
const { event, elements, onRotateUpdate } = option;
|
||||
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
@@ -497,7 +429,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateSingle(option: {
|
||||
orig: ReadonlyIBound;
|
||||
orig: ElementInitialSnapshot;
|
||||
model: GfxModel;
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -549,7 +481,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateMulti(option: {
|
||||
origs: ReadonlyIBound[];
|
||||
origs: ElementInitialSnapshot[];
|
||||
models: GfxModel[];
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -635,23 +567,23 @@ export class ResizeController {
|
||||
private getHandleSign(handle: ResizeHandle) {
|
||||
switch (handle) {
|
||||
case 'top-left':
|
||||
return { x: -1, y: -1 };
|
||||
return { xSign: -1, ySign: -1 };
|
||||
case 'top':
|
||||
return { x: 0, y: -1 };
|
||||
return { xSign: 0, ySign: -1 };
|
||||
case 'top-right':
|
||||
return { x: 1, y: -1 };
|
||||
return { xSign: 1, ySign: -1 };
|
||||
case 'right':
|
||||
return { x: 1, y: 0 };
|
||||
return { xSign: 1, ySign: 0 };
|
||||
case 'bottom-right':
|
||||
return { x: 1, y: 1 };
|
||||
return { xSign: 1, ySign: 1 };
|
||||
case 'bottom':
|
||||
return { x: 0, y: 1 };
|
||||
return { xSign: 0, ySign: 1 };
|
||||
case 'bottom-left':
|
||||
return { x: -1, y: 1 };
|
||||
return { xSign: -1, ySign: 1 };
|
||||
case 'left':
|
||||
return { x: -1, y: 0 };
|
||||
return { xSign: -1, ySign: 0 };
|
||||
default:
|
||||
return { x: 0, y: 0 };
|
||||
return { xSign: 0, ySign: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxModel } from '../../model/model';
|
||||
import type { ResizeHandle } from '../resize/manager';
|
||||
|
||||
@@ -10,16 +8,6 @@ export type ExtensionElementResizeContext = {
|
||||
export type ExtensionElementResizeStartContext = {
|
||||
elements: GfxModel[];
|
||||
|
||||
/**
|
||||
* The position of the handle in the browser coordinate space.
|
||||
*/
|
||||
handlePos: IVec;
|
||||
|
||||
/**
|
||||
* The sign (or normal vector) of the handle.
|
||||
*/
|
||||
handleSign: IPoint;
|
||||
|
||||
handle: ResizeHandle;
|
||||
};
|
||||
|
||||
@@ -28,14 +16,15 @@ export type ExtensionElementResizeEndContext =
|
||||
|
||||
export type ExtensionElementResizeMoveContext =
|
||||
ExtensionElementResizeStartContext & {
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
originalBound: IBound;
|
||||
|
||||
currentHandlePos: IVec;
|
||||
dx: number;
|
||||
dy: number;
|
||||
|
||||
lockRatio: boolean;
|
||||
|
||||
suggest: (distance: { scaleX: number; scaleY: number }) => void;
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
|
||||
suggest: (distance: { dx: number; dy: number }) => void;
|
||||
};
|
||||
|
||||
@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
|
||||
const block = store.getModelById(payload.id);
|
||||
|
||||
if (block instanceof GfxBlockElementModel) {
|
||||
this.delete(block);
|
||||
this.delete(block as GfxBlockElementModel);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -834,29 +834,20 @@ export class LayerManager extends GfxExtension {
|
||||
const watchSurface = (surface: SurfaceBlockModel) => {
|
||||
let lastChildMap = new Map(surface.childMap.peek());
|
||||
this._disposable.add(
|
||||
surface.childMap.subscribe(currentChildMap => {
|
||||
currentChildMap.forEach((_, id) => {
|
||||
surface.childMap.subscribe(val => {
|
||||
val.forEach((_, id) => {
|
||||
if (lastChildMap.has(id)) {
|
||||
lastChildMap.delete(id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
lastChildMap.forEach((_, id) => {
|
||||
const model = this._doc.getModelById(id);
|
||||
if (model instanceof GfxBlockElementModel) {
|
||||
this.delete(model);
|
||||
const block = this._doc.getBlock(id);
|
||||
if (block?.model) {
|
||||
this.delete(block.model as GfxBlockElementModel);
|
||||
}
|
||||
});
|
||||
currentChildMap.forEach((_, id) => {
|
||||
const model = store.getModelById(id);
|
||||
if (
|
||||
model instanceof GfxBlockElementModel &&
|
||||
!this.blocks.includes(model)
|
||||
) {
|
||||
this.add(model);
|
||||
}
|
||||
});
|
||||
lastChildMap = new Map(currentChildMap);
|
||||
lastChildMap = new Map(val);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -19,5 +19,3 @@ export type BlockSysProps = {
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
|
||||
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
type BlockModel,
|
||||
type BlockOptions,
|
||||
type BlockProps,
|
||||
type BlockSysProps,
|
||||
type PropsOfModel,
|
||||
type YBlock,
|
||||
} from '../block/index.js';
|
||||
import { DocCRUD } from './crud.js';
|
||||
@@ -854,12 +852,9 @@ export class Store {
|
||||
*
|
||||
* @category Block CRUD
|
||||
*/
|
||||
|
||||
updateBlock<T extends BlockModel = BlockModel>(
|
||||
modelOrId: T | string,
|
||||
callBackOrProps:
|
||||
| (() => void)
|
||||
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
|
||||
updateBlock(
|
||||
modelOrId: BlockModel | string,
|
||||
callBackOrProps: (() => void) | Partial<BlockProps>
|
||||
) {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
|
||||
@@ -113,8 +113,6 @@ type TransformerMiddlewareOptions = {
|
||||
transformerConfigs: Map<string, unknown>;
|
||||
};
|
||||
|
||||
type TransformerMiddlewareCleanup = () => void;
|
||||
|
||||
export type TransformerMiddleware = (
|
||||
options: TransformerMiddlewareOptions
|
||||
) => void | TransformerMiddlewareCleanup;
|
||||
) => void;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { nextTick } from '@blocksuite/global/utils';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -68,8 +67,6 @@ export class Transformer {
|
||||
|
||||
private readonly _docCRUD: DocCRUD;
|
||||
|
||||
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
||||
|
||||
private readonly _slots: TransformerSlots = {
|
||||
beforeImport: new Subject<BeforeImportPayload>(),
|
||||
afterImport: new Subject<AfterImportPayload>(),
|
||||
@@ -369,16 +366,13 @@ export class Transformer {
|
||||
this._docCRUD = docCRUD;
|
||||
|
||||
middlewares.forEach(middleware => {
|
||||
const cleanup = middleware({
|
||||
middleware({
|
||||
slots: this._slots,
|
||||
docCRUD: this._docCRUD,
|
||||
assetsManager: this._assetsManager,
|
||||
adapterConfigs: this._adapterConfigs,
|
||||
transformerConfigs: this._transformerConfigs,
|
||||
});
|
||||
if (cleanup) {
|
||||
this._disposables.add(cleanup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -652,9 +646,4 @@ export class Transformer {
|
||||
reset() {
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._disposables.dispose();
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,13 +132,12 @@ export class DocEngine {
|
||||
this.logger
|
||||
);
|
||||
|
||||
const subscriber = state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
cleanUp.push(
|
||||
state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
|
||||
@@ -153,15 +152,12 @@ export class DocEngine {
|
||||
this.priorityTarget,
|
||||
this.logger
|
||||
);
|
||||
|
||||
const subscriber = peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
|
||||
cleanUp.push(
|
||||
peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
return peer;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { DomRenderer } from '@blocksuite/affine-block-surface';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { wait } from '../utils/common.js';
|
||||
import { getSurface } from '../utils/edgeless.js';
|
||||
import { setupEditor } from '../utils/setup.js';
|
||||
|
||||
describe('Connector rendering with DOM renderer', () => {
|
||||
beforeEach(async () => {
|
||||
const cleanup = await setupEditor('edgeless', [], {
|
||||
enableDomRenderer: true,
|
||||
});
|
||||
return cleanup;
|
||||
});
|
||||
|
||||
test('should use DomRenderer when enable_dom_renderer flag is true', async () => {
|
||||
const surface = getSurface(doc, editor);
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface?.renderer).toBeInstanceOf(DomRenderer);
|
||||
});
|
||||
|
||||
test('should render a connector element as a DOM node', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
|
||||
// Create two shapes to connect
|
||||
const shape1Id = surfaceModel.addElement({
|
||||
type: 'shape',
|
||||
xywh: '[100, 100, 80, 60]',
|
||||
});
|
||||
|
||||
const shape2Id = surfaceModel.addElement({
|
||||
type: 'shape',
|
||||
xywh: '[300, 200, 80, 60]',
|
||||
});
|
||||
|
||||
// Create a connector between the shapes
|
||||
const connectorProps = {
|
||||
type: 'connector',
|
||||
source: { id: shape1Id },
|
||||
target: { id: shape2Id },
|
||||
stroke: '#000000',
|
||||
strokeWidth: 2,
|
||||
};
|
||||
const connectorId = surfaceModel.addElement(connectorProps);
|
||||
|
||||
await wait(100);
|
||||
|
||||
const connectorElement = surfaceView?.renderRoot.querySelector(
|
||||
`[data-element-id="${connectorId}"]`
|
||||
);
|
||||
|
||||
expect(connectorElement).not.toBeNull();
|
||||
expect(connectorElement).toBeInstanceOf(HTMLElement);
|
||||
|
||||
// Check if SVG element is present for connector rendering
|
||||
const svgElement = connectorElement?.querySelector('svg');
|
||||
expect(svgElement).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should render connector with different stroke styles', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
|
||||
// Create a dashed connector
|
||||
const connectorProps = {
|
||||
type: 'connector',
|
||||
source: { position: [100, 100] },
|
||||
target: { position: [200, 200] },
|
||||
strokeStyle: 'dash',
|
||||
stroke: '#ff0000',
|
||||
strokeWidth: 4,
|
||||
};
|
||||
const connectorId = surfaceModel.addElement(connectorProps);
|
||||
|
||||
// Wait for path generation and rendering
|
||||
await wait(500);
|
||||
|
||||
const connectorElement = surfaceView?.renderRoot.querySelector(
|
||||
`[data-element-id="${connectorId}"]`
|
||||
);
|
||||
|
||||
expect(connectorElement).not.toBeNull();
|
||||
|
||||
const svgElement = connectorElement?.querySelector('svg');
|
||||
expect(svgElement).not.toBeNull();
|
||||
|
||||
// Find the main path element (not the ones inside markers)
|
||||
const pathElements = svgElement?.querySelectorAll('path');
|
||||
// The main connector path should be the last one (after marker paths)
|
||||
const pathElement = pathElements?.[pathElements.length - 1];
|
||||
|
||||
expect(pathElement).not.toBeNull();
|
||||
|
||||
// Check stroke-dasharray attribute
|
||||
const strokeDasharray = pathElement!.getAttribute('stroke-dasharray');
|
||||
expect(strokeDasharray).toBe('12,12');
|
||||
});
|
||||
|
||||
test('should render connector with arrow endpoints', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
|
||||
const connectorProps = {
|
||||
type: 'connector',
|
||||
source: { position: [100, 100] },
|
||||
target: { position: [200, 200] },
|
||||
frontEndpointStyle: 'Triangle',
|
||||
rearEndpointStyle: 'Arrow',
|
||||
};
|
||||
const connectorId = surfaceModel.addElement(connectorProps);
|
||||
|
||||
await wait(100);
|
||||
|
||||
const connectorElement = surfaceView?.renderRoot.querySelector(
|
||||
`[data-element-id="${connectorId}"]`
|
||||
);
|
||||
|
||||
expect(connectorElement).not.toBeNull();
|
||||
|
||||
// Check for markers in defs
|
||||
const defsElement = connectorElement?.querySelector('defs');
|
||||
expect(defsElement).not.toBeNull();
|
||||
|
||||
const markers = defsElement?.querySelectorAll('marker');
|
||||
expect(markers?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should remove connector DOM node when element is deleted', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
|
||||
expect(surfaceView.renderer).toBeInstanceOf(DomRenderer);
|
||||
|
||||
const connectorProps = {
|
||||
type: 'connector',
|
||||
source: { position: [50, 50] },
|
||||
target: { position: [150, 150] },
|
||||
};
|
||||
const connectorId = surfaceModel.addElement(connectorProps);
|
||||
|
||||
await wait(100);
|
||||
|
||||
let connectorElement = surfaceView.renderRoot.querySelector(
|
||||
`[data-element-id="${connectorId}"]`
|
||||
);
|
||||
expect(connectorElement).not.toBeNull();
|
||||
|
||||
surfaceModel.deleteElement(connectorId);
|
||||
|
||||
await wait(100);
|
||||
|
||||
connectorElement = surfaceView.renderRoot.querySelector(
|
||||
`[data-element-id="${connectorId}"]`
|
||||
);
|
||||
expect(connectorElement).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -31,15 +31,12 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const getFrameTitle = (frameId: string) => {
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
frameId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
return frameTitleWidget?.shadowRoot?.querySelector('affine-frame-title');
|
||||
};
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
doc.root!.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
const frameTitle = getFrameTitle(frame);
|
||||
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
|
||||
const rect = frameTitle?.getBoundingClientRect();
|
||||
|
||||
expect(frameTitle).toBeTruthy();
|
||||
@@ -61,7 +58,7 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const nestedTitle = getFrameTitle(nestedFrame);
|
||||
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
|
||||
expect(nestedTitle).toBeTruthy();
|
||||
if (!nestedTitle) return;
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ describe('basic', () => {
|
||||
xywh: '[100, 0, 100, 100]',
|
||||
index: service.generateIndex(),
|
||||
})!;
|
||||
await wait(0); // wait next frame
|
||||
frameId = service.crud.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
|
||||
@@ -7,40 +7,30 @@ import type { InitFn } from './utils.js';
|
||||
const presetMarkdown = `Click the 🔁 button to switch between editors dynamically - they are fully compatible!`;
|
||||
|
||||
export const preset: InitFn = async (collection: Workspace, id: string) => {
|
||||
let doc = collection.getDoc(id);
|
||||
const hasDoc = !!doc;
|
||||
if (!doc) {
|
||||
doc = collection.createDoc(id);
|
||||
}
|
||||
const doc = collection.createDoc(id).getStore({ id });
|
||||
doc.load();
|
||||
// Add root block and surface block at root level
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
|
||||
const store = doc.getStore({ id });
|
||||
store.load();
|
||||
// Add note block inside root block
|
||||
const noteId = doc.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
|
||||
// Run only once on all clients.
|
||||
let noteId: string;
|
||||
if (!hasDoc) {
|
||||
// Add root block and surface block at root level
|
||||
const rootId = store.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
store.addBlock('affine:surface', {}, rootId);
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
|
||||
// Add note block inside root block
|
||||
noteId = store.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc: store,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
}
|
||||
|
||||
store.resetHistory();
|
||||
doc.resetHistory();
|
||||
};
|
||||
|
||||
preset.id = 'preset';
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"@types/katex": "^0.16.7",
|
||||
"browser-fs-access": "^0.37.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.11",
|
||||
"lit": "^3.2.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
@import '@toeverything/theme/style.css';
|
||||
@import '@toeverything/theme/fonts.css';
|
||||
|
||||
@import 'katex/dist/katex.min.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'color-emoji';
|
||||
src:
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"@vitest/coverage-istanbul": "3.1.3",
|
||||
"@vitest/ui": "3.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^35.0.0",
|
||||
"electron": "^36.0.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
|
||||
12
packages/backend/native/index.d.ts
vendored
12
packages/backend/native/index.d.ts
vendored
@@ -8,11 +8,6 @@ export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
|
||||
|
||||
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||
|
||||
export interface Chunk {
|
||||
index: number
|
||||
content: string
|
||||
}
|
||||
|
||||
export declare function fromModelName(modelName: string): Tokenizer | null
|
||||
|
||||
export declare function getMime(input: Uint8Array): string
|
||||
@@ -27,11 +22,6 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
|
||||
|
||||
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
||||
|
||||
export interface ParsedDoc {
|
||||
name: string
|
||||
chunks: Array<Chunk>
|
||||
}
|
||||
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.81",
|
||||
"@napi-rs/cli": "3.0.0-alpha.78",
|
||||
"lib0": "^0.2.99",
|
||||
"tiktoken": "^1.0.17",
|
||||
"tinybench": "^4.0.0",
|
||||
|
||||
@@ -2,21 +2,9 @@ use affine_common::doc_loader::Doc;
|
||||
use napi::{
|
||||
anyhow::anyhow,
|
||||
bindgen_prelude::{AsyncTask, Buffer},
|
||||
Env, Result, Task,
|
||||
Env, JsObject, Result, Task,
|
||||
};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct Chunk {
|
||||
pub index: i64,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ParsedDoc {
|
||||
pub name: String,
|
||||
pub chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
inner: Doc,
|
||||
}
|
||||
@@ -26,20 +14,24 @@ impl Document {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
fn chunks(&self) -> Vec<Chunk> {
|
||||
self
|
||||
.inner
|
||||
.chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
Chunk {
|
||||
index: i as i64,
|
||||
content,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Chunk>>()
|
||||
fn chunks(&self, env: Env) -> Result<JsObject> {
|
||||
let mut array = env.create_array_with_length(self.inner.chunks.len())?;
|
||||
for (i, chunk) in self.inner.chunks.iter().enumerate() {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("index", i as i64)?;
|
||||
obj.set_named_property("content", content)?;
|
||||
array.set_element(i as u32, obj)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn resolve(self, env: Env) -> Result<JsObject> {
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("name", self.name())?;
|
||||
obj.set_named_property("chunks", self.chunks(env)?)?;
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,22 +43,21 @@ pub struct AsyncParseDocResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncParseDocResponse {
|
||||
type Output = Document;
|
||||
type JsValue = ParsedDoc;
|
||||
type JsValue = JsObject;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
let doc = Doc::new(&self.file_path, &self.doc).map_err(|e| anyhow!(e))?;
|
||||
Ok(Document { inner: doc })
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
Ok(ParsedDoc {
|
||||
name: doc.name(),
|
||||
chunks: doc.chunks(),
|
||||
})
|
||||
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
doc.resolve(env)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
#[napi(
|
||||
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
|
||||
)]
|
||||
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
|
||||
AsyncTask::new(AsyncParseDocResponse {
|
||||
file_path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, Result as NapiResult, Task};
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
@@ -13,7 +13,7 @@ pub struct AsyncVerifyChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = bool;
|
||||
type JsValue = JsBoolean;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
@@ -23,8 +23,8 @@ impl Task for AsyncVerifyChallengeResponse {
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ pub struct AsyncMintChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = String;
|
||||
type JsValue = JsString;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,25 +15,16 @@ pub fn from_model_name(model_name: String) -> Option<Tokenizer> {
|
||||
impl Tokenizer {
|
||||
#[napi]
|
||||
pub fn count(&self, content: String, allowed_special: Option<Vec<String>>) -> u32 {
|
||||
let allowed_special = if let Some(allowed_special) = &allowed_special {
|
||||
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
self.inner.encode(&content, &allowed_special).0.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenizer() {
|
||||
let tokenizer = from_model_name("gpt-4.1".to_string()).unwrap();
|
||||
let content = "Hello, world!";
|
||||
let count = tokenizer.count(content.to_string(), None);
|
||||
assert!(count > 0);
|
||||
self
|
||||
.inner
|
||||
.encode(
|
||||
&content,
|
||||
if let Some(allowed_special) = &allowed_special {
|
||||
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
)
|
||||
.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user