Compare commits

..

1 Commits

Author SHA1 Message Date
Peng Xiao adaee0ef5f feat(component): sortable 2025-03-31 17:21:52 +08:00
492 changed files with 8812 additions and 9099 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22"
"version": "18"
},
"ghcr.io/devcontainers/features/rust:1": {}
},
+7 -42
View File
@@ -9,27 +9,27 @@
"properties": {
"db": {
"type": "number",
"description": "The database index of redis server to be used(Must be less than 10).\n@default 0\n@environment `REDIS_SERVER_DATABASE`",
"description": "The database index of redis server to be used(Must be less than 10).\n@default 0\n@environment `REDIS_DATABASE`",
"default": 0
},
"host": {
"type": "string",
"description": "The host of the redis server.\n@default \"localhost\"\n@environment `REDIS_SERVER_HOST`",
"description": "The host of the redis server.\n@default \"localhost\"\n@environment `REDIS_HOST`",
"default": "localhost"
},
"port": {
"type": "number",
"description": "The port of the redis server.\n@default 6379\n@environment `REDIS_SERVER_PORT`",
"description": "The port of the redis server.\n@default 6379\n@environment `REDIS_PORT`",
"default": 6379
},
"username": {
"type": "string",
"description": "The username of the redis server.\n@default \"\"\n@environment `REDIS_SERVER_USERNAME`",
"description": "The username of the redis server.\n@default \"\"\n@environment `REDIS_USERNAME`",
"default": ""
},
"password": {
"type": "string",
"description": "The password of the redis server.\n@default \"\"\n@environment `REDIS_SERVER_PASSWORD`",
"description": "The password of the redis server.\n@default \"\"\n@environment `REDIS_PASSWORD`",
"default": ""
},
"ioredis": {
@@ -427,14 +427,6 @@
"accountId": {
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"signDomain": {
"type": "string",
"description": "The presigned domain for the cloudflare r2 storage provider."
},
"signKey": {
"type": "string",
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
}
@@ -538,14 +530,6 @@
"accountId": {
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"signDomain": {
"type": "string",
"description": "The presigned domain for the cloudflare r2 storage provider."
},
"signKey": {
"type": "string",
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
}
@@ -573,8 +557,8 @@
},
"externalUrl": {
"type": "string",
"description": "Base url of AFFiNE server, used for generating external urls.\nDefault to be `[server.protocol]://[server.host][:server.port]` if not specified.\n \n@default \"\"\n@environment `AFFINE_SERVER_EXTERNAL_URL`",
"default": ""
"description": "Base url of AFFiNE server, used for generating external urls.\nDefault to be `[server.protocol]://[server.host][:server.port]` if not specified.\n \n@default \"http://localhost:3010\"\n@environment `AFFINE_SERVER_EXTERNAL_URL`",
"default": "http://localhost:3010"
},
"https": {
"type": "boolean",
@@ -609,17 +593,6 @@
}
}
},
"docService": {
"type": "object",
"description": "Configuration for docService module",
"properties": {
"endpoint": {
"type": "string",
"description": "The endpoint of the doc service.\n@default \"\"\n@environment `DOC_SERVICE_ENDPOINT`",
"default": ""
}
}
},
"client": {
"type": "object",
"description": "Configuration for client module",
@@ -792,14 +765,6 @@
"accountId": {
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"signDomain": {
"type": "string",
"description": "The presigned domain for the cloudflare r2 storage provider."
},
"signKey": {
"type": "string",
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
}
+1 -18
View File
@@ -142,19 +142,11 @@ jobs:
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: make
run: yarn affine @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: signing DMG
if: ${{ matrix.spec.platform == 'darwin' }}
@@ -248,20 +240,11 @@ jobs:
- name: Build Desktop Layers
run: yarn affine @affine/electron build
- name: Remove nbstore node_modules
shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: package
run: |
yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
run: yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
NODE_OPTIONS: --max-old-space-size=14384
- name: get all files to be signed
id: get_files_to_be_signed
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -12,4 +12,4 @@ npmPublishAccess: public
npmPublishRegistry: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.8.1.cjs
yarnPath: .yarn/releases/yarn-4.8.0.cjs
Generated
+4 -4
View File
@@ -2501,9 +2501,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
[[package]]
name = "oorandom"
@@ -3046,9 +3046,9 @@ dependencies = [
[[package]]
name = "rubato"
version = "0.16.2"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
checksum = "cdd96992d7e24b3d7f35fdfe02af037a356ac90d41b466945cf3333525a86eea"
dependencies = [
"num-complex",
"num-integer",
+1 -1
View File
@@ -181,6 +181,6 @@
"version": "0.20.0",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"vitest": "3.1.1"
"vitest": "3.0.9"
}
}
@@ -1,13 +1,9 @@
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { Container } from '@blocksuite/global/di';
import {
registerBlockSpecs,
registerStoreSpecs,
} from '../../extensions/register';
import { registerSpecs } from '../../extensions/register';
registerStoreSpecs();
registerBlockSpecs();
registerSpecs();
export function getProvider() {
const container = new Container();
@@ -1 +0,0 @@
export * from '@blocksuite/affine-components/open-doc-dropdown-menu';
+3 -4
View File
@@ -34,7 +34,6 @@ import { effects as componentHighlightDropdownMenuEffects } from '@blocksuite/af
import { IconButton } from '@blocksuite/affine-components/icon-button';
import { effects as componentLinkPreviewEffects } from '@blocksuite/affine-components/link-preview';
import { effects as componentLinkedDocTitleEffects } from '@blocksuite/affine-components/linked-doc-title';
import { effects as componentOpenDocDropdownMenuEffects } from '@blocksuite/affine-components/open-doc-dropdown-menu';
import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal';
import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu';
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
@@ -63,7 +62,7 @@ import { effects as widgetToolbarEffects } from '@blocksuite/affine-widget-toolb
import { effects as dataViewEffects } from '@blocksuite/data-view/effects';
import { effects as stdEffects } from '@blocksuite/std/effects';
import { registerBlockSpecs } from './extensions';
import { registerSpecs } from './extensions/register.js';
export declare const _GLOBAL_:
| typeof stdEffects
@@ -113,7 +112,8 @@ export declare const _GLOBAL_:
| typeof fragmentOutlineEffects;
export function effects() {
registerBlockSpecs();
registerSpecs();
stdEffects();
dataViewEffects();
@@ -165,7 +165,6 @@ export function effects() {
componentEdgelessLineWidthEffects();
componentEdgelessLineStylesEffects();
componentEdgelessShapeColorPickerEffects();
componentOpenDocDropdownMenuEffects();
widgetScrollAnchoringEffects();
widgetFrameTitleEffects();
@@ -1,5 +1,4 @@
export * from './common';
export * from './editor-specs';
export * from './preview-specs';
export * from './register';
export * from './store';
export * from './common.js';
export * from './editor-specs.js';
export * from './preview-specs.js';
export * from './store.js';
@@ -10,11 +10,8 @@ import {
} from './preview-specs.js';
import { StoreExtensions } from './store.js';
export function registerStoreSpecs() {
export function registerSpecs() {
SpecProvider._.addSpec('store', StoreExtensions);
}
export function registerBlockSpecs() {
SpecProvider._.addSpec('page', PageEditorBlockSpecs);
SpecProvider._.addSpec('edgeless', EdgelessEditorBlockSpecs);
SpecProvider._.addSpec('preview:page', PreviewPageEditorBlockSpecs);
@@ -1,9 +1,9 @@
import { fontXSStyle, panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const renameStyles = css`
${panelBaseStyle('.affine-attachment-rename-container')}
.affine-attachment-rename-container {
${PANEL_BASE};
position: relative;
display: flex;
align-items: center;
@@ -35,8 +35,8 @@ export const renameStyles = css`
outline: none;
background: transparent;
color: var(--affine-text-primary-color);
${FONT_XS};
}
${fontXSStyle('.affine-attachment-rename-input-wrapper input')}
.affine-attachment-rename-input-wrapper input::placeholder {
color: var(--affine-placeholder-color);
@@ -2,7 +2,7 @@ import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
import { type CalloutBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
ThemeProvider,
@@ -95,9 +95,7 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -1,10 +1,7 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import type { CodeBlockModel } from '@blocksuite/affine-model';
import { focusTextModel, type RichText } from '@blocksuite/affine-rich-text';
import {
BRACKET_PAIRS,
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import { BRACKET_PAIRS, NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
NotificationProvider,
@@ -82,9 +79,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -12,7 +12,7 @@ import {
import { CopyIcon, DeleteIcon } from '@blocksuite/affine-components/icons';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
NotificationProvider,
@@ -225,9 +225,7 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -8,7 +8,7 @@ import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import {
DocModeProvider,
NotificationProvider,
@@ -354,9 +354,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -339,8 +339,6 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
minWidth: !hasMaxWidth ? '220px' : undefined,
};
this.contentEditable = String(editing && !this.doc.readonly$.value);
return html`
<div
class="edgeless-text-block-container"
@@ -352,6 +350,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
pointerEvents: editing ? 'auto' : 'none',
userSelect: editing ? 'auto' : 'none',
})}
contenteditable=${editing}
>
${this.renderPageContent()}
</div>
@@ -34,7 +34,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "3.1.1"
"vitest": "3.0.9"
},
"exports": {
".": "./src/index.ts",
@@ -90,15 +90,15 @@ export const insertEmbedIframeWithUrlCommand: Command<
surfaceBlock.model
);
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
gfx.selection.set({
elements: [newBlockId],
editing: false,
});
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
}
if (!newBlockId) {
@@ -1,9 +1,5 @@
import { createLitPortal } from '@blocksuite/affine-components/portal';
import type { EmbedIframeBlockModel } from '@blocksuite/affine-model';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit';
import { EditIcon, InformationIcon, ResetIcon } from '@blocksuite/icons/lit';
@@ -195,7 +191,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
.model=${this.model}
.abortController=${this._editAbortController}
.std=${this.std}
.inSurface=${this.inSurface}
></embed-iframe-link-edit-popup>`,
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
@@ -215,15 +210,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
private readonly _handleRetry = (e: MouseEvent) => {
e.stopPropagation();
this.onRetry();
// track retry event
this.telemetryService?.track('ReloadLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'reload button',
});
};
override render() {
@@ -287,16 +273,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
return this.model.doc.readonly;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@query('.button.edit')
accessor _editButton: HTMLElement | null = null;
@@ -312,9 +288,6 @@ export class EmbedIframeErrorCard extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor inSurface = false;
@property({ attribute: false })
accessor options: EmbedIframeStatusCardOptions = {
layout: 'horizontal',
@@ -1,7 +1,3 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher } from '@blocksuite/global/lit';
import { DoneIcon } from '@blocksuite/icons/lit';
@@ -71,17 +67,6 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
}
`;
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('EditLink', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: 'editor',
module: 'embed block',
control: 'edit button',
other: status,
});
}
override render() {
const isInputEmpty = this.isInputEmpty();
const { url$ } = this.model.props;
@@ -109,14 +94,4 @@ export class EmbedIframeLinkEditPopup extends SignalWatcher(
</div>
`;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
}
@@ -5,22 +5,11 @@ import {
} from '@blocksuite/affine-shared/services';
import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { noop } from '@blocksuite/global/utils';
import {
BlockSelection,
type BlockStdScope,
SurfaceSelection,
} from '@blocksuite/std';
import { BlockSelection, type BlockStdScope } from '@blocksuite/std';
import { LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
// this method is used to track the event when the user inputs the link
// it should be overridden by the subclass
protected track(status: 'success' | 'failure') {
noop(status);
}
protected isInputEmpty() {
return this._linkInputValue.trim() === '';
}
@@ -44,20 +33,9 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
this.store.transact(() => {
const blockId = this.store.addBlock(flavour, { url }, parent, index);
this.store.deleteBlock(model);
if (this.inSurface) {
this.std.selection.setGroup('gfx', [
this.std.selection.create(
SurfaceSelection,
blockId,
[blockId],
false
),
]);
} else {
this.std.selection.setGroup('note', [
this.std.selection.create(BlockSelection, { blockId }),
]);
}
this.std.selection.setGroup('note', [
this.std.selection.create(BlockSelection, { blockId }),
]);
});
this.abortController?.abort();
@@ -72,7 +50,6 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
const embedIframeService = this.std.get(EmbedIframeService);
if (!embedIframeService) {
console.error('iframe EmbedIframeService not found');
this.track('failure');
return;
}
@@ -91,9 +68,7 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
title: '',
description: '',
});
this.track('success');
} catch (error) {
this.track('failure');
this.notificationService?.notify({
title: 'Error in embed iframe creation',
message: error instanceof Error ? error.message : 'Please try again',
@@ -154,7 +129,4 @@ export class EmbedIframeLinkInputBase extends WithDisposable(LitElement) {
@property({ attribute: false })
accessor abortController: AbortController | undefined = undefined;
@property({ attribute: false })
accessor inSurface = false;
}
@@ -1,7 +1,3 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { CloseIcon } from '@blocksuite/icons/lit';
import { baseTheme } from '@toeverything/theme';
@@ -20,7 +16,6 @@ export type EmbedLinkInputPopupOptions = {
title?: string;
description?: string;
placeholder?: string;
telemetrySegment?: string;
};
const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
@@ -29,7 +24,6 @@ const DEFAULT_OPTIONS: EmbedLinkInputPopupOptions = {
title: 'Embed Link',
description: 'Works with links of Google Drive, Spotify…',
placeholder: 'Paste the Embed link...',
telemetrySegment: 'editor',
};
export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
@@ -222,17 +216,6 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
this.abortController?.abort();
};
protected override track(status: 'success' | 'failure') {
this.telemetryService?.track('CreateEmbedBlock', {
type: 'embed iframe block',
page: this.editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
segment: this.options?.telemetrySegment ?? 'editor',
module: 'embed block',
control: 'confirm embed link',
other: status,
});
}
override render() {
const options = { ...DEFAULT_OPTIONS, ...this.options };
const { showCloseButton, variant, title, description, placeholder } =
@@ -278,16 +261,6 @@ export class EmbedIframeLinkInputPopup extends EmbedIframeLinkInputBase {
`;
}
get telemetryService() {
return this.std.getOptional(TelemetryProvider);
}
get editorMode() {
const docModeService = this.std.get(DocModeProvider);
const mode = docModeService.getEditorMode();
return mode ?? 'page';
}
@property({ attribute: false })
accessor options: EmbedLinkInputPopupOptions | undefined = undefined;
}
@@ -31,9 +31,6 @@ export const embedIframeSlashMenuConfig: SlashMenuConfig = {
.pipe(insertEmptyEmbedIframeCommand, {
place: 'after',
removeEmptyLine: true,
linkInputPopupOptions: {
telemetrySegment: 'slash menu',
},
})
.run();
},
@@ -67,11 +67,6 @@ const openLinkAction = (id: string): ToolbarAction => {
run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component?.open();
ctx.track('OpenLink', {
...trackBaseProps,
control: 'open original link',
});
},
};
};
@@ -268,11 +263,6 @@ export const builtinToolbarConfig = {
.copySlice(slice)
.then(() => toast(ctx.host, 'Copied to clipboard'))
.catch(console.error);
ctx.track('CopiedLink', {
...trackBaseProps,
control: 'copy link',
});
},
},
{
@@ -300,11 +290,6 @@ export const builtinToolbarConfig = {
run(ctx) {
const component = ctx.getCurrentBlockByType(EmbedIframeBlockComponent);
component?.refreshData().catch(console.error);
ctx.track('ReloadLink', {
...trackBaseProps,
control: 'reload link',
});
},
},
{
@@ -225,7 +225,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
.model=${this.model}
.abortController=${this._linkInputAbortController}
.std=${this.std}
.inSurface=${this.inSurface}
.options=${options}
></embed-iframe-link-input-popup>`,
portalStyles: {
@@ -348,7 +347,6 @@ export class EmbedIframeBlockComponent extends CaptionedBlockComponent<EmbedIfra
.model=${this.model}
.onRetry=${this._handleRetry}
.std=${this.std}
.inSurface=${this.inSurface}
.options=${this._statusCardOptions}
></embed-iframe-error-card>`;
}
@@ -11,7 +11,6 @@ import {
} from '@blocksuite/affine-shared/consts';
import {
ActionPlacement,
DocDisplayMetaProvider,
type LinkEventType,
type OpenDocMode,
type ToolbarAction,
@@ -77,13 +76,11 @@ const docTitleAction = {
if (!model.props.title) return null;
const originalTitle =
ctx.std.get(DocDisplayMetaProvider).title(model.props.pageId).value ||
'Untitled';
const open = (event: MouseEvent) => block.open({ event });
ctx.workspace.getDoc(model.props.pageId)?.meta?.title || 'Untitled';
return html`<affine-linked-doc-title
.title=${originalTitle}
.open=${open}
.open=${(event: MouseEvent) => block.open({ event })}
></affine-linked-doc-title>`;
},
} as const satisfies ToolbarAction;
@@ -2,18 +2,12 @@ import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { LoomLogoDuotoneIcon } from '@blocksuite/icons/lit';
import { LoomTooltip } from './tooltips';
export const embedLoomSlashMenuConfig: SlashMenuConfig = {
items: [
{
name: 'Loom',
icon: LoomLogoDuotoneIcon(),
description: 'Embed a Loom video.',
tooltip: {
figure: LoomTooltip,
caption: 'loom',
},
group: '4_Content & Media@9',
when: ({ model }) =>
model.doc.schema.flavourSchemaMap.has('affine:embed-loom'),
File diff suppressed because one or more lines are too long
@@ -38,7 +38,9 @@ export class FrameTool extends BaseTool {
override dragEnd(): void {
if (this._frame) {
const frame = this._frame;
frame.pop('xywh');
this.doc.transact(() => {
frame.pop('xywh');
});
// @ts-expect-error TODO: refactor gfx tool
this.gfx.tool.setTool('default');
this.gfx.selection.set({
@@ -50,6 +52,8 @@ export class FrameTool extends BaseTool {
frame,
getTopElements(this.frameManager.getElementsInFrameBound(frame))
);
this.doc.captureSync();
}
this._frame = null;
@@ -1,121 +1,19 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
import {
BookmarkIcon,
CaptionIcon,
CopyIcon,
DeleteIcon,
DownloadIcon,
DuplicateIcon,
} from '@blocksuite/icons/lit';
import { CaptionIcon, DownloadIcon } from '@blocksuite/icons/lit';
import { BlockFlavourIdentifier } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { ImageBlockComponent } from '../image-block';
import { ImageEdgelessBlockComponent } from '../image-edgeless-block';
import { duplicate } from '../utils';
const trackBaseProps = {
category: 'image',
type: 'card view',
};
const builtinToolbarConfig = {
actions: [
{
id: 'a.download',
tooltip: 'Download',
icon: DownloadIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
block?.download();
},
},
{
id: 'b.caption',
tooltip: 'Caption',
icon: CaptionIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
block?.captionEditor?.show();
ctx.track('OpenedCaptionEditor', {
...trackBaseProps,
control: 'add caption',
});
},
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',
actions: [
{
id: 'a.copy',
label: 'Copy',
icon: CopyIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
block?.copy();
},
},
{
id: 'b.duplicate',
label: 'Duplicate',
icon: DuplicateIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (!block) return;
duplicate(block);
},
},
],
},
{
placement: ActionPlacement.More,
id: 'b.conversions',
actions: [
{
id: 'a.turn-into-card-view',
label: 'Turn into card view',
icon: BookmarkIcon(),
when(ctx) {
const supported =
ctx.store.schema.flavourSchemaMap.has('affine:attachment');
if (!supported) return false;
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
return Boolean(block?.blob);
},
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
block?.convertToCardView();
},
},
],
},
{
placement: ActionPlacement.More,
id: 'c.delete',
label: 'Delete',
icon: DeleteIcon(),
variant: 'destructive',
run(ctx) {
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
if (!block) return;
ctx.store.deleteBlock(block.model);
},
},
],
placement: 'inner',
} as const satisfies ToolbarModuleConfig;
const builtinSurfaceToolbarConfig = {
actions: [
{
@@ -152,11 +50,6 @@ export const createBuiltinToolbarConfigExtension = (
const name = flavour.split(':').pop();
return [
ToolbarModuleExtension({
id: BlockFlavourIdentifier(flavour),
config: builtinToolbarConfig,
}),
ToolbarModuleExtension({
id: BlockFlavourIdentifier(`affine:surface:${name}`),
config: builtinSurfaceToolbarConfig,
@@ -1,8 +1,6 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { whenHover } from '@blocksuite/affine-components/hover';
import { Peekable } from '@blocksuite/affine-components/peek';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { ToolbarRegistryIdentifier } from '@blocksuite/affine-shared/services';
import { IS_MOBILE } from '@blocksuite/global/env';
import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit';
@@ -56,33 +54,9 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
selectionManager.setGroup('note', [blockSelection]);
}
private _initHover() {
const { setReference, setFloating, dispose } = whenHover(
hovered => {
const message$ = this.std.get(ToolbarRegistryIdentifier).message$;
if (hovered) {
message$.value = {
flavour: this.model.flavour,
element: this,
setFloating,
};
return;
}
// Clears previous bindings
message$.value = null;
setFloating();
},
{ enterDelay: 500 }
);
setReference(this);
this._disposables.add(dispose);
}
override connectedCallback() {
super.connectedCallback();
this._initHover();
this.refreshData();
this.contentEditable = 'false';
this._disposables.add(
@@ -1,6 +1,10 @@
import { ImageBlockSchema } from '@blocksuite/affine-model';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import {
BlockViewExtension,
FlavourExtension,
WidgetViewExtension,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
@@ -12,6 +16,12 @@ import { ImageDropOption } from './image-service';
const flavour = ImageBlockSchema.model.flavour;
export const imageToolbarWidget = WidgetViewExtension(
flavour,
'imageToolbar',
literal`affine-image-toolbar-widget`
);
export const ImageBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
BlockViewExtension(flavour, model => {
@@ -23,6 +33,7 @@ export const ImageBlockSpec: ExtensionType[] = [
return literal`affine-image`;
}),
imageToolbarWidget,
ImageDropOption,
ImageBlockAdapterExtensions,
createBuiltinToolbarConfigExtension(flavour),
@@ -11,19 +11,13 @@ import {
} from '@blocksuite/affine-shared/services';
import {
downloadBlob,
getBlockProps,
humanFileSize,
isInsidePageEditor,
readImageSize,
transformModel,
withTempBlobData,
} from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/gfx';
import {
BlockSelection,
type BlockStdScope,
type EditorHost,
} from '@blocksuite/std';
import type { BlockStdScope, EditorHost } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
@@ -558,49 +552,3 @@ export function calcBoundByOrigin(
? new Bound(point[0], point[1], width, height)
: Bound.fromCenter(point, width, height);
}
export function duplicate(block: ImageBlockComponent) {
const model = block.model;
const blockProps = getBlockProps(model);
const {
width: _width,
height: _height,
xywh: _xywh,
rotate: _rotate,
zIndex: _zIndex,
...duplicateProps
} = blockProps;
const { doc } = model;
const parent = doc.getParent(model);
if (!parent) {
console.error(`Parent not found for block(${model.flavour}) ${model.id}`);
return;
}
const index = parent?.children.indexOf(model);
const duplicateId = doc.addBlock(
model.flavour,
duplicateProps,
parent,
index + 1
);
const editorHost = block.host;
editorHost.updateComplete
.then(() => {
const { selection } = editorHost;
selection.setGroup('note', [
selection.create(BlockSelection, {
blockId: duplicateId,
}),
]);
if (isInsidePageEditor(editorHost)) {
const duplicateElement = editorHost.view.getBlock(duplicateId);
if (duplicateElement) {
duplicateElement.scrollIntoView(true);
}
}
})
.catch(console.error);
}
@@ -7,7 +7,6 @@ import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { TeXIcon } from '@blocksuite/icons/lit';
import { insertLatexBlockCommand } from '../commands';
import { LatexTooltip } from './tooltips';
export const latexSlashMenuConfig: SlashMenuConfig = {
items: [
@@ -16,14 +15,6 @@ export const latexSlashMenuConfig: SlashMenuConfig = {
group: '0_Basic@8',
description: 'Create a inline equation.',
icon: TeXIcon(),
tooltip: {
figure: LatexTooltip(
'Energy. Mass. Light. In a single equation,',
'E=mc^2',
false
),
caption: 'Inline equation',
},
searchAlias: ['inlineMath, inlineEquation', 'inlineLatex'],
action: ({ std }) => {
std.command
@@ -37,14 +28,6 @@ export const latexSlashMenuConfig: SlashMenuConfig = {
name: 'Equation',
description: 'Create a equation block.',
icon: TeXIcon(),
tooltip: {
figure: LatexTooltip(
'Create a equation via LaTeX.',
String.raw`\frac{a}{b} \pm \frac{c}{d} = \frac{ad \pm bc}{bd}`,
true
),
caption: 'Equation',
},
searchAlias: ['mathBlock, equationBlock', 'latexBlock'],
group: '4_Content & Media@10',
action: ({ std }) => {
@@ -1,41 +0,0 @@
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import katex from 'katex';
import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
export const LatexTooltip = (
str: string,
latex: string,
displayMode: boolean = false
) =>
html` <style>
.latex-tooltip {
background: ${unsafeCSSVarV2('layer/pureWhite')};
border-radius: 2px;
width: 170px;
padding: 5px 5px 5px 6px;
box-sizing: border-box;
}
.latex-tooltip-content {
width: 159px;
color: #121212;
font-family: var(--affine-font-family);
font-size: 10px;
font-style: normal;
.katex > math[display='block'] {
margin-top: 1em;
}
}
</style>
<div class="latex-tooltip">
<div class="latex-tooltip-content">
<span>${str}</span>
${unsafeHTML(
katex.renderToString(latex, {
displayMode,
output: 'mathml',
})
)}
</div>
</div>`;
@@ -31,7 +31,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "3.1.1"
"vitest": "3.0.9"
},
"exports": {
".": "./src/index.ts",
@@ -8,7 +8,7 @@ import type { ListBlockModel } from '@blocksuite/affine-model';
import type { RichText } from '@blocksuite/affine-rich-text';
import {
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
NOTE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { getViewportElement } from '@blocksuite/affine-shared/utils';
@@ -84,9 +84,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -227,21 +227,6 @@ export const UnderlineTooltip = html`<svg width="170" height="68" viewBox="0 0 1
</svg>
`;
// prettier-ignore
export const TodoTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="170" height="68" rx="2" fill="white"/>
<mask id="mask0_5604_203551" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
<rect width="170" height="68" rx="2" fill="white"/>
</mask>
<g mask="url(#mask0_5604_203551)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6667 19C12.7462 19 12 19.7462 12 20.6667V27.3333C12 28.2538 12.7462 29 13.6667 29H20.3333C21.2538 29 22 28.2538 22 27.3333V20.6667C22 19.7462 21.2538 19 20.3333 19H13.6667ZM12.9091 20.6667C12.9091 20.2483 13.2483 19.9091 13.6667 19.9091H20.3333C20.7517 19.9091 21.0909 20.2483 21.0909 20.6667V27.3333C21.0909 27.7517 20.7517 28.0909 20.3333 28.0909H13.6667C13.2483 28.0909 12.9091 27.7517 12.9091 27.3333V20.6667Z" fill="#77757D"/>
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="27.6364">Here is an example of todo list.</tspan></text>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 40.6667C12 39.7462 12.7462 39 13.6667 39H20.3333C21.2538 39 22 39.7462 22 40.6667V47.3333C22 48.2538 21.2538 49 20.3333 49H13.6667C12.7462 49 12 48.2538 12 47.3333V40.6667ZM19.7457 42.5032C19.9232 42.3257 19.9232 42.0379 19.7457 41.8604C19.5681 41.6829 19.2803 41.6829 19.1028 41.8604L16.0909 44.8723L15.2002 43.9816C15.0227 43.8041 14.7349 43.8041 14.5574 43.9816C14.3799 44.1591 14.3799 44.4469 14.5574 44.6244L15.7695 45.8366C15.947 46.0141 16.2348 46.0141 16.4123 45.8366L19.7457 42.5032Z" fill="#1E96EB"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="47.6364">Make a list for building preview.</tspan></text>
</g>
</svg>
`
export const tooltips: Record<string, SlashMenuTooltip> = {
Text: {
figure: TextTooltip,
@@ -322,9 +307,4 @@ export const tooltips: Record<string, SlashMenuTooltip> = {
figure: StrikethroughTooltip,
caption: 'Strikethrough',
},
'To-do List': {
figure: TodoTooltip,
caption: 'To-do List',
},
};
@@ -268,10 +268,7 @@ export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
<edgeless-page-block-title
.note=${this.model}
></edgeless-page-block-title>
<div
contenteditable=${String(!this.doc.readonly$.value)}
class="edgeless-note-page-content"
>
<div class="edgeless-note-page-content">
${this.renderPageContent()}
</div>
</div>
@@ -5,7 +5,7 @@ import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import type { RichText } from '@blocksuite/affine-rich-text';
import {
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
NOTE_SELECTOR,
} from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import {
@@ -96,9 +96,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
@@ -471,7 +471,6 @@ export class EdgelessTemplatePanel extends WithDisposable(LitElement) {
${template.name
? html`<affine-tooltip
.offset=${12}
.autoHide=${true}
tip-position="top"
>
${template.name}
@@ -10,8 +10,6 @@ import {
type ConnectorElementModel,
GroupElementModel,
MindmapElementModel,
NoteBlockModel,
NoteDisplayMode,
} from '@blocksuite/affine-model';
import { resetNativeSelection } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
@@ -143,12 +141,6 @@ export class DefaultTool extends BaseTool {
if (el instanceof MindmapElementModel) {
return bound.contains(el.elementBound);
}
if (
el instanceof NoteBlockModel &&
el.props.displayMode === NoteDisplayMode.DocOnly
) {
return false;
}
return true;
});
@@ -1,2 +0,0 @@
// TODO(@fundon): move to pen module
export const drawingCursor = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3E%3Cg filter='url(%23filter0_d_5033_225305)'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16.138 6.9046C16.6785 6.36513 17.5553 6.36513 18.0958 6.9046C18.6358 7.44353 18.6358 8.31689 18.0958 8.85582L17.3186 9.63134L15.3621 7.67873L16.138 6.9046ZM14.6542 8.38506L16.6107 10.3377L8.96075 17.9707L6.61523 18.384L6.94908 16.461C7.00206 16.1558 7.14823 15.8745 7.36749 15.6557L14.6542 8.38506Z' fill='black'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18.095 6.9046C17.5545 6.36513 16.6777 6.36513 16.1372 6.9046L15.3613 7.67873L17.3178 9.63134L18.095 8.85582C18.635 8.31689 18.635 7.44353 18.095 6.9046ZM18.8014 9.56366C19.7328 8.63405 19.7329 7.12641 18.8014 6.1968C17.8705 5.26773 16.3616 5.26773 15.4307 6.1968L6.66035 14.9478C6.29491 15.3124 6.05131 15.7813 5.96301 16.2899L5.50738 18.9145C5.47951 19.075 5.53158 19.239 5.6469 19.354C5.76223 19.469 5.92636 19.5207 6.08678 19.4924L9.28847 18.9282C9.38935 18.9104 9.48233 18.8621 9.55485 18.7898L17.671 10.6918L18.8014 9.56366ZM16.6099 10.3377L14.6534 8.38506L7.36668 15.6557C7.14741 15.8745 7.00125 16.1558 6.94827 16.461L6.61442 18.384L8.95993 17.9707L16.6099 10.3377Z' fill='white'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='filter0_d_5033_225305' x='-1.8' y='-0.8' width='27.6' height='27.6' filterUnits='userSpaceOnUse' color-interpolation-filters='sRGB'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeColorMatrix in='SourceAlpha' type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0' result='hardAlpha'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='0.9'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0'/%3E%3CfeBlend mode='normal' in2='BackgroundImageFix' result='effect1_dropShadow_5033_225305'/%3E%3CfeBlend mode='normal' in='SourceGraphic' in2='effect1_dropShadow_5033_225305' result='shape'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E") 4 20, crosshair`;
@@ -31,8 +31,6 @@ import type {
} from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { drawingCursor } from './cursors';
export function isEdgelessTextBlock(
element: BlockModel | GfxModel | null
): element is EdgelessTextBlockModel {
@@ -216,8 +214,6 @@ export function getCursorMode(edgelessTool: GfxToolsFullOptionValue | null) {
case 'pan':
return edgelessTool.panning ? 'grabbing' : 'grab';
case 'brush':
case 'highlighter':
return drawingCursor;
case 'eraser':
case 'shape':
case 'connector':
@@ -31,6 +31,7 @@ import { AffineTemplateLoading } from './edgeless/components/toolbar/template/te
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
import { EdgelessTemplateButton } from './edgeless/components/toolbar/template/template-tool-button.js';
import {
AffineImageToolbarWidget,
AffineModalWidget,
EdgelessRootBlockComponent,
EdgelessRootPreviewBlockComponent,
@@ -47,6 +48,8 @@ import {
} from './widgets/edgeless-zoom-toolbar/index.js';
import { ZoomBarToggleButton } from './widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
import { EdgelessZoomToolbar } from './widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
import { AffineImageToolbar } from './widgets/image-toolbar/components/image-toolbar.js';
import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './widgets/image-toolbar/index.js';
import {
AFFINE_INNER_MODAL_WIDGET,
AffineInnerModalWidget,
@@ -106,6 +109,7 @@ function registerWidgets() {
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
AffinePageDraggingAreaWidget
);
customElements.define(AFFINE_IMAGE_TOOLBAR_WIDGET, AffineImageToolbarWidget);
customElements.define(
AFFINE_VIEWPORT_OVERLAY_WIDGET,
AffineViewportOverlayWidget
@@ -142,6 +146,7 @@ function registerMiscComponents() {
customElements.define('affine-template-loading', AffineTemplateLoading);
// Toolbar and UI components
customElements.define('affine-image-toolbar', AffineImageToolbar);
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
customElements.define('overlay-scrollbar', OverlayScrollbar);
@@ -195,8 +200,10 @@ declare global {
'affine-page-root': PageRootBlockComponent;
'zoom-bar-toggle-button': ZoomBarToggleButton;
'edgeless-zoom-toolbar': EdgelessZoomToolbar;
'affine-image-toolbar': AffineImageToolbar;
[AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET]: AffineEdgelessZoomToolbarWidget;
[AFFINE_IMAGE_TOOLBAR_WIDGET]: AffineImageToolbarWidget;
[AFFINE_INNER_MODAL_WIDGET]: AffineInnerModalWidget;
}
}
@@ -415,8 +415,6 @@ export class PageRootBlockComponent extends BlockComponent<
return !(isNote && displayOnEdgeless);
});
this.contentEditable = String(!this.doc.readonly$.value);
return html`
<div class="affine-page-root-block-container">${children} ${widgets}</div>
`;
@@ -0,0 +1,138 @@
import { createLitPortal } from '@blocksuite/affine-components/portal';
import type {
EditorIconButton,
MenuItemGroup,
} from '@blocksuite/affine-components/toolbar';
import { renderGroups } from '@blocksuite/affine-components/toolbar';
import { noop } from '@blocksuite/global/utils';
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom';
import { html, LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { ImageToolbarContext } from '../context.js';
import { styles } from '../styles.js';
export class AffineImageToolbar extends LitElement {
static override styles = styles;
private _currentOpenMenu: AbortController | null = null;
private _popMenuAbortController: AbortController | null = null;
closeCurrentMenu = () => {
if (this._currentOpenMenu && !this._currentOpenMenu.signal.aborted) {
this._currentOpenMenu.abort();
this._currentOpenMenu = null;
}
};
private _clearPopMenu() {
if (this._popMenuAbortController) {
this._popMenuAbortController.abort();
this._popMenuAbortController = null;
}
}
private _toggleMoreMenu() {
// If the menu we're trying to open is already open, return
if (
this._currentOpenMenu &&
!this._currentOpenMenu.signal.aborted &&
this._currentOpenMenu === this._popMenuAbortController
) {
this.closeCurrentMenu();
this._moreMenuOpen = false;
return;
}
this.closeCurrentMenu();
this._popMenuAbortController = new AbortController();
this._popMenuAbortController.signal.addEventListener('abort', () => {
this._moreMenuOpen = false;
this.onActiveStatusChange(false);
});
this.onActiveStatusChange(true);
this._currentOpenMenu = this._popMenuAbortController;
if (!this._moreButton) {
return;
}
createLitPortal({
template: html`
<editor-menu-content
data-show
class="image-more-popup-menu"
style=${styleMap({
'--content-padding': '8px',
'--packed-height': '4px',
})}
>
<div data-size="large" data-orientation="vertical">
${renderGroups(this.moreGroups, this.context)}
</div>
</editor-menu-content>
`,
container: this.context.host,
// stacking-context(editor-host)
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
},
computePosition: {
referenceElement: this._moreButton,
placement: 'bottom-start',
middleware: [flip(), offset(4)],
autoUpdate: { animationFrame: true },
},
abortController: this._popMenuAbortController,
closeOnClickAway: true,
});
this._moreMenuOpen = true;
}
override disconnectedCallback() {
super.disconnectedCallback();
this.closeCurrentMenu();
this._clearPopMenu();
}
override render() {
return html`
<editor-toolbar class="affine-image-toolbar-container" data-without-bg>
${renderGroups(this.primaryGroups, this.context)}
<editor-icon-button
class="image-toolbar-button more"
aria-label="More"
.tooltip=${'More'}
.tooltipOffset=${4}
.showTooltip=${!this._moreMenuOpen}
.iconSize=${'20px'}
@click=${() => this._toggleMoreMenu()}
>
${MoreVerticalIcon()}
</editor-icon-button>
</editor-toolbar>
`;
}
@query('editor-icon-button.more')
private accessor _moreButton!: EditorIconButton;
@state()
private accessor _moreMenuOpen = false;
@property({ attribute: false })
accessor context!: ImageToolbarContext;
@property({ attribute: false })
accessor moreGroups!: MenuItemGroup<ImageToolbarContext>[];
@property({ attribute: false })
accessor onActiveStatusChange: (active: boolean) => void = noop;
@property({ attribute: false })
accessor primaryGroups!: MenuItemGroup<ImageToolbarContext>[];
}
@@ -0,0 +1,144 @@
import {
CaptionIcon,
CopyIcon,
DeleteIcon,
DownloadIcon,
} from '@blocksuite/affine-components/icons';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { BookmarkIcon, DuplicateIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import type { ImageToolbarContext } from './context.js';
import { duplicate } from './utils.js';
export const PRIMARY_GROUPS: MenuItemGroup<ImageToolbarContext>[] = [
{
type: 'primary',
items: [
{
type: 'download',
label: 'Download',
icon: DownloadIcon,
generate: ({ blockComponent }) => {
return {
action: () => {
blockComponent.download();
},
render: item => html`
<editor-icon-button
class="image-toolbar-button download"
aria-label=${ifDefined(item.label)}
.tooltip=${item.label}
.tooltipOffset=${4}
@click=${(e: MouseEvent) => {
e.stopPropagation();
item.action();
}}
>
${item.icon}
</editor-icon-button>
`,
};
},
},
{
type: 'caption',
label: 'Caption',
icon: CaptionIcon,
when: ({ doc }) => !doc.readonly,
generate: ({ blockComponent }) => {
return {
action: () => {
blockComponent.captionEditor?.show();
},
render: item => html`
<editor-icon-button
class="image-toolbar-button caption"
aria-label=${ifDefined(item.label)}
.tooltip=${item.label}
.tooltipOffset=${4}
@click=${(e: MouseEvent) => {
e.stopPropagation();
item.action();
}}
>
${item.icon}
</editor-icon-button>
`,
};
},
},
],
},
];
// Clipboard Group
export const clipboardGroup: MenuItemGroup<ImageToolbarContext> = {
type: 'clipboard',
items: [
{
type: 'copy',
label: 'Copy',
icon: CopyIcon,
action: ({ blockComponent, close }) => {
blockComponent.copy();
close();
},
},
{
type: 'duplicate',
label: 'Duplicate',
icon: DuplicateIcon(),
when: ({ doc }) => !doc.readonly,
action: ({ blockComponent, abortController }) => {
duplicate(blockComponent, abortController);
},
},
],
};
// Conversions Group
export const conversionsGroup: MenuItemGroup<ImageToolbarContext> = {
type: 'conversions',
items: [
{
label: 'Turn into card view',
type: 'turn-into-card-view',
icon: BookmarkIcon(),
when: ({ doc, blockComponent }) => {
const supportAttachment =
doc.schema.flavourSchemaMap.has('affine:attachment');
const readonly = doc.readonly;
return supportAttachment && !readonly && !!blockComponent.blob;
},
action: ({ blockComponent, close }) => {
blockComponent.convertToCardView();
close();
},
},
],
};
// Delete Group
export const deleteGroup: MenuItemGroup<ImageToolbarContext> = {
type: 'delete',
items: [
{
type: 'delete',
label: 'Delete',
icon: DeleteIcon,
when: ({ doc }) => !doc.readonly,
action: ({ doc, blockComponent, close }) => {
doc.deleteBlock(blockComponent.model);
close();
},
},
],
};
export const MORE_GROUPS: MenuItemGroup<ImageToolbarContext>[] = [
clipboardGroup,
conversionsGroup,
deleteGroup,
];
@@ -0,0 +1,43 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { MenuContext } from '@blocksuite/affine-components/toolbar';
export class ImageToolbarContext extends MenuContext {
override close = () => {
this.abortController.abort();
};
get doc() {
return this.blockComponent.doc;
}
get host() {
return this.blockComponent.host;
}
get selectedBlockModels() {
return [this.blockComponent.model];
}
get std() {
return this.blockComponent.std;
}
constructor(
public blockComponent: ImageBlockComponent,
public abortController: AbortController
) {
super();
}
isEmpty() {
return false;
}
isMultiple() {
return false;
}
isSingle() {
return true;
}
}
@@ -0,0 +1,173 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import { HoverController } from '@blocksuite/affine-components/hover';
import type {
AdvancedMenuItem,
MenuItemGroup,
} from '@blocksuite/affine-components/toolbar';
import {
cloneGroups,
getMoreMenuConfig,
} from '@blocksuite/affine-components/toolbar';
import type { ImageBlockModel } from '@blocksuite/affine-model';
import { PAGE_HEADER_HEIGHT } from '@blocksuite/affine-shared/consts';
import {
BlockSelection,
TextSelection,
WidgetComponent,
} from '@blocksuite/std';
import { limitShift, shift } from '@floating-ui/dom';
import { html } from 'lit';
import { MORE_GROUPS, PRIMARY_GROUPS } from './config.js';
import { ImageToolbarContext } from './context.js';
export const AFFINE_IMAGE_TOOLBAR_WIDGET = 'affine-image-toolbar-widget';
export class AffineImageToolbarWidget extends WidgetComponent<
ImageBlockModel,
ImageBlockComponent
> {
private _hoverController: HoverController | null = null;
private _isActivated = false;
private readonly _setHoverController = () => {
this._hoverController = null;
this._hoverController = new HoverController(
this,
({ abortController }) => {
const imageBlock = this.block;
if (!imageBlock) {
return null;
}
const selection = this.host.selection;
const textSelection = selection.find(TextSelection);
if (
!!textSelection &&
(!!textSelection.to || !!textSelection.from.length)
) {
return null;
}
const blockSelections = selection.filter(BlockSelection);
if (
blockSelections.length > 1 ||
(blockSelections.length === 1 &&
blockSelections[0].blockId !== imageBlock.blockId)
) {
return null;
}
const imageContainer =
imageBlock.resizableImg ?? imageBlock.fallbackCard;
if (!imageContainer) {
return null;
}
const context = new ImageToolbarContext(imageBlock, abortController);
return {
template: html`<affine-image-toolbar
.context=${context}
.primaryGroups=${this.primaryGroups}
.moreGroups=${this.moreGroups}
.onActiveStatusChange=${(active: boolean) => {
this._isActivated = active;
if (!active && !this._hoverController?.isHovering) {
this._hoverController?.abort();
}
}}
></affine-image-toolbar>`,
container: this.block,
// stacking-context(editor-host)
portalStyles: {
zIndex: 'var(--affine-z-index-popover)',
},
computePosition: {
referenceElement: imageContainer,
placement: 'right-start',
middleware: [
shift({
crossAxis: true,
padding: {
top: PAGE_HEADER_HEIGHT + 12,
bottom: 12,
right: 12,
},
limiter: limitShift(),
}),
],
autoUpdate: true,
},
};
},
{ allowMultiple: true }
);
const imageBlock = this.block;
if (!imageBlock) {
return;
}
this._hoverController.setReference(imageBlock);
this._hoverController.onAbort = () => {
// If the more menu is opened, don't close it.
if (this._isActivated) return;
this._hoverController?.abort();
return;
};
};
addMoreItems = (
items: AdvancedMenuItem<ImageToolbarContext>[],
index?: number,
type?: string
) => {
let group;
if (type) {
group = this.moreGroups.find(g => g.type === type);
}
if (!group) {
group = this.moreGroups[0];
}
if (index === undefined) {
group.items.push(...items);
return this;
}
group.items.splice(index, 0, ...items);
return this;
};
addPrimaryItems = (
items: AdvancedMenuItem<ImageToolbarContext>[],
index?: number
) => {
if (index === undefined) {
this.primaryGroups[0].items.push(...items);
return this;
}
this.primaryGroups[0].items.splice(index, 0, ...items);
return this;
};
/*
* Caches the more menu items.
* Currently only supports configuring more menu.
*/
moreGroups: MenuItemGroup<ImageToolbarContext>[] = cloneGroups(MORE_GROUPS);
primaryGroups: MenuItemGroup<ImageToolbarContext>[] =
cloneGroups(PRIMARY_GROUPS);
override firstUpdated() {
if (this.doc.getParent(this.model.id)?.flavour === 'affine:surface') {
return;
}
this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups);
this._setHoverController();
}
}
@@ -0,0 +1,24 @@
import { css } from 'lit';
export const styles = css`
:host {
position: absolute;
top: 0;
right: 0;
z-index: var(--affine-z-index-popover);
}
.affine-image-toolbar-container {
height: 24px;
gap: 4px;
padding: 4px;
margin: 0;
}
.image-toolbar-button {
color: var(--affine-icon-color);
background-color: var(--affine-background-primary-color);
box-shadow: var(--affine-shadow-1);
border-radius: 4px;
}
`;
@@ -0,0 +1,56 @@
import type { ImageBlockComponent } from '@blocksuite/affine-block-image';
import {
getBlockProps,
isInsidePageEditor,
} from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/std';
export function duplicate(
block: ImageBlockComponent,
abortController?: AbortController
) {
const model = block.model;
const blockProps = getBlockProps(model);
const {
width: _width,
height: _height,
xywh: _xywh,
rotate: _rotate,
zIndex: _zIndex,
...duplicateProps
} = blockProps;
const { doc } = model;
const parent = doc.getParent(model);
if (!parent) {
console.error(`Parent not found for block(${model.flavour}) ${model.id}`);
return;
}
const index = parent?.children.indexOf(model);
const duplicateId = doc.addBlock(
model.flavour,
duplicateProps,
parent,
index + 1
);
abortController?.abort();
const editorHost = block.host;
editorHost.updateComplete
.then(() => {
const { selection } = editorHost;
selection.setGroup('note', [
selection.create(BlockSelection, {
blockId: duplicateId,
}),
]);
if (isInsidePageEditor(editorHost)) {
const duplicateElement = editorHost.view.getBlock(duplicateId);
if (duplicateElement) {
duplicateElement.scrollIntoView(true);
}
}
})
.catch(console.error);
}
@@ -1,4 +1,5 @@
export { AffineEdgelessZoomToolbarWidget } from './edgeless-zoom-toolbar/index.js';
export { AffineImageToolbarWidget } from './image-toolbar/index.js';
export { AffineInnerModalWidget } from './inner-modal/inner-modal.js';
export * from './keyboard-toolbar/index.js';
export {
@@ -493,7 +493,6 @@ const embedToolGroup: KeyboardToolPanelGroup = {
linkInputPopupOptions: {
showCloseButton: true,
variant: 'mobile',
telemetrySegment: 'keyboard toolbar',
},
})
.run();
@@ -304,11 +304,6 @@ export class LinkedDocPopover extends SignalWatcher(
data-id=${key}
.text=${name}
hover=${this._activatedItemKey === key}
@pointerdown=${(e: PointerEvent) => {
// Prevent event listeners being registered on the root document
// eg., radix-ui dialogs usePointerDownOutside hooks
e.stopPropagation();
}}
@click=${() => {
action()?.catch(console.error);
}}
@@ -13,7 +13,7 @@ import { BlockSelection } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { insertSurfaceRefBlockCommand } from '../commands';
import { EdgelessTooltip, FrameTooltip, MindMapTooltip } from './tooltips';
import { EdgelessTooltip } from './tooltips';
const surfaceRefSlashMenuConfig: SlashMenuConfig = {
items: ({ std, model }) => {
@@ -56,7 +56,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = {
description: 'Insert a blank frame',
icon: FrameIcon(),
tooltip: {
figure: FrameTooltip,
figure: EdgelessTooltip,
caption: 'Frame',
},
group: `5_Edgeless Element@${index++}`,
@@ -72,7 +72,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = {
description: 'Insert a mind map',
icon: MindmapIcon(),
tooltip: {
figure: MindMapTooltip,
figure: EdgelessTooltip,
caption: 'Edgeless',
},
group: `5_Edgeless Element@${index++}`,
@@ -26,86 +26,3 @@ export const EdgelessTooltip = html`<svg width="170" height="106" viewBox="0 0 1
</g>
</svg>
`;
// prettier-ignore
export const FrameTooltip = html`<svg width="170" height="89" viewBox="0 0 170 89" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5269_147682)">
<rect width="170" height="89" fill="white"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Create a blank frame in Edgeless</tspan></text>
<rect x="16" y="45" width="164" height="59" rx="3" stroke="black" stroke-opacity="0.52" stroke-width="2"/>
<rect x="15" y="27" width="32" height="13" rx="3" fill="black" fill-opacity="0.95"/>
<text fill="white" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="8" font-weight="500" letter-spacing="0px"><tspan x="19" y="35.8182">Frame</tspan></text>
</g>
<defs>
<clipPath id="clip0_5269_147682">
<rect width="170" height="89" fill="white"/>
</clipPath>
</defs>
</svg>
`
// prettier-ignore
export const MindMapTooltip = html`<svg width="170" height="106" viewBox="0 0 170 106" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5150_67028)">
<rect width="170" height="106" fill="white"/>
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">Create a mind map in Edgeless</tspan></text>
<g filter="url(#filter0_d_5150_67028)">
<rect x="21" y="53" width="59" height="19" rx="5" stroke="#29A3FA" stroke-width="2"/>
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Poppins" font-size="8" font-weight="500" letter-spacing="0px"><tspan x="30.5" y="65.076">Mind Map</tspan></text>
</g>
<g filter="url(#filter1_d_5150_67028)">
<rect x="119.75" y="30" width="28.25" height="13.125" rx="5" stroke="#6E52DF" stroke-width="2"/>
</g>
<g filter="url(#filter2_d_5150_67028)">
<rect x="119.75" y="55.8013" width="28.25" height="13.125" rx="5" stroke="#E660A4" stroke-width="2"/>
</g>
<g filter="url(#filter3_d_5150_67028)">
<rect x="119.75" y="81.603" width="28.25" height="13.125" rx="5" stroke="#FF8C38" stroke-width="2"/>
</g>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 88.7686 120.514 88.7686" stroke="#FF8C38" stroke-width="2"/>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 62.7686 120.514 62.7686" stroke="#E660A4" stroke-width="2"/>
<path d="M81.5139 62.7686C104.205 62.7686 97.1139 36.7686 120.514 36.7686" stroke="#6E52DF" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d_5150_67028" x="8" y="46" width="85" height="45" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter1_d_5150_67028" x="106.75" y="23" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter2_d_5150_67028" x="106.75" y="48.8013" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<filter id="filter3_d_5150_67028" x="106.75" y="74.603" width="54.25" height="39.125" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="6"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.14 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5150_67028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5150_67028" result="shape"/>
</filter>
<clipPath id="clip0_5150_67028">
<rect width="170" height="106" fill="white"/>
</clipPath>
</defs>
</svg>
`
@@ -32,7 +32,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "3.1.1"
"vitest": "3.0.9"
},
"exports": {
".": "./src/index.ts",
@@ -1,7 +1,6 @@
import type { Command } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { SurfaceBlockModel } from '../surface-model';
import { SurfaceBlockService } from '../surface-service';
/**
* Re-associate bindings for block that have been converted.
@@ -14,14 +13,14 @@ export const reassociateConnectorsCommand: Command<{
newId: string;
}> = (ctx, next) => {
const { oldId, newId } = ctx;
const gfx = ctx.std.get(GfxControllerIdentifier);
if (!oldId || !newId || !gfx.surface) {
const service = ctx.std.get(SurfaceBlockService);
if (!oldId || !newId || !service) {
next();
return;
}
const surface = gfx.surface;
const connectors = (surface as SurfaceBlockModel).getConnectors(oldId);
const surface = service.surface;
const connectors = surface.getConnectors(oldId);
for (const { id, source, target } of connectors) {
if (source.id === oldId) {
surface.updateElement(id, {
@@ -56,6 +56,7 @@ export {
SurfaceBlockSchema,
SurfaceBlockSchemaExtension,
} from './surface-model.js';
export type { SurfaceBlockService } from './surface-service.js';
export {
EdgelessSurfaceBlockSpec,
PageSurfaceBlockSpec,
@@ -2,8 +2,12 @@ import { BlockComponent } from '@blocksuite/std';
import { nothing } from 'lit';
import type { SurfaceBlockModel } from './surface-model.js';
import type { SurfaceBlockService } from './surface-service.js';
export class SurfaceBlockVoidComponent extends BlockComponent<SurfaceBlockModel> {
export class SurfaceBlockVoidComponent extends BlockComponent<
SurfaceBlockModel,
SurfaceBlockService
> {
override render() {
return nothing;
}
@@ -17,6 +17,7 @@ import {
} from './renderer/elements/index.js';
import { OverlayIdentifier } from './renderer/overlay.js';
import type { SurfaceBlockModel } from './surface-model.js';
import type { SurfaceBlockService } from './surface-service.js';
export interface SurfaceContext {
viewport: Viewport;
@@ -30,7 +31,10 @@ export interface SurfaceContext {
};
}
export class SurfaceBlockComponent extends BlockComponent<SurfaceBlockModel> {
export class SurfaceBlockComponent extends BlockComponent<
SurfaceBlockModel,
SurfaceBlockService
> {
static isConnector = (element: unknown): element is ConnectorElementModel => {
return element instanceof ConnectorElementModel;
};
@@ -0,0 +1,37 @@
import { BlockService } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { type SurfaceBlockModel, SurfaceBlockSchema } from './surface-model.js';
import { getSurfaceBlock } from './utils/get-surface-block.js';
export class SurfaceBlockService extends BlockService {
static override readonly flavour = SurfaceBlockSchema.model.flavour;
surface!: SurfaceBlockModel;
get layer() {
return this.std.get(GfxControllerIdentifier).layer;
}
override mounted(): void {
super.mounted();
const surface = getSurfaceBlock(this.doc);
// FIXME: BS-2271
this.surface = surface!;
if (!this.surface) {
const disposable = this.doc.slots.blockUpdated.subscribe(payload => {
if (payload.flavour === 'affine:surface') {
disposable.unsubscribe();
const surface = this.doc.getModelById(
payload.id
) as SurfaceBlockModel | null;
if (!surface) return;
this.surface = surface;
}
});
}
}
}
@@ -11,9 +11,11 @@ import {
EdgelessLegacySlotExtension,
} from './extensions';
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
import { SurfaceBlockService } from './surface-service';
const CommonSurfaceBlockSpec: ExtensionType[] = [
FlavourExtension('affine:surface'),
SurfaceBlockService,
EdgelessCRUDExtension,
EdgelessLegacySlotExtension,
ExportManagerExtension,
@@ -1,6 +1,6 @@
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import type { TableBlockModel } from '@blocksuite/affine-model';
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { VirtualPaddingController } from '@blocksuite/affine-shared/utils';
import { IS_MOBILE } from '@blocksuite/global/env';
@@ -42,9 +42,7 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
override get topContenteditableElement() {
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
return this.closest<BlockComponent>(
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR
);
return this.closest<BlockComponent>(NOTE_SELECTOR);
}
return this.rootComponent;
}
+1 -2
View File
@@ -68,8 +68,7 @@
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",
"./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts",
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts",
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts",
"./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts"
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts"
},
"files": [
"src",
@@ -9,7 +9,6 @@ import {
stdContext,
TextSelection,
} from '@blocksuite/std';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import type { BlockModel, Store } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import { consume } from '@lit/context';
@@ -122,8 +121,6 @@ export class BlockCaptionEditor<
override connectedCallback(): void {
super.connectedCallback();
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.caption = this.model.props.caption;
this.disposables.add(
@@ -1,4 +1,4 @@
import { fontSMStyle, fontXSStyle } from '@blocksuite/affine-shared/styles';
import { FONT_SM, FONT_XS } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const COLOR_PICKER_STYLE = css`
@@ -21,7 +21,6 @@ export const COLOR_PICKER_STYLE = css`
background: var(--affine-hover-color);
}
${fontXSStyle('nav button')}
nav button {
display: flex;
padding: 4px 8px;
@@ -30,6 +29,7 @@ export const COLOR_PICKER_STYLE = css`
align-items: center;
flex: 1 0 0;
${FONT_XS};
color: var(--affine-text-secondary-color);
font-weight: 600;
@@ -61,7 +61,6 @@ export const COLOR_PICKER_STYLE = css`
flex: 1 0 0;
}
${fontXSStyle('.modes .mode button')}
.modes .mode button {
position: relative;
display: flex;
@@ -76,6 +75,7 @@ export const COLOR_PICKER_STYLE = css`
border: 1px solid var(--affine-border-color);
box-sizing: border-box;
${FONT_XS};
font-weight: 400;
color: #8e8d91;
}
@@ -268,7 +268,6 @@ export const COLOR_PICKER_STYLE = css`
gap: 0;
}
${fontSMStyle('input')}
input {
display: flex;
width: 100%;
@@ -276,6 +275,7 @@ export const COLOR_PICKER_STYLE = css`
background: transparent;
border: none;
outline: none;
${FONT_SM};
font-weight: 400;
color: var(--affine-text-primary-color);
}
@@ -8,7 +8,6 @@ import {
SearchIcon,
} from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import {
autoPlacement,
autoUpdate,
@@ -384,7 +383,6 @@ export const getDefaultModalRoot = (ele: HTMLElement) => {
};
export const createModal = (container: HTMLElement = document.body) => {
const div = document.createElement('div');
div.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
div.style.pointerEvents = 'auto';
div.style.position = 'absolute';
div.style.left = '0';
@@ -9,7 +9,7 @@ import {
type TelemetryEvent,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { fontSMStyle, fontXSStyle } from '@blocksuite/affine-shared/styles';
import { FONT_SM, FONT_XS } from '@blocksuite/affine-shared/styles';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
listenClickAway,
@@ -90,8 +90,8 @@ export class EmbedCardEditModal extends SignalWatcher(
background: transparent;
border: 1px solid ${unsafeCSSVarV2('input/border/default')};
color: var(--affine-text-primary-color);
${FONT_SM};
}
${fontSMStyle('.row .input')}
.input::placeholder {
color: var(--affine-placeholder-color);
}
@@ -117,9 +117,9 @@ export class EmbedCardEditModal extends SignalWatcher(
border-radius: 4px;
border: 1px solid ${unsafeCSSVarV2('button/innerBlackBorder')};
background: ${unsafeCSSVarV2('button/secondary')};
${FONT_XS};
color: ${unsafeCSSVarV2('text/primary')};
}
${fontXSStyle('.row.actions .button')}
.row.actions .button[disabled],
.row.actions .button:disabled {
pointer-events: none;
@@ -1,4 +1,4 @@
import { fontXSStyle, panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const embedCardModalStyles = css`
@@ -12,8 +12,8 @@ export const embedCardModalStyles = css`
z-index: 1;
}
${panelBaseStyle('.embed-card-modal-wrapper')}
.embed-card-modal-wrapper {
${PANEL_BASE};
flex-direction: column;
position: absolute;
left: 0;
@@ -50,8 +50,8 @@ export const embedCardModalStyles = css`
border: 1px solid var(--affine-border-color);
background: var(--affine-white-10);
color: var(--affine-text-primary-color);
${FONT_XS};
}
${fontXSStyle('.embed-card-modal-input')}
input.embed-card-modal-input {
padding-top: 4px;
padding-bottom: 4px;
@@ -1,12 +1,10 @@
import {
panelBaseStyle,
scrollbarStyle,
} from '@blocksuite/affine-shared/styles';
import { PANEL_BASE, scrollbarStyle } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const filterableListStyles = css`
${panelBaseStyle(':host')}
:host {
${PANEL_BASE};
flex-direction: column;
padding: 0;
@@ -1,121 +0,0 @@
import {
type OpenDocMode,
type ToolbarAction,
ToolbarContext,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { PropTypes, requiredProperties } from '@blocksuite/std';
import type { ReadonlySignal } from '@preact/signals-core';
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { repeat } from 'lit-html/directives/repeat.js';
import { EditorChevronDown } from '../toolbar';
@requiredProperties({
actions: PropTypes.array,
context: PropTypes.instanceOf(ToolbarContext),
openDocMode$: PropTypes.object,
updateOpenDocMode: PropTypes.instanceOf(Function),
})
export class OpenDocDropdownMenu extends SignalWatcher(
WithDisposable(LitElement)
) {
static override styles = css`
div[data-orientation] {
width: 264px;
gap: 4px;
min-width: unset;
overflow: unset;
}
editor-menu-action {
.label {
display: flex;
flex: 1;
justify-content: space-between;
}
.shortcut {
color: ${unsafeCSSVarV2('text/secondary')};
}
}
`;
@property({ attribute: false })
accessor actions!: (ToolbarAction & {
mode: OpenDocMode;
shortcut?: string;
})[];
@property({ attribute: false })
accessor context!: ToolbarContext;
@property({ attribute: false })
accessor openDocMode$!: ReadonlySignal<OpenDocMode>;
@property({ attribute: false })
accessor updateOpenDocMode!: (mode: OpenDocMode) => void;
override render() {
const {
actions,
context,
openDocMode$: { value: openDocMode },
updateOpenDocMode,
} = this;
const currentAction =
actions.find(a => a.mode === openDocMode) ?? actions[0];
return html`
<editor-menu-button
aria-label="Open doc menu"
.contentPadding="${'8px'}"
.button=${html`
<editor-icon-button
data-open-doc-mode="${currentAction.label}"
aria-label="Open doc"
.tooltip="${'Open doc'}"
.justify="${'space-between'}"
.labelHeight="${'20px'}"
.iconContainerWidth="${'84px'}"
>
${currentAction.icon}
<span class="label">Open</span> ${EditorChevronDown}
</editor-icon-button>
`}
>
<div data-orientation="vertical">
${repeat(
actions,
action => action.id,
({ label, icon, run, disabled, mode, shortcut }) => html`
<editor-menu-action
aria-label=${ifDefined(label)}
?disabled=${ifDefined(disabled)}
@click=${() => {
run?.(context);
updateOpenDocMode(mode);
}}
>
${icon}
<div class="label">
${label}
<span class="shortcut">${shortcut}</span>
</div>
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-open-doc-dropdown-menu': OpenDocDropdownMenu;
}
}
@@ -1,7 +0,0 @@
import { OpenDocDropdownMenu } from './dropdown-menu';
export * from './dropdown-menu';
export function effects() {
customElements.define('affine-open-doc-dropdown-menu', OpenDocDropdownMenu);
}
@@ -1,4 +1,4 @@
import { panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { PANEL_BASE } from '@blocksuite/affine-shared/styles';
import {
type ButtonPopperOptions,
createButtonPopper,
@@ -117,8 +117,8 @@ export class EditorMenuContent extends LitElement {
overflow-y: auto;
}
${panelBaseStyle(':host([data-show])')}
:host([data-show]) {
${PANEL_BASE};
justify-content: center;
padding: var(--content-padding, 0 6px);
}
@@ -1,12 +1,12 @@
import { panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { css, html, LitElement } from 'lit';
export class EditorToolbar extends WithDisposable(LitElement) {
static override styles = css`
${panelBaseStyle(':host')}
:host {
${PANEL_BASE};
height: 36px;
box-sizing: content-box;
}
@@ -3,7 +3,6 @@ import {
arrow,
type ComputePositionReturn,
flip,
hide,
offset,
type Placement,
shift,
@@ -143,7 +142,6 @@ export class Tooltip extends LitElement {
// return null;
if (this.hidden) return null;
let arrowStyles: StyleInfo = {};
let tooltipStyles: StyleInfo = {};
return {
template: ({ positionSlot, updatePortal }) => {
positionSlot.subscribe(data => {
@@ -154,15 +152,6 @@ export class Tooltip extends LitElement {
} else {
arrowStyles = {};
}
if (this.autoHide) {
tooltipStyles.visibility = data.middlewareData.hide
?.referenceHidden
? 'hidden'
: '';
arrowStyles.visibility = tooltipStyles.visibility;
}
updatePortal();
});
@@ -174,13 +163,7 @@ export class Tooltip extends LitElement {
<style>
${this._getStyles()}
</style>
<div
class="affine-tooltip"
role="tooltip"
style=${styleMap(tooltipStyles)}
>
${children}
</div>
<div class="affine-tooltip" role="tooltip">${children}</div>
<div class="arrow" style=${styleMap(arrowStyles)}></div>
`;
},
@@ -194,7 +177,6 @@ export class Tooltip extends LitElement {
arrow({
element: portalRoot.shadowRoot!.querySelector('.arrow')!,
}),
this.autoHide && hide({ strategy: 'referenceHidden' }),
],
autoUpdate: true,
}),
@@ -275,14 +257,6 @@ export class Tooltip extends LitElement {
@property({ attribute: false })
accessor autoFlip = true;
/**
* Hide the tooltip when the reference element is not in view.
*
* See https://floating-ui.com/docs/hide
*/
@property({ attribute: false })
accessor autoHide = false;
/**
* shifts the floating element to keep it in view.
* this prevents the floating element from
@@ -16,7 +16,7 @@ import {
} from '@blocksuite/icons/lit';
import { ShadowlessElement } from '@blocksuite/std';
import { nanoid } from '@blocksuite/store';
import { autoPlacement, offset, shift } from '@floating-ui/dom';
import { flip, offset, shift } from '@floating-ui/dom';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { nothing } from 'lit';
@@ -588,18 +588,7 @@ export const popTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
};
const remove = createPopup(target, component, {
onClose: ops.onComplete,
middleware: [
autoPlacement({
allowedPlacements: [
'bottom-start',
'bottom-end',
'top-start',
'top-end',
],
}),
offset({ mainAxis: -36 }),
shift(),
],
middleware: [flip(), offset({ mainAxis: -28, crossAxis: 112 }), shift()],
container: ops.container,
});
return remove;
@@ -13,7 +13,7 @@ import {
import { ShadowlessElement } from '@blocksuite/std';
import { computed, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property } from 'lit/decorators.js';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit/static-html.js';
@@ -249,7 +249,7 @@ export class RecordField extends SignalWatcher(
'field-content': true,
empty: !this.isEditing$.value && this.cell$.value.isEmpty$.value,
'is-editing': this.isEditing$.value,
'is-focus': this.isFocus$.value,
'is-focus': this.isFocus,
});
return html`
<div>
@@ -271,7 +271,8 @@ export class RecordField extends SignalWatcher(
isEditing$ = signal(false);
isFocus$ = signal(false);
@state()
accessor isFocus = false;
@property({ attribute: false })
accessor view!: SingleView;
@@ -57,7 +57,7 @@ export class DetailSelection {
return;
}
container.isFocus$.value = false;
container.isFocus = false;
const cell = container.cell;
if (selection.isEditing) {
@@ -80,7 +80,7 @@ export class DetailSelection {
if (!container) {
return;
}
container.isFocus$.value = true;
container.isFocus = true;
const cell = container.cell;
if (selection.isEditing) {
if (cell?.focusCell()) {
@@ -59,10 +59,8 @@ export class MultiSelectCell extends BaseCellRenderer<
}
override beforeExitEditingMode() {
requestAnimationFrame(() => {
this.closePopup?.();
this.closePopup = undefined;
});
this.closePopup?.();
this.closePopup = undefined;
}
override render() {
@@ -277,7 +277,7 @@ export const popViewOptions = (
const data: MenuButtonData = {
content: () => html`
<div
style="color:var(--affine-text-emphasis-color);width:100%;display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 6px 16px;white-space: nowrap"
style="color:var(--affine-text-emphasis-color);width:100%;display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 6px 16px;"
>
<div style="${iconStyle}">
${renderUniLit(meta.renderer.icon)}
@@ -265,7 +265,6 @@ export class FramePanelBody extends SignalWatcher(
html`<affine-frame-card
data-frame-id=${frame.id}
.frame=${frame}
.std=${this.editorHost.std}
.cardIndex=${cardIndex}
.frameIndex=${frameIndex}
.status=${selectedFrames.has(frame.id)
@@ -1,7 +1,7 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { on, once } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/lit';
import { type BlockStdScope, ShadowlessElement } from '@blocksuite/std';
import { ShadowlessElement } from '@blocksuite/std';
import { css, html, nothing } from 'lit';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -177,18 +177,18 @@ export class FrameCard extends WithDisposable(ShadowlessElement) {
override render() {
const { pos, stackOrder, width } = this;
const containerStyle = styleMap(
const containerStyle =
this.status === 'dragging'
? {
? styleMap({
transform: `${
stackOrder === 0
? `translate(${pos.x - 16}px, ${pos.y - 8}px)`
: `translate(${pos.x - 10}px, ${pos.y - 16}px) scale(0.96)`
}`,
width: width ? `${width}px` : undefined,
}
: {}
);
})
: {};
return html`<div
class="frame-card-container ${this.status ?? ''}"
style=${containerStyle}
@@ -207,18 +207,12 @@ export class FrameCard extends WithDisposable(ShadowlessElement) {
>
${this.status === 'dragging' && stackOrder !== 0
? nothing
: html`<frame-preview
.frame=${this.frame}
.std=${this.std}
></frame-preview>`}
: html`<frame-preview .frame=${this.frame}></frame-preview>`}
${this._DraggingCardNumber()}
</div>
</div>`;
}
@property({ attribute: false })
accessor std!: BlockStdScope;
@property({ attribute: false })
accessor cardIndex!: number;
@@ -1,9 +1,5 @@
import type { FrameBlockModel } from '@blocksuite/affine-model';
import {
DocModeExtension,
DocModeProvider,
ViewportElementExtension,
} from '@blocksuite/affine-shared/services';
import { ViewportElementExtension } from '@blocksuite/affine-shared/services';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { DisposableGroup } from '@blocksuite/global/disposable';
import { Bound, deserializeXYWH } from '@blocksuite/global/gfx';
@@ -51,14 +47,11 @@ const styles = css`
overflow: hidden;
pointer-events: none;
user-select: none;
}
.frame-preview-viewport
> editor-host
> affine-edgeless-root-preview
> .edgeless-background {
background-color: transparent;
background-image: none;
.edgeless-background {
background-color: transparent;
background-image: none;
}
}
`;
@@ -141,12 +134,9 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
});
}
}
const docModeService = this.std.get(DocModeProvider);
this._previewSpec.extend([
ViewportElementExtension('.frame-preview-viewport'),
FramePreviewWatcher,
DocModeExtension(docModeService),
]);
}
@@ -230,9 +220,6 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
}
}
@property({ attribute: false })
accessor std!: BlockStdScope;
@state()
accessor fillScreen = false;
@@ -55,7 +55,6 @@ export function startDragging(
el.stackOrder = arr.length - 1 - idx;
el.pos = start;
el.width = options.width;
el.std = container.editorHost.std;
if (frames.length > 1 && el.stackOrder === 0)
el.draggingCardNumber = frames.length;
@@ -12,13 +12,7 @@ import {
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import { consume } from '@lit/context';
import {
batch,
computed,
effect,
type Signal,
signal,
} from '@preact/signals-core';
import { effect, signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
@@ -59,22 +53,7 @@ export class OutlinePanelBody extends SignalWatcher(
private readonly _edgelessOnlyNotes$ = signal<NoteBlockModel[]>([]);
private readonly _selectedNotes$: Record<
NoteDisplayMode,
Signal<NoteBlockModel[]>
> = {
[NoteDisplayMode.DocOnly]: signal<NoteBlockModel[]>([]),
[NoteDisplayMode.DocAndEdgeless]: signal<NoteBlockModel[]>([]),
[NoteDisplayMode.EdgelessOnly]: signal<NoteBlockModel[]>([]),
};
private readonly _allSelectedNotes$ = computed(() =>
[
NoteDisplayMode.DocAndEdgeless,
NoteDisplayMode.DocOnly,
NoteDisplayMode.EdgelessOnly,
].flatMap(mode => this._selectedNotes$[mode].value)
);
private readonly _selectedNotes$ = signal<NoteBlockModel[]>([]);
private _clearHighlightMask = () => {};
@@ -157,7 +136,7 @@ export class OutlinePanelBody extends SignalWatcher(
if (!this.doc.root) return;
const pageVisibleNotes = this._pageVisibleNotes$.peek();
const selected = this._allSelectedNotes$.peek();
const selected = this._selectedNotes$.peek();
const children = this.doc.root.children.slice();
const noteIndex = new Map<NoteBlockModel, number>();
@@ -224,39 +203,23 @@ export class OutlinePanelBody extends SignalWatcher(
const note = this.doc.getBlock(id)?.model;
if (!note || !matchModels(note, [NoteBlockModel])) return;
// map from signal to value
const selectedNotes = Object.fromEntries(
Object.entries(this._selectedNotes$).map(([k, v]) => [k, v.peek()])
) as Record<NoteDisplayMode, NoteBlockModel[]>;
let selectedNotes = this._selectedNotes$.peek();
if (multiselect) {
selectedNotes[note.props.displayMode] = selected
? [...selectedNotes[note.props.displayMode], note]
: selectedNotes[note.props.displayMode].filter(_note => _note !== note);
if (!selected) {
selectedNotes = selectedNotes.filter(_note => _note !== note);
} else if (multiselect) {
selectedNotes = [...selectedNotes, note];
} else {
selectedNotes[note.props.displayMode] = selected ? [note] : [];
Object.keys(this._selectedNotes$).forEach(mode => {
if (mode !== note.props.displayMode) {
selectedNotes[mode as NoteDisplayMode] = [];
}
});
selectedNotes = [note];
}
// We use gfx.selection and effect to keep sync between canvas and outline panel
if (editorMode === 'edgeless') {
gfx.selection.set({
elements: [...selectedNotes.both, ...selectedNotes.edgeless].map(
({ id }) => id
),
elements: selectedNotes.map(({ id }) => id),
editing: false,
});
this._selectedNotes$.doc.value = selectedNotes.doc;
} else {
[NoteDisplayMode.DocOnly, NoteDisplayMode.DocAndEdgeless].forEach(
mode => {
this._selectedNotes$[mode].value = selectedNotes[mode];
}
);
this._selectedNotes$.value = selectedNotes;
}
}
@@ -274,16 +237,13 @@ export class OutlinePanelBody extends SignalWatcher(
return !!model && matchModels(model, [NoteBlockModel]);
});
// update selected notes from edgeless selection
batch(() => {
[NoteDisplayMode.DocAndEdgeless, NoteDisplayMode.EdgelessOnly].forEach(
mode => {
this._selectedNotes$[mode].value = currSelectedNotes.filter(
note => note.props.displayMode === mode
);
}
);
});
const preSelected = this._selectedNotes$.peek();
if (
preSelected.length !== currSelectedNotes.length ||
preSelected.some(note => !currSelectedNotes.includes(note))
) {
this._selectedNotes$.value = currSelectedNotes;
}
});
}
@@ -320,6 +280,11 @@ export class OutlinePanelBody extends SignalWatcher(
std.dnd.monitor<NoteCardEntity, NoteDropPayload>({
onDragStart: () => {
this._dragging$.value = true;
this._selectedNotes$.value = this._selectedNotes$
.peek()
.filter(note => {
return this._pageVisibleNotes$.value.includes(note);
});
},
onDrag: data => {
const target = data.location.current.dropTargets[0];
@@ -412,7 +377,7 @@ export class OutlinePanelBody extends SignalWatcher(
index=${index}
.note=${note}
.activeHeadingId=${this._activeHeadingId$.value}
.status=${this._allSelectedNotes$.value.includes(note)
.status=${this._selectedNotes$.value.includes(note)
? this._dragging$.value
? 'dragging'
: 'selected'
@@ -73,7 +73,7 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
private readonly _onPickColor = (e: ColorEvent) => {
let color = e.detail.value;
if (this.pen$.peek() === 'highlighter') {
color = adjustColorAlpha(color, 0.3);
color = adjustColorAlpha(color, 0.5);
}
this.onChange({ color });
};
@@ -87,15 +87,6 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
override type: Pen[] = ['brush', 'highlighter'];
override firstUpdated() {
this.disposables.add(
this.gfx.tool.currentToolName$.subscribe(tool => {
if (this.type.map(String).includes(tool)) return;
this.tryDisposePopper();
})
);
}
private _togglePenMenu() {
if (this.tryDisposePopper()) return;
!this.active && this.setEdgelessTool(this.pen$.peek());
@@ -90,7 +90,7 @@ export const highlighterToolbarConfig = {
);
const onPick = (e: PickColorEvent) => {
if (e.type === 'pick') {
const color = adjustColorAlpha(e.detail.value, 0.3);
const color = adjustColorAlpha(e.detail.value, 0.5);
for (const model of models) {
const props = packColor(field, color);
ctx.std
@@ -1,9 +1,9 @@
import { fontXSStyle, panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
const editLinkStyle = css`
${panelBaseStyle('.affine-link-edit-popover')}
.affine-link-edit-popover {
${PANEL_BASE};
display: grid;
grid-template-columns: auto auto;
grid-template-rows: repeat(2, 1fr);
@@ -18,20 +18,20 @@ const editLinkStyle = css`
box-sizing: content-box;
}
${fontXSStyle('.affine-link-edit-popover label')}
.affine-link-edit-popover label {
box-sizing: border-box;
color: var(--affine-icon-color);
${FONT_XS};
font-weight: 400;
}
${fontXSStyle('.affine-link-edit-popover input')}
.affine-link-edit-popover input {
color: inherit;
padding: 0;
border: none;
background: transparent;
color: var(--affine-text-primary-color);
${FONT_XS};
}
.affine-link-edit-popover input::placeholder {
color: var(--affine-placeholder-color);
@@ -125,8 +125,8 @@ export const linkPopupStyle = css`
z-index: var(--affine-z-index-popover);
}
${panelBaseStyle('.affine-link-popover.create')}
.affine-link-popover.create {
${PANEL_BASE};
gap: 12px;
padding: 12px;
@@ -144,8 +144,8 @@ export const linkPopupStyle = css`
border-style: solid;
border-color: var(--affine-border-color);
color: var(--affine-text-primary-color);
${FONT_XS};
}
${fontXSStyle('.affine-link-popover-input')}
.affine-link-popover-input::placeholder {
color: var(--affine-placeholder-color);
}
@@ -1,7 +1,6 @@
import { notifyLinkedDocSwitchedToEmbed } from '@blocksuite/affine-components/notification';
import {
ActionPlacement,
DocDisplayMetaProvider,
type ToolbarAction,
type ToolbarActionGroup,
type ToolbarModuleConfig,
@@ -35,14 +34,9 @@ export const builtinInlineReferenceToolbarConfig = {
if (!(target instanceof AffineReference)) return null;
if (!target.referenceInfo.title) return null;
const originalTitle =
ctx.std.get(DocDisplayMetaProvider).title(target.referenceInfo.pageId)
.value || 'Untitled';
const open = (event: MouseEvent) => target.open({ event });
return html`<affine-linked-doc-title
.title=${originalTitle}
.open=${open}
.title=${target.docTitle}
.open=${(event: MouseEvent) => target.open({ event })}
></affine-linked-doc-title>`;
},
},
@@ -6,7 +6,7 @@ import {
type TelemetryEvent,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { fontXSStyle, panelBaseStyle } from '@blocksuite/affine-shared/styles';
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { stopPropagation } from '@blocksuite/affine-shared/utils';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
@@ -36,8 +36,8 @@ export class ReferencePopup extends SignalWatcher(
height: 100vh;
}
${panelBaseStyle('.popover-container')}
.popover-container {
${PANEL_BASE};
position: absolute;
display: flex;
width: 321px;
@@ -68,8 +68,8 @@ export class ReferencePopup extends SignalWatcher(
border: none;
background: transparent;
color: var(--affine-text-primary-color);
${FONT_XS};
}
${fontXSStyle('input')}
input::placeholder {
color: var(--affine-placeholder-color);
}
@@ -77,8 +77,8 @@ export class ReferencePopup extends SignalWatcher(
outline: none;
}
${fontXSStyle('editor-icon-button.save .label')}
editor-icon-button.save .label {
${FONT_XS};
color: inherit;
text-transform: none;
}
@@ -124,8 +124,8 @@ export const DefaultTheme: Theme = {
shapeFillColor: Medium.Yellow,
connectorColor: Medium.Grey,
noteBackgrounColor: NoteBackgroundColorMap.White,
// 30% transparent `Medium.Blue`
hightlighterColor: '#84cfff4d',
// 50% transparent `Default.black`
hightlighterColor: { dark: '#ffffff80', light: '#00000080' },
Palettes,
ShapeTextColorPalettes,
NoteBackgroundColorMap,
+1 -1
View File
@@ -67,7 +67,7 @@
"!dist/__tests__"
],
"devDependencies": {
"vitest": "3.1.1"
"vitest": "3.0.9"
},
"version": "0.20.0"
}
+2 -2
View File
@@ -1,2 +1,2 @@
export const EDGELESS_TOP_CONTENTEDITABLE_SELECTOR =
'affine-edgeless-note .edgeless-note-page-content, affine-edgeless-text';
export const NOTE_SELECTOR =
'affine-note, affine-edgeless-note .edgeless-note-page-content, affine-edgeless-text';
@@ -15,12 +15,7 @@ import {
} from '@blocksuite/icons/lit';
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/std';
import type { Store } from '@blocksuite/store';
import {
computed,
type ReadonlySignal,
type Signal,
signal,
} from '@preact/signals-core';
import { computed, type Signal, signal } from '@preact/signals-core';
import type { TemplateResult } from 'lit';
import { referenceToNode } from '../utils/reference.js';
@@ -49,11 +44,11 @@ export interface DocDisplayMetaExtension {
icon: (
docId: string,
referenceInfo?: DocDisplayMetaParams
) => ReadonlySignal<TemplateResult>;
) => Signal<TemplateResult>;
title: (
docId: string,
referenceInfo?: DocDisplayMetaParams
) => ReadonlySignal<string>;
) => Signal<string>;
}
export const DocDisplayMetaProvider = createIdentifier<DocDisplayMetaExtension>(
@@ -98,11 +93,11 @@ export class DocDisplayMetaService
icon(
pageId: string,
{ params, title, referenced }: DocDisplayMetaParams = {}
): ReadonlySignal<TemplateResult> {
): Signal<TemplateResult> {
const doc = this.std.workspace.getDoc(pageId);
if (!doc) {
return computed(() => DocDisplayMetaService.icons.deleted);
return signal(DocDisplayMetaService.icons.deleted);
}
const store = doc.getStore();
@@ -165,14 +160,11 @@ export class DocDisplayMetaService
});
}
title(
pageId: string,
{ title }: DocDisplayMetaParams = {}
): ReadonlySignal<string> {
title(pageId: string, { title }: DocDisplayMetaParams = {}): Signal<string> {
const doc = this.std.workspace.getDoc(pageId);
if (!doc) {
return computed(() => title || 'Deleted doc');
return signal(title || 'Deleted doc');
}
const store = doc.getStore();
@@ -11,181 +11,181 @@ export const AffineCanvasTextFonts: FontConfig[] = [
// Inter, https://fonts.cdnfonts.com/css/inter?styles=29139,29134,29135,29136,29140,29141
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-Light-BETA.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-Light-BETA.woff',
weight: FontWeight.Light,
style: FontStyle.Normal,
},
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-SemiBold.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-SemiBold.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Normal,
},
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-LightItalic-BETA.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-LightItalic-BETA.woff',
weight: FontWeight.Light,
style: FontStyle.Italic,
},
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-Italic.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-Italic.woff',
weight: FontWeight.Regular,
style: FontStyle.Italic,
},
{
font: FontFamily.Inter,
url: 'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Italic,
},
// Kalam, https://fonts.cdnfonts.com/css/kalam?styles=15166,170689,170687
{
font: FontFamily.Kalam,
url: 'https://cdn.affine.pro/fonts/Kalam-Light.woff2',
url: 'https://cdn.affine.pro/fonts/Kalam-Light.woff',
weight: FontWeight.Light,
style: FontStyle.Normal,
},
{
font: FontFamily.Kalam,
url: 'https://cdn.affine.pro/fonts/Kalam-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/Kalam-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
{
font: FontFamily.Kalam,
url: 'https://cdn.affine.pro/fonts/Kalam-Bold.woff2',
url: 'https://cdn.affine.pro/fonts/Kalam-Bold.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Normal,
},
// Satoshi, https://fonts.cdnfonts.com/css/satoshi?styles=135009,135004,135005,135006,135002,135003
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-Light.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-Light.woff',
weight: FontWeight.Light,
style: FontStyle.Normal,
},
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-Bold.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-Bold.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Normal,
},
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-LightItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-LightItalic.woff',
weight: FontWeight.Light,
style: FontStyle.Italic,
},
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-Italic.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-Italic.woff',
weight: FontWeight.Regular,
style: FontStyle.Italic,
},
{
font: FontFamily.Satoshi,
url: 'https://cdn.affine.pro/fonts/Satoshi-BoldItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Satoshi-BoldItalic.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Italic,
},
// Poppins, https://fonts.cdnfonts.com/css/poppins?styles=20394,20389,20390,20391,20395,20396
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-Light.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-Light.woff',
weight: FontWeight.Light,
style: FontStyle.Normal,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-Medium.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-Medium.woff',
weight: FontWeight.Medium,
style: FontStyle.Normal,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBold.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBold.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Normal,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-LightItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-LightItalic.woff',
weight: FontWeight.Light,
style: FontStyle.Italic,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-Italic.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-Italic.woff',
weight: FontWeight.Regular,
style: FontStyle.Italic,
},
{
font: FontFamily.Poppins,
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBoldItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Poppins-SemiBoldItalic.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Italic,
},
// Lora, https://fonts.cdnfonts.com/css/lora-4?styles=50357,50356,50354,50355
{
font: FontFamily.Lora,
url: 'https://cdn.affine.pro/fonts/Lora-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/Lora-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
{
font: FontFamily.Lora,
url: 'https://cdn.affine.pro/fonts/Lora-Bold.woff2',
url: 'https://cdn.affine.pro/fonts/Lora-Bold.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Normal,
},
{
font: FontFamily.Lora,
url: 'https://cdn.affine.pro/fonts/Lora-Italic.woff2',
url: 'https://cdn.affine.pro/fonts/Lora-Italic.woff',
weight: FontWeight.Regular,
style: FontStyle.Italic,
},
{
font: FontFamily.Lora,
url: 'https://cdn.affine.pro/fonts/Lora-BoldItalic.woff2',
url: 'https://cdn.affine.pro/fonts/Lora-BoldItalic.woff',
weight: FontWeight.SemiBold,
style: FontStyle.Italic,
},
// BebasNeue, https://fonts.cdnfonts.com/css/bebas-neue?styles=169713,17622,17620
{
font: FontFamily.BebasNeue,
url: 'https://cdn.affine.pro/fonts/BebasNeue-Light.woff2',
url: 'https://cdn.affine.pro/fonts/BebasNeue-Light.woff',
weight: FontWeight.Light,
style: FontStyle.Normal,
},
{
font: FontFamily.BebasNeue,
url: 'https://cdn.affine.pro/fonts/BebasNeue-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/BebasNeue-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},
// OrelegaOne, https://fonts.cdnfonts.com/css/orelega-one?styles=148618
{
font: FontFamily.OrelegaOne,
url: 'https://cdn.affine.pro/fonts/OrelegaOne-Regular.woff2',
url: 'https://cdn.affine.pro/fonts/OrelegaOne-Regular.woff',
weight: FontWeight.Regular,
style: FontStyle.Normal,
},

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