mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
refactor(editor): extract root block (#10356)
Closes: [BS-2207](https://linear.app/affine-design/issue/BS-2207/move-root-block-to-affineblock-root)
This commit is contained in:
@@ -13,10 +13,9 @@ import {
|
||||
import { ImageBlockHtmlAdapterExtension } from '@blocksuite/affine-block-image';
|
||||
import { ListBlockHtmlAdapterExtension } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
|
||||
import { RootBlockHtmlAdapterExtension } from '@blocksuite/affine-block-root';
|
||||
import { TableBlockHtmlAdapterExtension } from '@blocksuite/affine-block-table';
|
||||
|
||||
import { RootBlockHtmlAdapterExtension } from '../../../root-block/adapters/html.js';
|
||||
|
||||
export const defaultBlockHtmlAdapterMatchers = [
|
||||
ListBlockHtmlAdapterExtension,
|
||||
ParagraphBlockHtmlAdapterExtension,
|
||||
|
||||
@@ -15,10 +15,9 @@ import { LatexBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-lat
|
||||
import { ListBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-list';
|
||||
import { DocNoteBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-note';
|
||||
import { ParagraphBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-paragraph';
|
||||
import { RootBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-root';
|
||||
import { TableBlockMarkdownAdapterExtension } from '@blocksuite/affine-block-table';
|
||||
|
||||
import { RootBlockMarkdownAdapterExtension } from '../../../root-block/adapters/markdown.js';
|
||||
|
||||
export const defaultBlockMarkdownAdapterMatchers = [
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
|
||||
@@ -13,10 +13,9 @@ import { ImageBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-i
|
||||
import { LatexBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-latex';
|
||||
import { ListBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-list';
|
||||
import { ParagraphBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-paragraph';
|
||||
import { RootBlockNotionHtmlAdapterExtension } from '@blocksuite/affine-block-root';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { RootBlockNotionHtmlAdapterExtension } from '../../../root-block/adapters/notion-html.js';
|
||||
|
||||
export const defaultBlockNotionHtmlAdapterMatchers: ExtensionType[] = [
|
||||
ListBlockNotionHtmlAdapterExtension,
|
||||
ParagraphBlockNotionHtmlAdapterExtension,
|
||||
|
||||
@@ -1,59 +1,9 @@
|
||||
import {
|
||||
EdgelessFrameManager,
|
||||
FrameOverlay,
|
||||
PresentTool,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
|
||||
import { EdgelessBuiltInSpecs } from '@blocksuite/affine-block-root';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { EdgelessRootBlockSpec } from '../../root-block/edgeless/edgeless-root-spec.js';
|
||||
import { BrushTool } from '../../root-block/edgeless/gfx-tool/brush-tool.js';
|
||||
import { ConnectorTool } from '../../root-block/edgeless/gfx-tool/connector-tool.js';
|
||||
import { CopilotTool } from '../../root-block/edgeless/gfx-tool/copilot-tool.js';
|
||||
import { DefaultTool } from '../../root-block/edgeless/gfx-tool/default-tool.js';
|
||||
import { MindMapIndicatorOverlay } from '../../root-block/edgeless/gfx-tool/default-tool-ext/mind-map-ext/indicator-overlay.js';
|
||||
import { EmptyTool } from '../../root-block/edgeless/gfx-tool/empty-tool.js';
|
||||
import { EraserTool } from '../../root-block/edgeless/gfx-tool/eraser-tool.js';
|
||||
import { FrameTool } from '../../root-block/edgeless/gfx-tool/frame-tool.js';
|
||||
import { LassoTool } from '../../root-block/edgeless/gfx-tool/lasso-tool.js';
|
||||
import { NoteTool } from '../../root-block/edgeless/gfx-tool/note-tool.js';
|
||||
import { PanTool } from '../../root-block/edgeless/gfx-tool/pan-tool.js';
|
||||
import { ShapeTool } from '../../root-block/edgeless/gfx-tool/shape-tool.js';
|
||||
import { TemplateTool } from '../../root-block/edgeless/gfx-tool/template-tool.js';
|
||||
import { TextTool } from '../../root-block/edgeless/gfx-tool/text-tool.js';
|
||||
import { EditPropsMiddlewareBuilder } from '../../root-block/edgeless/middlewares/base.js';
|
||||
import { EdgelessSnapManager } from '../../root-block/edgeless/utils/snap-manager.js';
|
||||
import { EdgelessFirstPartyBlockSpecs } from '../common.js';
|
||||
|
||||
export const EdgelessToolExtension: ExtensionType[] = [
|
||||
DefaultTool,
|
||||
PanTool,
|
||||
EraserTool,
|
||||
TextTool,
|
||||
ShapeTool,
|
||||
NoteTool,
|
||||
BrushTool,
|
||||
ConnectorTool,
|
||||
CopilotTool,
|
||||
TemplateTool,
|
||||
EmptyTool,
|
||||
FrameTool,
|
||||
LassoTool,
|
||||
PresentTool,
|
||||
];
|
||||
|
||||
export const EdgelessBuiltInManager: ExtensionType[] = [
|
||||
ConnectionOverlay,
|
||||
FrameOverlay,
|
||||
MindMapIndicatorOverlay,
|
||||
EdgelessSnapManager,
|
||||
EdgelessFrameManager,
|
||||
EditPropsMiddlewareBuilder,
|
||||
];
|
||||
import { EdgelessFirstPartyBlockSpecs } from '../common';
|
||||
|
||||
export const EdgelessEditorBlockSpecs: ExtensionType[] = [
|
||||
EdgelessRootBlockSpec,
|
||||
EdgelessBuiltInSpecs,
|
||||
EdgelessFirstPartyBlockSpecs,
|
||||
EdgelessToolExtension,
|
||||
EdgelessBuiltInManager,
|
||||
].flat();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageRootBlockSpec } from '@blocksuite/affine-block-root';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { PageRootBlockSpec } from '../../root-block/page/page-root-spec.js';
|
||||
import { PageFirstPartyBlockSpecs } from '../common.js';
|
||||
|
||||
export const PageEditorBlockSpecs: ExtensionType[] = [
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
PreviewEdgelessRootBlockSpec,
|
||||
PreviewPageRootBlockSpec,
|
||||
} from '@blocksuite/affine-block-root';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { PreviewEdgelessRootBlockSpec } from '../../root-block/edgeless/edgeless-root-spec.js';
|
||||
import { PreviewPageRootBlockSpec } from '../../root-block/page/page-root-spec.js';
|
||||
import {
|
||||
EdgelessFirstPartyBlockSpecs,
|
||||
PageFirstPartyBlockSpecs,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { effects as blockLatexEffects } from '@blocksuite/affine-block-latex/eff
|
||||
import { effects as blockListEffects } from '@blocksuite/affine-block-list/effects';
|
||||
import { effects as blockNoteEffects } from '@blocksuite/affine-block-note/effects';
|
||||
import { effects as blockParagraphEffects } from '@blocksuite/affine-block-paragraph/effects';
|
||||
import { effects as blockRootEffects } from '@blocksuite/affine-block-root/effects';
|
||||
import { effects as blockSurfaceEffects } from '@blocksuite/affine-block-surface/effects';
|
||||
import { effects as blockSurfaceRefEffects } from '@blocksuite/affine-block-surface-ref/effects';
|
||||
import { effects as blockTableEffects } from '@blocksuite/affine-block-table/effects';
|
||||
@@ -45,141 +46,6 @@ import { effects as dataViewEffects } from '@blocksuite/data-view/effects';
|
||||
import { effects as inlineEffects } from '@blocksuite/inline/effects';
|
||||
|
||||
import { registerSpecs } from './_specs/register-specs.js';
|
||||
import { EdgelessAutoCompletePanel } from './root-block/edgeless/components/auto-complete/auto-complete-panel.js';
|
||||
import { EdgelessAutoComplete } from './root-block/edgeless/components/auto-complete/edgeless-auto-complete.js';
|
||||
import { EdgelessToolIconButton } from './root-block/edgeless/components/buttons/tool-icon-button.js';
|
||||
import { EdgelessToolbarButton } from './root-block/edgeless/components/buttons/toolbar-button.js';
|
||||
import { EdgelessConnectorHandle } from './root-block/edgeless/components/connector/connector-handle.js';
|
||||
import {
|
||||
NOTE_SLICER_WIDGET,
|
||||
NoteSlicer,
|
||||
} from './root-block/edgeless/components/note-slicer/index.js';
|
||||
import { EdgelessAlignPanel } from './root-block/edgeless/components/panel/align-panel.js';
|
||||
import { CardStylePanel } from './root-block/edgeless/components/panel/card-style-panel.js';
|
||||
import {
|
||||
EdgelessColorButton,
|
||||
EdgelessColorPanel,
|
||||
EdgelessTextColorIcon,
|
||||
} from './root-block/edgeless/components/panel/color-panel.js';
|
||||
import { EdgelessFontFamilyPanel } from './root-block/edgeless/components/panel/font-family-panel.js';
|
||||
import { EdgelessFontWeightAndStylePanel } from './root-block/edgeless/components/panel/font-weight-and-style-panel.js';
|
||||
import { EdgelessLineWidthPanel } from './root-block/edgeless/components/panel/line-width-panel.js';
|
||||
import { NoteDisplayModePanel } from './root-block/edgeless/components/panel/note-display-mode-panel.js';
|
||||
import { EdgelessNoteShadowPanel } from './root-block/edgeless/components/panel/note-shadow-panel.js';
|
||||
import { EdgelessScalePanel } from './root-block/edgeless/components/panel/scale-panel.js';
|
||||
import { EdgelessShapePanel } from './root-block/edgeless/components/panel/shape-panel.js';
|
||||
import { EdgelessShapeStylePanel } from './root-block/edgeless/components/panel/shape-style-panel.js';
|
||||
import { EdgelessSizePanel } from './root-block/edgeless/components/panel/size-panel.js';
|
||||
import { StrokeStylePanel } from './root-block/edgeless/components/panel/stroke-style-panel.js';
|
||||
import {
|
||||
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
|
||||
EdgelessNavigatorBlackBackgroundWidget,
|
||||
} from './root-block/edgeless/components/presentation/edgeless-navigator-black-background.js';
|
||||
import {
|
||||
EDGELESS_DRAGGING_AREA_WIDGET,
|
||||
EdgelessDraggingAreaRectWidget,
|
||||
} from './root-block/edgeless/components/rects/edgeless-dragging-area-rect.js';
|
||||
import {
|
||||
EDGELESS_SELECTED_RECT_WIDGET,
|
||||
EdgelessSelectedRectWidget,
|
||||
} from './root-block/edgeless/components/rects/edgeless-selected-rect.js';
|
||||
import { EdgelessConnectorLabelEditor } from './root-block/edgeless/components/text/edgeless-connector-label-editor.js';
|
||||
import { EdgelessFrameTitleEditor } from './root-block/edgeless/components/text/edgeless-frame-title-editor.js';
|
||||
import { EdgelessGroupTitleEditor } from './root-block/edgeless/components/text/edgeless-group-title-editor.js';
|
||||
import { EdgelessShapeTextEditor } from './root-block/edgeless/components/text/edgeless-shape-text-editor.js';
|
||||
import { EdgelessTextEditor } from './root-block/edgeless/components/text/edgeless-text-editor.js';
|
||||
import { EdgelessBrushMenu } from './root-block/edgeless/components/toolbar/brush/brush-menu.js';
|
||||
import { EdgelessBrushToolButton } from './root-block/edgeless/components/toolbar/brush/brush-tool-button.js';
|
||||
import { EdgelessSlideMenu } from './root-block/edgeless/components/toolbar/common/slide-menu.js';
|
||||
import { ToolbarArrowUpIcon } from './root-block/edgeless/components/toolbar/common/toolbar-arrow-up-icon.js';
|
||||
import { EdgelessConnectorMenu } from './root-block/edgeless/components/toolbar/connector/connector-menu.js';
|
||||
import { EdgelessConnectorToolButton } from './root-block/edgeless/components/toolbar/connector/connector-tool-button.js';
|
||||
import { EdgelessDefaultToolButton } from './root-block/edgeless/components/toolbar/default/default-tool-button.js';
|
||||
import {
|
||||
EDGELESS_TOOLBAR_WIDGET,
|
||||
EdgelessToolbarWidget,
|
||||
} from './root-block/edgeless/components/toolbar/edgeless-toolbar.js';
|
||||
import { EdgelessEraserToolButton } from './root-block/edgeless/components/toolbar/eraser/eraser-tool-button.js';
|
||||
import { EdgelessFrameMenu } from './root-block/edgeless/components/toolbar/frame/frame-menu.js';
|
||||
import { EdgelessFrameToolButton } from './root-block/edgeless/components/toolbar/frame/frame-tool-button.js';
|
||||
import { EdgelessLassoToolButton } from './root-block/edgeless/components/toolbar/lasso/lasso-tool-button.js';
|
||||
import { EdgelessLinkToolButton } from './root-block/edgeless/components/toolbar/link/link-tool-button.js';
|
||||
import { MindMapPlaceholder } from './root-block/edgeless/components/toolbar/mindmap/mindmap-importing-placeholder.js';
|
||||
import { EdgelessMindmapMenu } from './root-block/edgeless/components/toolbar/mindmap/mindmap-menu.js';
|
||||
import { EdgelessMindmapToolButton } from './root-block/edgeless/components/toolbar/mindmap/mindmap-tool-button.js';
|
||||
import { EdgelessNoteMenu } from './root-block/edgeless/components/toolbar/note/note-menu.js';
|
||||
import { EdgelessNoteSeniorButton } from './root-block/edgeless/components/toolbar/note/note-senior-button.js';
|
||||
import { EdgelessNoteToolButton } from './root-block/edgeless/components/toolbar/note/note-tool-button.js';
|
||||
import { EdgelessFrameOrderButton } from './root-block/edgeless/components/toolbar/present/frame-order-button.js';
|
||||
import { EdgelessFrameOrderMenu } from './root-block/edgeless/components/toolbar/present/frame-order-menu.js';
|
||||
import { EdgelessNavigatorSettingButton } from './root-block/edgeless/components/toolbar/present/navigator-setting-button.js';
|
||||
import { EdgelessPresentButton } from './root-block/edgeless/components/toolbar/present/present-button.js';
|
||||
import { PresentationToolbar } from './root-block/edgeless/components/toolbar/presentation-toolbar.js';
|
||||
import { EdgelessToolbarShapeDraggable } from './root-block/edgeless/components/toolbar/shape/shape-draggable.js';
|
||||
import { EdgelessShapeMenu } from './root-block/edgeless/components/toolbar/shape/shape-menu.js';
|
||||
import { EdgelessShapeToolButton } from './root-block/edgeless/components/toolbar/shape/shape-tool-button.js';
|
||||
import { EdgelessShapeToolElement } from './root-block/edgeless/components/toolbar/shape/shape-tool-element.js';
|
||||
import { OverlayScrollbar } from './root-block/edgeless/components/toolbar/template/overlay-scrollbar.js';
|
||||
import { AffineTemplateLoading } from './root-block/edgeless/components/toolbar/template/template-loading.js';
|
||||
import { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
|
||||
import { EdgelessTemplateButton } from './root-block/edgeless/components/toolbar/template/template-tool-button.js';
|
||||
import { EdgelessTextMenu } from './root-block/edgeless/components/toolbar/text/text-menu.js';
|
||||
import { EdgelessRootPreviewBlockComponent } from './root-block/edgeless/edgeless-root-preview-block.js';
|
||||
import {
|
||||
AFFINE_AI_PANEL_WIDGET,
|
||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||
AFFINE_EMBED_CARD_TOOLBAR_WIDGET,
|
||||
AFFINE_FORMAT_BAR_WIDGET,
|
||||
AffineAIPanelWidget,
|
||||
AffineEdgelessZoomToolbarWidget,
|
||||
AffineFormatBarWidget,
|
||||
AffineImageToolbarWidget,
|
||||
AffineInnerModalWidget,
|
||||
AffineModalWidget,
|
||||
AffinePageDraggingAreaWidget,
|
||||
AffineSlashMenuWidget,
|
||||
AffineSurfaceRefToolbar,
|
||||
EdgelessCopilotToolbarEntry,
|
||||
EdgelessCopilotWidget,
|
||||
EdgelessRootBlockComponent,
|
||||
EmbedCardToolbar,
|
||||
FramePreview,
|
||||
PageRootBlockComponent,
|
||||
PreviewRootBlockComponent,
|
||||
} from './root-block/index.js';
|
||||
import { AIFinishTip } from './root-block/widgets/ai-panel/components/finish-tip.js';
|
||||
import { GeneratingPlaceholder } from './root-block/widgets/ai-panel/components/generating-placeholder.js';
|
||||
import {
|
||||
AIPanelAnswer,
|
||||
AIPanelDivider,
|
||||
AIPanelError,
|
||||
AIPanelGenerating,
|
||||
AIPanelInput,
|
||||
} from './root-block/widgets/ai-panel/components/index.js';
|
||||
import { EdgelessCopilotPanel } from './root-block/widgets/edgeless-copilot-panel/index.js';
|
||||
import { AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET } from './root-block/widgets/edgeless-zoom-toolbar/index.js';
|
||||
import { ZoomBarToggleButton } from './root-block/widgets/edgeless-zoom-toolbar/zoom-bar-toggle-button.js';
|
||||
import { EdgelessZoomToolbar } from './root-block/widgets/edgeless-zoom-toolbar/zoom-toolbar.js';
|
||||
import { effects as widgetEdgelessElementToolbarEffects } from './root-block/widgets/element-toolbar/effects.js';
|
||||
import { AffineImageToolbar } from './root-block/widgets/image-toolbar/components/image-toolbar.js';
|
||||
import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './root-block/widgets/image-toolbar/index.js';
|
||||
import { AFFINE_INNER_MODAL_WIDGET } from './root-block/widgets/inner-modal/inner-modal.js';
|
||||
import { effects as widgetMobileToolbarEffects } from './root-block/widgets/keyboard-toolbar/effects.js';
|
||||
import { effects as widgetLinkedDocEffects } from './root-block/widgets/linked-doc/effects.js';
|
||||
import { Loader } from './root-block/widgets/linked-doc/import-doc/loader';
|
||||
import { AffineCustomModal } from './root-block/widgets/modal/custom-modal.js';
|
||||
import { AFFINE_MODAL_WIDGET } from './root-block/widgets/modal/modal.js';
|
||||
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from './root-block/widgets/page-dragging-area/page-dragging-area.js';
|
||||
import { AFFINE_SLASH_MENU_WIDGET } from './root-block/widgets/slash-menu/index.js';
|
||||
import {
|
||||
InnerSlashMenu,
|
||||
SlashMenu,
|
||||
} from './root-block/widgets/slash-menu/slash-menu-popover.js';
|
||||
import { AFFINE_SURFACE_REF_TOOLBAR } from './root-block/widgets/surface-ref-toolbar/surface-ref-toolbar.js';
|
||||
import {
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
AffineViewportOverlayWidget,
|
||||
} from './root-block/widgets/viewport-overlay/viewport-overlay.js';
|
||||
|
||||
export function effects() {
|
||||
registerSpecs();
|
||||
@@ -206,7 +72,7 @@ export function effects() {
|
||||
blockDataViewEffects();
|
||||
blockCodeEffects();
|
||||
blockTableEffects();
|
||||
|
||||
blockRootEffects();
|
||||
componentCaptionEffects();
|
||||
componentContextMenuEffects();
|
||||
componentDatePickerEffects();
|
||||
@@ -221,10 +87,7 @@ export function effects() {
|
||||
componentDocTitleEffects();
|
||||
|
||||
widgetScrollAnchoringEffects();
|
||||
widgetMobileToolbarEffects();
|
||||
widgetLinkedDocEffects();
|
||||
widgetFrameTitleEffects();
|
||||
widgetEdgelessElementToolbarEffects();
|
||||
widgetRemoteSelectionEffects();
|
||||
widgetDragHandleEffects();
|
||||
widgetEdgelessAutoConnectEffects();
|
||||
@@ -232,173 +95,10 @@ export function effects() {
|
||||
fragmentFramePanelEffects();
|
||||
fragmentOutlineEffects();
|
||||
|
||||
customElements.define('affine-page-root', PageRootBlockComponent);
|
||||
customElements.define('affine-preview-root', PreviewRootBlockComponent);
|
||||
customElements.define('affine-edgeless-root', EdgelessRootBlockComponent);
|
||||
customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel);
|
||||
customElements.define(
|
||||
'edgeless-copilot-toolbar-entry',
|
||||
EdgelessCopilotToolbarEntry
|
||||
);
|
||||
customElements.define('edgeless-connector-handle', EdgelessConnectorHandle);
|
||||
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
|
||||
customElements.define(
|
||||
'affine-edgeless-root-preview',
|
||||
EdgelessRootPreviewBlockComponent
|
||||
);
|
||||
customElements.define('affine-custom-modal', AffineCustomModal);
|
||||
customElements.define('affine-slash-menu', SlashMenu);
|
||||
customElements.define('inner-slash-menu', InnerSlashMenu);
|
||||
customElements.define('generating-placeholder', GeneratingPlaceholder);
|
||||
customElements.define('ai-finish-tip', AIFinishTip);
|
||||
customElements.define('ai-panel-divider', AIPanelDivider);
|
||||
customElements.define(NOTE_SLICER_WIDGET, NoteSlicer);
|
||||
customElements.define(
|
||||
EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET,
|
||||
EdgelessNavigatorBlackBackgroundWidget
|
||||
);
|
||||
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
|
||||
customElements.define(
|
||||
EDGELESS_DRAGGING_AREA_WIDGET,
|
||||
EdgelessDraggingAreaRectWidget
|
||||
);
|
||||
customElements.define('icon-button', IconButton);
|
||||
customElements.define('loader-element', Loader);
|
||||
customElements.define('edgeless-brush-menu', EdgelessBrushMenu);
|
||||
customElements.define('edgeless-brush-tool-button', EdgelessBrushToolButton);
|
||||
customElements.define(
|
||||
'edgeless-connector-tool-button',
|
||||
EdgelessConnectorToolButton
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-default-tool-button',
|
||||
EdgelessDefaultToolButton
|
||||
);
|
||||
customElements.define('edgeless-connector-menu', EdgelessConnectorMenu);
|
||||
customElements.define('smooth-corner', SmoothCorner);
|
||||
customElements.define('toggle-switch', ToggleSwitch);
|
||||
customElements.define('ai-panel-answer', AIPanelAnswer);
|
||||
customElements.define(
|
||||
'edgeless-eraser-tool-button',
|
||||
EdgelessEraserToolButton
|
||||
);
|
||||
customElements.define('edgeless-frame-menu', EdgelessFrameMenu);
|
||||
customElements.define('edgeless-frame-tool-button', EdgelessFrameToolButton);
|
||||
customElements.define('ai-panel-input', AIPanelInput);
|
||||
customElements.define('ai-panel-generating', AIPanelGenerating);
|
||||
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
|
||||
customElements.define('edgeless-mindmap-menu', EdgelessMindmapMenu);
|
||||
customElements.define('edgeless-lasso-tool-button', EdgelessLassoToolButton);
|
||||
customElements.define('affine-filterable-list', FilterableListComponent);
|
||||
customElements.define('ai-panel-error', AIPanelError);
|
||||
customElements.define(
|
||||
EDGELESS_SELECTED_RECT_WIDGET,
|
||||
EdgelessSelectedRectWidget
|
||||
);
|
||||
customElements.define('mindmap-import-placeholder', MindMapPlaceholder);
|
||||
customElements.define(
|
||||
'edgeless-note-senior-button',
|
||||
EdgelessNoteSeniorButton
|
||||
);
|
||||
customElements.define('edgeless-align-panel', EdgelessAlignPanel);
|
||||
customElements.define('card-style-panel', CardStylePanel);
|
||||
customElements.define('edgeless-color-button', EdgelessColorButton);
|
||||
customElements.define('edgeless-color-panel', EdgelessColorPanel);
|
||||
customElements.define('edgeless-text-color-icon', EdgelessTextColorIcon);
|
||||
customElements.define(
|
||||
'edgeless-mindmap-tool-button',
|
||||
EdgelessMindmapToolButton
|
||||
);
|
||||
customElements.define('edgeless-note-tool-button', EdgelessNoteToolButton);
|
||||
customElements.define('edgeless-note-menu', EdgelessNoteMenu);
|
||||
customElements.define('edgeless-line-width-panel', EdgelessLineWidthPanel);
|
||||
customElements.define(
|
||||
'edgeless-frame-order-button',
|
||||
EdgelessFrameOrderButton
|
||||
);
|
||||
customElements.define('edgeless-frame-order-menu', EdgelessFrameOrderMenu);
|
||||
customElements.define(
|
||||
'edgeless-auto-complete-panel',
|
||||
EdgelessAutoCompletePanel
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-navigator-setting-button',
|
||||
EdgelessNavigatorSettingButton
|
||||
);
|
||||
customElements.define('edgeless-present-button', EdgelessPresentButton);
|
||||
customElements.define('overlay-scrollbar', OverlayScrollbar);
|
||||
customElements.define('affine-template-loading', AffineTemplateLoading);
|
||||
customElements.define('edgeless-auto-complete', EdgelessAutoComplete);
|
||||
customElements.define(
|
||||
'edgeless-font-weight-and-style-panel',
|
||||
EdgelessFontWeightAndStylePanel
|
||||
);
|
||||
customElements.define('edgeless-note-shadow-panel', EdgelessNoteShadowPanel);
|
||||
customElements.define('edgeless-templates-panel', EdgelessTemplatePanel);
|
||||
customElements.define('edgeless-text-menu', EdgelessTextMenu);
|
||||
customElements.define('edgeless-template-button', EdgelessTemplateButton);
|
||||
customElements.define('edgeless-tool-icon-button', EdgelessToolIconButton);
|
||||
customElements.define('edgeless-size-panel', EdgelessSizePanel);
|
||||
customElements.define('edgeless-scale-panel', EdgelessScalePanel);
|
||||
customElements.define('edgeless-font-family-panel', EdgelessFontFamilyPanel);
|
||||
customElements.define('edgeless-shape-panel', EdgelessShapePanel);
|
||||
customElements.define('note-display-mode-panel', NoteDisplayModePanel);
|
||||
customElements.define('edgeless-toolbar-button', EdgelessToolbarButton);
|
||||
customElements.define('frame-preview', FramePreview);
|
||||
customElements.define('presentation-toolbar', PresentationToolbar);
|
||||
customElements.define('edgeless-shape-menu', EdgelessShapeMenu);
|
||||
customElements.define('stroke-style-panel', StrokeStylePanel);
|
||||
customElements.define('edgeless-shape-tool-button', EdgelessShapeToolButton);
|
||||
customElements.define(
|
||||
'edgeless-connector-label-editor',
|
||||
EdgelessConnectorLabelEditor
|
||||
);
|
||||
customElements.define('block-zero-width', BlockZeroWidth);
|
||||
customElements.define(
|
||||
'edgeless-shape-tool-element',
|
||||
EdgelessShapeToolElement
|
||||
);
|
||||
customElements.define('edgeless-shape-text-editor', EdgelessShapeTextEditor);
|
||||
customElements.define(
|
||||
'edgeless-group-title-editor',
|
||||
EdgelessGroupTitleEditor
|
||||
);
|
||||
customElements.define(EDGELESS_TOOLBAR_WIDGET, EdgelessToolbarWidget);
|
||||
customElements.define('edgeless-shape-style-panel', EdgelessShapeStylePanel);
|
||||
customElements.define(
|
||||
'edgeless-frame-title-editor',
|
||||
EdgelessFrameTitleEditor
|
||||
);
|
||||
customElements.define('edgeless-text-editor', EdgelessTextEditor);
|
||||
customElements.define('affine-image-toolbar', AffineImageToolbar);
|
||||
customElements.define('affine-block-selection', BlockSelection);
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
customElements.define(
|
||||
'edgeless-toolbar-shape-draggable',
|
||||
EdgelessToolbarShapeDraggable
|
||||
);
|
||||
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
||||
|
||||
customElements.define(AFFINE_AI_PANEL_WIDGET, AffineAIPanelWidget);
|
||||
customElements.define(AFFINE_EMBED_CARD_TOOLBAR_WIDGET, EmbedCardToolbar);
|
||||
customElements.define(AFFINE_INNER_MODAL_WIDGET, AffineInnerModalWidget);
|
||||
customElements.define(AFFINE_MODAL_WIDGET, AffineModalWidget);
|
||||
customElements.define(
|
||||
AFFINE_PAGE_DRAGGING_AREA_WIDGET,
|
||||
AffinePageDraggingAreaWidget
|
||||
);
|
||||
customElements.define(AFFINE_EDGELESS_COPILOT_WIDGET, EdgelessCopilotWidget);
|
||||
|
||||
customElements.define(AFFINE_IMAGE_TOOLBAR_WIDGET, AffineImageToolbarWidget);
|
||||
customElements.define(AFFINE_SLASH_MENU_WIDGET, AffineSlashMenuWidget);
|
||||
customElements.define(
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
AffineViewportOverlayWidget
|
||||
);
|
||||
customElements.define(
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
AffineEdgelessZoomToolbarWidget
|
||||
);
|
||||
customElements.define(AFFINE_SURFACE_REF_TOOLBAR, AffineSurfaceRefToolbar);
|
||||
customElements.define(AFFINE_FORMAT_BAR_WIDGET, AffineFormatBarWidget);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
/* oxlint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference path="./effects.ts" />
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { deserializeXYWH, Point } from '@blocksuite/global/utils';
|
||||
|
||||
import { splitElements } from './root-block/edgeless/utils/clipboard-utils.js';
|
||||
import { isCanvasElement } from './root-block/edgeless/utils/query.js';
|
||||
|
||||
export * from './_common/adapters/index.js';
|
||||
export * from './_specs/index.js';
|
||||
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
|
||||
export type {
|
||||
Template,
|
||||
TemplateCategory,
|
||||
TemplateManager,
|
||||
} from './root-block/edgeless/components/toolbar/template/template-type.js';
|
||||
export { CopilotTool } from './root-block/edgeless/gfx-tool/copilot-tool.js';
|
||||
export * from './root-block/edgeless/gfx-tool/index.js';
|
||||
export { EditPropsMiddlewareBuilder } from './root-block/edgeless/middlewares/base.js';
|
||||
export { EdgelessSnapManager } from './root-block/edgeless/utils/snap-manager.js';
|
||||
export * from './root-block/index.js';
|
||||
export * from './schemas.js';
|
||||
export * from '@blocksuite/affine-block-attachment';
|
||||
export * from '@blocksuite/affine-block-bookmark';
|
||||
@@ -34,6 +18,7 @@ export * from '@blocksuite/affine-block-latex';
|
||||
export * from '@blocksuite/affine-block-list';
|
||||
export * from '@blocksuite/affine-block-note';
|
||||
export * from '@blocksuite/affine-block-paragraph';
|
||||
export * from '@blocksuite/affine-block-root';
|
||||
export * from '@blocksuite/affine-block-surface';
|
||||
export * from '@blocksuite/affine-block-surface-ref';
|
||||
export * from '@blocksuite/affine-block-table';
|
||||
@@ -124,14 +109,6 @@ export {
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
export type { DragBlockPayload } from '@blocksuite/affine-widget-drag-handle';
|
||||
|
||||
export const BlocksUtils = {
|
||||
splitElements,
|
||||
matchModels,
|
||||
deserializeXYWH,
|
||||
isCanvasElement,
|
||||
Point,
|
||||
};
|
||||
|
||||
const env: Record<string, unknown> =
|
||||
typeof globalThis !== 'undefined'
|
||||
? globalThis
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { RootBlockHtmlAdapterExtension } from './html.js';
|
||||
import { RootBlockMarkdownAdapterExtension } from './markdown.js';
|
||||
import { RootBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||
|
||||
export const RootBlockAdapterExtensions: ExtensionType[] = [
|
||||
RootBlockHtmlAdapterExtension,
|
||||
RootBlockMarkdownAdapterExtension,
|
||||
RootBlockNotionHtmlAdapterExtension,
|
||||
];
|
||||
@@ -1,135 +0,0 @@
|
||||
import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const rootBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: RootBlockSchema.model.flavour,
|
||||
toMatch: o => HastUtils.isElement(o.node) && o.node.tagName === 'header',
|
||||
fromMatch: o => o.node.flavour === RootBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'header') {
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (_, context) => {
|
||||
const { walkerContext } = context;
|
||||
const htmlRootDocContext =
|
||||
walkerContext.getGlobalContext('hast:html-root-doc');
|
||||
const isRootDoc = htmlRootDocContext ?? true;
|
||||
if (!isRootDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'html',
|
||||
properties: {},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'head',
|
||||
properties: {},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'style',
|
||||
properties: {},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'text',
|
||||
value: `
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
label:before {
|
||||
background: rgb(30, 150, 235);
|
||||
border-radius: 3px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type='checkbox'] + label:before {
|
||||
content: '';
|
||||
background: rgb(30, 150, 235);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
input[type='checkbox']:checked + label:before {
|
||||
content: '✓';
|
||||
}
|
||||
`.replace(/\s\s+/g, ''),
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode()
|
||||
.closeNode()
|
||||
.closeNode()
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'body',
|
||||
properties: {},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode(
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: {
|
||||
style: 'width: 70vw; margin: 60px auto;',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode({
|
||||
type: 'comment',
|
||||
value: 'BlockSuiteDocTitlePlaceholder',
|
||||
})
|
||||
.closeNode();
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext } = context;
|
||||
const htmlRootDocContext =
|
||||
walkerContext.getGlobalContext('hast:html-root-doc');
|
||||
const isRootDoc = htmlRootDocContext ?? true;
|
||||
if (!isRootDoc) {
|
||||
return;
|
||||
}
|
||||
walkerContext.closeNode().closeNode().closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RootBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
rootBlockHtmlAdapterMatcher
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './html.js';
|
||||
export * from './markdown.js';
|
||||
export * from './notion-html.js';
|
||||
@@ -1,36 +0,0 @@
|
||||
import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
|
||||
export const rootBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
flavour: RootBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === RootBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const title = (o.node.props.title ?? { delta: [] }) as {
|
||||
delta: DeltaInsert[];
|
||||
};
|
||||
const { walkerContext, deltaConverter } = context;
|
||||
if (title.delta.length === 0) return;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'heading',
|
||||
depth: 1,
|
||||
children: deltaConverter.deltaToAST(title.delta, 0),
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const RootBlockMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
|
||||
rootBlockMarkdownAdapterMatcher
|
||||
);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { RootBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
export const rootBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
|
||||
{
|
||||
flavour: RootBlockSchema.model.flavour,
|
||||
toMatch: o => HastUtils.isElement(o.node) && o.node.tagName === 'header',
|
||||
fromMatch: () => false,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'header') {
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const RootBlockNotionHtmlAdapterExtension =
|
||||
BlockNotionHtmlAdapterExtension(rootBlockNotionHtmlAdapterMatcher);
|
||||
@@ -1,92 +0,0 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type {
|
||||
BlockSnapshot,
|
||||
DocSnapshot,
|
||||
FromBlockSnapshotPayload,
|
||||
FromBlockSnapshotResult,
|
||||
FromDocSnapshotPayload,
|
||||
FromDocSnapshotResult,
|
||||
FromSliceSnapshotPayload,
|
||||
FromSliceSnapshotResult,
|
||||
SliceSnapshot,
|
||||
ToBlockSnapshotPayload,
|
||||
ToDocSnapshotPayload,
|
||||
ToSliceSnapshotPayload,
|
||||
} from '@blocksuite/store';
|
||||
import { BaseAdapter } from '@blocksuite/store';
|
||||
|
||||
import { decodeClipboardBlobs, encodeClipboardBlobs } from './utils.js';
|
||||
|
||||
export type FileSnapshot = {
|
||||
name: string;
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export class ClipboardAdapter extends BaseAdapter<string> {
|
||||
static MIME = 'BLOCKSUITE/SNAPSHOT';
|
||||
|
||||
override fromBlockSnapshot(
|
||||
_payload: FromBlockSnapshotPayload
|
||||
): Promise<FromBlockSnapshotResult<string>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ClipboardAdapter.fromBlockSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override fromDocSnapshot(
|
||||
_payload: FromDocSnapshotPayload
|
||||
): Promise<FromDocSnapshotResult<string>> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ClipboardAdapter.fromDocSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override async fromSliceSnapshot(
|
||||
payload: FromSliceSnapshotPayload
|
||||
): Promise<FromSliceSnapshotResult<string>> {
|
||||
const snapshot = payload.snapshot;
|
||||
const assets = payload.assets;
|
||||
assertExists(assets);
|
||||
const map = assets.getAssets();
|
||||
const blobs: Record<string, FileSnapshot> = await encodeClipboardBlobs(map);
|
||||
return {
|
||||
file: JSON.stringify({
|
||||
snapshot,
|
||||
blobs,
|
||||
}),
|
||||
assetsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
override toBlockSnapshot(
|
||||
_payload: ToBlockSnapshotPayload<string>
|
||||
): Promise<BlockSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ClipboardAdapter.toBlockSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override toDocSnapshot(
|
||||
_payload: ToDocSnapshotPayload<string>
|
||||
): Promise<DocSnapshot> {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.TransformerNotImplementedError,
|
||||
'ClipboardAdapter.toDocSnapshot is not implemented'
|
||||
);
|
||||
}
|
||||
|
||||
override toSliceSnapshot(
|
||||
payload: ToSliceSnapshotPayload<string>
|
||||
): Promise<SliceSnapshot> {
|
||||
const json = JSON.parse(payload.file);
|
||||
const { blobs, snapshot } = json;
|
||||
const map = payload.assets?.getAssets();
|
||||
decodeClipboardBlobs(blobs, map);
|
||||
return Promise.resolve(snapshot);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './page-clipboard.js';
|
||||
export * from './readonly-clipboard.js';
|
||||
@@ -1,169 +0,0 @@
|
||||
import { deleteTextCommand } from '@blocksuite/affine-components/rich-text';
|
||||
import {
|
||||
pasteMiddleware,
|
||||
replaceIdMiddleware,
|
||||
surfaceRefToEmbed,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
clearAndSelectFirstModelCommand,
|
||||
deleteSelectedModelsCommand,
|
||||
getBlockIndexCommand,
|
||||
getBlockSelectionsCommand,
|
||||
getImageSelectionsCommand,
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
retainFirstModelCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type { UIEventHandler } from '@blocksuite/block-std';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { BlockSnapshot, Store } from '@blocksuite/store';
|
||||
|
||||
import { ReadOnlyClipboard } from './readonly-clipboard';
|
||||
|
||||
/**
|
||||
* PageClipboard is a class that provides a clipboard for the page root block.
|
||||
* It is supported to copy and paste models in the page root block.
|
||||
*/
|
||||
export class PageClipboard extends ReadOnlyClipboard {
|
||||
protected _init = () => {
|
||||
this._initAdapters();
|
||||
const paste = pasteMiddleware(this._std);
|
||||
// Use surfaceRefToEmbed middleware to convert surface-ref to embed-linked-doc
|
||||
// When pastina a surface-ref block to another doc
|
||||
const surfaceRefToEmbedMiddleware = surfaceRefToEmbed(this._std);
|
||||
const replaceId = replaceIdMiddleware(
|
||||
this._std.store.workspace.idGenerator
|
||||
);
|
||||
this._std.clipboard.use(paste);
|
||||
this._std.clipboard.use(surfaceRefToEmbedMiddleware);
|
||||
this._std.clipboard.use(replaceId);
|
||||
this._disposables.add({
|
||||
dispose: () => {
|
||||
this._std.clipboard.unuse(paste);
|
||||
this._std.clipboard.unuse(surfaceRefToEmbedMiddleware);
|
||||
this._std.clipboard.unuse(replaceId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
onBlockSnapshotPaste = async (
|
||||
snapshot: BlockSnapshot,
|
||||
doc: Store,
|
||||
parent?: string,
|
||||
index?: number
|
||||
) => {
|
||||
const block = await this._std.clipboard.pasteBlockSnapshot(
|
||||
snapshot,
|
||||
doc,
|
||||
parent,
|
||||
index
|
||||
);
|
||||
return block?.id ?? null;
|
||||
};
|
||||
|
||||
onPageCut: UIEventHandler = ctx => {
|
||||
const e = ctx.get('clipboardState').raw;
|
||||
e.preventDefault();
|
||||
|
||||
this._copySelected(() => {
|
||||
this._std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand),
|
||||
cmd.pipe(getSelectedModelsCommand).pipe(deleteSelectedModelsCommand),
|
||||
])
|
||||
.run();
|
||||
}).run();
|
||||
};
|
||||
|
||||
onPagePaste: UIEventHandler = ctx => {
|
||||
const e = ctx.get('clipboardState').raw;
|
||||
e.preventDefault();
|
||||
|
||||
this._std.store.captureSync();
|
||||
this._std.command
|
||||
.chain()
|
||||
.try<{}>(cmd => [
|
||||
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
|
||||
const { currentTextSelection } = ctx;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
const { from, to } = currentTextSelection;
|
||||
if (to && from.blockId !== to.blockId) {
|
||||
this._std.command.exec(deleteTextCommand, {
|
||||
currentTextSelection,
|
||||
});
|
||||
}
|
||||
return next();
|
||||
}),
|
||||
cmd
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(clearAndSelectFirstModelCommand)
|
||||
.pipe(retainFirstModelCommand)
|
||||
.pipe(deleteSelectedModelsCommand),
|
||||
])
|
||||
.try<{ currentSelectionPath: string }>(cmd => [
|
||||
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
|
||||
const textSelection = ctx.currentTextSelection;
|
||||
if (!textSelection) {
|
||||
return;
|
||||
}
|
||||
next({ currentSelectionPath: textSelection.from.blockId });
|
||||
}),
|
||||
cmd.pipe(getBlockSelectionsCommand).pipe((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
if (!currentBlockSelections) {
|
||||
return;
|
||||
}
|
||||
const blockSelection = currentBlockSelections.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
next({ currentSelectionPath: blockSelection.blockId });
|
||||
}),
|
||||
cmd.pipe(getImageSelectionsCommand).pipe((ctx, next) => {
|
||||
const currentImageSelections = ctx.currentImageSelections;
|
||||
if (!currentImageSelections) {
|
||||
return;
|
||||
}
|
||||
const imageSelection = currentImageSelections.at(-1);
|
||||
if (!imageSelection) {
|
||||
return;
|
||||
}
|
||||
next({ currentSelectionPath: imageSelection.blockId });
|
||||
}),
|
||||
])
|
||||
.pipe(getBlockIndexCommand)
|
||||
.pipe((ctx, next) => {
|
||||
if (!ctx.parentBlock) {
|
||||
return;
|
||||
}
|
||||
this._std.clipboard
|
||||
.paste(
|
||||
e,
|
||||
this._std.store,
|
||||
ctx.parentBlock.model.id,
|
||||
ctx.blockIndex ? ctx.blockIndex + 1 : 1
|
||||
)
|
||||
.catch(console.error);
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
};
|
||||
|
||||
override hostConnected() {
|
||||
if (this._disposables.disposed) {
|
||||
this._disposables = new DisposableGroup();
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
this.host.handleEvent('copy', this.onPageCopy);
|
||||
this.host.handleEvent('paste', this.onPagePaste);
|
||||
this.host.handleEvent('cut', this.onPageCut);
|
||||
this._init();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { pasteMiddleware };
|
||||
@@ -1,125 +0,0 @@
|
||||
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
|
||||
import {
|
||||
AttachmentAdapter,
|
||||
copyMiddleware,
|
||||
HtmlAdapter,
|
||||
ImageAdapter,
|
||||
MixTextAdapter,
|
||||
NotionTextAdapter,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
copySelectedModelsCommand,
|
||||
draftSelectedModelsCommand,
|
||||
getSelectedModelsCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std';
|
||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||
|
||||
import { ClipboardAdapter } from './adapter.js';
|
||||
|
||||
/**
|
||||
* ReadOnlyClipboard is a class that provides a read-only clipboard for the root block.
|
||||
* It is supported to copy models in the root block.
|
||||
*/
|
||||
export class ReadOnlyClipboard {
|
||||
protected readonly _copySelected = (onCopy?: () => void) => {
|
||||
return this._std.command
|
||||
.chain()
|
||||
.with({ onCopy })
|
||||
.pipe(getSelectedModelsCommand)
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.pipe(copySelectedModelsCommand);
|
||||
};
|
||||
|
||||
protected _disposables = new DisposableGroup();
|
||||
|
||||
protected _initAdapters = () => {
|
||||
this._std.clipboard.registerAdapter(
|
||||
ClipboardAdapter.MIME,
|
||||
ClipboardAdapter,
|
||||
100
|
||||
);
|
||||
this._std.clipboard.registerAdapter(
|
||||
'text/_notion-text-production',
|
||||
NotionTextAdapter,
|
||||
95
|
||||
);
|
||||
this._std.clipboard.registerAdapter('text/html', HtmlAdapter, 90);
|
||||
[
|
||||
'image/apng',
|
||||
'image/avif',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
].forEach(type =>
|
||||
this._std.clipboard.registerAdapter(type, ImageAdapter, 80)
|
||||
);
|
||||
this._std.clipboard.registerAdapter('text/plain', MixTextAdapter, 70);
|
||||
this._std.clipboard.registerAdapter('*/*', AttachmentAdapter, 60);
|
||||
const copy = copyMiddleware(this._std);
|
||||
this._std.clipboard.use(copy);
|
||||
this._std.clipboard.use(
|
||||
titleMiddleware(this._std.store.workspace.meta.docMetas)
|
||||
);
|
||||
this._std.clipboard.use(defaultImageProxyMiddleware);
|
||||
|
||||
this._disposables.add({
|
||||
dispose: () => {
|
||||
this._std.clipboard.unregisterAdapter(ClipboardAdapter.MIME);
|
||||
this._std.clipboard.unregisterAdapter('text/plain');
|
||||
[
|
||||
'image/apng',
|
||||
'image/avif',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
'image/webp',
|
||||
].forEach(type => this._std.clipboard.unregisterAdapter(type));
|
||||
this._std.clipboard.unregisterAdapter('text/html');
|
||||
this._std.clipboard.unregisterAdapter('*/*');
|
||||
this._std.clipboard.unuse(copy);
|
||||
this._std.clipboard.unuse(
|
||||
titleMiddleware(this._std.store.workspace.meta.docMetas)
|
||||
);
|
||||
this._std.clipboard.unuse(defaultImageProxyMiddleware);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
host: BlockComponent;
|
||||
|
||||
onPageCopy: UIEventHandler = ctx => {
|
||||
const e = ctx.get('clipboardState').raw;
|
||||
e.preventDefault();
|
||||
|
||||
this._copySelected().run();
|
||||
};
|
||||
|
||||
protected get _std() {
|
||||
return this.host.std;
|
||||
}
|
||||
|
||||
constructor(host: BlockComponent) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
if (this._disposables.disposed) {
|
||||
this._disposables = new DisposableGroup();
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
this.host.handleEvent('copy', this.onPageCopy);
|
||||
this._initAdapters();
|
||||
}
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { copyMiddleware };
|
||||
@@ -1,124 +0,0 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
import type { FileSnapshot } from './adapter.js';
|
||||
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
// Use a lookup table to find the index.
|
||||
const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
lookup[chars.charCodeAt(i)] = i;
|
||||
}
|
||||
|
||||
export const encode = (arraybuffer: ArrayBuffer): string => {
|
||||
const bytes = new Uint8Array(arraybuffer);
|
||||
const len = bytes.length;
|
||||
let i,
|
||||
base64 = '';
|
||||
|
||||
for (i = 0; i < len; i += 3) {
|
||||
base64 += chars[bytes[i] >> 2];
|
||||
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
||||
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
||||
base64 += chars[bytes[i + 2] & 63];
|
||||
}
|
||||
|
||||
if (len % 3 === 2) {
|
||||
base64 = base64.substring(0, base64.length - 1) + '=';
|
||||
} else if (len % 3 === 1) {
|
||||
base64 = base64.substring(0, base64.length - 2) + '==';
|
||||
}
|
||||
|
||||
return base64;
|
||||
};
|
||||
|
||||
export const decode = (base64: string): ArrayBuffer => {
|
||||
const len = base64.length;
|
||||
let bufferLength = base64.length * 0.75,
|
||||
i,
|
||||
p = 0,
|
||||
encoded1,
|
||||
encoded2,
|
||||
encoded3,
|
||||
encoded4;
|
||||
|
||||
if (base64[base64.length - 1] === '=') {
|
||||
bufferLength--;
|
||||
if (base64[base64.length - 2] === '=') {
|
||||
bufferLength--;
|
||||
}
|
||||
}
|
||||
|
||||
const arraybuffer = new ArrayBuffer(bufferLength),
|
||||
bytes = new Uint8Array(arraybuffer);
|
||||
|
||||
for (i = 0; i < len; i += 4) {
|
||||
encoded1 = lookup[base64.charCodeAt(i)];
|
||||
encoded2 = lookup[base64.charCodeAt(i + 1)];
|
||||
encoded3 = lookup[base64.charCodeAt(i + 2)];
|
||||
encoded4 = lookup[base64.charCodeAt(i + 3)];
|
||||
|
||||
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
||||
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
||||
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
||||
}
|
||||
|
||||
return arraybuffer;
|
||||
};
|
||||
|
||||
export async function encodeClipboardBlobs(map: Map<string, Blob>) {
|
||||
const blobs: Record<string, FileSnapshot> = {};
|
||||
let sumSize = 0;
|
||||
await Promise.all(
|
||||
Array.from(map.entries()).map(async ([id, blob]) => {
|
||||
if (blob.size > 4 * 1024 * 1024) {
|
||||
const host = document.querySelector('editor-host');
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
toast(
|
||||
host,
|
||||
(blob as File).name ?? 'File' + ' is too large to be copied'
|
||||
);
|
||||
return;
|
||||
}
|
||||
sumSize += blob.size;
|
||||
if (sumSize > 6 * 1024 * 1024) {
|
||||
const host = document.querySelector('editor-host');
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
toast(
|
||||
host,
|
||||
(blob as File).name ??
|
||||
'File' + ' cannot be copied due to the clipboard size limit'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const content = encode(await blob.arrayBuffer());
|
||||
const file: FileSnapshot = {
|
||||
name: (blob as File).name,
|
||||
type: blob.type,
|
||||
content,
|
||||
};
|
||||
blobs[id] = file;
|
||||
})
|
||||
);
|
||||
return blobs;
|
||||
}
|
||||
|
||||
export function decodeClipboardBlobs(
|
||||
blobs: Record<string, FileSnapshot>,
|
||||
map: Map<string, Blob> | undefined
|
||||
) {
|
||||
Object.entries<FileSnapshot>(blobs).forEach(([sourceId, file]) => {
|
||||
const blob = new Blob([decode(file.content)]);
|
||||
const f = new File([blob], file.name, {
|
||||
type: file.type,
|
||||
});
|
||||
assertExists(map);
|
||||
map.set(sourceId, f);
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { FileDropExtension } from '@blocksuite/affine-components/drop-indicator';
|
||||
import {
|
||||
DNDAPIExtension,
|
||||
DocModeService,
|
||||
EmbedOptionService,
|
||||
PageViewportServiceExtension,
|
||||
ThemeService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { FlavourExtension } from '@blocksuite/block-std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { RootBlockAdapterExtensions } from '../adapters/extension';
|
||||
import {
|
||||
docRemoteSelectionWidget,
|
||||
dragHandleWidget,
|
||||
embedCardToolbarWidget,
|
||||
formatBarWidget,
|
||||
innerModalWidget,
|
||||
linkedDocWidget,
|
||||
modalWidget,
|
||||
scrollAnchoringWidget,
|
||||
slashMenuWidget,
|
||||
viewportOverlayWidget,
|
||||
} from './widgets';
|
||||
|
||||
export const CommonSpecs: ExtensionType[] = [
|
||||
FlavourExtension('affine:page'),
|
||||
DocModeService,
|
||||
ThemeService,
|
||||
EmbedOptionService,
|
||||
PageViewportServiceExtension,
|
||||
DNDAPIExtension,
|
||||
FileDropExtension,
|
||||
...RootBlockAdapterExtensions,
|
||||
|
||||
modalWidget,
|
||||
innerModalWidget,
|
||||
slashMenuWidget,
|
||||
linkedDocWidget,
|
||||
dragHandleWidget,
|
||||
embedCardToolbarWidget,
|
||||
formatBarWidget,
|
||||
docRemoteSelectionWidget,
|
||||
viewportOverlayWidget,
|
||||
scrollAnchoringWidget,
|
||||
];
|
||||
|
||||
export * from './widgets';
|
||||
@@ -1,64 +0,0 @@
|
||||
import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle';
|
||||
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection';
|
||||
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
|
||||
import { WidgetViewExtension } from '@blocksuite/block-std';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from '../widgets/embed-card-toolbar/embed-card-toolbar.js';
|
||||
import { AFFINE_FORMAT_BAR_WIDGET } from '../widgets/format-bar/format-bar.js';
|
||||
import { AFFINE_INNER_MODAL_WIDGET } from '../widgets/inner-modal/inner-modal.js';
|
||||
import { AFFINE_LINKED_DOC_WIDGET } from '../widgets/linked-doc/config.js';
|
||||
import { AFFINE_MODAL_WIDGET } from '../widgets/modal/modal.js';
|
||||
import { AFFINE_SLASH_MENU_WIDGET } from '../widgets/slash-menu/index.js';
|
||||
import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js';
|
||||
|
||||
export const modalWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_MODAL_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_MODAL_WIDGET)}`
|
||||
);
|
||||
export const innerModalWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_INNER_MODAL_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_INNER_MODAL_WIDGET)}`
|
||||
);
|
||||
export const slashMenuWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_SLASH_MENU_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_SLASH_MENU_WIDGET)}`
|
||||
);
|
||||
export const linkedDocWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_LINKED_DOC_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_LINKED_DOC_WIDGET)}`
|
||||
);
|
||||
export const dragHandleWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_DRAG_HANDLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_DRAG_HANDLE_WIDGET)}`
|
||||
);
|
||||
export const embedCardToolbarWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_EMBED_CARD_TOOLBAR_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_EMBED_CARD_TOOLBAR_WIDGET)}`
|
||||
);
|
||||
export const formatBarWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_FORMAT_BAR_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FORMAT_BAR_WIDGET)}`
|
||||
);
|
||||
export const docRemoteSelectionWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_DOC_REMOTE_SELECTION_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}`
|
||||
);
|
||||
export const viewportOverlayWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_VIEWPORT_OVERLAY_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}`
|
||||
);
|
||||
export const scrollAnchoringWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_SCROLL_ANCHORING_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,664 +0,0 @@
|
||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||
import {
|
||||
CanvasElementType,
|
||||
EdgelessCRUDIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { FontFamilyIcon } from '@blocksuite/affine-components/icons';
|
||||
import type {
|
||||
Connection,
|
||||
ConnectorElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DefaultTheme,
|
||||
FontFamily,
|
||||
FontStyle,
|
||||
FontWeight,
|
||||
getShapeName,
|
||||
GroupElementModel,
|
||||
NoteBlockModel,
|
||||
ShapeStyle,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
FeatureFlagService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { captureEventTarget } from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, stdContext } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import type { XYWH } from '@blocksuite/global/utils';
|
||||
import {
|
||||
assertInstanceOf,
|
||||
Bound,
|
||||
clamp,
|
||||
normalizeDegAngle,
|
||||
serializeXYWH,
|
||||
toDegree,
|
||||
Vec,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { FrameIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import {
|
||||
SHAPE_OVERLAY_HEIGHT,
|
||||
SHAPE_OVERLAY_WIDTH,
|
||||
} from '../../utils/consts.js';
|
||||
import {
|
||||
mountShapeTextEditor,
|
||||
mountTextElementEditor,
|
||||
} from '../../utils/text.js';
|
||||
import { ShapeComponentConfig } from '../toolbar/shape/shape-menu-config.js';
|
||||
import {
|
||||
type AUTO_COMPLETE_TARGET_TYPE,
|
||||
AutoCompleteFrameOverlay,
|
||||
AutoCompleteNoteOverlay,
|
||||
AutoCompleteShapeOverlay,
|
||||
AutoCompleteTextOverlay,
|
||||
capitalizeFirstLetter,
|
||||
createShapeElement,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT,
|
||||
DEFAULT_TEXT_HEIGHT,
|
||||
DEFAULT_TEXT_WIDTH,
|
||||
Direction,
|
||||
isShape,
|
||||
PANEL_HEIGHT,
|
||||
PANEL_WIDTH,
|
||||
type TARGET_SHAPE_TYPE,
|
||||
} from './utils.js';
|
||||
|
||||
export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.auto-complete-panel-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 136px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.row-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
height: 28px;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
|
||||
private _overlay:
|
||||
| AutoCompleteShapeOverlay
|
||||
| AutoCompleteNoteOverlay
|
||||
| AutoCompleteFrameOverlay
|
||||
| AutoCompleteTextOverlay
|
||||
| null = null;
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
constructor(
|
||||
position: [number, number],
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
currentSource: ShapeElementModel | NoteBlockModel,
|
||||
connector: ConnectorElementModel
|
||||
) {
|
||||
super();
|
||||
this.position = position;
|
||||
this.edgeless = edgeless;
|
||||
this.currentSource = currentSource;
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
private _addFrame() {
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
|
||||
const { h } = bound;
|
||||
const w = h / 0.75;
|
||||
const target = this._getTargetXYWH(w, h);
|
||||
if (!target) return;
|
||||
|
||||
const { xywh, position } = target;
|
||||
|
||||
const edgeless = this.edgeless;
|
||||
const { service, surfaceBlockModel } = edgeless;
|
||||
const frameMgr = service.frame;
|
||||
const frameIndex = service.frames.length + 1;
|
||||
const props = this.std.get(EditPropsStore).applyLastProps('affine:frame', {
|
||||
title: new Y.Text(`Frame ${frameIndex}`),
|
||||
xywh: serializeXYWH(...xywh),
|
||||
presentationIndex: frameMgr.generatePresentationIndex(),
|
||||
});
|
||||
const id = this.crud.addBlock('affine:frame', props, surfaceBlockModel);
|
||||
edgeless.doc.captureSync();
|
||||
const frame = this.crud.getElementById(id);
|
||||
if (!frame) return;
|
||||
|
||||
this.connector.target = {
|
||||
id,
|
||||
position,
|
||||
};
|
||||
|
||||
edgeless.service.selection.set({
|
||||
elements: [frame.id],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _addNote() {
|
||||
const { doc } = this.edgeless;
|
||||
const target = this._getTargetXYWH(
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT
|
||||
);
|
||||
if (!target) return;
|
||||
|
||||
const { xywh, position } = target;
|
||||
const id = this.crud.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
xywh: serializeXYWH(...xywh),
|
||||
},
|
||||
doc.root?.id
|
||||
);
|
||||
const note = doc.getBlock(id)?.model;
|
||||
assertInstanceOf(note, NoteBlockModel);
|
||||
doc.addBlock('affine:paragraph', { type: 'text' }, id);
|
||||
const group = this.currentSource.group;
|
||||
|
||||
if (group instanceof GroupElementModel) {
|
||||
group.addChild(note);
|
||||
}
|
||||
this.connector.target = {
|
||||
id,
|
||||
position: position as [number, number],
|
||||
};
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id, position },
|
||||
});
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _addShape(targetType: TARGET_SHAPE_TYPE) {
|
||||
const edgeless = this.edgeless;
|
||||
const result = this._generateTarget(this.connector);
|
||||
if (!result) return;
|
||||
|
||||
const currentSource = this.currentSource;
|
||||
const { nextBound, position } = result;
|
||||
const id = createShapeElement(edgeless, currentSource, targetType);
|
||||
if (!id) return;
|
||||
|
||||
this.crud.updateElement(id, { xywh: nextBound.serialize() });
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id, position },
|
||||
});
|
||||
|
||||
mountShapeTextEditor(
|
||||
this.crud.getElementById(id) as ShapeElementModel,
|
||||
this.edgeless
|
||||
);
|
||||
this.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: true,
|
||||
});
|
||||
edgeless.doc.captureSync();
|
||||
}
|
||||
|
||||
private _addText() {
|
||||
const target = this._getTargetXYWH(DEFAULT_TEXT_WIDTH, DEFAULT_TEXT_HEIGHT);
|
||||
if (!target) return;
|
||||
const { xywh, position } = target;
|
||||
const bound = Bound.fromXYWH(xywh);
|
||||
|
||||
const textFlag = this.edgeless.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
if (textFlag) {
|
||||
const [_, { textId }] = this.edgeless.std.command.exec(
|
||||
insertEdgelessTextCommand,
|
||||
{
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
}
|
||||
);
|
||||
if (!textId) return;
|
||||
|
||||
const textElement = this.crud.getElementById(textId);
|
||||
if (!textElement) return;
|
||||
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id: textId, position },
|
||||
});
|
||||
if (this.currentSource.group instanceof GroupElementModel) {
|
||||
this.currentSource.group.addChild(textElement);
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [textId],
|
||||
editing: false,
|
||||
});
|
||||
this.edgeless.doc.captureSync();
|
||||
} else {
|
||||
const textId = this.crud.addElement(CanvasElementType.TEXT, {
|
||||
xywh: bound.serialize(),
|
||||
text: new Y.Text(),
|
||||
textAlign: 'left',
|
||||
fontSize: 24,
|
||||
fontFamily: FontFamily.Inter,
|
||||
color: DefaultTheme.textColor,
|
||||
fontWeight: FontWeight.Regular,
|
||||
fontStyle: FontStyle.Normal,
|
||||
});
|
||||
if (!textId) return;
|
||||
const textElement = this.crud.getElementById(textId);
|
||||
assertInstanceOf(textElement, TextElementModel);
|
||||
|
||||
this.crud.updateElement(this.connector.id, {
|
||||
target: { id: textId, position },
|
||||
});
|
||||
if (this.currentSource.group instanceof GroupElementModel) {
|
||||
this.currentSource.group.addChild(textElement);
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [textId],
|
||||
editing: false,
|
||||
});
|
||||
this.edgeless.doc.captureSync();
|
||||
|
||||
mountTextElementEditor(textElement, this.edgeless);
|
||||
}
|
||||
}
|
||||
|
||||
private _autoComplete(targetType: AUTO_COMPLETE_TARGET_TYPE) {
|
||||
this._removeOverlay();
|
||||
if (!this._connectorExist()) return;
|
||||
|
||||
switch (targetType) {
|
||||
case 'text':
|
||||
this._addText();
|
||||
break;
|
||||
case 'note':
|
||||
this._addNote();
|
||||
break;
|
||||
case 'frame':
|
||||
this._addFrame();
|
||||
break;
|
||||
default:
|
||||
this._addShape(targetType);
|
||||
}
|
||||
|
||||
this.remove();
|
||||
}
|
||||
|
||||
private _connectorExist() {
|
||||
return !!this.crud.getElementById(this.connector.id);
|
||||
}
|
||||
|
||||
private _generateTarget(connector: ConnectorElementModel) {
|
||||
const { currentSource } = this;
|
||||
let w = SHAPE_OVERLAY_WIDTH;
|
||||
let h = SHAPE_OVERLAY_HEIGHT;
|
||||
if (isShape(currentSource)) {
|
||||
const bound = Bound.deserialize(currentSource.xywh);
|
||||
w = bound.w;
|
||||
h = bound.h;
|
||||
}
|
||||
const point = connector.target.position;
|
||||
if (!point) return;
|
||||
|
||||
const len = connector.path.length;
|
||||
const angle = normalizeDegAngle(
|
||||
toDegree(Vec.angle(connector.path[len - 2], connector.path[len - 1]))
|
||||
);
|
||||
let nextBound: Bound;
|
||||
let position: Connection['position'];
|
||||
// direction of the connector target arrow
|
||||
let direction: Direction;
|
||||
|
||||
if (angle >= 45 && angle <= 135) {
|
||||
nextBound = new Bound(point[0] - w / 2, point[1], w, h);
|
||||
position = [0.5, 0];
|
||||
direction = Direction.Bottom;
|
||||
} else if (angle >= 135 && angle <= 225) {
|
||||
nextBound = new Bound(point[0] - w, point[1] - h / 2, w, h);
|
||||
position = [1, 0.5];
|
||||
direction = Direction.Left;
|
||||
} else if (angle >= 225 && angle <= 315) {
|
||||
nextBound = new Bound(point[0] - w / 2, point[1] - h, w, h);
|
||||
position = [0.5, 1];
|
||||
direction = Direction.Top;
|
||||
} else {
|
||||
nextBound = new Bound(point[0], point[1] - h / 2, w, h);
|
||||
position = [0, 0.5];
|
||||
direction = Direction.Right;
|
||||
}
|
||||
|
||||
return { nextBound, position, direction };
|
||||
}
|
||||
|
||||
private _getCurrentSourceInfo(): {
|
||||
style: ShapeStyle;
|
||||
type: AUTO_COMPLETE_TARGET_TYPE;
|
||||
} {
|
||||
const { currentSource } = this;
|
||||
if (isShape(currentSource)) {
|
||||
const { shapeType, shapeStyle, radius } = currentSource;
|
||||
return {
|
||||
style: shapeStyle,
|
||||
type: getShapeName(shapeType, radius),
|
||||
};
|
||||
}
|
||||
return {
|
||||
style: ShapeStyle.General,
|
||||
type: 'note',
|
||||
};
|
||||
}
|
||||
|
||||
private _getPanelPosition() {
|
||||
const { viewport } = this.edgeless.service;
|
||||
const { boundingClientRect: viewportRect, zoom } = viewport;
|
||||
const result = this._getTargetXYWH(PANEL_WIDTH / zoom, PANEL_HEIGHT / zoom);
|
||||
const pos = result ? result.xywh.slice(0, 2) : this.position;
|
||||
const coord = viewport.toViewCoord(pos[0], pos[1]);
|
||||
const { width, height } = viewportRect;
|
||||
|
||||
coord[0] = clamp(coord[0], 20, width - 20 - PANEL_WIDTH);
|
||||
coord[1] = clamp(coord[1], 20, height - 20 - PANEL_HEIGHT);
|
||||
|
||||
return coord;
|
||||
}
|
||||
|
||||
private _getTargetXYWH(width: number, height: number) {
|
||||
const result = this._generateTarget(this.connector);
|
||||
if (!result) return null;
|
||||
|
||||
const { nextBound: bound, direction, position } = result;
|
||||
if (!bound) return null;
|
||||
|
||||
const { w, h } = bound;
|
||||
let x = bound.x;
|
||||
let y = bound.y;
|
||||
|
||||
switch (direction) {
|
||||
case Direction.Right:
|
||||
y += h / 2 - height / 2;
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
x -= width / 2 - w / 2;
|
||||
break;
|
||||
case Direction.Left:
|
||||
y += h / 2 - height / 2;
|
||||
x -= width - w;
|
||||
break;
|
||||
case Direction.Top:
|
||||
x -= width / 2 - w / 2;
|
||||
y += h - height;
|
||||
break;
|
||||
}
|
||||
|
||||
const xywh = [x, y, width, height] as XYWH;
|
||||
|
||||
return { xywh, position };
|
||||
}
|
||||
|
||||
private _removeOverlay() {
|
||||
if (this._overlay)
|
||||
this.edgeless.surface.renderer.removeOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showFrameOverlay() {
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
|
||||
const { h } = bound;
|
||||
const w = h / 0.75;
|
||||
const xywh = this._getTargetXYWH(w, h)?.xywh;
|
||||
if (!xywh) return;
|
||||
|
||||
const strokeColor = this.std
|
||||
.get(ThemeProvider)
|
||||
.getCssVariableColor('--affine-black-30');
|
||||
this._overlay = new AutoCompleteFrameOverlay(this.gfx, xywh, strokeColor);
|
||||
this.edgeless.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showNoteOverlay() {
|
||||
const xywh = this._getTargetXYWH(
|
||||
DEFAULT_NOTE_WIDTH,
|
||||
DEFAULT_NOTE_OVERLAY_HEIGHT
|
||||
)?.xywh;
|
||||
if (!xywh) return;
|
||||
|
||||
const background = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value['affine:note']
|
||||
.background,
|
||||
DefaultTheme.noteBackgrounColor,
|
||||
true
|
||||
);
|
||||
this._overlay = new AutoCompleteNoteOverlay(this.gfx, xywh, background);
|
||||
this.edgeless.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showOverlay(targetType: AUTO_COMPLETE_TARGET_TYPE) {
|
||||
this._removeOverlay();
|
||||
if (!this._connectorExist()) return;
|
||||
|
||||
switch (targetType) {
|
||||
case 'text':
|
||||
this._showTextOverlay();
|
||||
break;
|
||||
case 'note':
|
||||
this._showNoteOverlay();
|
||||
break;
|
||||
case 'frame':
|
||||
this._showFrameOverlay();
|
||||
break;
|
||||
default:
|
||||
this._showShapeOverlay(targetType);
|
||||
}
|
||||
|
||||
this.edgeless.surface.refresh();
|
||||
}
|
||||
|
||||
private _showShapeOverlay(targetType: TARGET_SHAPE_TYPE) {
|
||||
const bound = this._generateTarget(this.connector)?.nextBound;
|
||||
if (!bound) return;
|
||||
|
||||
const { x, y, w, h } = bound;
|
||||
const xywh = [x, y, w, h] as XYWH;
|
||||
const { shapeStyle, strokeColor, fillColor, strokeWidth, roughness } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value[
|
||||
`shape:${targetType}`
|
||||
];
|
||||
|
||||
const stroke = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(strokeColor, DefaultTheme.shapeStrokeColor, true);
|
||||
const fill = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(fillColor, DefaultTheme.shapeFillColor, true);
|
||||
|
||||
const options = {
|
||||
seed: 666,
|
||||
roughness: roughness,
|
||||
strokeLineDash: [0, 0],
|
||||
stroke,
|
||||
strokeWidth,
|
||||
fill,
|
||||
};
|
||||
|
||||
this._overlay = new AutoCompleteShapeOverlay(
|
||||
this.gfx,
|
||||
xywh,
|
||||
targetType,
|
||||
options,
|
||||
shapeStyle
|
||||
);
|
||||
|
||||
this.edgeless.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
private _showTextOverlay() {
|
||||
const xywh = this._getTargetXYWH(
|
||||
DEFAULT_TEXT_WIDTH,
|
||||
DEFAULT_TEXT_HEIGHT
|
||||
)?.xywh;
|
||||
if (!xywh) return;
|
||||
|
||||
this._overlay = new AutoCompleteTextOverlay(this.gfx, xywh);
|
||||
this.edgeless.surface.renderer.addOverlay(this._overlay);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.edgeless.handleEvent('click', ctx => {
|
||||
const { target } = ctx.get('pointerState').raw;
|
||||
const element = captureEventTarget(target);
|
||||
const clickAway = !element?.closest('edgeless-auto-complete-panel');
|
||||
if (clickAway) this.remove();
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeOverlay();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
this.edgeless.service.viewport.viewportUpdated.on(() =>
|
||||
this.requestUpdate()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const position = this._getPanelPosition();
|
||||
if (!position) return nothing;
|
||||
|
||||
const style = styleMap({
|
||||
left: `${position[0]}px`,
|
||||
top: `${position[1]}px`,
|
||||
});
|
||||
const { style: currentSourceStyle, type: currentSourceType } =
|
||||
this._getCurrentSourceInfo();
|
||||
|
||||
const shapeButtons = repeat(
|
||||
ShapeComponentConfig,
|
||||
({ name, generalIcon, scribbledIcon, tooltip }) => html`
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${tooltip}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay(name)}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete(name)}
|
||||
>
|
||||
${currentSourceStyle === 'General' ? generalIcon : scribbledIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
`
|
||||
);
|
||||
|
||||
return html`<div class="auto-complete-panel-container" style=${style}>
|
||||
${shapeButtons}
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Text'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('text')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('text')}
|
||||
>
|
||||
${FontFamilyIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Note'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('note')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('note')}
|
||||
>
|
||||
${PageIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Frame'}
|
||||
.iconSize=${'20px'}
|
||||
@pointerenter=${() => this._showOverlay('frame')}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete('frame')}
|
||||
>
|
||||
${FrameIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.iconContainerPadding=${0}
|
||||
.tooltip=${capitalizeFirstLetter(currentSourceType)}
|
||||
@pointerenter=${() => this._showOverlay(currentSourceType)}
|
||||
@pointerleave=${() => this._removeOverlay()}
|
||||
@click=${() => this._autoComplete(currentSourceType)}
|
||||
>
|
||||
<div class="row-button">Add a same object</div>
|
||||
</edgeless-tool-icon-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor connector: ConnectorElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor currentSource: ShapeElementModel | NoteBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor position: [number, number];
|
||||
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-auto-complete-panel': EdgelessAutoCompletePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,757 +0,0 @@
|
||||
import {
|
||||
CanvasElementType,
|
||||
type ConnectionOverlay,
|
||||
ConnectorPathGenerator,
|
||||
EdgelessCRUDIdentifier,
|
||||
isNoteBlock,
|
||||
Overlay,
|
||||
OverlayIdentifier,
|
||||
type RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
Connection,
|
||||
ConnectorElementModel,
|
||||
NoteBlockModel,
|
||||
ShapeType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DefaultTheme,
|
||||
LayoutType,
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
shapeMethods,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { handleNativeRangeAtPoint } from '@blocksuite/affine-shared/utils';
|
||||
import { type BlockStdScope, stdContext } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import type { Bound, IVec } from '@blocksuite/global/utils';
|
||||
import {
|
||||
assertExists,
|
||||
DisposableGroup,
|
||||
Vec,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowUpBigIcon,
|
||||
PlusIcon,
|
||||
SiblingNodeIcon,
|
||||
SubNodeIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import { mountShapeTextEditor } from '../../utils/text.js';
|
||||
import type { SelectedRect } from '../rects/edgeless-selected-rect.js';
|
||||
import { EdgelessAutoCompletePanel } from './auto-complete-panel.js';
|
||||
import {
|
||||
createEdgelessElement,
|
||||
Direction,
|
||||
getPosition,
|
||||
isShape,
|
||||
MAIN_GAP,
|
||||
nextBound,
|
||||
} from './utils.js';
|
||||
|
||||
class AutoCompleteOverlay extends Overlay {
|
||||
linePoints: IVec[] = [];
|
||||
|
||||
renderShape: ((ctx: CanvasRenderingContext2D) => void) | null = null;
|
||||
|
||||
stroke = '';
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, _rc: RoughCanvas) {
|
||||
if (this.linePoints.length && this.renderShape) {
|
||||
ctx.setLineDash([2, 2]);
|
||||
ctx.strokeStyle = this.stroke;
|
||||
ctx.beginPath();
|
||||
this.linePoints.forEach((p, index) => {
|
||||
if (index === 0) ctx.moveTo(p[0], p[1]);
|
||||
else ctx.lineTo(p[0], p[1]);
|
||||
});
|
||||
ctx.stroke();
|
||||
|
||||
this.renderShape(ctx);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EdgelessAutoComplete extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.edgeless-auto-complete-container {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.edgeless-auto-complete-arrow-wrapper {
|
||||
width: 72px;
|
||||
height: 44px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.edgeless-auto-complete-arrow-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
.edgeless-auto-complete-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 19px;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transition:
|
||||
background 0.3s linear,
|
||||
box-shadow 0.2s linear;
|
||||
}
|
||||
.edgeless-auto-complete-arrow-wrapper.mindmap {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.edgeless-auto-complete-arrow-wrapper:hover
|
||||
> .edgeless-auto-complete-arrow {
|
||||
border: 1px solid var(--affine-border-color);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
background: var(--affine-white);
|
||||
}
|
||||
|
||||
.edgeless-auto-complete-arrow-wrapper
|
||||
> .edgeless-auto-complete-arrow:hover {
|
||||
border: 1px solid var(--affine-white-10);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
background: var(--affine-primary-color);
|
||||
}
|
||||
|
||||
.edgeless-auto-complete-arrow-wrapper.mindmap
|
||||
> .edgeless-auto-complete-arrow {
|
||||
border: 1px solid var(--affine-border-color);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
background: var(--affine-white);
|
||||
|
||||
transition:
|
||||
background 0.3s linear,
|
||||
color 0.2s linear;
|
||||
}
|
||||
|
||||
.edgeless-auto-complete-arrow-wrapper.mindmap
|
||||
> .edgeless-auto-complete-arrow:hover {
|
||||
border: 1px solid var(--affine-white-10);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
background: var(--affine-primary-color);
|
||||
}
|
||||
|
||||
.edgeless-auto-complete-arrow svg {
|
||||
fill: #77757d;
|
||||
color: #77757d;
|
||||
}
|
||||
.edgeless-auto-complete-arrow:hover svg {
|
||||
fill: #ffffff;
|
||||
color: #ffffff;
|
||||
}
|
||||
`;
|
||||
|
||||
private _autoCompleteOverlay!: AutoCompleteOverlay;
|
||||
|
||||
private readonly _onPointerDown = (e: PointerEvent, type: Direction) => {
|
||||
const viewportRect = this.gfx.viewport.boundingClientRect;
|
||||
const start = this.gfx.viewport.toModelCoord(
|
||||
e.clientX - viewportRect.left,
|
||||
e.clientY - viewportRect.top
|
||||
);
|
||||
|
||||
if (!this.edgeless.dispatcher) return;
|
||||
|
||||
let connector: ConnectorElementModel | null;
|
||||
|
||||
this._disposables.addFromEvent(document, 'pointermove', e => {
|
||||
const point = this.gfx.viewport.toModelCoord(
|
||||
e.clientX - viewportRect.left,
|
||||
e.clientY - viewportRect.top
|
||||
);
|
||||
if (Vec.dist(start, point) > 8 && !this._isMoving) {
|
||||
if (!this.canShowAutoComplete) return;
|
||||
this._isMoving = true;
|
||||
const { startPosition } = getPosition(type);
|
||||
connector = this._addConnector(
|
||||
{
|
||||
id: this.current.id,
|
||||
position: startPosition,
|
||||
},
|
||||
{
|
||||
position: point,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (this._isMoving) {
|
||||
assertExists(connector);
|
||||
const otherSideId = connector.source.id;
|
||||
|
||||
connector.target = this.connectionOverlay.renderConnector(
|
||||
point,
|
||||
otherSideId ? [otherSideId] : []
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this._disposables.addFromEvent(document, 'pointerup', e => {
|
||||
if (!this._isMoving) {
|
||||
this._generateElementOnClick(type);
|
||||
} else if (connector && !connector.target.id) {
|
||||
this.edgeless.service.selection.clear();
|
||||
this._createAutoCompletePanel(e, connector);
|
||||
}
|
||||
|
||||
this._isMoving = false;
|
||||
this.connectionOverlay.clear();
|
||||
this._disposables.dispose();
|
||||
this._disposables = new DisposableGroup();
|
||||
});
|
||||
};
|
||||
|
||||
private _pathGenerator!: ConnectorPathGenerator;
|
||||
|
||||
private _timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
get canShowAutoComplete() {
|
||||
const { current } = this;
|
||||
return isShape(current) || isNoteBlock(current);
|
||||
}
|
||||
|
||||
get connectionOverlay() {
|
||||
return this.std.get(OverlayIdentifier('connection')) as ConnectionOverlay;
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private _addConnector(source: Connection, target: Connection) {
|
||||
const id = this.crud.addElement(CanvasElementType.CONNECTOR, {
|
||||
source,
|
||||
target,
|
||||
});
|
||||
if (!id) return null;
|
||||
return this.crud.getElementById(id) as ConnectorElementModel;
|
||||
}
|
||||
|
||||
private _addMindmapNode(target: 'sibling' | 'child') {
|
||||
const mindmap = this.current.group;
|
||||
|
||||
if (!(mindmap instanceof MindmapElementModel)) return;
|
||||
|
||||
const parent =
|
||||
target === 'sibling'
|
||||
? (mindmap.getParentNode(this.current.id) ?? this.current)
|
||||
: this.current;
|
||||
|
||||
const parentNode = mindmap.getNode(parent.id);
|
||||
|
||||
if (!parentNode) return;
|
||||
|
||||
const newNode = mindmap.addNode(
|
||||
parentNode.id,
|
||||
target === 'sibling' ? this.current.id : undefined,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
|
||||
if (parentNode.detail.collapsed) {
|
||||
mindmap.toggleCollapse(parentNode);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
mountShapeTextEditor(
|
||||
this.crud.getElementById(newNode) as ShapeElementModel,
|
||||
this.edgeless
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLine(
|
||||
type: Direction,
|
||||
curShape: ShapeElementModel,
|
||||
nextBound: Bound
|
||||
) {
|
||||
const startBound = this.current.elementBound;
|
||||
const { startPosition, endPosition } = getPosition(type);
|
||||
const nextShape = {
|
||||
xywh: nextBound.serialize(),
|
||||
rotate: curShape.rotate,
|
||||
shapeType: curShape.shapeType,
|
||||
};
|
||||
const startPoint = curShape.getRelativePointLocation(startPosition);
|
||||
const endPoint = curShape.getRelativePointLocation.call(
|
||||
nextShape,
|
||||
endPosition
|
||||
);
|
||||
|
||||
return this._pathGenerator.generateOrthogonalConnectorPath({
|
||||
startBound,
|
||||
endBound: nextBound,
|
||||
startPoint,
|
||||
endPoint,
|
||||
});
|
||||
}
|
||||
|
||||
private _computeNextBound(type: Direction) {
|
||||
if (isShape(this.current)) {
|
||||
const connectedShapes = this._getConnectedElements(this.current).filter(
|
||||
e => e instanceof ShapeElementModel
|
||||
) as ShapeElementModel[];
|
||||
return nextBound(type, this.current, connectedShapes);
|
||||
} else {
|
||||
const bound = this.current.elementBound;
|
||||
switch (type) {
|
||||
case Direction.Right: {
|
||||
bound.x += bound.w + MAIN_GAP;
|
||||
break;
|
||||
}
|
||||
case Direction.Bottom: {
|
||||
bound.y += bound.h + MAIN_GAP;
|
||||
break;
|
||||
}
|
||||
case Direction.Left: {
|
||||
bound.x -= bound.w + MAIN_GAP;
|
||||
break;
|
||||
}
|
||||
case Direction.Top: {
|
||||
bound.y -= bound.h + MAIN_GAP;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
}
|
||||
|
||||
private _createAutoCompletePanel(
|
||||
e: PointerEvent,
|
||||
connector: ConnectorElementModel
|
||||
) {
|
||||
if (!this.canShowAutoComplete) return;
|
||||
|
||||
const position = this.edgeless.service.viewport.toModelCoord(
|
||||
e.clientX,
|
||||
e.clientY
|
||||
);
|
||||
const autoCompletePanel = new EdgelessAutoCompletePanel(
|
||||
position,
|
||||
this.edgeless,
|
||||
this.current,
|
||||
connector
|
||||
);
|
||||
|
||||
this.edgeless.append(autoCompletePanel);
|
||||
}
|
||||
|
||||
private _generateElementOnClick(type: Direction) {
|
||||
const { doc, service } = this.edgeless;
|
||||
const bound = this._computeNextBound(type);
|
||||
const id = createEdgelessElement(this.edgeless, this.current, bound);
|
||||
if (!id) return;
|
||||
if (isShape(this.current)) {
|
||||
const { startPosition, endPosition } = getPosition(type);
|
||||
this._addConnector(
|
||||
{
|
||||
id: this.current.id,
|
||||
position: startPosition,
|
||||
},
|
||||
{
|
||||
id,
|
||||
position: endPosition,
|
||||
}
|
||||
);
|
||||
|
||||
mountShapeTextEditor(
|
||||
this.crud.getElementById(id) as ShapeElementModel,
|
||||
this.edgeless
|
||||
);
|
||||
} else {
|
||||
const model = doc.getBlockById(id);
|
||||
assertExists(model);
|
||||
const [x, y] = service.viewport.toViewCoord(
|
||||
bound.center[0],
|
||||
bound.y + DEFAULT_NOTE_HEIGHT / 2
|
||||
);
|
||||
requestAnimationFrame(() => {
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
});
|
||||
}
|
||||
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [id],
|
||||
editing: true,
|
||||
});
|
||||
this.removeOverlay();
|
||||
}
|
||||
|
||||
private _getConnectedElements(element: ShapeElementModel) {
|
||||
const service = this.edgeless.service;
|
||||
|
||||
return service.getConnectors(element.id).reduce((prev, current) => {
|
||||
if (current.target.id === element.id && current.source.id) {
|
||||
prev.push(
|
||||
this.crud.getElementById(current.source.id) as ShapeElementModel
|
||||
);
|
||||
}
|
||||
if (current.source.id === element.id && current.target.id) {
|
||||
prev.push(
|
||||
this.crud.getElementById(current.target.id) as ShapeElementModel
|
||||
);
|
||||
}
|
||||
|
||||
return prev;
|
||||
}, [] as ShapeElementModel[]);
|
||||
}
|
||||
|
||||
private _getMindmapButtons() {
|
||||
const mindmap = this.current.group as MindmapElementModel;
|
||||
const mindmapDirection =
|
||||
this.current instanceof ShapeElementModel &&
|
||||
mindmap instanceof MindmapElementModel
|
||||
? mindmap.getLayoutDir(this.current.id)
|
||||
: null;
|
||||
const isRoot = mindmap?.tree.id === this.current.id;
|
||||
const mindmapNode = mindmap.getNode(this.current.id);
|
||||
|
||||
let buttons: [
|
||||
Direction,
|
||||
'child' | 'sibling',
|
||||
LayoutType.LEFT | LayoutType.RIGHT,
|
||||
][] = [];
|
||||
|
||||
switch (mindmapDirection) {
|
||||
case LayoutType.LEFT:
|
||||
buttons = [[Direction.Left, 'child', LayoutType.LEFT]];
|
||||
|
||||
if (!isRoot) {
|
||||
buttons.push([Direction.Bottom, 'sibling', mindmapDirection]);
|
||||
}
|
||||
break;
|
||||
case LayoutType.RIGHT:
|
||||
buttons = [[Direction.Right, 'child', LayoutType.RIGHT]];
|
||||
|
||||
if (!isRoot) {
|
||||
buttons.push([Direction.Bottom, 'sibling', mindmapDirection]);
|
||||
}
|
||||
break;
|
||||
case LayoutType.BALANCE:
|
||||
buttons = [
|
||||
[Direction.Right, 'child', LayoutType.RIGHT],
|
||||
[Direction.Left, 'child', LayoutType.LEFT],
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [];
|
||||
}
|
||||
|
||||
return buttons.length
|
||||
? {
|
||||
mindmapNode,
|
||||
buttons,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
private _initOverlay() {
|
||||
const { surface } = this.edgeless;
|
||||
this._autoCompleteOverlay = new AutoCompleteOverlay(this.gfx);
|
||||
surface.renderer.addOverlay(this._autoCompleteOverlay);
|
||||
}
|
||||
|
||||
private _renderArrow() {
|
||||
const isShape = this.current instanceof ShapeElementModel;
|
||||
const { selectedRect } = this;
|
||||
const { zoom } = this.edgeless.service.viewport;
|
||||
const width = 72;
|
||||
const height = 44;
|
||||
|
||||
// Auto-complete arrows for shape and note are different
|
||||
// Shape: right, bottom, left, top
|
||||
// Note: right, left
|
||||
const arrowDirections = isShape
|
||||
? [Direction.Right, Direction.Bottom, Direction.Left, Direction.Top]
|
||||
: [Direction.Right, Direction.Left];
|
||||
const arrowMargin = isShape ? height / 2 : height * (2 / 3);
|
||||
const Arrows = arrowDirections.map(type => {
|
||||
let transform = '';
|
||||
|
||||
const iconSize = { width: '16px', height: '16px' };
|
||||
const icon = (isShape ? ArrowUpBigIcon : PlusIcon)(iconSize);
|
||||
|
||||
switch (type) {
|
||||
case Direction.Top:
|
||||
transform += `translate(${
|
||||
selectedRect.width / 2
|
||||
}px, ${-arrowMargin}px)`;
|
||||
break;
|
||||
case Direction.Right:
|
||||
transform += `translate(${selectedRect.width + arrowMargin}px, ${
|
||||
selectedRect.height / 2
|
||||
}px)`;
|
||||
|
||||
isShape && (transform += `rotate(90deg)`);
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
transform += `translate(${selectedRect.width / 2}px, ${
|
||||
selectedRect.height + arrowMargin
|
||||
}px)`;
|
||||
isShape && (transform += `rotate(180deg)`);
|
||||
break;
|
||||
case Direction.Left:
|
||||
transform += `translate(${-arrowMargin}px, ${
|
||||
selectedRect.height / 2
|
||||
}px)`;
|
||||
isShape && (transform += `rotate(-90deg)`);
|
||||
break;
|
||||
}
|
||||
transform += `translate(${-width / 2}px, ${-height / 2}px)`;
|
||||
const arrowWrapperClasses = classMap({
|
||||
'edgeless-auto-complete-arrow-wrapper': true,
|
||||
hidden: !isShape && type === Direction.Left && zoom >= 1.5,
|
||||
});
|
||||
|
||||
return html`<div
|
||||
class=${arrowWrapperClasses}
|
||||
style=${styleMap({
|
||||
transform,
|
||||
transformOrigin: 'left top',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="edgeless-auto-complete-arrow"
|
||||
@mouseenter=${() => {
|
||||
this._timer = setTimeout(() => {
|
||||
if (this.current instanceof ShapeElementModel) {
|
||||
const bound = this._computeNextBound(type);
|
||||
const path = this._computeLine(type, this.current, bound);
|
||||
this._showNextShape(
|
||||
this.current,
|
||||
bound,
|
||||
path,
|
||||
this.current.shapeType
|
||||
);
|
||||
}
|
||||
}, 300);
|
||||
}}
|
||||
@mouseleave=${() => {
|
||||
this.removeOverlay();
|
||||
}}
|
||||
@pointerdown=${(e: PointerEvent) => {
|
||||
this._onPointerDown(e, type);
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return Arrows;
|
||||
}
|
||||
|
||||
private _renderMindMapButtons() {
|
||||
const mindmapButtons = this._getMindmapButtons();
|
||||
|
||||
if (!mindmapButtons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selectedRect } = this;
|
||||
const { zoom } = this.edgeless.service.viewport;
|
||||
const size = 26;
|
||||
const buttonMargin =
|
||||
(mindmapButtons.mindmapNode?.children.length ?? 0) > 0
|
||||
? size / 2 + 32 * zoom
|
||||
: size / 2 + 6;
|
||||
const verticalMargin = size / 2 + 6;
|
||||
|
||||
return mindmapButtons.buttons.map(type => {
|
||||
let transform = '';
|
||||
|
||||
const [position, target, layout] = type;
|
||||
const isLeftLayout = layout === LayoutType.LEFT;
|
||||
const icon = (target === 'child' ? SubNodeIcon : SiblingNodeIcon)({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
});
|
||||
|
||||
switch (position) {
|
||||
case Direction.Bottom:
|
||||
transform += `translate(${selectedRect.width / 2}px, ${
|
||||
selectedRect.height + verticalMargin
|
||||
}px)`;
|
||||
isLeftLayout && (transform += `scale(-1)`);
|
||||
break;
|
||||
case Direction.Right:
|
||||
transform += `translate(${selectedRect.width + buttonMargin}px, ${
|
||||
selectedRect.height / 2
|
||||
}px)`;
|
||||
break;
|
||||
case Direction.Left:
|
||||
transform += `translate(${-buttonMargin}px, ${
|
||||
selectedRect.height / 2
|
||||
}px)`;
|
||||
|
||||
transform += `scale(-1)`;
|
||||
break;
|
||||
}
|
||||
|
||||
transform += `translate(${-size / 2}px, ${-size / 2}px)`;
|
||||
|
||||
const arrowWrapperClasses = classMap({
|
||||
'edgeless-auto-complete-arrow-wrapper': true,
|
||||
hidden: position === Direction.Left && zoom >= 1.5,
|
||||
mindmap: true,
|
||||
});
|
||||
|
||||
return html`<div
|
||||
class=${arrowWrapperClasses}
|
||||
style=${styleMap({
|
||||
transform,
|
||||
transformOrigin: 'left top',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="edgeless-auto-complete-arrow"
|
||||
@pointerdown=${() => {
|
||||
this._addMindmapNode(target);
|
||||
}}
|
||||
>
|
||||
${icon}
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
private _showNextShape(
|
||||
current: ShapeElementModel,
|
||||
bound: Bound,
|
||||
path: IVec[],
|
||||
targetType: ShapeType
|
||||
) {
|
||||
const { surface } = this.edgeless;
|
||||
|
||||
this._autoCompleteOverlay.stroke = surface.renderer.getColorValue(
|
||||
current.strokeColor,
|
||||
DefaultTheme.shapeStrokeColor,
|
||||
true
|
||||
);
|
||||
this._autoCompleteOverlay.linePoints = path;
|
||||
this._autoCompleteOverlay.renderShape = ctx => {
|
||||
shapeMethods[targetType].draw(ctx, { ...bound, rotate: current.rotate });
|
||||
};
|
||||
surface.refresh();
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._pathGenerator = new ConnectorPathGenerator({
|
||||
getElementById: id => this.crud.getElementById(id),
|
||||
});
|
||||
this._initOverlay();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { _disposables, edgeless } = this;
|
||||
|
||||
_disposables.add(
|
||||
this.gfx.selection.slots.updated.on(() => {
|
||||
this._autoCompleteOverlay.linePoints = [];
|
||||
this._autoCompleteOverlay.renderShape = null;
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(() => this.removeOverlay());
|
||||
|
||||
_disposables.add(
|
||||
edgeless.host.event.add('pointerMove', ctx => {
|
||||
const evt = ctx.get('pointerState');
|
||||
const [x, y] = edgeless.gfx.viewport.toModelCoord(evt.x, evt.y);
|
||||
const elm = edgeless.gfx.getElementByPoint(x, y);
|
||||
|
||||
if (!elm) {
|
||||
this._isHover = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._isHover = elm === this.current ? true : false;
|
||||
})
|
||||
);
|
||||
|
||||
this.edgeless.handleEvent('dragStart', () => {
|
||||
this._isMoving = true;
|
||||
});
|
||||
this.edgeless.handleEvent('dragEnd', () => {
|
||||
this._isMoving = false;
|
||||
});
|
||||
}
|
||||
|
||||
removeOverlay() {
|
||||
this._timer && clearTimeout(this._timer);
|
||||
this.edgeless.surface.renderer.removeOverlay(this._autoCompleteOverlay);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const isShape = this.current instanceof ShapeElementModel;
|
||||
const isMindMap = this.current.group instanceof MindmapElementModel;
|
||||
|
||||
if (this._isMoving || (this._isHover && !isShape)) {
|
||||
this.removeOverlay();
|
||||
return nothing;
|
||||
}
|
||||
const { selectedRect } = this;
|
||||
|
||||
return html`<div
|
||||
class="edgeless-auto-complete-container"
|
||||
style=${styleMap({
|
||||
top: selectedRect.top + 'px',
|
||||
left: selectedRect.left + 'px',
|
||||
width: selectedRect.width + 'px',
|
||||
height: selectedRect.height + 'px',
|
||||
transform: `rotate(${selectedRect.rotate}deg)`,
|
||||
})}
|
||||
>
|
||||
${isMindMap ? this._renderMindMapButtons() : this._renderArrow()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _isHover = true;
|
||||
|
||||
@state()
|
||||
private accessor _isMoving = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor current!: ShapeElementModel | NoteBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor selectedRect!: SelectedRect;
|
||||
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-auto-complete': EdgelessAutoComplete;
|
||||
}
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import {
|
||||
type Options,
|
||||
Overlay,
|
||||
type RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
type Connection,
|
||||
getShapeRadius,
|
||||
getShapeType,
|
||||
GroupElementModel,
|
||||
type NoteBlockModel,
|
||||
ShapeElementModel,
|
||||
type ShapeName,
|
||||
type ShapeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { GfxController, GfxModel } from '@blocksuite/block-std/gfx';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { XYWH } from '@blocksuite/global/utils';
|
||||
import { assertType, Bound, normalizeDegAngle } from '@blocksuite/global/utils';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import { type Shape, ShapeFactory } from '../../utils/tool-overlay.js';
|
||||
|
||||
export enum Direction {
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
Top,
|
||||
}
|
||||
|
||||
export const PANEL_WIDTH = 136;
|
||||
export const PANEL_HEIGHT = 108;
|
||||
|
||||
export const MAIN_GAP = 100;
|
||||
export const SECOND_GAP = 20;
|
||||
export const DEFAULT_NOTE_OVERLAY_HEIGHT = 110;
|
||||
export const DEFAULT_TEXT_WIDTH = 116;
|
||||
export const DEFAULT_TEXT_HEIGHT = 24;
|
||||
|
||||
export type TARGET_SHAPE_TYPE = ShapeName;
|
||||
export type AUTO_COMPLETE_TARGET_TYPE =
|
||||
| TARGET_SHAPE_TYPE
|
||||
| 'text'
|
||||
| 'note'
|
||||
| 'frame';
|
||||
|
||||
class AutoCompleteTargetOverlay extends Overlay {
|
||||
xywh: XYWH;
|
||||
|
||||
constructor(gfx: GfxController, xywh: XYWH) {
|
||||
super(gfx);
|
||||
this.xywh = xywh;
|
||||
}
|
||||
|
||||
override render(_ctx: CanvasRenderingContext2D, _rc: RoughCanvas) {}
|
||||
}
|
||||
|
||||
export class AutoCompleteTextOverlay extends AutoCompleteTargetOverlay {
|
||||
constructor(gfx: GfxController, xywh: XYWH) {
|
||||
super(gfx, xywh);
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, _rc: RoughCanvas) {
|
||||
const [x, y, w, h] = this.xywh;
|
||||
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.strokeStyle = '#1e96eb';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
|
||||
// fill text placeholder
|
||||
ctx.font = '15px sans-serif';
|
||||
ctx.fillStyle = '#C0BFC1';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText("Type '/' to insert", x + w / 2, y + h / 2);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoCompleteNoteOverlay extends AutoCompleteTargetOverlay {
|
||||
private readonly _background: string;
|
||||
|
||||
constructor(gfx: GfxController, xywh: XYWH, background: string) {
|
||||
super(gfx, xywh);
|
||||
this._background = background;
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, _rc: RoughCanvas) {
|
||||
const [x, y, w, h] = this.xywh;
|
||||
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.fillStyle = this._background;
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.10)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, w, h, 8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// fill text placeholder
|
||||
ctx.font = '15px sans-serif';
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText("Type '/' for command", x + 24, y + h / 2);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoCompleteFrameOverlay extends AutoCompleteTargetOverlay {
|
||||
private readonly _strokeColor;
|
||||
|
||||
constructor(gfx: GfxController, xywh: XYWH, strokeColor: string) {
|
||||
super(gfx, xywh);
|
||||
this._strokeColor = strokeColor;
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, _rc: RoughCanvas) {
|
||||
const [x, y, w, h] = this.xywh;
|
||||
// frame title background
|
||||
const titleWidth = 72;
|
||||
const titleHeight = 30;
|
||||
const titleY = y - titleHeight - 10;
|
||||
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, titleY, titleWidth, titleHeight, 4);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// fill title text
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.font = '14px sans-serif';
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('Frame', x + titleWidth / 2, titleY + titleHeight / 2);
|
||||
|
||||
// frame stroke
|
||||
ctx.globalAlpha = 0.4;
|
||||
ctx.strokeStyle = this._strokeColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, w, h, 8);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoCompleteShapeOverlay extends Overlay {
|
||||
private readonly _shape: Shape;
|
||||
|
||||
constructor(
|
||||
gfx: GfxController,
|
||||
xywh: XYWH,
|
||||
type: TARGET_SHAPE_TYPE,
|
||||
options: Options,
|
||||
shapeStyle: ShapeStyle
|
||||
) {
|
||||
super(gfx);
|
||||
this._shape = ShapeFactory.createShape(xywh, type, options, shapeStyle);
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, rc: RoughCanvas) {
|
||||
ctx.globalAlpha = 0.4;
|
||||
this._shape.draw(ctx, rc);
|
||||
}
|
||||
}
|
||||
|
||||
export function nextBound(
|
||||
type: Direction,
|
||||
curShape: ShapeElementModel,
|
||||
elements: ShapeElementModel[]
|
||||
) {
|
||||
const bound = Bound.deserialize(curShape.xywh);
|
||||
const { x, y, w, h } = bound;
|
||||
let nextBound: Bound;
|
||||
let angle = 0;
|
||||
switch (type) {
|
||||
case Direction.Right:
|
||||
angle = 0;
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
angle = 90;
|
||||
break;
|
||||
case Direction.Left:
|
||||
angle = 180;
|
||||
break;
|
||||
case Direction.Top:
|
||||
angle = 270;
|
||||
break;
|
||||
}
|
||||
angle = normalizeDegAngle(angle + curShape.rotate);
|
||||
|
||||
if (angle >= 45 && angle <= 135) {
|
||||
nextBound = new Bound(x, y + h + MAIN_GAP, w, h);
|
||||
} else if (angle >= 135 && angle <= 225) {
|
||||
nextBound = new Bound(x - w - MAIN_GAP, y, w, h);
|
||||
} else if (angle >= 225 && angle <= 315) {
|
||||
nextBound = new Bound(x, y - h - MAIN_GAP, w, h);
|
||||
} else {
|
||||
nextBound = new Bound(x + w + MAIN_GAP, y, w, h);
|
||||
}
|
||||
|
||||
function isValidBound(bound: Bound) {
|
||||
return !elements.some(a => bound.isOverlapWithBound(a.elementBound));
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
function findValidBound() {
|
||||
count++;
|
||||
const number = Math.ceil(count / 2);
|
||||
const next = nextBound.clone();
|
||||
switch (type) {
|
||||
case Direction.Right:
|
||||
case Direction.Left:
|
||||
next.y =
|
||||
count % 2 === 1
|
||||
? nextBound.y - (h + SECOND_GAP) * number
|
||||
: nextBound.y + (h + SECOND_GAP) * number;
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
case Direction.Top:
|
||||
next.x =
|
||||
count % 2 === 1
|
||||
? nextBound.x - (w + SECOND_GAP) * number
|
||||
: nextBound.x + (w + SECOND_GAP) * number;
|
||||
break;
|
||||
}
|
||||
if (isValidBound(next)) return next;
|
||||
return findValidBound();
|
||||
}
|
||||
|
||||
return isValidBound(nextBound) ? nextBound : findValidBound();
|
||||
}
|
||||
|
||||
export function getPosition(type: Direction) {
|
||||
let startPosition: Connection['position'];
|
||||
let endPosition: Connection['position'];
|
||||
|
||||
switch (type) {
|
||||
case Direction.Right:
|
||||
startPosition = [1, 0.5];
|
||||
endPosition = [0, 0.5];
|
||||
break;
|
||||
case Direction.Bottom:
|
||||
startPosition = [0.5, 1];
|
||||
endPosition = [0.5, 0];
|
||||
break;
|
||||
case Direction.Left:
|
||||
startPosition = [0, 0.5];
|
||||
endPosition = [1, 0.5];
|
||||
break;
|
||||
case Direction.Top:
|
||||
startPosition = [0.5, 0];
|
||||
endPosition = [0.5, 1];
|
||||
break;
|
||||
}
|
||||
return { startPosition, endPosition };
|
||||
}
|
||||
|
||||
export function isShape(element: unknown): element is ShapeElementModel {
|
||||
return element instanceof ShapeElementModel;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function createEdgelessElement(
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
current: ShapeElementModel | NoteBlockModel,
|
||||
bound: Bound
|
||||
) {
|
||||
let id;
|
||||
const { service } = edgeless;
|
||||
const { crud } = service;
|
||||
|
||||
let element: GfxModel | null = null;
|
||||
|
||||
if (isShape(current)) {
|
||||
id = crud.addElement(current.type, {
|
||||
...current.serialize(),
|
||||
text: new Y.Text(),
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
if (!id) return null;
|
||||
element = crud.getElementById(id);
|
||||
} else {
|
||||
const { doc } = edgeless;
|
||||
id = doc.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
background: current.background,
|
||||
displayMode: current.displayMode,
|
||||
edgeless: current.edgeless,
|
||||
xywh: bound.serialize(),
|
||||
},
|
||||
edgeless.model.id
|
||||
);
|
||||
const note = doc.getBlock(id)?.model;
|
||||
if (!note) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.GfxBlockElementError,
|
||||
'Note block is not found after creation'
|
||||
);
|
||||
}
|
||||
assertType<NoteBlockModel>(note);
|
||||
doc.updateBlock(note, () => {
|
||||
note.edgeless.collapse = true;
|
||||
});
|
||||
doc.addBlock('affine:paragraph', {}, note.id);
|
||||
|
||||
element = note;
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.GfxBlockElementError,
|
||||
'Element is not found after creation'
|
||||
);
|
||||
}
|
||||
|
||||
const group = current.group;
|
||||
if (group instanceof GroupElementModel) {
|
||||
group.addChild(element);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export function createShapeElement(
|
||||
edgeless: EdgelessRootBlockComponent,
|
||||
current: ShapeElementModel | NoteBlockModel,
|
||||
targetType: TARGET_SHAPE_TYPE
|
||||
) {
|
||||
const { crud } = edgeless.service;
|
||||
const id = crud.addElement('shape', {
|
||||
shapeType: getShapeType(targetType),
|
||||
radius: getShapeRadius(targetType),
|
||||
text: new Y.Text(),
|
||||
});
|
||||
if (!id) return null;
|
||||
const element = crud.getElementById(id);
|
||||
const group = current.group;
|
||||
if (group instanceof GroupElementModel && element) {
|
||||
group.addChild(element);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
@@ -1,198 +0,0 @@
|
||||
import type { Placement } from '@floating-ui/dom';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { cache } from 'lit/directives/cache.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export class EdgelessToolIconButton extends LitElement {
|
||||
static override styles = css`
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--icon-container-padding);
|
||||
color: var(--affine-icon-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
width: var(--icon-container-width, unset);
|
||||
justify-content: var(--justify, unset);
|
||||
}
|
||||
|
||||
.icon-container.active-mode-color[active] {
|
||||
color: var(--affine-primary-color);
|
||||
}
|
||||
|
||||
.icon-container.active-mode-background[active] {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.icon-container[disabled] {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
color: var(--affine-text-disable-color);
|
||||
}
|
||||
|
||||
.icon-container[coming] {
|
||||
cursor: not-allowed;
|
||||
color: var(--affine-text-disable-color);
|
||||
}
|
||||
|
||||
::slotted(svg) {
|
||||
flex-shrink: 0;
|
||||
width: var(--icon-size, unset);
|
||||
height: var(--icon-size, unset);
|
||||
}
|
||||
|
||||
::slotted(.label) {
|
||||
flex: 1;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
line-height: var(--label-height, inherit);
|
||||
}
|
||||
::slotted(.label.padding0) {
|
||||
padding: 0;
|
||||
}
|
||||
::slotted(.label.ellipsis) {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
::slotted(.label.medium) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon-container[with-hover]::before {
|
||||
content: '';
|
||||
display: block;
|
||||
background: var(--affine-hover-color);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addEventListener(
|
||||
'click',
|
||||
event => {
|
||||
if (this.disabled) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.role = 'button';
|
||||
}
|
||||
|
||||
override render() {
|
||||
const tooltip = this.coming ? '(Coming soon)' : this.tooltip;
|
||||
const classnames = `icon-container active-mode-${this.activeMode} ${this.hoverState ? 'hovered' : ''}`;
|
||||
const padding = this.iconContainerPadding;
|
||||
const iconContainerStyles = styleMap({
|
||||
'--icon-container-width': this.iconContainerWidth,
|
||||
'--icon-container-padding': Array.isArray(padding)
|
||||
? padding.map(v => `${v}px`).join(' ')
|
||||
: `${padding}px`,
|
||||
'--icon-size': this.iconSize,
|
||||
'--justify': this.justify,
|
||||
'--label-height': this.labelHeight,
|
||||
});
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.icon-container:hover,
|
||||
.icon-container.hovered {
|
||||
background: ${this.hover ? `var(--affine-hover-color)` : 'inherit'};
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class=${classnames}
|
||||
style=${iconContainerStyles}
|
||||
?with-hover=${this.withHover}
|
||||
?disabled=${this.disabled}
|
||||
?active=${this.active}
|
||||
>
|
||||
<slot></slot>
|
||||
${cache(
|
||||
this.showTooltip && tooltip
|
||||
? html`<affine-tooltip
|
||||
tip-position=${this.tipPosition}
|
||||
.arrow=${this.arrow}
|
||||
.offset=${this.tooltipOffset}
|
||||
>${tooltip}</affine-tooltip
|
||||
>`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor active = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor activeMode: 'color' | 'background' = 'color';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor arrow = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor coming = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor disabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hover = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hoverState = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor iconContainerPadding: number | number[] = 2;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor iconContainerWidth: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor iconSize: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor justify: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor labelHeight: string | undefined = undefined;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor showTooltip = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tipPosition: Placement = 'top';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tooltip!: string | TemplateResult<1>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tooltipOffset = 8;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor withHover: boolean | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-tool-icon-button': EdgelessToolIconButton;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { css, html } from 'lit';
|
||||
|
||||
import { EdgelessToolIconButton } from './tool-icon-button.js';
|
||||
|
||||
export class EdgelessToolbarButton extends EdgelessToolIconButton {
|
||||
static override styles = css`
|
||||
.icon-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
color: var(--affine-icon-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-container.active-mode-color[active] {
|
||||
color: var(--affine-primary-color);
|
||||
}
|
||||
|
||||
.icon-container.active-mode-background[active] {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.icon-container[disabled] {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon-container[coming] {
|
||||
cursor: not-allowed;
|
||||
color: var(--affine-text-disable-color);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html` ${super.render()} `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-toolbar-button': EdgelessToolbarButton;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import {
|
||||
type ConnectionOverlay,
|
||||
OverlayIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
docContext,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { DisposableGroup, Vec, WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { Store } from '@blocksuite/store';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
const SIZE = 12;
|
||||
const HALF_SIZE = SIZE / 2;
|
||||
|
||||
export class EdgelessConnectorHandle extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.line-controller {
|
||||
position: absolute;
|
||||
width: ${SIZE}px;
|
||||
height: ${SIZE}px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--affine-text-emphasis-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
pointer-events: all;
|
||||
/**
|
||||
* Fix: pointerEvent stops firing after a short time.
|
||||
* When a gesture is started, the browser intersects the touch-action values of the touched element and its ancestors,
|
||||
* up to the one that implements the gesture (in other words, the first containing scrolling element)
|
||||
* https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action
|
||||
*/
|
||||
touch-action: none;
|
||||
}
|
||||
.line-controller-hidden {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private _lastZoom = 1;
|
||||
|
||||
get connectionOverlay() {
|
||||
return this.std.get(OverlayIdentifier('connection')) as ConnectionOverlay;
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private _bindEvent() {
|
||||
const edgeless = this.edgeless;
|
||||
|
||||
this._disposables.addFromEvent(this._startHandler, 'pointerdown', e => {
|
||||
edgeless.slots.elementResizeStart.emit();
|
||||
this._capPointerDown(e, 'source');
|
||||
});
|
||||
this._disposables.addFromEvent(this._endHandler, 'pointerdown', e => {
|
||||
edgeless.slots.elementResizeStart.emit();
|
||||
this._capPointerDown(e, 'target');
|
||||
});
|
||||
this._disposables.add(() => {
|
||||
this.connectionOverlay.clear();
|
||||
});
|
||||
}
|
||||
|
||||
private _capPointerDown(e: PointerEvent, connection: 'target' | 'source') {
|
||||
const { edgeless, connector, _disposables } = this;
|
||||
const { service } = edgeless;
|
||||
e.stopPropagation();
|
||||
_disposables.addFromEvent(document, 'pointermove', e => {
|
||||
const point = service.viewport.toModelCoordFromClientCoord([e.x, e.y]);
|
||||
const isStartPointer = connection === 'source';
|
||||
const otherSideId = connector[isStartPointer ? 'target' : 'source'].id;
|
||||
|
||||
connector[connection] = this.connectionOverlay.renderConnector(
|
||||
point,
|
||||
otherSideId ? [otherSideId] : []
|
||||
);
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
_disposables.addFromEvent(document, 'pointerup', () => {
|
||||
this.doc.captureSync();
|
||||
_disposables.dispose();
|
||||
this._disposables = new DisposableGroup();
|
||||
this._bindEvent();
|
||||
edgeless.slots.elementResizeEnd.emit();
|
||||
});
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { edgeless } = this;
|
||||
const { viewport } = edgeless.service;
|
||||
|
||||
this._lastZoom = viewport.zoom;
|
||||
edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
if (viewport.zoom !== this._lastZoom) {
|
||||
this._lastZoom = viewport.zoom;
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
this._bindEvent();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { service } = this.edgeless;
|
||||
// path is relative to the element's xywh
|
||||
const { path } = this.connector;
|
||||
const zoom = service.viewport.zoom;
|
||||
const startPoint = Vec.subScalar(Vec.mul(path[0], zoom), HALF_SIZE);
|
||||
const endPoint = Vec.subScalar(
|
||||
Vec.mul(path[path.length - 1], zoom),
|
||||
HALF_SIZE
|
||||
);
|
||||
const startStyle = {
|
||||
transform: `translate3d(${startPoint[0]}px,${startPoint[1]}px,0)`,
|
||||
};
|
||||
const endStyle = {
|
||||
transform: `translate3d(${endPoint[0]}px,${endPoint[1]}px,0)`,
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
class="line-controller line-start"
|
||||
style=${styleMap(startStyle)}
|
||||
></div>
|
||||
<div class="line-controller line-end" style=${styleMap(endStyle)}></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.line-end')
|
||||
private accessor _endHandler!: HTMLDivElement;
|
||||
|
||||
@query('.line-start')
|
||||
private accessor _startHandler!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor connector!: ConnectorElementModel;
|
||||
|
||||
@consume({
|
||||
context: docContext,
|
||||
})
|
||||
accessor doc!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@consume({
|
||||
context: stdContext,
|
||||
})
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-connector-handle': EdgelessConnectorHandle;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockServiceWatcher,
|
||||
BlockStdScope,
|
||||
type EditorHost,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
Bound,
|
||||
debounce,
|
||||
deserializeXYWH,
|
||||
DisposableGroup,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { type Query, type Store } from '@blocksuite/store';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { SpecProvider } from '../../../../_specs/index.js';
|
||||
import type { EdgelessRootPreviewBlockComponent } from '../../edgeless-root-preview-block.js';
|
||||
|
||||
const DEFAULT_PREVIEW_CONTAINER_WIDTH = 280;
|
||||
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
|
||||
|
||||
const styles = css`
|
||||
.frame-preview-container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-preview-surface-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.frame-preview-viewport {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
|
||||
.edgeless-background {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export class FramePreview extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _clearFrameDisposables = () => {
|
||||
this._frameDisposables?.dispose();
|
||||
this._frameDisposables = null;
|
||||
};
|
||||
|
||||
private readonly _docFilter: Query = {
|
||||
mode: 'loose',
|
||||
match: [
|
||||
{
|
||||
flavour: 'affine:frame',
|
||||
viewType: 'hidden',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private _frameDisposables: DisposableGroup | null = null;
|
||||
|
||||
private _previewDoc: Store | null = null;
|
||||
|
||||
private readonly _previewSpec =
|
||||
SpecProvider.getInstance().getSpec('preview:edgeless');
|
||||
|
||||
private readonly _updateFrameViewportWH = () => {
|
||||
const [, , w, h] = deserializeXYWH(this.frame.xywh);
|
||||
|
||||
let scale = 1;
|
||||
if (this.fillScreen) {
|
||||
scale = Math.max(this.surfaceWidth / w, this.surfaceHeight / h);
|
||||
} else {
|
||||
scale = Math.min(this.surfaceWidth / w, this.surfaceHeight / h);
|
||||
}
|
||||
|
||||
this.frameViewportWH = {
|
||||
width: w * scale,
|
||||
height: h * scale,
|
||||
};
|
||||
};
|
||||
|
||||
get _originalDoc() {
|
||||
return this.frame.doc;
|
||||
}
|
||||
|
||||
private _initPreviewDoc() {
|
||||
this._previewDoc = this._originalDoc.workspace.getDoc(
|
||||
this._originalDoc.id,
|
||||
{
|
||||
query: this._docFilter,
|
||||
readonly: true,
|
||||
}
|
||||
);
|
||||
this.disposables.add(() => {
|
||||
this._originalDoc.doc.clearQuery(this._docFilter);
|
||||
});
|
||||
}
|
||||
|
||||
private _initSpec() {
|
||||
const refreshViewport = this._refreshViewport.bind(this);
|
||||
class FramePreviewWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
override mounted() {
|
||||
const blockService = this.blockService;
|
||||
blockService.disposables.add(
|
||||
blockService.specSlots.viewConnected.on(({ component }) => {
|
||||
const edgelessBlock =
|
||||
component as EdgelessRootPreviewBlockComponent;
|
||||
|
||||
edgelessBlock.editorViewportSelector = 'frame-preview-viewport';
|
||||
edgelessBlock.service.viewport.sizeUpdated.once(() => {
|
||||
refreshViewport();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
this._previewSpec.extend([FramePreviewWatcher]);
|
||||
}
|
||||
|
||||
private _refreshViewport() {
|
||||
const previewEditorHost = this.previewEditor;
|
||||
|
||||
if (!previewEditorHost) return;
|
||||
|
||||
const { viewport } = previewEditorHost.std.get(GfxControllerIdentifier);
|
||||
const frameBound = Bound.deserialize(this.frame.xywh);
|
||||
viewport.setViewportByBound(frameBound);
|
||||
}
|
||||
|
||||
private _renderSurfaceContent() {
|
||||
if (!this._previewDoc || !this.frame) return nothing;
|
||||
const { width, height } = this.frameViewportWH;
|
||||
|
||||
const _previewSpec = this._previewSpec.value;
|
||||
return html`<div
|
||||
class="frame-preview-surface-container"
|
||||
style=${styleMap({
|
||||
width: `${this.surfaceWidth}px`,
|
||||
height: `${this.surfaceHeight}px`,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="frame-preview-viewport"
|
||||
style=${styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
})}
|
||||
>
|
||||
${new BlockStdScope({
|
||||
store: this._previewDoc,
|
||||
extensions: _previewSpec,
|
||||
}).render()}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _setFrameDisposables(frame: FrameBlockModel) {
|
||||
this._clearFrameDisposables();
|
||||
this._frameDisposables = new DisposableGroup();
|
||||
this._frameDisposables.add(
|
||||
frame.propsUpdated.on(debounce(this._updateFrameViewportWH, 10))
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._initSpec();
|
||||
this._initPreviewDoc();
|
||||
this._updateFrameViewportWH();
|
||||
this._setFrameDisposables(this.frame);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearFrameDisposables();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { frame } = this;
|
||||
const noContent = !frame || !frame.xywh;
|
||||
|
||||
return html`<div class="frame-preview-container">
|
||||
${noContent ? nothing : this._renderSurfaceContent()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('frame')) {
|
||||
this._setFrameDisposables(this.frame);
|
||||
}
|
||||
if (_changedProperties.has('frameViewportWH')) {
|
||||
this._refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor fillScreen = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frame!: FrameBlockModel;
|
||||
|
||||
@state()
|
||||
accessor frameViewportWH = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
@query('editor-host')
|
||||
accessor previewEditor: EditorHost | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor surfaceHeight: number = DEFAULT_PREVIEW_CONTAINER_HEIGHT;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor surfaceWidth: number = DEFAULT_PREVIEW_CONTAINER_WIDTH;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'frame-preview': FramePreview;
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
import { isNoteBlock } from '@blocksuite/affine-block-surface';
|
||||
import { DEFAULT_NOTE_HEIGHT } from '@blocksuite/affine-model';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getRectByBlockComponent } from '@blocksuite/affine-shared/utils';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
deserializeXYWH,
|
||||
DisposableGroup,
|
||||
Point,
|
||||
serializeXYWH,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { ScissorsIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type {
|
||||
EdgelessRootBlockComponent,
|
||||
NoteBlockComponent,
|
||||
NoteBlockModel,
|
||||
RootBlockModel,
|
||||
} from '../../../../index.js';
|
||||
|
||||
const DIVIDING_LINE_OFFSET = 4;
|
||||
const NEW_NOTE_GAP = 40;
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-slicer-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-slicer-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--affine-icon-color);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
background-color: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-menu-shadow);
|
||||
cursor: pointer;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform-origin: left top;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
opacity: 0;
|
||||
transition: opacity 150ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
.note-slicer-dividing-line-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-slicer-dividing-line {
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--affine-black-10) 50%,
|
||||
transparent 50%
|
||||
);
|
||||
background-size: 4px 100%;
|
||||
}
|
||||
.note-slicer-dividing-line-container.active .note-slicer-dividing-line {
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--affine-black-60) 50%,
|
||||
transparent 50%
|
||||
);
|
||||
animation: slide 0.3s linear infinite;
|
||||
}
|
||||
@keyframes slide {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -4px 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NOTE_SLICER_WIDGET = 'note-slicer';
|
||||
|
||||
export class NoteSlicer extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
static override styles = styles;
|
||||
|
||||
private _divingLinePositions: Point[] = [];
|
||||
|
||||
private _hidden = false;
|
||||
|
||||
private _noteBlockIds: string[] = [];
|
||||
|
||||
private _noteDisposables: DisposableGroup | null = null;
|
||||
|
||||
get _editorHost() {
|
||||
return this.std.host;
|
||||
}
|
||||
|
||||
get _noteBlock() {
|
||||
if (!this._editorHost) return null;
|
||||
const noteBlock = this._editorHost.view.getBlock(
|
||||
this._anchorNote?.id ?? ''
|
||||
);
|
||||
return noteBlock ? (noteBlock as NoteBlockComponent) : null;
|
||||
}
|
||||
|
||||
get _selection() {
|
||||
return this.gfx.selection;
|
||||
}
|
||||
|
||||
get _viewportOffset() {
|
||||
const { viewport } = this.gfx;
|
||||
return {
|
||||
left: viewport.left ?? 0,
|
||||
top: viewport.top ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
get _zoom() {
|
||||
return this.gfx.viewport.zoom;
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get selectedRectEle() {
|
||||
return this.block.selectedRectWidget;
|
||||
}
|
||||
|
||||
private _sliceNote() {
|
||||
if (!this._anchorNote || !this._noteBlockIds.length) return;
|
||||
const doc = this.doc;
|
||||
|
||||
const {
|
||||
index: originIndex,
|
||||
xywh,
|
||||
background,
|
||||
children,
|
||||
displayMode,
|
||||
} = this._anchorNote;
|
||||
const {
|
||||
collapse: _,
|
||||
collapsedHeight: __,
|
||||
...restOfEdgeless
|
||||
} = this._anchorNote.edgeless;
|
||||
const anchorBlockId = this._noteBlockIds[this._activeSlicerIndex];
|
||||
if (!anchorBlockId) return;
|
||||
const sliceIndex = children.findIndex(block => block.id === anchorBlockId);
|
||||
const resetBlocks = children.slice(sliceIndex + 1);
|
||||
const [x, , width] = deserializeXYWH(xywh);
|
||||
const sliceVerticalPos =
|
||||
this._divingLinePositions[this._activeSlicerIndex].y;
|
||||
const newY = this.gfx.viewport.toModelCoord(x, sliceVerticalPos)[1];
|
||||
const newNoteId = this.doc.addBlock(
|
||||
'affine:note',
|
||||
{
|
||||
background,
|
||||
displayMode,
|
||||
xywh: serializeXYWH(x, newY + NEW_NOTE_GAP, width, DEFAULT_NOTE_HEIGHT),
|
||||
index: originIndex + 1,
|
||||
edgeless: restOfEdgeless,
|
||||
},
|
||||
doc.root?.id
|
||||
);
|
||||
|
||||
doc.moveBlocks(resetBlocks, doc.getBlockById(newNoteId) as NoteBlockModel);
|
||||
|
||||
this._activeSlicerIndex = 0;
|
||||
this._selection.set({
|
||||
elements: [newNoteId],
|
||||
editing: false,
|
||||
});
|
||||
|
||||
this.std.getOptional(TelemetryProvider)?.track('SplitNote', {
|
||||
control: 'NoteSlicer',
|
||||
});
|
||||
}
|
||||
|
||||
private _updateActiveSlicerIndex(pos: Point) {
|
||||
const { _divingLinePositions } = this;
|
||||
const curY = pos.y + DIVIDING_LINE_OFFSET * this._zoom;
|
||||
let index = -1;
|
||||
for (let i = 0; i < _divingLinePositions.length; i++) {
|
||||
const currentY = _divingLinePositions[i].y;
|
||||
const previousY = i > 0 ? _divingLinePositions[i - 1].y : 0;
|
||||
const midY = (currentY + previousY) / 2;
|
||||
if (curY < midY) {
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
if (index < 0) index = 0;
|
||||
this._activeSlicerIndex = index;
|
||||
}
|
||||
|
||||
private _updateDivingLineAndBlockIds() {
|
||||
if (!this._anchorNote || !this._noteBlock) {
|
||||
this._divingLinePositions = [];
|
||||
this._noteBlockIds = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const divingLinePositions: Point[] = [];
|
||||
const noteBlockIds: string[] = [];
|
||||
const noteRect = this._noteBlock.getBoundingClientRect();
|
||||
const noteTop = noteRect.top;
|
||||
const noteBottom = noteRect.bottom;
|
||||
|
||||
for (let i = 0; i < this._anchorNote.children.length - 1; i++) {
|
||||
const child = this._anchorNote.children[i];
|
||||
const rect = this.host.view.getBlock(child.id)?.getBoundingClientRect();
|
||||
|
||||
if (rect && rect.bottom > noteTop && rect.bottom < noteBottom) {
|
||||
const x = rect.x - this._viewportOffset.left;
|
||||
const y =
|
||||
rect.bottom +
|
||||
DIVIDING_LINE_OFFSET * this._zoom -
|
||||
this._viewportOffset.top;
|
||||
divingLinePositions.push(new Point(x, y));
|
||||
noteBlockIds.push(child.id);
|
||||
}
|
||||
}
|
||||
|
||||
this._divingLinePositions = divingLinePositions;
|
||||
this._noteBlockIds = noteBlockIds;
|
||||
}
|
||||
|
||||
private _updateSlicedNote() {
|
||||
const { selectedElements } = this.gfx.selection;
|
||||
|
||||
if (
|
||||
!this.gfx.selection.editing &&
|
||||
selectedElements.length === 1 &&
|
||||
isNoteBlock(selectedElements[0])
|
||||
) {
|
||||
this._anchorNote = selectedElements[0];
|
||||
} else {
|
||||
this._anchorNote = null;
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
const { disposables, std, block, gfx } = this;
|
||||
|
||||
this._updateDivingLineAndBlockIds();
|
||||
|
||||
disposables.add(
|
||||
block.slots.elementResizeStart.on(() => {
|
||||
this._isResizing = true;
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
block.slots.elementResizeEnd.on(() => {
|
||||
this._isResizing = false;
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
std.event.add('pointerMove', ctx => {
|
||||
if (this._hidden) this._hidden = false;
|
||||
|
||||
const state = ctx.get('pointerState');
|
||||
const pos = new Point(state.x, state.y);
|
||||
this._updateActiveSlicerIndex(pos);
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
gfx.viewport.viewportUpdated.on(() => {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
gfx.selection.slots.updated.on(() => {
|
||||
this._enableNoteSlicer = false;
|
||||
this._updateSlicedNote();
|
||||
|
||||
if (this.selectedRectEle) {
|
||||
this.selectedRectEle.autoCompleteOff = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
disposables.add(
|
||||
block.slots.toggleNoteSlicer.on(() => {
|
||||
this._enableNoteSlicer = !this._enableNoteSlicer;
|
||||
|
||||
if (this.selectedRectEle && this._enableNoteSlicer) {
|
||||
this.selectedRectEle.autoCompleteOff = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const { surface } = block;
|
||||
requestAnimationFrame(() => {
|
||||
if (surface.isConnected && std.event) {
|
||||
disposables.add(
|
||||
std.event.add('click', ctx => {
|
||||
const event = ctx.get('pointerState');
|
||||
const { raw } = event;
|
||||
const target = raw.target as HTMLElement;
|
||||
if (!target) return;
|
||||
|
||||
if (target.closest('note-slicer')) {
|
||||
this._sliceNote();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.disposables.dispose();
|
||||
this._noteDisposables?.dispose();
|
||||
this._noteDisposables = null;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
if (!this.block.service) return;
|
||||
this.disposables.add(
|
||||
this.block.service.uiEventDispatcher.add('wheel', () => {
|
||||
this._hidden = true;
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (
|
||||
this.doc.readonly ||
|
||||
this._hidden ||
|
||||
this._isResizing ||
|
||||
!this._anchorNote ||
|
||||
!this._enableNoteSlicer
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
this._updateDivingLineAndBlockIds();
|
||||
|
||||
const noteBlock = this._noteBlock;
|
||||
if (!noteBlock || !this._divingLinePositions.length) return nothing;
|
||||
|
||||
const rect = getRectByBlockComponent(noteBlock);
|
||||
const width = rect.width - 2 * EDGELESS_BLOCK_CHILD_PADDING * this._zoom;
|
||||
const buttonPosition = this._divingLinePositions[this._activeSlicerIndex];
|
||||
|
||||
return html`<div class="note-slicer-container">
|
||||
<div
|
||||
class="note-slicer-button"
|
||||
style=${styleMap({
|
||||
left: `${buttonPosition.x - 66 * this._zoom}px`,
|
||||
top: `${buttonPosition.y}px`,
|
||||
opacity: 1,
|
||||
transform: 'translateY(-50%)',
|
||||
})}
|
||||
>
|
||||
${ScissorsIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
${this._divingLinePositions.map((pos, idx) => {
|
||||
const dividingLineClasses = classMap({
|
||||
'note-slicer-dividing-line-container': true,
|
||||
active: idx === this._activeSlicerIndex,
|
||||
});
|
||||
return html`<div
|
||||
class=${dividingLineClasses}
|
||||
style=${styleMap({
|
||||
left: `${pos.x}px`,
|
||||
top: `${pos.y}px`,
|
||||
width: `${width}px`,
|
||||
})}
|
||||
>
|
||||
<span class="note-slicer-dividing-line"></span>
|
||||
</div>`;
|
||||
})}
|
||||
</div> `;
|
||||
}
|
||||
|
||||
protected override updated(_changedProperties: PropertyValues) {
|
||||
super.updated(_changedProperties);
|
||||
if (_changedProperties.has('anchorNote')) {
|
||||
this._noteDisposables?.dispose();
|
||||
this._noteDisposables = null;
|
||||
if (this._anchorNote) {
|
||||
this._noteDisposables = new DisposableGroup();
|
||||
this._noteDisposables.add(
|
||||
this._anchorNote.propsUpdated.on(({ key }) => {
|
||||
if (key === 'children' || key === 'xywh') {
|
||||
this.requestUpdate();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _activeSlicerIndex = 0;
|
||||
|
||||
@state()
|
||||
private accessor _anchorNote: NoteBlockModel | null = null;
|
||||
|
||||
@state()
|
||||
private accessor _enableNoteSlicer = false;
|
||||
|
||||
@state()
|
||||
private accessor _isResizing = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'note-slicer': NoteSlicer;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const TEXT_ALIGN_LIST = [
|
||||
{
|
||||
name: 'Left',
|
||||
value: TextAlign.Left,
|
||||
icon: TextAlignLeftIcon(),
|
||||
},
|
||||
{
|
||||
name: 'Center',
|
||||
value: TextAlign.Center,
|
||||
icon: TextAlignCenterIcon(),
|
||||
},
|
||||
{
|
||||
name: 'Right',
|
||||
value: TextAlign.Right,
|
||||
icon: TextAlignRightIcon(),
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgelessAlignPanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
private _onSelect(value: TextAlign) {
|
||||
this.value = value;
|
||||
if (this.onSelect) {
|
||||
this.onSelect(value);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
TEXT_ALIGN_LIST,
|
||||
item => item.name,
|
||||
({ name, value, icon }) => html`
|
||||
<edgeless-tool-icon-button
|
||||
.activeMode=${'background'}
|
||||
aria-label=${name}
|
||||
.tooltip=${name}
|
||||
.active=${this.value === value}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => this._onSelect(value)}
|
||||
>
|
||||
${icon}
|
||||
</edgeless-tool-icon-button>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect: undefined | ((value: TextAlign) => void) = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value: TextAlign = TextAlign.Left;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-align-panel': EdgelessAlignPanel;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { EmbedCardStyle } from '@blocksuite/affine-model';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
export class CardStylePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
icon-button {
|
||||
padding: var(--1, 0px);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
icon-button.selected {
|
||||
border: 1px solid var(--affine-brand-color);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const options = this.options;
|
||||
if (!options?.length) return nothing;
|
||||
|
||||
return repeat(
|
||||
options,
|
||||
options => options.style,
|
||||
({ style, Icon, tooltip }) => html`
|
||||
<icon-button
|
||||
width="76px"
|
||||
height="76px"
|
||||
class=${classMap({
|
||||
selected: this.value === style,
|
||||
})}
|
||||
@click=${() => {
|
||||
this.onSelect(style);
|
||||
this.value = style;
|
||||
}}
|
||||
>
|
||||
${Icon}
|
||||
<affine-tooltip .offset=${4}>${tooltip}</affine-tooltip>
|
||||
</icon-button>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect!: (value: EmbedCardStyle) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor options!: {
|
||||
style: EmbedCardStyle;
|
||||
Icon: TemplateResult<1>;
|
||||
tooltip: string;
|
||||
}[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value: EmbedCardStyle | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'card-style-panel': CardStylePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
import type { Color, ColorScheme, Palette } from '@blocksuite/affine-model';
|
||||
import { isTransparent, resolveColor } from '@blocksuite/affine-model';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ColorEvent } from '@blocksuite/affine-shared/utils';
|
||||
import { css, html, LitElement, nothing, svg, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
function TransparentIcon(hollowCircle = false) {
|
||||
const CircleIcon: TemplateResult | typeof nothing = hollowCircle
|
||||
? svg`<circle cx="10" cy="10" r="8" fill="white" />`
|
||||
: nothing;
|
||||
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M-1.17405 5.17857C-1.2241 5.5285 -1.25 5.88623 -1.25 6.25V8.39286H1.96429V11.6071H-1.25V13.75C-1.25 14.1138 -1.2241 14.4715 -1.17405 14.8214H1.96429V18.0357H0.0943102C0.602244 18.7639 1.23609 19.3978 1.96429 19.9057V18.0357L5.17857 18.0357V21.174C5.5285 21.2241 5.88623 21.25 6.25 21.25H8.39286V18.0357H11.6071V21.25H13.75C14.1138 21.25 14.4715 21.2241 14.8214 21.174V18.0357H18.0357L18.0357 19.9057C18.7639 19.3978 19.3978 18.7639 19.9057 18.0357L18.0357 18.0357V14.8214H21.174C21.2241 14.4715 21.25 14.1138 21.25 13.75V11.6071H18.0357V8.39286H21.25V6.25C21.25 5.88623 21.2241 5.5285 21.174 5.17857H18.0357V1.96429H19.9057C19.3978 1.23609 18.7639 0.602244 18.0357 0.09431L18.0357 1.96429H14.8214V-1.17405C14.4715 -1.2241 14.1138 -1.25 13.75 -1.25H11.6071V1.96429H8.39286V-1.25H6.25C5.88623 -1.25 5.5285 -1.2241 5.17857 -1.17405V1.96429H1.96429V0.0943099C1.23609 0.602244 0.602244 1.23609 0.0943099 1.96429H1.96429V5.17857H-1.17405ZM5.17857 5.17857V1.96429H8.39286V5.17857H5.17857ZM5.17857 8.39286H1.96429V5.17857H5.17857V8.39286ZM8.39286 8.39286V5.17857H11.6071V8.39286H8.39286ZM8.39286 11.6071V8.39286H5.17857V11.6071H1.96429V14.8214H5.17857V18.0357H8.39286V14.8214H11.6071V18.0357H14.8214V14.8214H18.0357V11.6071H14.8214V8.39286H18.0357V5.17857H14.8214V1.96429H11.6071V5.17857H14.8214V8.39286H11.6071V11.6071H8.39286ZM8.39286 11.6071V14.8214H5.17857V11.6071H8.39286ZM11.6071 11.6071H14.8214V14.8214H11.6071V11.6071Z"
|
||||
fill="#D9D9D9"
|
||||
/>
|
||||
${CircleIcon}
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
function CircleIcon(color: string) {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="${color}"
|
||||
>
|
||||
<circle cx="10" cy="10" r="10" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
function HollowCircleIcon(color: string) {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="${color}"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17ZM10 20C15.5228 20 20 15.5228 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20Z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
function AdditionIcon(color: string, hollowCircle: boolean) {
|
||||
if (isTransparent(color)) {
|
||||
return TransparentIcon(hollowCircle);
|
||||
}
|
||||
|
||||
if (hollowCircle) {
|
||||
return HollowCircleIcon(color);
|
||||
}
|
||||
|
||||
return CircleIcon(color);
|
||||
}
|
||||
|
||||
export class EdgelessColorButton extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-unit {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.color-unit svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host .color-unit::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-width: 0.5px;
|
||||
border-style: solid;
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/blackBorder')};
|
||||
}
|
||||
:host(.black) .color-unit:after {
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
}
|
||||
|
||||
:host(.large) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
:host(.large) .color-unit {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
:host::after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host([active])::after {
|
||||
border: 1.5px solid var(--affine-primary-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get preprocessColor() {
|
||||
const value = resolveColor(this.color, this.theme);
|
||||
return value.startsWith('--') ? `var(${value})` : value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { label, preprocessColor, hollowCircle } = this;
|
||||
const additionIcon = AdditionIcon(preprocessColor, !!hollowCircle);
|
||||
return html`<div class="color-unit" aria-label=${ifDefined(label)}>
|
||||
${additionIcon}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
accessor active: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor color!: Color;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hollowCircle: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor label: string | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
}
|
||||
|
||||
export class EdgelessColorPanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
}
|
||||
|
||||
/* note */
|
||||
:host(.small) {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
||||
/* edgeless toolbar */
|
||||
:host(.one-way) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 2px;
|
||||
gap: 14px;
|
||||
box-sizing: border-box;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
}
|
||||
`;
|
||||
|
||||
onSelect(palette: Palette) {
|
||||
this.dispatchEvent(
|
||||
new ColorEvent('select', {
|
||||
detail: palette,
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get resolvedValue() {
|
||||
return this.value && resolveColor(this.value, this.theme);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const resolvedValue = this.resolvedValue;
|
||||
return html`
|
||||
${repeat(
|
||||
this.palettes,
|
||||
palette => palette.key,
|
||||
palette => {
|
||||
const resolvedColor = resolveColor(palette.value, this.theme);
|
||||
const activated = isEqual(resolvedColor, resolvedValue);
|
||||
return html`<edgeless-color-button
|
||||
class=${classMap({ large: true })}
|
||||
.label=${palette.key}
|
||||
.color=${palette.value}
|
||||
.theme=${this.theme}
|
||||
.hollowCircle=${this.hollowCircle}
|
||||
?active=${activated}
|
||||
@click=${() => {
|
||||
this.onSelect(palette);
|
||||
this.value = resolvedColor;
|
||||
}}
|
||||
>
|
||||
</edgeless-color-button>`;
|
||||
}
|
||||
)}
|
||||
<slot name="custom"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hasTransparent: boolean = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hollowCircle = false;
|
||||
|
||||
@property()
|
||||
accessor openColorPicker!: (e: MouseEvent) => void;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor palettes: readonly Palette[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value: Color | null = null;
|
||||
}
|
||||
|
||||
export class EdgelessTextColorIcon extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
get preprocessColor() {
|
||||
const color = this.color;
|
||||
return color.startsWith('--') ? `var(${color})` : color;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
fill="currentColor"
|
||||
d="M8.71093 3.85123C8.91241 3.31395 9.42603 2.95801 9.99984 2.95801C10.5737 2.95801 11.0873 3.31395 11.2888 3.85123L14.7517 13.0858C14.8729 13.409 14.7092 13.7692 14.386 13.8904C14.0628 14.0116 13.7025 13.8479 13.5813 13.5247L12.5648 10.8141H7.43487L6.41838 13.5247C6.29718 13.8479 5.93693 14.0116 5.61373 13.8904C5.29052 13.7692 5.12677 13.409 5.24797 13.0858L8.71093 3.85123ZM7.90362 9.56405H12.0961L10.1183 4.29013C10.0998 4.24073 10.0526 4.20801 9.99984 4.20801C9.94709 4.20801 9.89986 4.24073 9.88134 4.29013L7.90362 9.56405Z"
|
||||
/>
|
||||
<rect
|
||||
x="3.3335"
|
||||
y="15"
|
||||
width="13.3333"
|
||||
height="2.08333"
|
||||
rx="1"
|
||||
fill=${this.preprocessColor}
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor color!: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-color-panel': EdgelessColorPanel;
|
||||
'edgeless-color-button': EdgelessColorButton;
|
||||
'edgeless-text-color-icon': EdgelessTextColorIcon;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { TextUtils } from '@blocksuite/affine-block-surface';
|
||||
import { FontFamily, FontFamilyList } from '@blocksuite/affine-model';
|
||||
import { DoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
export class EdgelessFontFamilyPanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
min-width: 136px;
|
||||
}
|
||||
|
||||
edgeless-tool-icon-button {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
private _onSelect(value: FontFamily) {
|
||||
this.value = value;
|
||||
if (this.onSelect) {
|
||||
this.onSelect(value);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
FontFamilyList,
|
||||
item => item[0],
|
||||
([font, name]) => {
|
||||
const active = this.value === font;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
data-font="${name}"
|
||||
style="font-family: ${TextUtils.wrapFontFamily(font)}"
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.justify=${'space-between'}
|
||||
.active=${active}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => this._onSelect(font)}
|
||||
>
|
||||
${name} ${active ? DoneIcon() : nothing}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect: ((value: FontFamily) => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value: FontFamily = FontFamily.Inter;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-font-family-panel': EdgelessFontFamilyPanel;
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import { TextUtils } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
FontFamily,
|
||||
FontFamilyMap,
|
||||
FontStyle,
|
||||
FontWeight,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { DoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.js';
|
||||
import { join } from 'lit/directives/join.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const FONT_WEIGHT_CHOOSE: [FontWeight, () => string][] = [
|
||||
[FontWeight.Light, () => 'Light'],
|
||||
[FontWeight.Regular, () => 'Regular'],
|
||||
[FontWeight.SemiBold, () => 'Semibold'],
|
||||
];
|
||||
|
||||
export class EdgelessFontWeightAndStylePanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
min-width: 124px;
|
||||
}
|
||||
|
||||
edgeless-tool-icon-button {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
private _isActive(
|
||||
fontWeight: FontWeight,
|
||||
fontStyle: FontStyle = FontStyle.Normal
|
||||
) {
|
||||
return this.fontWeight === fontWeight && this.fontStyle === fontStyle;
|
||||
}
|
||||
|
||||
private _isDisabled(
|
||||
fontWeight: FontWeight,
|
||||
fontStyle: FontStyle = FontStyle.Normal
|
||||
) {
|
||||
// Compatible with old data
|
||||
if (!(this.fontFamily in FontFamilyMap)) return false;
|
||||
|
||||
const fontFace = TextUtils.getFontFaces()
|
||||
.filter(TextUtils.isSameFontFamily(this.fontFamily))
|
||||
.find(
|
||||
fontFace =>
|
||||
fontFace.weight === fontWeight && fontFace.style === fontStyle
|
||||
);
|
||||
|
||||
return !fontFace;
|
||||
}
|
||||
|
||||
private _onSelect(
|
||||
fontWeight: FontWeight,
|
||||
fontStyle: FontStyle = FontStyle.Normal
|
||||
) {
|
||||
this.fontWeight = fontWeight;
|
||||
this.fontStyle = fontStyle;
|
||||
if (this.onSelect) {
|
||||
this.onSelect(fontWeight, fontStyle);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
let fontFaces = TextUtils.getFontFacesByFontFamily(this.fontFamily);
|
||||
// Compatible with old data
|
||||
if (fontFaces.length === 0) {
|
||||
fontFaces = TextUtils.getFontFacesByFontFamily(FontFamily.Inter);
|
||||
}
|
||||
const fontFacesWithNormal = fontFaces.filter(
|
||||
fontFace => fontFace.style === FontStyle.Normal
|
||||
);
|
||||
const fontFacesWithItalic = fontFaces.filter(
|
||||
fontFace => fontFace.style === FontStyle.Italic
|
||||
);
|
||||
|
||||
return join(
|
||||
[
|
||||
fontFacesWithNormal.length > 0
|
||||
? repeat(
|
||||
fontFacesWithNormal,
|
||||
fontFace => fontFace.weight,
|
||||
fontFace => {
|
||||
const active = this._isActive(fontFace.weight as FontWeight);
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
data-weight="${fontFace.weight}"
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.justify=${'space-between'}
|
||||
.disabled=${this._isDisabled(fontFace.weight as FontWeight)}
|
||||
.active=${active}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() =>
|
||||
this._onSelect(fontFace.weight as FontWeight)}
|
||||
>
|
||||
${choose(fontFace.weight, FONT_WEIGHT_CHOOSE)}
|
||||
${active ? DoneIcon() : nothing}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
)
|
||||
: nothing,
|
||||
fontFacesWithItalic.length > 0
|
||||
? repeat(
|
||||
fontFacesWithItalic,
|
||||
fontFace => fontFace.weight,
|
||||
fontFace => {
|
||||
const active = this._isActive(
|
||||
fontFace.weight as FontWeight,
|
||||
FontStyle.Italic
|
||||
);
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
data-weight="${fontFace.weight} italic"
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.justify=${'space-between'}
|
||||
.disabled=${this._isDisabled(
|
||||
fontFace.weight as FontWeight,
|
||||
FontStyle.Italic
|
||||
)}
|
||||
.active=${active}
|
||||
@click=${() =>
|
||||
this._onSelect(
|
||||
fontFace.weight as FontWeight,
|
||||
FontStyle.Italic
|
||||
)}
|
||||
>
|
||||
${choose(fontFace.weight, FONT_WEIGHT_CHOOSE)} Italic
|
||||
${active ? DoneIcon() : nothing}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
)
|
||||
: nothing,
|
||||
].filter(item => item !== nothing),
|
||||
() => html`
|
||||
<edgeless-menu-divider
|
||||
data-orientation="horizontal"
|
||||
></edgeless-menu-divider>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fontFamily = FontFamily.Inter;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fontStyle = FontStyle.Normal;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fontWeight = FontWeight.Regular;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect:
|
||||
| ((fontWeight: FontWeight, fontStyle: FontStyle) => void)
|
||||
| undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-font-weight-and-style-panel': EdgelessFontWeightAndStylePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const NoteNoShadowIcon = html`
|
||||
<svg
|
||||
width="60"
|
||||
height="72"
|
||||
viewBox="0 0 60 72"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="60" height="72" />
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="58.0769"
|
||||
height="71"
|
||||
stroke="black"
|
||||
stroke-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M21.9576 26.8962L38.6423 43.5809C42.5269 38.9268 42.2845 31.993 37.9149 27.6235C33.5454 23.254 26.6117 23.0115 21.9576 26.8962ZM37.1193 45.1038L20.4346 28.4192C16.55 33.0732 16.7924 40.007 21.162 44.3765C25.5315 48.746 32.4652 48.9885 37.1193 45.1038ZM19.639 26.1005C25.1063 20.6332 33.9706 20.6332 39.4379 26.1005C44.9053 31.5678 44.9053 40.4322 39.4379 45.8995C33.9706 51.3668 25.1063 51.3668 19.639 45.8995C14.1716 40.4322 14.1716 31.5678 19.639 26.1005Z"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const NoteShadowSampleIcon = html`
|
||||
<svg
|
||||
width="60"
|
||||
height="72"
|
||||
viewBox="0 0 60 72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="60" height="72" />
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="12.0771"
|
||||
width="32.3077"
|
||||
height="4.61538"
|
||||
rx="2"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="25.8462"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="35.6152"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="45.3843"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="55.1533"
|
||||
width="13.8462"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { LineWidth, StrokeStyle } from '@blocksuite/affine-model';
|
||||
import { BanIcon, DashLineIcon, StraightLineIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { LineWidthEvent } from './line-width-panel.js';
|
||||
|
||||
export type LineStyleEvent =
|
||||
| {
|
||||
type: 'size';
|
||||
value: LineWidth;
|
||||
}
|
||||
| {
|
||||
type: 'lineStyle';
|
||||
value: StrokeStyle;
|
||||
};
|
||||
|
||||
interface LineStylesPanelProps {
|
||||
onClick?: (e: LineStyleEvent) => void;
|
||||
selectedLineSize?: LineWidth;
|
||||
selectedLineStyle?: StrokeStyle;
|
||||
lineStyles?: StrokeStyle[];
|
||||
}
|
||||
|
||||
const LINE_STYLE_LIST = [
|
||||
{
|
||||
name: 'Solid',
|
||||
value: StrokeStyle.Solid,
|
||||
icon: StraightLineIcon(),
|
||||
},
|
||||
{
|
||||
name: 'Dash',
|
||||
value: StrokeStyle.Dash,
|
||||
icon: DashLineIcon(),
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: StrokeStyle.None,
|
||||
icon: BanIcon(),
|
||||
},
|
||||
];
|
||||
|
||||
export function LineStylesPanel({
|
||||
onClick,
|
||||
selectedLineStyle,
|
||||
selectedLineSize = LineWidth.Two,
|
||||
lineStyles = [StrokeStyle.Solid, StrokeStyle.Dash, StrokeStyle.None],
|
||||
}: LineStylesPanelProps = {}) {
|
||||
const lineSizePanel = html`
|
||||
<edgeless-line-width-panel
|
||||
?disabled=${selectedLineStyle === StrokeStyle.None}
|
||||
.selectedSize=${selectedLineSize}
|
||||
@select=${(e: LineWidthEvent) => {
|
||||
onClick?.({
|
||||
type: 'size',
|
||||
value: e.detail,
|
||||
});
|
||||
}}
|
||||
></edgeless-line-width-panel>
|
||||
`;
|
||||
|
||||
const lineStyleButtons = repeat(
|
||||
LINE_STYLE_LIST.filter(item => lineStyles.includes(item.value)),
|
||||
item => item.value,
|
||||
({ name, icon, value }) => {
|
||||
const active = selectedLineStyle === value;
|
||||
const classInfo = {
|
||||
'line-style-button': true,
|
||||
[`mode-${value}`]: true,
|
||||
};
|
||||
if (active) classInfo['active'] = true;
|
||||
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class=${classMap(classInfo)}
|
||||
.active=${active}
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${name}
|
||||
.iconSize=${'24px'}
|
||||
@click=${() =>
|
||||
onClick?.({
|
||||
type: 'lineStyle',
|
||||
value,
|
||||
})}
|
||||
>
|
||||
${icon}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
return html`
|
||||
${lineSizePanel}
|
||||
<editor-toolbar-separator></editor-toolbar-separator>
|
||||
${lineStyleButtons}
|
||||
`;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
import { LINE_WIDTHS, LineWidth } from '@blocksuite/affine-model';
|
||||
import { clamp, on, once } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
interface Config {
|
||||
width: number;
|
||||
itemSize: number;
|
||||
itemIconSize: number;
|
||||
dragHandleSize: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class LineWidthEvent extends Event {
|
||||
detail: LineWidth;
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
{
|
||||
detail,
|
||||
composed,
|
||||
bubbles,
|
||||
}: { detail: LineWidth; composed: boolean; bubbles: boolean }
|
||||
) {
|
||||
super(type, { bubbles, composed });
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
export class EdgelessLineWidthPanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
|
||||
--width: 140px;
|
||||
--item-size: 16px;
|
||||
--item-icon-size: 8px;
|
||||
--drag-handle-size: 14px;
|
||||
--cursor: 0;
|
||||
--count: 6;
|
||||
/* (16 - 14) / 2 + (cursor / (count - 1)) * (140 - 16) */
|
||||
--drag-handle-center-x: calc(
|
||||
(var(--item-size) - var(--drag-handle-size)) / 2 +
|
||||
(var(--cursor) / (var(--count) - 1)) *
|
||||
(var(--width) - var(--item-size))
|
||||
);
|
||||
}
|
||||
|
||||
:host([disabled]) {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line-width-panel {
|
||||
width: var(--width);
|
||||
height: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.line-width-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--item-size);
|
||||
height: var(--item-size);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.line-width-icon {
|
||||
width: var(--item-icon-size);
|
||||
height: var(--item-icon-size);
|
||||
background-color: var(--affine-border-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.line-width-button[data-selected] .line-width-icon {
|
||||
background-color: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
position: absolute;
|
||||
width: var(--drag-handle-size);
|
||||
height: var(--drag-handle-size);
|
||||
border-radius: 50%;
|
||||
background-color: var(--affine-icon-color);
|
||||
z-index: 3;
|
||||
transform: translateX(var(--drag-handle-center-x));
|
||||
}
|
||||
|
||||
.bottom-line,
|
||||
.line-width-overlay {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
left: calc(var(--item-size) / 2);
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
width: calc(100% - var(--item-size));
|
||||
background-color: var(--affine-border-color);
|
||||
}
|
||||
|
||||
.line-width-overlay {
|
||||
background-color: var(--affine-icon-color);
|
||||
z-index: 1;
|
||||
width: var(--drag-handle-center-x);
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _getDragHandlePosition = (e: PointerEvent) => {
|
||||
return clamp(e.offsetX, 0, this.config.width);
|
||||
};
|
||||
|
||||
private readonly _onPointerDown = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
this._onPointerMove(e);
|
||||
|
||||
const dispose = on(this, 'pointermove', this._onPointerMove);
|
||||
this._disposables.add(once(this, 'pointerup', dispose));
|
||||
this._disposables.add(once(this, 'pointerout', dispose));
|
||||
};
|
||||
|
||||
private readonly _onPointerMove = (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const x = this._getDragHandlePosition(e);
|
||||
|
||||
this._updateLineWidthPanelByDragHandlePosition(x);
|
||||
};
|
||||
|
||||
private _onSelect(lineWidth: LineWidth) {
|
||||
// If the selected size is the same as the previous one, do nothing.
|
||||
if (lineWidth === this.selectedSize) return;
|
||||
this.dispatchEvent(
|
||||
new LineWidthEvent('select', {
|
||||
detail: lineWidth,
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
this.selectedSize = lineWidth;
|
||||
}
|
||||
|
||||
private _updateLineWidthPanel(selectedSize: LineWidth) {
|
||||
if (!this._lineWidthOverlay) return;
|
||||
const index = this.lineWidths.findIndex(w => w === selectedSize);
|
||||
if (index === -1) return;
|
||||
|
||||
this.style.setProperty('--cursor', `${index}`);
|
||||
}
|
||||
|
||||
private _updateLineWidthPanelByDragHandlePosition(x: number) {
|
||||
// Calculate the selected size based on the drag handle position.
|
||||
// Need to select the nearest size.
|
||||
|
||||
const {
|
||||
config: { width, itemSize, count },
|
||||
lineWidths,
|
||||
} = this;
|
||||
const targetWidth = width - itemSize;
|
||||
const halfItemSize = itemSize / 2;
|
||||
const offsetX = halfItemSize + (width - itemSize * count) / (count - 1) / 2;
|
||||
const selectedSize = lineWidths.findLast((_, n) => {
|
||||
const cx = halfItemSize + (n / (count - 1)) * targetWidth;
|
||||
return x >= cx - offsetX && x < cx + offsetX;
|
||||
});
|
||||
if (!selectedSize) return;
|
||||
|
||||
this._updateLineWidthPanel(selectedSize);
|
||||
this._onSelect(selectedSize);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const {
|
||||
style,
|
||||
config: { width, itemSize, itemIconSize, dragHandleSize, count },
|
||||
} = this;
|
||||
style.setProperty('--width', `${width}px`);
|
||||
style.setProperty('--item-size', `${itemSize}px`);
|
||||
style.setProperty('--item-icon-size', `${itemIconSize}px`);
|
||||
style.setProperty('--drag-handle-size', `${dragHandleSize}px`);
|
||||
style.setProperty('--count', `${count}`);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this._updateLineWidthPanel(this.selectedSize);
|
||||
this._disposables.addFromEvent(this, 'pointerdown', this._onPointerDown);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div class="line-width-panel">
|
||||
${repeat(
|
||||
this.lineWidths,
|
||||
w => w,
|
||||
(w, n) =>
|
||||
html`<div
|
||||
class="line-width-button"
|
||||
aria-label=${w}
|
||||
data-index=${n}
|
||||
?data-selected=${w <= this.selectedSize}
|
||||
>
|
||||
<div class="line-width-icon"></div>
|
||||
</div>`
|
||||
)}
|
||||
<div class="drag-handle"></div>
|
||||
<div class="bottom-line"></div>
|
||||
<div class="line-width-overlay"></div>
|
||||
${this.hasTooltip
|
||||
? html`<affine-tooltip .offset=${8}>Thickness</affine-tooltip>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has('selectedSize')) {
|
||||
this._updateLineWidthPanel(this.selectedSize);
|
||||
}
|
||||
}
|
||||
|
||||
@query('.line-width-overlay')
|
||||
private accessor _lineWidthOverlay!: HTMLElement;
|
||||
|
||||
accessor config: Config = {
|
||||
width: 140,
|
||||
itemSize: 16,
|
||||
itemIconSize: 8,
|
||||
dragHandleSize: 14,
|
||||
count: LINE_WIDTHS.length,
|
||||
};
|
||||
|
||||
@property({ attribute: false, type: Boolean })
|
||||
accessor disabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hasTooltip = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineWidths: LineWidth[] = LINE_WIDTHS;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor selectedSize: LineWidth = LineWidth.Two;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-line-width-panel': EdgelessLineWidthPanel;
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
export class NoteDisplayModePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 180px;
|
||||
width: var(--panel-width);
|
||||
gap: 4px;
|
||||
}
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
.item-label {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.item-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--affine-icon-color);
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.item:hover,
|
||||
.item.selected {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _DisplayModeIcon(mode: NoteDisplayMode) {
|
||||
switch (mode) {
|
||||
case NoteDisplayMode.DocAndEdgeless:
|
||||
return html`${PageIcon()} ${EdgelessIcon()}`;
|
||||
case NoteDisplayMode.DocOnly:
|
||||
return html`${PageIcon()}`;
|
||||
case NoteDisplayMode.EdgelessOnly:
|
||||
return html`${EdgelessIcon()}`;
|
||||
}
|
||||
}
|
||||
|
||||
private _DisplayModeLabel(mode: NoteDisplayMode) {
|
||||
switch (mode) {
|
||||
case NoteDisplayMode.DocAndEdgeless:
|
||||
return 'In Both';
|
||||
case NoteDisplayMode.DocOnly:
|
||||
return 'In Page Only';
|
||||
case NoteDisplayMode.EdgelessOnly:
|
||||
return 'In Edgeless Only';
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
this.style.setProperty('--panel-width', `${this.panelWidth}px`);
|
||||
|
||||
return repeat(
|
||||
Object.keys(NoteDisplayMode),
|
||||
mode => mode,
|
||||
mode => {
|
||||
const displayMode =
|
||||
NoteDisplayMode[mode as keyof typeof NoteDisplayMode];
|
||||
const isSelected = displayMode === this.displayMode;
|
||||
return html`<div
|
||||
class="item ${isSelected ? 'selected' : ''} ${displayMode}"
|
||||
@click=${() => this.onSelect(displayMode)}
|
||||
@dblclick=${stopPropagation}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
<div class="item-label">${this._DisplayModeLabel(displayMode)}</div>
|
||||
<div class="item-icon">${this._DisplayModeIcon(displayMode)}</div>
|
||||
</div>`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor displayMode!: NoteDisplayMode;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect!: (displayMode: NoteDisplayMode) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor panelWidth = 240;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'note-display-mode-panel': NoteDisplayModePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { ColorScheme, NoteShadow } from '@blocksuite/affine-model';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { NoteNoShadowIcon, NoteShadowSampleIcon } from './icons';
|
||||
|
||||
const SHADOWS = [
|
||||
{
|
||||
type: NoteShadow.None,
|
||||
styles: {
|
||||
light: '',
|
||||
dark: '',
|
||||
},
|
||||
tooltip: 'No shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Box,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0.2px 4.8px 0px rgba(66, 65, 73, 0.2), 0px 0px 1.6px 0px rgba(66, 65, 73, 0.2)',
|
||||
dark: '0px 0.2px 6px 0px rgba(0, 0, 0, 0.44), 0px 0px 2px 0px rgba(0, 0, 0, 0.66)',
|
||||
},
|
||||
tooltip: 'Box shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Sticker,
|
||||
styles: {
|
||||
light:
|
||||
'0px 9.6px 10.4px -4px rgba(66, 65, 73, 0.07), 0px 10.4px 7.2px -8px rgba(66, 65, 73, 0.22)',
|
||||
dark: '0px 9.6px 10.4px -4px rgba(0, 0, 0, 0.66), 0px 10.4px 7.2px -8px rgba(0, 0, 0, 0.44)',
|
||||
},
|
||||
tooltip: 'Sticker shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Paper,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0px 0px 4px rgba(255, 255, 255, 1), 0px 1.2px 2.4px 4.8px rgba(66, 65, 73, 0.16)',
|
||||
dark: '0px 1.2px 2.4px 4.8px rgba(0, 0, 0, 0.36), 0px 0px 0px 3.4px rgba(75, 75, 75, 1)',
|
||||
},
|
||||
tooltip: 'Paper shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Float,
|
||||
styles: {
|
||||
light:
|
||||
'0px 5.2px 12px 0px rgba(66, 65, 73, 0.13), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.06)',
|
||||
dark: '0px 5.2px 12px 0px rgba(0, 0, 0, 0.66), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.44)',
|
||||
},
|
||||
tooltip: 'Floation shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Film,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0px 0px 1.4px rgba(0, 0, 0, 1), 2.4px 2.4px 0px 1px rgba(0, 0, 0, 1)',
|
||||
dark: '0px 0px 0px 1.4px rgba(178, 178, 178, 1), 2.4px 2.4px 0px 1px rgba(178, 178, 178, 1)',
|
||||
},
|
||||
tooltip: 'Film shadow',
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgelessNoteShadowPanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
SHADOWS,
|
||||
shadow => shadow,
|
||||
(shadow, index) =>
|
||||
html`<style>
|
||||
.item-icon svg rect:first-of-type {
|
||||
fill: ${this.background.startsWith('--')
|
||||
? `var(${this.background})`
|
||||
: this.background};
|
||||
}
|
||||
</style>
|
||||
<div
|
||||
class="item"
|
||||
@click=${() => this.onSelect(shadow.type)}
|
||||
style=${styleMap({
|
||||
border:
|
||||
this.value === shadow.type
|
||||
? '1px solid var(--affine-brand-color)'
|
||||
: 'none',
|
||||
})}
|
||||
>
|
||||
<edgeless-tool-icon-button
|
||||
class="item-icon"
|
||||
data-testid=${shadow.type.replace('--', '')}
|
||||
.tooltip=${shadow.tooltip}
|
||||
.tipPosition=${'bottom'}
|
||||
.iconContainerPadding=${0}
|
||||
style=${styleMap({
|
||||
boxShadow: `${this.theme === ColorScheme.Dark ? shadow.styles.dark : shadow.styles.light}`,
|
||||
})}
|
||||
>
|
||||
${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor background!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect!: (value: string) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value!: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-shadow-panel': EdgelessNoteShadowPanel;
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { clamp, stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const MIN_SCALE = 0;
|
||||
const MAX_SCALE = 400;
|
||||
|
||||
const SCALE_LIST = [50, 100, 200] as const;
|
||||
|
||||
function format(scale: number) {
|
||||
return `${scale}%`;
|
||||
}
|
||||
|
||||
export class EdgelessScalePanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
edgeless-tool-icon-button {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.scale-input {
|
||||
display: flx;
|
||||
align-self: stretch;
|
||||
border: 0.5px solid var(--affine-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scale-input::placeholder {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
|
||||
.scale-input:focus {
|
||||
outline-color: var(--affine-primary-color);
|
||||
outline-width: 0.5px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _onKeydown = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
e.preventDefault();
|
||||
const input = e.target as HTMLInputElement;
|
||||
const scale = parseInt(input.value.trim());
|
||||
// Handle edge case where user enters a non-number
|
||||
if (isNaN(scale)) {
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle edge case when user enters a number that is out of range
|
||||
this._onSelect(clamp(scale, this.minScale, this.maxScale));
|
||||
input.value = '';
|
||||
this._onPopperClose();
|
||||
}
|
||||
};
|
||||
|
||||
private _onPopperClose() {
|
||||
this.onPopperCose?.();
|
||||
}
|
||||
|
||||
private _onSelect(scale: number) {
|
||||
this.onSelect?.(scale / 100);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${repeat(
|
||||
this.scaleList,
|
||||
scale => scale,
|
||||
scale => {
|
||||
const classes = `scale-${scale}`;
|
||||
return html`<edgeless-tool-icon-button
|
||||
class=${classes}
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.activeMode=${'background'}
|
||||
.active=${this.scale === scale}
|
||||
@click=${() => this._onSelect(scale)}
|
||||
>
|
||||
${format(scale)}
|
||||
</edgeless-tool-icon-button>`;
|
||||
}
|
||||
)}
|
||||
|
||||
<input
|
||||
class="scale-input"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
min="0"
|
||||
placeholder=${format(Math.trunc(this.scale))}
|
||||
@keydown=${this._onKeydown}
|
||||
@input=${stopPropagation}
|
||||
@click=${stopPropagation}
|
||||
@pointerdown=${stopPropagation}
|
||||
@cut=${stopPropagation}
|
||||
@copy=${stopPropagation}
|
||||
@paste=${stopPropagation}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor maxScale: number = MAX_SCALE;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor minScale: number = MIN_SCALE;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onPopperCose: (() => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect: ((size: number) => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor scale!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor scaleList: readonly number[] = SCALE_LIST;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-scale-panel': EdgelessScalePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { ShapeStyle } from '@blocksuite/affine-model';
|
||||
import { Slot } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { ShapeTool } from '../../gfx-tool/shape-tool.js';
|
||||
import { ShapeComponentConfig } from '../toolbar/shape/shape-menu-config.js';
|
||||
|
||||
export class EdgelessShapePanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
slots = {
|
||||
select: new Slot<ShapeTool['activatedOption']['shapeName']>(),
|
||||
};
|
||||
|
||||
private _onSelect(value: ShapeTool['activatedOption']['shapeName']) {
|
||||
this.selectedShape = value;
|
||||
this.slots.select.emit(value);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this.slots.select.dispose();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
ShapeComponentConfig,
|
||||
item => item.name,
|
||||
({ name, generalIcon, scribbledIcon, tooltip, disabled }) =>
|
||||
html`<edgeless-tool-icon-button
|
||||
.disabled=${disabled}
|
||||
.tooltip=${tooltip}
|
||||
.active=${this.selectedShape === name}
|
||||
.activeMode=${'background'}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => {
|
||||
if (disabled) return;
|
||||
this._onSelect(name);
|
||||
}}
|
||||
>
|
||||
${this.shapeStyle === ShapeStyle.General
|
||||
? generalIcon
|
||||
: scribbledIcon}
|
||||
</edgeless-tool-icon-button>`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor selectedShape:
|
||||
| ShapeTool['activatedOption']['shapeName']
|
||||
| null
|
||||
| undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor shapeStyle: ShapeStyle = ShapeStyle.Scribbled;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-shape-panel': EdgelessShapePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { ShapeStyle } from '@blocksuite/affine-model';
|
||||
import { StyleGeneralIcon, StyleScribbleIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const SHAPE_STYLE_LIST = [
|
||||
{
|
||||
value: ShapeStyle.General,
|
||||
icon: StyleGeneralIcon(),
|
||||
},
|
||||
{
|
||||
value: ShapeStyle.Scribbled,
|
||||
icon: StyleScribbleIcon(),
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgelessShapeStylePanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
private _onSelect(value: ShapeStyle) {
|
||||
this.value = value;
|
||||
if (this.onSelect) {
|
||||
this.onSelect(value);
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return repeat(
|
||||
SHAPE_STYLE_LIST,
|
||||
item => item.value,
|
||||
({ value, icon }) =>
|
||||
html`<edgeless-tool-icon-button
|
||||
.tipPosition=${'top'}
|
||||
.activeMode=${'background'}
|
||||
aria-label=${value}
|
||||
.tooltip=${value}
|
||||
.active=${this.value === value}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => this._onSelect(value)}
|
||||
>
|
||||
${icon}
|
||||
</edgeless-tool-icon-button>`
|
||||
);
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect: undefined | ((value: ShapeStyle) => void) = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value!: ShapeStyle;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-shape-style-panel': EdgelessShapeStylePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { clamp, stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { DoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
const MIN_SIZE = 1;
|
||||
const MAX_SIZE = 200;
|
||||
|
||||
type SizeItem = {
|
||||
name?: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
export class EdgelessSizePanel extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
edgeless-tool-icon-button {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.size-input {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
border: 0.5px solid var(--affine-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.size-input::placeholder {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
|
||||
.size-input:focus {
|
||||
outline-color: var(--affine-primary-color);
|
||||
outline-width: 0.5px;
|
||||
}
|
||||
|
||||
:host([data-type='check']) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
:host([data-type='check']) .size-input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _onKeydown = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
e.preventDefault();
|
||||
const input = e.target as HTMLInputElement;
|
||||
const size = parseInt(input.value.trim());
|
||||
// Handle edge case where user enters a non-number
|
||||
if (isNaN(size)) {
|
||||
input.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle edge case when user enters a number that is out of range
|
||||
this._onSelect(clamp(size, this.minSize, this.maxSize));
|
||||
input.value = '';
|
||||
this._onPopperClose();
|
||||
}
|
||||
};
|
||||
|
||||
renderItemWithCheck = ({ name, value }: SizeItem) => {
|
||||
const active = this.size === value;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.justify=${'space-between'}
|
||||
.active=${active}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => this._onSelect(value)}
|
||||
>
|
||||
${name ?? value} ${active ? DoneIcon() : nothing}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
};
|
||||
|
||||
renderItemWithNormal = ({ name, value }: SizeItem) => {
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
.iconContainerPadding=${[4, 8]}
|
||||
.active=${this.size === value}
|
||||
.activeMode=${'background'}
|
||||
@click=${() => this._onSelect(value)}
|
||||
>
|
||||
${name ?? value}
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
};
|
||||
|
||||
private _onPopperClose() {
|
||||
this.onPopperCose?.();
|
||||
}
|
||||
|
||||
private _onSelect(size: number) {
|
||||
this.onSelect?.(size);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${repeat(this.sizeList, sizeItem => sizeItem.name, this.renderItem())}
|
||||
|
||||
<input
|
||||
class="size-input"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
min="0"
|
||||
placeholder=${Math.trunc(this.size)}
|
||||
@keydown=${this._onKeydown}
|
||||
@input=${stopPropagation}
|
||||
@click=${stopPropagation}
|
||||
@pointerdown=${stopPropagation}
|
||||
@cut=${stopPropagation}
|
||||
@copy=${stopPropagation}
|
||||
@paste=${stopPropagation}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
renderItem() {
|
||||
return this.type === 'normal'
|
||||
? this.renderItemWithNormal
|
||||
: this.renderItemWithCheck;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor maxSize: number = MAX_SIZE;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor minSize: number = MIN_SIZE;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onPopperCose: (() => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onSelect: ((size: number) => void) | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor size!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor sizeList!: SizeItem[];
|
||||
|
||||
@property({ attribute: 'data-type' })
|
||||
accessor type: 'normal' | 'check' = 'normal';
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-size-panel': EdgelessSizePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import {
|
||||
type ColorScheme,
|
||||
DefaultTheme,
|
||||
type StrokeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { type LineStyleEvent, LineStylesPanel } from './line-styles-panel.js';
|
||||
|
||||
export class StrokeStylePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.line-styles {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="line-styles">
|
||||
${LineStylesPanel({
|
||||
selectedLineSize: this.strokeWidth,
|
||||
selectedLineStyle: this.strokeStyle,
|
||||
onClick: e => this.setStrokeStyle(e),
|
||||
})}
|
||||
</div>
|
||||
<editor-toolbar-separator
|
||||
data-orientation="horizontal"
|
||||
></editor-toolbar-separator>
|
||||
<edgeless-color-panel
|
||||
role="listbox"
|
||||
aria-label="Border colors"
|
||||
.value=${this.strokeColor}
|
||||
.theme=${this.theme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
.hollowCircle=${this.hollowCircle}
|
||||
@select=${(e: ColorEvent) => this.setStrokeColor(e)}
|
||||
>
|
||||
</edgeless-color-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hollowCircle: boolean | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor setStrokeColor!: (e: ColorEvent) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor setStrokeStyle!: (e: LineStyleEvent) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor strokeColor!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor strokeStyle!: StrokeStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor strokeWidth!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'stroke-style-panel': StrokeStylePanel;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { FrameBlockModel, RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { Bound } from '@blocksuite/global/utils';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
export const EDGELESS_NAVIGATOR_BLACK_BACKGROUND_WIDGET =
|
||||
'edgeless-navigator-black-background';
|
||||
export class EdgelessNavigatorBlackBackgroundWidget extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
static override styles = css`
|
||||
.edgeless-navigator-black-background {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background-color: transparent;
|
||||
box-shadow: 0 0 0 5000px black;
|
||||
}
|
||||
`;
|
||||
|
||||
private _blackBackground = false;
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private _tryLoadBlackBackground() {
|
||||
const value = this.std
|
||||
.get(EditPropsStore)
|
||||
.getStorage('presentBlackBackground');
|
||||
this._blackBackground = value ?? true;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { _disposables, gfx, block } = this;
|
||||
_disposables.add(
|
||||
block.slots.navigatorFrameChanged.on(frame => {
|
||||
this.frame = frame;
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
block.slots.navigatorSettingUpdated.on(({ blackBackground }) => {
|
||||
if (blackBackground !== undefined) {
|
||||
this.std
|
||||
.get(EditPropsStore)
|
||||
.setStorage('presentBlackBackground', blackBackground);
|
||||
|
||||
this._blackBackground = blackBackground;
|
||||
|
||||
this.show =
|
||||
blackBackground &&
|
||||
block.gfx.tool.currentToolOption$.peek().type === 'frameNavigator';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
effect(() => {
|
||||
const tool = gfx.tool.currentToolName$.value;
|
||||
|
||||
if (tool !== 'frameNavigator') {
|
||||
this.show = false;
|
||||
} else {
|
||||
this.show = this._blackBackground;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
block.slots.fullScreenToggled.on(
|
||||
() =>
|
||||
setTimeout(() => {
|
||||
this.requestUpdate();
|
||||
}, 500) // wait for full screen animation
|
||||
)
|
||||
);
|
||||
|
||||
this._tryLoadBlackBackground();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { frame, show, gfx } = this;
|
||||
|
||||
if (!show || !frame) return nothing;
|
||||
|
||||
const bound = Bound.deserialize(frame.xywh);
|
||||
const zoom = gfx.viewport.zoom;
|
||||
const width = bound.w * zoom;
|
||||
const height = bound.h * zoom;
|
||||
const [x, y] = gfx.viewport.toViewCoord(bound.x, bound.y);
|
||||
|
||||
return html` <style>
|
||||
.edgeless-navigator-black-background {
|
||||
width: ${width}px;
|
||||
height: ${height}px;
|
||||
top: ${y}px;
|
||||
left: ${x}px;
|
||||
}
|
||||
</style>
|
||||
<div class="edgeless-navigator-black-background"></div>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor frame: FrameBlockModel | undefined = undefined;
|
||||
|
||||
@state()
|
||||
private accessor show = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-navigator-black-background': EdgelessNavigatorBlackBackgroundWidget;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import { DefaultTool } from '../../gfx-tool/default-tool.js';
|
||||
import { DefaultModeDragType } from '../../gfx-tool/default-tool-ext/ext.js';
|
||||
|
||||
export const EDGELESS_DRAGGING_AREA_WIDGET = 'edgeless-dragging-area-rect';
|
||||
|
||||
export class EdgelessDraggingAreaRectWidget extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
static override styles = css`
|
||||
.affine-edgeless-dragging-area {
|
||||
position: absolute;
|
||||
background: ${unsafeCSS(
|
||||
cssVarV2('edgeless/selection/selectionMarqueeBackground', '#1E96EB14')
|
||||
)};
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: ${unsafeCSS(
|
||||
cssVarV2('edgeless/selection/selectionMarqueeBorder', '#1E96EB')
|
||||
)};
|
||||
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const rect = this.block.gfx.tool.draggingViewArea$.value;
|
||||
const tool = this.block.gfx.tool.currentTool$.value;
|
||||
|
||||
if (
|
||||
rect.w === 0 ||
|
||||
rect.h === 0 ||
|
||||
!(tool instanceof DefaultTool) ||
|
||||
tool.dragType !== DefaultModeDragType.Selecting
|
||||
)
|
||||
return nothing;
|
||||
|
||||
const style = {
|
||||
left: rect.x + 'px',
|
||||
top: rect.y + 'px',
|
||||
width: rect.w + 'px',
|
||||
height: rect.h + 'px',
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="affine-edgeless-dragging-area" style=${styleMap(style)}></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-dragging-area-rect': EdgelessDraggingAreaRectWidget;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,219 +0,0 @@
|
||||
import type { IVec } from '@blocksuite/global/utils';
|
||||
import { html, nothing } from 'lit';
|
||||
|
||||
export enum HandleDirection {
|
||||
Bottom = 'bottom',
|
||||
BottomLeft = 'bottom-left',
|
||||
BottomRight = 'bottom-right',
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
Top = 'top',
|
||||
TopLeft = 'top-left',
|
||||
TopRight = 'top-right',
|
||||
}
|
||||
|
||||
function ResizeHandle(
|
||||
handleDirection: HandleDirection,
|
||||
onPointerDown?: (e: PointerEvent, direction: HandleDirection) => void,
|
||||
updateCursor?: (
|
||||
dragging: boolean,
|
||||
options?: {
|
||||
type: 'resize' | 'rotate';
|
||||
target?: HTMLElement;
|
||||
point?: IVec;
|
||||
}
|
||||
) => void,
|
||||
hideEdgeHandle?: boolean
|
||||
) {
|
||||
const handlerPointerDown = (e: PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
onPointerDown && onPointerDown(e, handleDirection);
|
||||
};
|
||||
|
||||
const pointerEnter = (type: 'resize' | 'rotate') => (e: PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
if (e.buttons === 1 || !updateCursor) return;
|
||||
|
||||
const { clientX, clientY } = e;
|
||||
const target = e.target as HTMLElement;
|
||||
const point: IVec = [clientX, clientY];
|
||||
|
||||
updateCursor(true, { type, point, target });
|
||||
};
|
||||
|
||||
const pointerLeave = (e: PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
if (e.buttons === 1 || !updateCursor) return;
|
||||
|
||||
updateCursor(false);
|
||||
};
|
||||
|
||||
const rotationTpl =
|
||||
handleDirection === HandleDirection.Top ||
|
||||
handleDirection === HandleDirection.Bottom ||
|
||||
handleDirection === HandleDirection.Left ||
|
||||
handleDirection === HandleDirection.Right
|
||||
? nothing
|
||||
: html`<div
|
||||
class="rotate"
|
||||
@pointerover=${pointerEnter('rotate')}
|
||||
@pointerout=${pointerLeave}
|
||||
></div>`;
|
||||
|
||||
return html`<div
|
||||
class="handle"
|
||||
aria-label=${handleDirection}
|
||||
@pointerdown=${handlerPointerDown}
|
||||
>
|
||||
${rotationTpl}
|
||||
<div
|
||||
class="resize${hideEdgeHandle && ' transparent-handle'}"
|
||||
@pointerover=${pointerEnter('resize')}
|
||||
@pointerout=${pointerLeave}
|
||||
></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate how selected elements can be resized.
|
||||
*
|
||||
* - edge: The selected elements can only be resized dragging edge, usually when note element is selected
|
||||
* - all: The selected elements can be resize both dragging edge or corner, usually when all elements are `shape`
|
||||
* - none: The selected elements can't be resized, usually when all elements are `connector`
|
||||
* - corner: The selected elements can only be resize dragging corner, this is by default mode
|
||||
* - edgeAndCorner: The selected elements can be resize both dragging left right edge or corner, usually when all elements are 'text'
|
||||
*/
|
||||
export type ResizeMode = 'edge' | 'all' | 'none' | 'corner' | 'edgeAndCorner';
|
||||
|
||||
export function ResizeHandles(
|
||||
resizeMode: ResizeMode,
|
||||
onPointerDown: (e: PointerEvent, direction: HandleDirection) => void,
|
||||
updateCursor?: (
|
||||
dragging: boolean,
|
||||
options?: {
|
||||
type: 'resize' | 'rotate';
|
||||
target?: HTMLElement;
|
||||
point?: IVec;
|
||||
}
|
||||
) => void
|
||||
) {
|
||||
const getCornerHandles = () => {
|
||||
const handleTopLeft = ResizeHandle(
|
||||
HandleDirection.TopLeft,
|
||||
onPointerDown,
|
||||
updateCursor
|
||||
);
|
||||
const handleTopRight = ResizeHandle(
|
||||
HandleDirection.TopRight,
|
||||
onPointerDown,
|
||||
updateCursor
|
||||
);
|
||||
const handleBottomLeft = ResizeHandle(
|
||||
HandleDirection.BottomLeft,
|
||||
onPointerDown,
|
||||
updateCursor
|
||||
);
|
||||
const handleBottomRight = ResizeHandle(
|
||||
HandleDirection.BottomRight,
|
||||
onPointerDown,
|
||||
updateCursor
|
||||
);
|
||||
return {
|
||||
handleTopLeft,
|
||||
handleTopRight,
|
||||
handleBottomLeft,
|
||||
handleBottomRight,
|
||||
};
|
||||
};
|
||||
const getEdgeHandles = (hideEdgeHandle?: boolean) => {
|
||||
const handleLeft = ResizeHandle(
|
||||
HandleDirection.Left,
|
||||
onPointerDown,
|
||||
updateCursor,
|
||||
hideEdgeHandle
|
||||
);
|
||||
const handleRight = ResizeHandle(
|
||||
HandleDirection.Right,
|
||||
onPointerDown,
|
||||
updateCursor,
|
||||
hideEdgeHandle
|
||||
);
|
||||
return { handleLeft, handleRight };
|
||||
};
|
||||
const getEdgeVerticalHandles = (hideEdgeHandle?: boolean) => {
|
||||
const handleTop = ResizeHandle(
|
||||
HandleDirection.Top,
|
||||
onPointerDown,
|
||||
updateCursor,
|
||||
hideEdgeHandle
|
||||
);
|
||||
const handleBottom = ResizeHandle(
|
||||
HandleDirection.Bottom,
|
||||
onPointerDown,
|
||||
updateCursor,
|
||||
hideEdgeHandle
|
||||
);
|
||||
return { handleTop, handleBottom };
|
||||
};
|
||||
switch (resizeMode) {
|
||||
case 'corner': {
|
||||
const {
|
||||
handleTopLeft,
|
||||
handleTopRight,
|
||||
handleBottomLeft,
|
||||
handleBottomRight,
|
||||
} = getCornerHandles();
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
${handleTopLeft}
|
||||
${handleTopRight}
|
||||
${handleBottomLeft}
|
||||
${handleBottomRight}
|
||||
`;
|
||||
}
|
||||
case 'edge': {
|
||||
const { handleLeft, handleRight } = getEdgeHandles();
|
||||
return html`${handleLeft} ${handleRight}`;
|
||||
}
|
||||
case 'all': {
|
||||
const {
|
||||
handleTopLeft,
|
||||
handleTopRight,
|
||||
handleBottomLeft,
|
||||
handleBottomRight,
|
||||
} = getCornerHandles();
|
||||
const { handleLeft, handleRight } = getEdgeHandles(true);
|
||||
const { handleTop, handleBottom } = getEdgeVerticalHandles(true);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
${handleTopLeft}
|
||||
${handleTop}
|
||||
${handleTopRight}
|
||||
${handleRight}
|
||||
${handleBottomRight}
|
||||
${handleBottom}
|
||||
${handleBottomLeft}
|
||||
${handleLeft}
|
||||
`;
|
||||
}
|
||||
case 'edgeAndCorner': {
|
||||
const {
|
||||
handleTopLeft,
|
||||
handleTopRight,
|
||||
handleBottomLeft,
|
||||
handleBottomRight,
|
||||
} = getCornerHandles();
|
||||
const { handleLeft, handleRight } = getEdgeHandles(true);
|
||||
|
||||
return html`
|
||||
${handleTopLeft} ${handleTopRight} ${handleRight} ${handleBottomRight}
|
||||
${handleBottomLeft} ${handleLeft}
|
||||
`;
|
||||
}
|
||||
case 'none': {
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,706 +0,0 @@
|
||||
import { NOTE_MIN_WIDTH } from '@blocksuite/affine-model';
|
||||
import {
|
||||
assertExists,
|
||||
Bound,
|
||||
getQuadBoundWithRotation,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
type PointLocation,
|
||||
rotatePoints,
|
||||
} from '@blocksuite/global/utils';
|
||||
|
||||
import type { SelectableProps } from '../../utils/query.js';
|
||||
import { HandleDirection, type ResizeMode } from './resize-handles.js';
|
||||
|
||||
// 15deg
|
||||
const SHIFT_LOCKING_ANGLE = Math.PI / 12;
|
||||
|
||||
type DragStartHandler = () => void;
|
||||
type DragEndHandler = () => void;
|
||||
|
||||
type ResizeMoveHandler = (
|
||||
bounds: Map<
|
||||
string,
|
||||
{
|
||||
bound: Bound;
|
||||
path?: PointLocation[];
|
||||
matrix?: DOMMatrix;
|
||||
}
|
||||
>,
|
||||
direction: HandleDirection
|
||||
) => void;
|
||||
|
||||
type RotateMoveHandler = (point: IPoint, rotate: number) => void;
|
||||
|
||||
export class HandleResizeManager {
|
||||
private _aspectRatio = 1;
|
||||
|
||||
private _bounds = new Map<
|
||||
string,
|
||||
{
|
||||
bound: Bound;
|
||||
rotate: number;
|
||||
}
|
||||
>();
|
||||
|
||||
/**
|
||||
* Current rect of selected elements, it may change during resizing or moving
|
||||
*/
|
||||
private _currentRect = new DOMRect();
|
||||
|
||||
private _dragDirection: HandleDirection = HandleDirection.Left;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragPos: {
|
||||
start: { x: number; y: number };
|
||||
end: { x: number; y: number };
|
||||
} = {
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
private _locked = false;
|
||||
|
||||
private readonly _onDragEnd: DragEndHandler;
|
||||
|
||||
private readonly _onDragStart: DragStartHandler;
|
||||
|
||||
private readonly _onResizeMove: ResizeMoveHandler;
|
||||
|
||||
private readonly _onRotateMove: RotateMoveHandler;
|
||||
|
||||
private _origin: { x: number; y: number } = { x: 0, y: 0 };
|
||||
|
||||
/**
|
||||
* Record inital rect of selected elements
|
||||
*/
|
||||
private _originalRect = new DOMRect();
|
||||
|
||||
private _proportion = false;
|
||||
|
||||
private _proportional = false;
|
||||
|
||||
private _resizeMode: ResizeMode = 'none';
|
||||
|
||||
private _rotate = 0;
|
||||
|
||||
private _rotation = false;
|
||||
|
||||
private _shiftKey = false;
|
||||
|
||||
private _target: HTMLElement | null = null;
|
||||
|
||||
private _zoom = 1;
|
||||
|
||||
onPointerDown = (
|
||||
e: PointerEvent,
|
||||
direction: HandleDirection,
|
||||
proportional = false
|
||||
) => {
|
||||
// Prevent selection action from being triggered
|
||||
e.stopPropagation();
|
||||
|
||||
this._locked = false;
|
||||
this._target = e.target as HTMLElement;
|
||||
this._dragDirection = direction;
|
||||
this._dragPos.start = { x: e.x, y: e.y };
|
||||
this._dragPos.end = { x: e.x, y: e.y };
|
||||
this._rotation = this._target.classList.contains('rotate');
|
||||
this._proportional = proportional;
|
||||
|
||||
if (this._rotation) {
|
||||
const rect = this._target
|
||||
.closest('.affine-edgeless-selected-rect')
|
||||
?.getBoundingClientRect();
|
||||
assertExists(rect);
|
||||
const { left, top, right, bottom } = rect;
|
||||
const x = (left + right) / 2;
|
||||
const y = (top + bottom) / 2;
|
||||
// center of `selected-rect` in viewport
|
||||
this._origin = { x, y };
|
||||
}
|
||||
|
||||
this._dragging = true;
|
||||
this._onDragStart();
|
||||
|
||||
const _onPointerMove = ({ x, y, shiftKey }: PointerEvent) => {
|
||||
if (this._resizeMode === 'none') return;
|
||||
|
||||
this._shiftKey = shiftKey;
|
||||
this._dragPos.end = { x, y };
|
||||
|
||||
const proportional = this._proportional || this._shiftKey;
|
||||
|
||||
if (this._rotation) {
|
||||
this._onRotate(proportional);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onResize(proportional);
|
||||
};
|
||||
|
||||
const _onPointerUp = (_: PointerEvent) => {
|
||||
this._dragging = false;
|
||||
this._onDragEnd();
|
||||
|
||||
const { x, y, width, height } = this._currentRect;
|
||||
this._originalRect = new DOMRect(x, y, width, height);
|
||||
|
||||
this._locked = true;
|
||||
this._shiftKey = false;
|
||||
this._rotation = false;
|
||||
this._dragPos = {
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
document.removeEventListener('pointermove', _onPointerMove);
|
||||
document.removeEventListener('pointerup', _onPointerUp);
|
||||
};
|
||||
|
||||
document.addEventListener('pointermove', _onPointerMove);
|
||||
document.addEventListener('pointerup', _onPointerUp);
|
||||
};
|
||||
|
||||
get bounds() {
|
||||
return this._bounds;
|
||||
}
|
||||
|
||||
get currentRect() {
|
||||
return this._currentRect;
|
||||
}
|
||||
|
||||
get dragDirection() {
|
||||
return this._dragDirection;
|
||||
}
|
||||
|
||||
get dragging() {
|
||||
return this._dragging;
|
||||
}
|
||||
|
||||
get originalRect() {
|
||||
return this._originalRect;
|
||||
}
|
||||
|
||||
get rotation() {
|
||||
return this._rotation;
|
||||
}
|
||||
|
||||
constructor(
|
||||
onDragStart: DragStartHandler,
|
||||
onResizeMove: ResizeMoveHandler,
|
||||
onRotateMove: RotateMoveHandler,
|
||||
onDragEnd: DragEndHandler
|
||||
) {
|
||||
this._onDragStart = onDragStart;
|
||||
this._onResizeMove = onResizeMove;
|
||||
this._onRotateMove = onRotateMove;
|
||||
this._onDragEnd = onDragEnd;
|
||||
}
|
||||
|
||||
private _onResize(proportion: boolean) {
|
||||
const {
|
||||
_aspectRatio,
|
||||
_dragDirection,
|
||||
_dragPos,
|
||||
_rotate,
|
||||
_resizeMode,
|
||||
_zoom,
|
||||
_target,
|
||||
_originalRect,
|
||||
_currentRect,
|
||||
} = this;
|
||||
proportion ||= this._proportion;
|
||||
assertExists(_target);
|
||||
|
||||
const isAll = _resizeMode === 'all';
|
||||
const isCorner = _resizeMode === 'corner';
|
||||
const isEdgeAndCorner = _resizeMode === 'edgeAndCorner';
|
||||
|
||||
const {
|
||||
start: { x: startX, y: startY },
|
||||
end: { x: endX, y: endY },
|
||||
} = _dragPos;
|
||||
|
||||
const { left: minX, top: minY, right: maxX, bottom: maxY } = _originalRect;
|
||||
const original = {
|
||||
w: maxX - minX,
|
||||
h: maxY - minY,
|
||||
cx: (minX + maxX) / 2,
|
||||
cy: (minY + maxY) / 2,
|
||||
};
|
||||
const rect = { ...original };
|
||||
const scale = { x: 1, y: 1 };
|
||||
const flip = { x: 1, y: 1 };
|
||||
const direction = { x: 1, y: 1 };
|
||||
const fixedPoint = new DOMPoint(0, 0);
|
||||
const draggingPoint = new DOMPoint(0, 0);
|
||||
|
||||
const deltaX = (endX - startX) / _zoom;
|
||||
const deltaY = (endY - startY) / _zoom;
|
||||
|
||||
const m0 = new DOMMatrix()
|
||||
.translateSelf(original.cx, original.cy)
|
||||
.rotateSelf(_rotate)
|
||||
.translateSelf(-original.cx, -original.cy);
|
||||
|
||||
if (isCorner || isAll || isEdgeAndCorner) {
|
||||
switch (_dragDirection) {
|
||||
case HandleDirection.TopLeft: {
|
||||
direction.x = -1;
|
||||
direction.y = -1;
|
||||
fixedPoint.x = maxX;
|
||||
fixedPoint.y = maxY;
|
||||
draggingPoint.x = minX;
|
||||
draggingPoint.y = minY;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.TopRight: {
|
||||
direction.x = 1;
|
||||
direction.y = -1;
|
||||
fixedPoint.x = minX;
|
||||
fixedPoint.y = maxY;
|
||||
draggingPoint.x = maxX;
|
||||
draggingPoint.y = minY;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.BottomRight: {
|
||||
direction.x = 1;
|
||||
direction.y = 1;
|
||||
fixedPoint.x = minX;
|
||||
fixedPoint.y = minY;
|
||||
draggingPoint.x = maxX;
|
||||
draggingPoint.y = maxY;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.BottomLeft: {
|
||||
direction.x = -1;
|
||||
direction.y = 1;
|
||||
fixedPoint.x = maxX;
|
||||
fixedPoint.y = minY;
|
||||
draggingPoint.x = minX;
|
||||
draggingPoint.y = maxY;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Left: {
|
||||
direction.x = -1;
|
||||
direction.y = 1;
|
||||
fixedPoint.x = maxX;
|
||||
fixedPoint.y = original.cy;
|
||||
draggingPoint.x = minX;
|
||||
draggingPoint.y = original.cy;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Right: {
|
||||
direction.x = 1;
|
||||
direction.y = 1;
|
||||
fixedPoint.x = minX;
|
||||
fixedPoint.y = original.cy;
|
||||
draggingPoint.x = maxX;
|
||||
draggingPoint.y = original.cy;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Top: {
|
||||
const cx = (minX + maxX) / 2;
|
||||
direction.x = 1;
|
||||
direction.y = -1;
|
||||
fixedPoint.x = cx;
|
||||
fixedPoint.y = maxY;
|
||||
draggingPoint.x = cx;
|
||||
draggingPoint.y = minY;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Bottom: {
|
||||
const cx = (minX + maxX) / 2;
|
||||
direction.x = 1;
|
||||
direction.y = 1;
|
||||
fixedPoint.x = cx;
|
||||
fixedPoint.y = minY;
|
||||
draggingPoint.x = cx;
|
||||
draggingPoint.y = maxY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// force adjustment by aspect ratio
|
||||
proportion ||= this._bounds.size > 1;
|
||||
|
||||
const fp = fixedPoint.matrixTransform(m0);
|
||||
let dp = draggingPoint.matrixTransform(m0);
|
||||
|
||||
dp.x += deltaX;
|
||||
dp.y += deltaY;
|
||||
|
||||
if (
|
||||
_dragDirection === HandleDirection.Left ||
|
||||
_dragDirection === HandleDirection.Right ||
|
||||
_dragDirection === HandleDirection.Top ||
|
||||
_dragDirection === HandleDirection.Bottom
|
||||
) {
|
||||
const dpo = draggingPoint.matrixTransform(m0);
|
||||
const coorPoint: IVec = [0, 0];
|
||||
const [[x1, y1]] = rotatePoints([[dpo.x, dpo.y]], coorPoint, -_rotate);
|
||||
const [[x2, y2]] = rotatePoints([[dp.x, dp.y]], coorPoint, -_rotate);
|
||||
const point = { x: 0, y: 0 };
|
||||
if (
|
||||
_dragDirection === HandleDirection.Left ||
|
||||
_dragDirection === HandleDirection.Right
|
||||
) {
|
||||
point.x = x2;
|
||||
point.y = y1;
|
||||
} else {
|
||||
point.x = x1;
|
||||
point.y = y2;
|
||||
}
|
||||
|
||||
const [[x3, y3]] = rotatePoints(
|
||||
[[point.x, point.y]],
|
||||
coorPoint,
|
||||
_rotate
|
||||
);
|
||||
|
||||
dp.x = x3;
|
||||
dp.y = y3;
|
||||
}
|
||||
|
||||
const cx = (fp.x + dp.x) / 2;
|
||||
const cy = (fp.y + dp.y) / 2;
|
||||
|
||||
const m1 = new DOMMatrix()
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(-_rotate)
|
||||
.translateSelf(-cx, -cy);
|
||||
|
||||
const f = fp.matrixTransform(m1);
|
||||
const d = dp.matrixTransform(m1);
|
||||
|
||||
switch (_dragDirection) {
|
||||
case HandleDirection.TopLeft: {
|
||||
rect.w = f.x - d.x;
|
||||
rect.h = f.y - d.y;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.TopRight: {
|
||||
rect.w = d.x - f.x;
|
||||
rect.h = f.y - d.y;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.BottomRight: {
|
||||
rect.w = d.x - f.x;
|
||||
rect.h = d.y - f.y;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.BottomLeft: {
|
||||
rect.w = f.x - d.x;
|
||||
rect.h = d.y - f.y;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Left: {
|
||||
rect.w = f.x - d.x;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Right: {
|
||||
rect.w = d.x - f.x;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Top: {
|
||||
rect.h = f.y - d.y;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Bottom: {
|
||||
rect.h = d.y - f.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rect.cx = (d.x + f.x) / 2;
|
||||
rect.cy = (d.y + f.y) / 2;
|
||||
scale.x = rect.w / original.w;
|
||||
scale.y = rect.h / original.h;
|
||||
flip.x = scale.x < 0 ? -1 : 1;
|
||||
flip.y = scale.y < 0 ? -1 : 1;
|
||||
|
||||
const isDraggingCorner =
|
||||
_dragDirection === HandleDirection.TopLeft ||
|
||||
_dragDirection === HandleDirection.TopRight ||
|
||||
_dragDirection === HandleDirection.BottomRight ||
|
||||
_dragDirection === HandleDirection.BottomLeft;
|
||||
|
||||
// lock aspect ratio
|
||||
if (proportion && isDraggingCorner) {
|
||||
const newAspectRatio = Math.abs(rect.w / rect.h);
|
||||
if (_aspectRatio < newAspectRatio) {
|
||||
scale.y = Math.abs(scale.x) * flip.y;
|
||||
rect.h = scale.y * original.h;
|
||||
} else {
|
||||
scale.x = Math.abs(scale.y) * flip.x;
|
||||
rect.w = scale.x * original.w;
|
||||
}
|
||||
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
|
||||
draggingPoint.y = fixedPoint.y + rect.h * direction.y;
|
||||
|
||||
dp = draggingPoint.matrixTransform(m0);
|
||||
|
||||
rect.cx = (fp.x + dp.x) / 2;
|
||||
rect.cy = (fp.y + dp.y) / 2;
|
||||
}
|
||||
} else {
|
||||
// handle notes
|
||||
switch (_dragDirection) {
|
||||
case HandleDirection.Left: {
|
||||
direction.x = -1;
|
||||
fixedPoint.x = maxX;
|
||||
draggingPoint.x = minX + deltaX;
|
||||
rect.w = fixedPoint.x - draggingPoint.x;
|
||||
break;
|
||||
}
|
||||
case HandleDirection.Right: {
|
||||
direction.x = 1;
|
||||
fixedPoint.x = minX;
|
||||
draggingPoint.x = maxX + deltaX;
|
||||
rect.w = draggingPoint.x - fixedPoint.x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
scale.x = rect.w / original.w;
|
||||
flip.x = scale.x < 0 ? -1 : 1;
|
||||
|
||||
if (Math.abs(rect.w) < NOTE_MIN_WIDTH) {
|
||||
rect.w = NOTE_MIN_WIDTH * flip.x;
|
||||
scale.x = rect.w / original.w;
|
||||
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
|
||||
}
|
||||
|
||||
rect.cx = (draggingPoint.x + fixedPoint.x) / 2;
|
||||
}
|
||||
|
||||
const width = Math.abs(rect.w);
|
||||
const height = Math.abs(rect.h);
|
||||
const x = rect.cx - width / 2;
|
||||
const y = rect.cy - height / 2;
|
||||
|
||||
_currentRect.x = x;
|
||||
_currentRect.y = y;
|
||||
_currentRect.width = width;
|
||||
_currentRect.height = height;
|
||||
|
||||
const newBounds = new Map<
|
||||
string,
|
||||
{
|
||||
bound: Bound;
|
||||
path?: PointLocation[];
|
||||
matrix?: DOMMatrix;
|
||||
}
|
||||
>();
|
||||
|
||||
let process: (value: SelectableProps, key: string) => void;
|
||||
|
||||
if (isCorner || isAll || isEdgeAndCorner) {
|
||||
if (this._bounds.size === 1) {
|
||||
process = (_, id) => {
|
||||
newBounds.set(id, {
|
||||
bound: new Bound(x, y, width, height),
|
||||
});
|
||||
};
|
||||
} else {
|
||||
const fp = fixedPoint.matrixTransform(m0);
|
||||
const m2 = new DOMMatrix()
|
||||
.translateSelf(fp.x, fp.y)
|
||||
.rotateSelf(_rotate)
|
||||
.translateSelf(-fp.x, -fp.y)
|
||||
.scaleSelf(scale.x, scale.y, 1, fp.x, fp.y, 0)
|
||||
.translateSelf(fp.x, fp.y)
|
||||
.rotateSelf(-_rotate)
|
||||
.translateSelf(-fp.x, -fp.y);
|
||||
|
||||
// TODO: on same rotate
|
||||
process = ({ bound: { x, y, w, h }, path }, id) => {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
const center = new DOMPoint(cx, cy).matrixTransform(m2);
|
||||
const newWidth = Math.abs(w * scale.x);
|
||||
const newHeight = Math.abs(h * scale.y);
|
||||
|
||||
newBounds.set(id, {
|
||||
bound: new Bound(
|
||||
center.x - newWidth / 2,
|
||||
center.y - newHeight / 2,
|
||||
newWidth,
|
||||
newHeight
|
||||
),
|
||||
matrix: m2,
|
||||
path,
|
||||
});
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// include notes, <---->
|
||||
const m2 = new DOMMatrix().scaleSelf(
|
||||
scale.x,
|
||||
scale.y,
|
||||
1,
|
||||
fixedPoint.x,
|
||||
fixedPoint.y,
|
||||
0
|
||||
);
|
||||
process = ({ bound: { x, y, w, h }, rotate = 0, path }, id) => {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
|
||||
const center = new DOMPoint(cx, cy).matrixTransform(m2);
|
||||
|
||||
let newWidth: number;
|
||||
let newHeight: number;
|
||||
|
||||
// TODO: determine if it is a note
|
||||
if (rotate) {
|
||||
const { width } = getQuadBoundWithRotation({ x, y, w, h, rotate });
|
||||
const hrw = width / 2;
|
||||
|
||||
center.y = cy;
|
||||
|
||||
if (_currentRect.width <= width) {
|
||||
newWidth = w * (_currentRect.width / width);
|
||||
newHeight = newWidth / (w / h);
|
||||
center.x = _currentRect.left + _currentRect.width / 2;
|
||||
} else {
|
||||
const p = (cx - hrw - _originalRect.left) / _originalRect.width;
|
||||
const lx = _currentRect.left + p * _currentRect.width + hrw;
|
||||
center.x = Math.max(
|
||||
_currentRect.left + hrw,
|
||||
Math.min(lx, _currentRect.left + _currentRect.width - hrw)
|
||||
);
|
||||
newWidth = w;
|
||||
newHeight = h;
|
||||
}
|
||||
} else {
|
||||
newWidth = Math.abs(w * scale.x);
|
||||
newHeight = Math.abs(h * scale.y);
|
||||
}
|
||||
|
||||
newBounds.set(id, {
|
||||
bound: new Bound(
|
||||
center.x - newWidth / 2,
|
||||
center.y - newHeight / 2,
|
||||
newWidth,
|
||||
newHeight
|
||||
),
|
||||
matrix: m2,
|
||||
path,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this._bounds.forEach(process);
|
||||
this._onResizeMove(newBounds, this._dragDirection);
|
||||
}
|
||||
|
||||
private _onRotate(shiftKey = false) {
|
||||
const {
|
||||
_originalRect: { left: minX, top: minY, right: maxX, bottom: maxY },
|
||||
_dragPos: {
|
||||
start: { x: startX, y: startY },
|
||||
end: { x: endX, y: endY },
|
||||
},
|
||||
_origin: { x: centerX, y: centerY },
|
||||
_rotate,
|
||||
} = this;
|
||||
|
||||
const startRad = Math.atan2(startY - centerY, startX - centerX);
|
||||
const endRad = Math.atan2(endY - centerY, endX - centerX);
|
||||
let deltaRad = endRad - startRad;
|
||||
|
||||
// snap angle
|
||||
// 15deg * n = 0, 15, 30, 45, ... 360
|
||||
if (shiftKey) {
|
||||
const prevRad = (_rotate * Math.PI) / 180;
|
||||
let angle = prevRad + deltaRad;
|
||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
||||
deltaRad = angle - prevRad;
|
||||
}
|
||||
|
||||
const delta = (deltaRad * 180) / Math.PI;
|
||||
|
||||
let x = endX;
|
||||
let y = endY;
|
||||
if (shiftKey) {
|
||||
const point = new DOMPoint(startX, startY).matrixTransform(
|
||||
new DOMMatrix()
|
||||
.translateSelf(centerX, centerY)
|
||||
.rotateSelf(delta)
|
||||
.translateSelf(-centerX, -centerY)
|
||||
);
|
||||
x = point.x;
|
||||
y = point.y;
|
||||
}
|
||||
|
||||
this._onRotateMove(
|
||||
// center of element in suface
|
||||
{ x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
|
||||
delta
|
||||
);
|
||||
|
||||
this._dragPos.start = { x, y };
|
||||
this._rotate += delta;
|
||||
}
|
||||
|
||||
onPressShiftKey(pressed: boolean) {
|
||||
if (!this._target) return;
|
||||
if (this._locked) return;
|
||||
|
||||
if (this._shiftKey === pressed) return;
|
||||
this._shiftKey = pressed;
|
||||
|
||||
const proportional = this._proportional || this._shiftKey;
|
||||
|
||||
if (this._rotation) {
|
||||
this._onRotate(proportional);
|
||||
return;
|
||||
}
|
||||
|
||||
this._onResize(proportional);
|
||||
}
|
||||
|
||||
updateBounds(bounds: Map<string, SelectableProps>) {
|
||||
this._bounds = bounds;
|
||||
}
|
||||
|
||||
updateRectPosition(delta: { x: number; y: number }) {
|
||||
this._currentRect.x += delta.x;
|
||||
this._currentRect.y += delta.y;
|
||||
this._originalRect.x = this._currentRect.x;
|
||||
this._originalRect.y = this._currentRect.y;
|
||||
|
||||
return this._originalRect;
|
||||
}
|
||||
|
||||
updateState(
|
||||
resizeMode: ResizeMode,
|
||||
rotate: number,
|
||||
zoom: number,
|
||||
position?: { x: number; y: number },
|
||||
originalRect?: DOMRect,
|
||||
proportion = false
|
||||
) {
|
||||
this._resizeMode = resizeMode;
|
||||
this._rotate = rotate;
|
||||
this._zoom = zoom;
|
||||
this._proportion = proportion;
|
||||
|
||||
if (position) {
|
||||
this._currentRect.x = position.x;
|
||||
this._currentRect.y = position.y;
|
||||
this._originalRect.x = this._currentRect.x;
|
||||
this._originalRect.y = this._currentRect.y;
|
||||
}
|
||||
|
||||
if (originalRect) {
|
||||
this._originalRect = originalRect;
|
||||
this._aspectRatio = originalRect.width / originalRect.height;
|
||||
this._currentRect = DOMRect.fromRect(originalRect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { almostEqual } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import {
|
||||
assertExists,
|
||||
Bound,
|
||||
Vec,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
const HORIZONTAL_PADDING = 2;
|
||||
const VERTICAL_PADDING = 2;
|
||||
const BORDER_WIDTH = 1;
|
||||
|
||||
export class EdgelessConnectorLabelEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
static override styles = css`
|
||||
.edgeless-connector-label-editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transform-origin: center;
|
||||
z-index: 10;
|
||||
padding: ${VERTICAL_PADDING}px ${HORIZONTAL_PADDING}px;
|
||||
border: ${BORDER_WIDTH}px solid var(--affine-primary-color, #1e96eb);
|
||||
background: var(--affine-background-primary-color, #fff);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
|
||||
.inline-editor {
|
||||
white-space: pre-wrap !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inline-editor span {
|
||||
word-break: normal !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
}
|
||||
|
||||
.edgeless-connector-label-editor-placeholder {
|
||||
pointer-events: none;
|
||||
color: var(--affine-text-disable-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
private _isComposition = false;
|
||||
|
||||
private _keeping = false;
|
||||
|
||||
private _resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
private readonly _updateLabelRect = () => {
|
||||
const { connector, edgeless } = this;
|
||||
if (!connector || !edgeless) return;
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
|
||||
const newWidth = this.inlineEditorContainer.scrollWidth;
|
||||
const newHeight = this.inlineEditorContainer.scrollHeight;
|
||||
const center = connector.getPointByOffsetDistance(
|
||||
connector.labelOffset.distance
|
||||
);
|
||||
const bounds = Bound.fromCenter(center, newWidth, newHeight);
|
||||
const labelXYWH = bounds.toXYWH();
|
||||
|
||||
if (
|
||||
!connector.labelXYWH ||
|
||||
labelXYWH.some((p, i) => !almostEqual(p, connector.labelXYWH![i]))
|
||||
) {
|
||||
this.crud.updateElement(connector.id, {
|
||||
labelXYWH,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
get inlineEditor() {
|
||||
assertExists(this.richText.inlineEditor);
|
||||
return this.richText.inlineEditor;
|
||||
}
|
||||
|
||||
get inlineEditorContainer() {
|
||||
return this.inlineEditor.rootElement;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._resizeObserver?.disconnect();
|
||||
this._resizeObserver = null;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { edgeless, connector } = this;
|
||||
const { dispatcher } = edgeless;
|
||||
assertExists(dispatcher);
|
||||
|
||||
this._resizeObserver = new ResizeObserver(() => {
|
||||
this._updateLabelRect();
|
||||
this.requestUpdate();
|
||||
});
|
||||
this._resizeObserver.observe(this.richText);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this.inlineEditor.selectAll();
|
||||
|
||||
this.inlineEditor.slots.renderComplete.on(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
dispatcher.add('keyDown', ctx => {
|
||||
const state = ctx.get('keyboardState');
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey, isComposing } =
|
||||
state.raw;
|
||||
const onlyCmd = (ctrlKey || metaKey) && !altKey && !shiftKey;
|
||||
const isModEnter = onlyCmd && key === 'Enter';
|
||||
const isEscape = key === 'Escape';
|
||||
if (!isComposing && (isModEnter || isEscape)) {
|
||||
this.inlineEditorContainer?.blur();
|
||||
|
||||
edgeless.service.selection.set({
|
||||
elements: [connector.id],
|
||||
editing: false,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
edgeless.service.surface.elementUpdated.on(({ id }) => {
|
||||
if (id === connector.id) this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(dispatcher.add('click', () => true));
|
||||
this.disposables.add(dispatcher.add('doubleClick', () => true));
|
||||
|
||||
this.disposables.add(() => {
|
||||
if (connector.text) {
|
||||
const text = connector.text.toString();
|
||||
const trimed = text.trim();
|
||||
const len = trimed.length;
|
||||
if (len === 0) {
|
||||
// reset
|
||||
this.crud.updateElement(connector.id, {
|
||||
text: undefined,
|
||||
labelXYWH: undefined,
|
||||
labelOffset: undefined,
|
||||
});
|
||||
} else if (len < text.length) {
|
||||
this.crud.updateElement(connector.id, {
|
||||
// @TODO: trim in Y.Text?
|
||||
text: new Y.Text(trimed),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connector.lableEditing = false;
|
||||
|
||||
edgeless.service.selection.set({
|
||||
elements: [],
|
||||
editing: false,
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'blur',
|
||||
() => {
|
||||
if (this._keeping) return;
|
||||
this.remove();
|
||||
}
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'compositionstart',
|
||||
() => {
|
||||
this._isComposition = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'compositionend',
|
||||
() => {
|
||||
this._isComposition = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
|
||||
connector.lableEditing = true;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { connector } = this;
|
||||
const {
|
||||
labelOffset: { distance },
|
||||
labelStyle: {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
textAlign,
|
||||
color: labelColor,
|
||||
},
|
||||
labelConstraints: { hasMaxWidth, maxWidth },
|
||||
} = connector;
|
||||
|
||||
const lineHeight = TextUtils.getLineHeight(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight
|
||||
);
|
||||
const { translateX, translateY, zoom } = this.edgeless.service.viewport;
|
||||
const [x, y] = Vec.mul(connector.getPointByOffsetDistance(distance), zoom);
|
||||
const transformOperation = [
|
||||
'translate(-50%, -50%)',
|
||||
`translate(${translateX}px, ${translateY}px)`,
|
||||
`translate(${x}px, ${y}px)`,
|
||||
`scale(${zoom})`,
|
||||
];
|
||||
|
||||
const isEmpty = !connector.text?.length && !this._isComposition;
|
||||
const color = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(labelColor, '#000000');
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-connector-label-editor"
|
||||
style=${styleMap({
|
||||
fontFamily: `"${fontFamily}"`,
|
||||
fontSize: `${fontSize}px`,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
textAlign,
|
||||
lineHeight: `${lineHeight}px`,
|
||||
maxWidth: hasMaxWidth
|
||||
? `${maxWidth + BORDER_WIDTH * 2 + HORIZONTAL_PADDING * 2}px`
|
||||
: 'initial',
|
||||
color,
|
||||
transform: transformOperation.join(' '),
|
||||
})}
|
||||
>
|
||||
<rich-text
|
||||
.yText=${connector.text}
|
||||
.enableFormat=${false}
|
||||
style=${isEmpty
|
||||
? styleMap({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
padding: `${VERTICAL_PADDING}px ${HORIZONTAL_PADDING}px`,
|
||||
})
|
||||
: nothing}
|
||||
></rich-text>
|
||||
${isEmpty
|
||||
? html`
|
||||
<span class="edgeless-connector-label-editor-placeholder">
|
||||
Add text
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setKeeping(keeping: boolean) {
|
||||
this._keeping = keeping;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor connector!: ConnectorElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-connector-label-editor': EdgelessConnectorLabelEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
frameTitleStyleVars,
|
||||
} from '@blocksuite/affine-widget-frame-title';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { assertExists, Bound, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
static override styles = css`
|
||||
.frame-title-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transform-origin: top left;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
padding: 0 4px;
|
||||
outline: none;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--affine-primary-color);
|
||||
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
|
||||
overflow: hidden;
|
||||
font-family: var(--affine-font-family);
|
||||
}
|
||||
`;
|
||||
|
||||
get editorHost() {
|
||||
return this.edgeless.host;
|
||||
}
|
||||
|
||||
get inlineEditor() {
|
||||
return this.richText?.inlineEditor;
|
||||
}
|
||||
|
||||
private _unmount() {
|
||||
// dispose in advance to avoid execute `this.remove()` twice
|
||||
this.disposables.dispose();
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [],
|
||||
editing: false,
|
||||
});
|
||||
this.remove();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.dispatcher;
|
||||
assertExists(dispatcher);
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.inlineEditor) return;
|
||||
|
||||
this.inlineEditor.selectAll();
|
||||
|
||||
this.inlineEditor.slots.renderComplete.on(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
dispatcher.add('keyDown', ctx => {
|
||||
const state = ctx.get('keyboardState');
|
||||
if (state.raw.key === 'Enter' && !state.raw.isComposing) {
|
||||
this._unmount();
|
||||
return true;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
return false;
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
this.edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(dispatcher.add('click', () => true));
|
||||
this.disposables.add(dispatcher.add('doubleClick', () => true));
|
||||
|
||||
if (!this.inlineEditor.rootElement) return;
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditor.rootElement,
|
||||
'blur',
|
||||
() => {
|
||||
this._unmount();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const rootBlockId = this.editorHost.doc.root?.id;
|
||||
if (!rootBlockId) return nothing;
|
||||
|
||||
const viewport = this.edgeless.service.viewport;
|
||||
const bound = Bound.deserialize(this.frameModel.xywh);
|
||||
const [x, y] = viewport.toViewCoord(bound.x, bound.y);
|
||||
const isInner = this.edgeless.service.gfx.grid.has(
|
||||
this.frameModel.elementBound,
|
||||
true,
|
||||
true,
|
||||
model => model !== this.frameModel && model instanceof FrameBlockModel
|
||||
);
|
||||
|
||||
const frameTitleWidget = this.edgeless.std.view.getWidget(
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
rootBlockId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
if (!frameTitleWidget) return nothing;
|
||||
|
||||
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
|
||||
|
||||
const colors = frameTitle?.colors ?? {
|
||||
background: cssVarV2('edgeless/frame/background/white'),
|
||||
text: 'var(--affine-text-primary-color)',
|
||||
};
|
||||
|
||||
const inlineEditorStyle = styleMap({
|
||||
fontSize: frameTitleStyleVars.fontSize + 'px',
|
||||
position: 'absolute',
|
||||
left: (isInner ? x + 4 : x) + 'px',
|
||||
top: (isInner ? y + 4 : y - (frameTitleStyleVars.height + 8 / 2)) + 'px',
|
||||
minWidth: '8px',
|
||||
height: frameTitleStyleVars.height + 'px',
|
||||
background: colors.background,
|
||||
color: colors.text,
|
||||
});
|
||||
|
||||
const richTextStyle = styleMap({
|
||||
height: 'fit-content',
|
||||
});
|
||||
|
||||
return html`<div class="frame-title-editor" style=${inlineEditorStyle}>
|
||||
<rich-text
|
||||
.yText=${this.frameModel.title.yText}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${false}
|
||||
style=${richTextStyle}
|
||||
></rich-text>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frameModel!: FrameBlockModel;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText: RichText | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-frame-title-editor': EdgelessFrameTitleEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import {
|
||||
GROUP_TITLE_FONT_SIZE,
|
||||
GROUP_TITLE_OFFSET,
|
||||
GROUP_TITLE_PADDING,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import type { GroupElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import { assertExists, Bound, WithDisposable } from '@blocksuite/global/utils';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessGroupTitleEditor extends WithDisposable(
|
||||
ShadowlessElement
|
||||
) {
|
||||
get inlineEditor() {
|
||||
assertExists(this.richText.inlineEditor);
|
||||
return this.richText.inlineEditor;
|
||||
}
|
||||
|
||||
get inlineEditorContainer() {
|
||||
return this.inlineEditor.rootElement;
|
||||
}
|
||||
|
||||
private _unmount() {
|
||||
// dispose in advance to avoid execute `this.remove()` twice
|
||||
this.disposables.dispose();
|
||||
this.group.showTitle = true;
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [this.group.id],
|
||||
editing: false,
|
||||
});
|
||||
this.remove();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.dispatcher;
|
||||
assertExists(dispatcher);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this.inlineEditor.selectAll();
|
||||
|
||||
this.group.showTitle = false;
|
||||
|
||||
this.inlineEditor.slots.renderComplete.on(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
dispatcher.add('keyDown', ctx => {
|
||||
const state = ctx.get('keyboardState');
|
||||
if (state.raw.key === 'Enter' && !state.raw.isComposing) {
|
||||
this._unmount();
|
||||
return true;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
return false;
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
this.edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(dispatcher.add('click', () => true));
|
||||
this.disposables.add(dispatcher.add('doubleClick', () => true));
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'blur',
|
||||
() => {
|
||||
this._unmount();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.group.externalXYWH) {
|
||||
console.error('group.externalXYWH is not set');
|
||||
return nothing;
|
||||
}
|
||||
const viewport = this.edgeless.service.viewport;
|
||||
const bound = Bound.deserialize(this.group.externalXYWH);
|
||||
const [x, y] = viewport.toViewCoord(bound.x, bound.y);
|
||||
|
||||
const inlineEditorStyle = styleMap({
|
||||
transformOrigin: 'top left',
|
||||
borderRadius: '2px',
|
||||
width: 'fit-content',
|
||||
maxHeight: '30px',
|
||||
height: 'fit-content',
|
||||
padding: `${GROUP_TITLE_PADDING[1]}px ${GROUP_TITLE_PADDING[0]}px`,
|
||||
fontSize: GROUP_TITLE_FONT_SIZE + 'px',
|
||||
position: 'absolute',
|
||||
left: x + 'px',
|
||||
top: `${y - GROUP_TITLE_OFFSET + 2}px`,
|
||||
minWidth: '8px',
|
||||
fontFamily: 'var(--affine-font-family)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
background: 'var(--affine-white-10)',
|
||||
outline: 'none',
|
||||
zIndex: '1',
|
||||
border: `1px solid
|
||||
var(--affine-primary-color)`,
|
||||
boxShadow: 'var(--affine-active-shadow)',
|
||||
});
|
||||
return html`<rich-text
|
||||
.yText=${this.group.title}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${false}
|
||||
style=${inlineEditorStyle}
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor group!: GroupElementModel;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-group-title-editor': EdgelessGroupTitleEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { MindmapElementModel, TextResizing } from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import {
|
||||
assertExists,
|
||||
Bound,
|
||||
toRadian,
|
||||
Vec,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
private _keeping = false;
|
||||
|
||||
private _lastXYWH = '';
|
||||
|
||||
private _resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
get inlineEditor() {
|
||||
assertExists(this.richText.inlineEditor);
|
||||
return this.richText.inlineEditor;
|
||||
}
|
||||
|
||||
get inlineEditorContainer() {
|
||||
return this.inlineEditor.rootElement;
|
||||
}
|
||||
|
||||
get isMindMapNode() {
|
||||
return this.element.group instanceof MindmapElementModel;
|
||||
}
|
||||
|
||||
private _initMindmapKeyBindings() {
|
||||
if (!this.isMindMapNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const service = this.edgeless.service;
|
||||
|
||||
this._disposables.addFromEvent(this, 'keydown', evt => {
|
||||
switch (evt.key) {
|
||||
case 'Enter': {
|
||||
evt.stopPropagation();
|
||||
if (evt.shiftKey || evt.isComposing) return;
|
||||
|
||||
(this.ownerDocument.activeElement as HTMLElement).blur();
|
||||
service.selection.set({
|
||||
elements: [this.element.id],
|
||||
editing: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'Esc':
|
||||
case 'Tab': {
|
||||
evt.stopPropagation();
|
||||
(this.ownerDocument.activeElement as HTMLElement).blur();
|
||||
service.selection.set({
|
||||
elements: [this.element.id],
|
||||
editing: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _stashMindMapTree() {
|
||||
if (!this.isMindMapNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mindmap = this.element.group as MindmapElementModel;
|
||||
const pop = mindmap.stashTree(mindmap.tree);
|
||||
|
||||
this._disposables.add(() => {
|
||||
mindmap.layout();
|
||||
pop?.();
|
||||
});
|
||||
}
|
||||
|
||||
private _unmount() {
|
||||
this._resizeObserver?.disconnect();
|
||||
this._resizeObserver = null;
|
||||
|
||||
if (this.element.text) {
|
||||
const text = this.element.text.toString();
|
||||
const trimed = text.trim();
|
||||
const len = trimed.length;
|
||||
if (len === 0) {
|
||||
this.element.text = undefined;
|
||||
} else if (len < text.length) {
|
||||
this.element.text = new Y.Text(trimed);
|
||||
}
|
||||
}
|
||||
|
||||
this.element.textDisplay = true;
|
||||
|
||||
this.remove();
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [],
|
||||
editing: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _updateElementWH() {
|
||||
const bcr = this.richText.getBoundingClientRect();
|
||||
const containerHeight = this.richText.offsetHeight;
|
||||
const containerWidth = this.richText.offsetWidth;
|
||||
const textResizing = this.element.textResizing;
|
||||
|
||||
if (
|
||||
(containerHeight !== this.element.h &&
|
||||
textResizing === TextResizing.AUTO_HEIGHT) ||
|
||||
(textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT &&
|
||||
(containerWidth !== this.element.w ||
|
||||
containerHeight !== this.element.h))
|
||||
) {
|
||||
const [leftTopX, leftTopY] = Vec.rotWith(
|
||||
[this.richText.offsetLeft, this.richText.offsetTop],
|
||||
[bcr.left + bcr.width / 2, bcr.top + bcr.height / 2],
|
||||
toRadian(-this.element.rotate)
|
||||
);
|
||||
|
||||
const [modelLeftTopX, modelLeftTopY] =
|
||||
this.edgeless.service.viewport.toModelCoord(leftTopX, leftTopY);
|
||||
|
||||
this.crud.updateElement(this.element.id, {
|
||||
xywh: new Bound(
|
||||
modelLeftTopX,
|
||||
modelLeftTopY,
|
||||
textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT
|
||||
? containerWidth
|
||||
: this.element.w,
|
||||
containerHeight
|
||||
).serialize(),
|
||||
});
|
||||
|
||||
if (this._lastXYWH !== this.element.xywh) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
if (this.isMindMapNode) {
|
||||
const mindmap = this.element.group as MindmapElementModel;
|
||||
|
||||
mindmap.layout();
|
||||
}
|
||||
|
||||
this.richText.style.minHeight = `${containerHeight}px`;
|
||||
}
|
||||
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [this.element.id],
|
||||
editing: true,
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const dispatcher = this.edgeless.dispatcher;
|
||||
assertExists(dispatcher);
|
||||
|
||||
this.element.textDisplay = false;
|
||||
|
||||
this.disposables.add(
|
||||
this.edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this._updateElementWH();
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
dispatcher.add('click', () => {
|
||||
return true;
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
dispatcher.add('doubleClick', () => {
|
||||
return true;
|
||||
})
|
||||
);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (this.element.group instanceof MindmapElementModel) {
|
||||
this.inlineEditor.selectAll();
|
||||
} else {
|
||||
this.inlineEditor.focusEnd();
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
this.inlineEditor.slots.renderComplete.on(() => {
|
||||
this._updateElementWH();
|
||||
})
|
||||
);
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'blur',
|
||||
() => {
|
||||
if (this._keeping) return;
|
||||
this._unmount();
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
this.disposables.addFromEvent(this, 'keydown', evt => {
|
||||
if (evt.key === 'Escape') {
|
||||
requestAnimationFrame(() => {
|
||||
this.edgeless.service.selection.set({
|
||||
elements: [this.element.id],
|
||||
editing: false,
|
||||
});
|
||||
});
|
||||
|
||||
(this.ownerDocument.activeElement as HTMLElement).blur();
|
||||
}
|
||||
});
|
||||
|
||||
this._initMindmapKeyBindings();
|
||||
this._stashMindMapTree();
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.element.text) {
|
||||
console.error('Failed to mount shape editor because of no text.');
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const [verticalPadding, horiPadding] = this.element.padding;
|
||||
const textResizing = this.element.textResizing;
|
||||
const viewport = this.edgeless.service.viewport;
|
||||
const zoom = viewport.zoom;
|
||||
const rect = getSelectedRect([this.element]);
|
||||
const rotate = this.element.rotate;
|
||||
const [leftTopX, leftTopY] = Vec.rotWith(
|
||||
[rect.left, rect.top],
|
||||
[rect.left + rect.width / 2, rect.top + rect.height / 2],
|
||||
toRadian(rotate)
|
||||
);
|
||||
const [x, y] = this.edgeless.service.viewport.toViewCoord(
|
||||
leftTopX,
|
||||
leftTopY
|
||||
);
|
||||
const autoWidth = textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT;
|
||||
const color = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(this.element.color, '#000000');
|
||||
|
||||
const inlineEditorStyle = styleMap({
|
||||
position: 'absolute',
|
||||
left: x + 'px',
|
||||
top: y + 'px',
|
||||
width:
|
||||
textResizing === TextResizing.AUTO_HEIGHT
|
||||
? rect.width + 'px'
|
||||
: 'fit-content',
|
||||
// override rich-text style (height: 100%)
|
||||
height: 'initial',
|
||||
minHeight:
|
||||
textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT
|
||||
? '1em'
|
||||
: `${rect.height}px`,
|
||||
maxWidth:
|
||||
textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT
|
||||
? this.element.maxWidth
|
||||
? `${this.element.maxWidth}px`
|
||||
: undefined
|
||||
: undefined,
|
||||
boxSizing: 'border-box',
|
||||
fontSize: this.element.fontSize + 'px',
|
||||
fontFamily: TextUtils.wrapFontFamily(this.element.fontFamily),
|
||||
fontWeight: this.element.fontWeight,
|
||||
lineHeight: 'normal',
|
||||
outline: 'none',
|
||||
transform: `scale(${zoom}, ${zoom}) rotate(${rotate}deg)`,
|
||||
transformOrigin: 'top left',
|
||||
color,
|
||||
padding: `${verticalPadding}px ${horiPadding}px`,
|
||||
textAlign: this.element.textAlign,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '100%',
|
||||
alignItems:
|
||||
this.element.textVerticalAlign === 'center'
|
||||
? 'center'
|
||||
: this.element.textVerticalAlign === 'bottom'
|
||||
? 'end'
|
||||
: 'start',
|
||||
alignContent: 'center',
|
||||
gap: '0',
|
||||
zIndex: '1',
|
||||
});
|
||||
|
||||
this._lastXYWH = this.element.xywh;
|
||||
|
||||
return html` <style>
|
||||
edgeless-shape-text-editor v-text [data-v-text] {
|
||||
overflow-wrap: ${autoWidth ? 'normal' : 'anywhere'};
|
||||
word-break: ${autoWidth ? 'normal' : 'break-word'} !important;
|
||||
white-space: ${autoWidth ? 'pre' : 'pre-wrap'} !important;
|
||||
}
|
||||
|
||||
edgeless-shape-text-editor .inline-editor {
|
||||
min-width: 1px;
|
||||
}
|
||||
</style>
|
||||
<rich-text
|
||||
.yText=${this.element.text}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${false}
|
||||
style=${inlineEditorStyle}
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
setKeeping(keeping: boolean) {
|
||||
this._keeping = keeping;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor element!: ShapeElementModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor mountEditor:
|
||||
| ((
|
||||
element: ShapeElementModel,
|
||||
edgeless: EdgelessRootBlockComponent
|
||||
) => void)
|
||||
| undefined = undefined;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-shape-text-editor': EdgelessShapeTextEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,427 +0,0 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { RichText } from '@blocksuite/affine-components/rich-text';
|
||||
import type { TextElementModel } from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getSelectedRect } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
RANGE_SYNC_EXCLUDE_ATTR,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/block-std';
|
||||
import {
|
||||
assertExists,
|
||||
Bound,
|
||||
toRadian,
|
||||
Vec,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import { deleteElements } from '../../utils/crud.js';
|
||||
|
||||
export class EdgelessTextEditor extends WithDisposable(ShadowlessElement) {
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
static BORDER_WIDTH = 1;
|
||||
|
||||
static PADDING_HORIZONTAL = 10;
|
||||
|
||||
static PADDING_VERTICAL = 6;
|
||||
|
||||
static PLACEHOLDER_TEXT = 'Type from here';
|
||||
|
||||
static override styles = css`
|
||||
.edgeless-text-editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
transform-origin: left top;
|
||||
font-kerning: none;
|
||||
border: ${EdgelessTextEditor.BORDER_WIDTH}px solid
|
||||
var(--affine-primary-color, #1e96eb);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3);
|
||||
padding: ${EdgelessTextEditor.PADDING_VERTICAL}px
|
||||
${EdgelessTextEditor.PADDING_HORIZONTAL}px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.edgeless-text-editor .inline-editor {
|
||||
white-space: pre-wrap !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.edgeless-text-editor .inline-editor span {
|
||||
word-break: normal !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
}
|
||||
|
||||
.edgeless-text-editor-placeholder {
|
||||
pointer-events: none;
|
||||
color: var(--affine-text-disable-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
private _isComposition = false;
|
||||
|
||||
private _keeping = false;
|
||||
|
||||
private readonly _updateRect = () => {
|
||||
const edgeless = this.edgeless;
|
||||
const element = this.element;
|
||||
|
||||
if (!edgeless || !element || !this.inlineEditorContainer) return;
|
||||
|
||||
const newWidth = this.inlineEditorContainer.scrollWidth;
|
||||
const newHeight = this.inlineEditorContainer.scrollHeight;
|
||||
const bound = new Bound(element.x, element.y, newWidth, newHeight);
|
||||
const { x, y, w, h, rotate } = element;
|
||||
|
||||
switch (element.textAlign) {
|
||||
case 'left':
|
||||
{
|
||||
const newPos = this.getCoordsOnLeftAlign(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
r: toRadian(rotate),
|
||||
},
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
|
||||
bound.x = newPos.x;
|
||||
bound.y = newPos.y;
|
||||
}
|
||||
break;
|
||||
case 'center':
|
||||
{
|
||||
const newPos = this.getCoordsOnCenterAlign(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
r: toRadian(rotate),
|
||||
},
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
|
||||
bound.x = newPos.x;
|
||||
bound.y = newPos.y;
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
{
|
||||
const newPos = this.getCoordsOnRightAlign(
|
||||
{
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
r: toRadian(rotate),
|
||||
},
|
||||
newWidth,
|
||||
newHeight
|
||||
);
|
||||
|
||||
bound.x = newPos.x;
|
||||
bound.y = newPos.y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this.crud.updateElement(element.id, {
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
};
|
||||
|
||||
get inlineEditor() {
|
||||
assertExists(this.richText.inlineEditor);
|
||||
return this.richText.inlineEditor;
|
||||
}
|
||||
|
||||
get inlineEditorContainer() {
|
||||
return this.inlineEditor.rootElement;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!this.edgeless) {
|
||||
console.error('edgeless is not set.');
|
||||
return;
|
||||
}
|
||||
if (!this.element) {
|
||||
console.error('text element is not set.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
const edgeless = this.edgeless;
|
||||
const element = this.element;
|
||||
const { dispatcher } = this.edgeless;
|
||||
assertExists(dispatcher);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this.inlineEditor.slots.renderComplete.on(() => {
|
||||
this._updateRect();
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
this.disposables.add(
|
||||
edgeless.service.surface.elementUpdated.on(({ id }) => {
|
||||
if (id === element.id) this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(
|
||||
edgeless.service.viewport.viewportUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.add(dispatcher.add('click', () => true));
|
||||
this.disposables.add(dispatcher.add('doubleClick', () => true));
|
||||
|
||||
this.disposables.add(() => {
|
||||
element.display = true;
|
||||
|
||||
if (element.text.length === 0) {
|
||||
deleteElements(edgeless, [element]);
|
||||
}
|
||||
|
||||
edgeless.service.selection.set({
|
||||
elements: [],
|
||||
editing: false,
|
||||
});
|
||||
});
|
||||
|
||||
if (!this.inlineEditorContainer) return;
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'blur',
|
||||
() => !this._keeping && this.remove()
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'compositionstart',
|
||||
() => {
|
||||
this._isComposition = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
this.disposables.addFromEvent(
|
||||
this.inlineEditorContainer,
|
||||
'compositionend',
|
||||
() => {
|
||||
this._isComposition = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
);
|
||||
|
||||
element.display = false;
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
getContainerOffset() {
|
||||
const { PADDING_VERTICAL, PADDING_HORIZONTAL, BORDER_WIDTH } =
|
||||
EdgelessTextEditor;
|
||||
return `-${PADDING_HORIZONTAL + BORDER_WIDTH}px, -${
|
||||
PADDING_VERTICAL + BORDER_WIDTH
|
||||
}px`;
|
||||
}
|
||||
|
||||
getCoordsOnCenterAlign(
|
||||
rect: { w: number; h: number; r: number; x: number; y: number },
|
||||
w1: number,
|
||||
h1: number
|
||||
): { x: number; y: number } {
|
||||
const centerX = rect.x + rect.w / 2;
|
||||
const centerY = rect.y + rect.h / 2;
|
||||
|
||||
let deltaXPrime = 0;
|
||||
let deltaYPrime = (-rect.h / 2) * Math.cos(rect.r);
|
||||
|
||||
const vX = centerX + deltaXPrime;
|
||||
const vY = centerY + deltaYPrime;
|
||||
|
||||
deltaXPrime = 0;
|
||||
deltaYPrime = (-h1 / 2) * Math.cos(rect.r);
|
||||
|
||||
const newCenterX = vX - deltaXPrime;
|
||||
const newCenterY = vY - deltaYPrime;
|
||||
|
||||
return { x: newCenterX - w1 / 2, y: newCenterY - h1 / 2 };
|
||||
}
|
||||
|
||||
getCoordsOnLeftAlign(
|
||||
rect: { w: number; h: number; r: number; x: number; y: number },
|
||||
w1: number,
|
||||
h1: number
|
||||
): { x: number; y: number } {
|
||||
const cX = rect.x + rect.w / 2;
|
||||
const cY = rect.y + rect.h / 2;
|
||||
|
||||
let deltaXPrime =
|
||||
(-rect.w / 2) * Math.cos(rect.r) + (rect.h / 2) * Math.sin(rect.r);
|
||||
let deltaYPrime =
|
||||
(-rect.w / 2) * Math.sin(rect.r) - (rect.h / 2) * Math.cos(rect.r);
|
||||
|
||||
const vX = cX + deltaXPrime;
|
||||
const vY = cY + deltaYPrime;
|
||||
|
||||
deltaXPrime = (-w1 / 2) * Math.cos(rect.r) + (h1 / 2) * Math.sin(rect.r);
|
||||
deltaYPrime = (-w1 / 2) * Math.sin(rect.r) - (h1 / 2) * Math.cos(rect.r);
|
||||
|
||||
const newCenterX = vX - deltaXPrime;
|
||||
const newCenterY = vY - deltaYPrime;
|
||||
|
||||
return { x: newCenterX - w1 / 2, y: newCenterY - h1 / 2 };
|
||||
}
|
||||
|
||||
getCoordsOnRightAlign(
|
||||
rect: { w: number; h: number; r: number; x: number; y: number },
|
||||
w1: number,
|
||||
h1: number
|
||||
): { x: number; y: number } {
|
||||
const centerX = rect.x + rect.w / 2;
|
||||
const centerY = rect.y + rect.h / 2;
|
||||
|
||||
let deltaXPrime =
|
||||
(rect.w / 2) * Math.cos(rect.r) - (-rect.h / 2) * Math.sin(rect.r);
|
||||
let deltaYPrime =
|
||||
(rect.w / 2) * Math.sin(rect.r) + (-rect.h / 2) * Math.cos(rect.r);
|
||||
|
||||
const vX = centerX + deltaXPrime;
|
||||
const vY = centerY + deltaYPrime;
|
||||
|
||||
deltaXPrime = (w1 / 2) * Math.cos(rect.r) - (-h1 / 2) * Math.sin(rect.r);
|
||||
deltaYPrime = (w1 / 2) * Math.sin(rect.r) + (-h1 / 2) * Math.cos(rect.r);
|
||||
|
||||
const newCenterX = vX - deltaXPrime;
|
||||
const newCenterY = vY - deltaYPrime;
|
||||
|
||||
return { x: newCenterX - w1 / 2, y: newCenterY - h1 / 2 };
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
getVisualPosition(element: TextElementModel) {
|
||||
const { x, y, w, h, rotate } = element;
|
||||
return Vec.rotWith([x, y], [x + w / 2, y + h / 2], toRadian(rotate));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const {
|
||||
text,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight,
|
||||
fontStyle,
|
||||
textAlign,
|
||||
rotate,
|
||||
hasMaxWidth,
|
||||
w,
|
||||
} = this.element;
|
||||
const lineHeight = TextUtils.getLineHeight(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight
|
||||
);
|
||||
const rect = getSelectedRect([this.element]);
|
||||
|
||||
const { translateX, translateY, zoom } = this.edgeless.service.viewport;
|
||||
const [visualX, visualY] = this.getVisualPosition(this.element);
|
||||
const containerOffset = this.getContainerOffset();
|
||||
const transformOperation = [
|
||||
`translate(${translateX}px, ${translateY}px)`,
|
||||
`translate(${visualX * zoom}px, ${visualY * zoom}px)`,
|
||||
`scale(${zoom})`,
|
||||
`rotate(${rotate}deg)`,
|
||||
`translate(${containerOffset})`,
|
||||
];
|
||||
|
||||
const isEmpty = !text.length && !this._isComposition;
|
||||
const color = this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(this.element.color, '#000000');
|
||||
|
||||
return html`<div
|
||||
style=${styleMap({
|
||||
transform: transformOperation.join(' '),
|
||||
minWidth: hasMaxWidth ? `${rect.width}px` : 'none',
|
||||
maxWidth: hasMaxWidth ? `${w}px` : 'none',
|
||||
fontFamily: TextUtils.wrapFontFamily(fontFamily),
|
||||
fontSize: `${fontSize}px`,
|
||||
fontWeight,
|
||||
fontStyle,
|
||||
color,
|
||||
textAlign,
|
||||
lineHeight: `${lineHeight}px`,
|
||||
boxSizing: 'content-box',
|
||||
})}
|
||||
class="edgeless-text-editor"
|
||||
>
|
||||
<rich-text
|
||||
.yText=${text}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${false}
|
||||
style=${isEmpty
|
||||
? styleMap({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
padding: `${EdgelessTextEditor.PADDING_VERTICAL}px
|
||||
${EdgelessTextEditor.PADDING_HORIZONTAL}px`,
|
||||
})
|
||||
: nothing}
|
||||
></rich-text>
|
||||
${isEmpty
|
||||
? html`<span class="edgeless-text-editor-placeholder">
|
||||
Type from here
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
setKeeping(keeping: boolean) {
|
||||
this._keeping = keeping;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor element!: TextElementModel;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-text-editor': EdgelessTextEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { DefaultTheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
FeatureFlagService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { LineWidthEvent } from '../../panel/line-width-panel.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
|
||||
export class EdgelessBrushMenu extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
menu-divider {
|
||||
height: 24px;
|
||||
margin: 0 9px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _props$ = computed(() => {
|
||||
const { color, lineWidth } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value.brush;
|
||||
return {
|
||||
color,
|
||||
lineWidth,
|
||||
};
|
||||
});
|
||||
|
||||
private readonly _theme$ = computed(() => {
|
||||
return this.edgeless.std.get(ThemeProvider).theme$.value;
|
||||
});
|
||||
|
||||
type: GfxToolsFullOptionValue['type'] = 'brush';
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<edgeless-slide-menu>
|
||||
<div class="menu-content">
|
||||
<edgeless-line-width-panel
|
||||
.selectedSize=${this._props$.value.lineWidth}
|
||||
@select=${(e: LineWidthEvent) =>
|
||||
this.onChange({ lineWidth: e.detail })}
|
||||
>
|
||||
</edgeless-line-width-panel>
|
||||
<menu-divider .vertical=${true}></menu-divider>
|
||||
<edgeless-color-panel
|
||||
class="one-way"
|
||||
.value=${this._props$.value.color}
|
||||
.theme=${this._theme$.value}
|
||||
.palettes=${DefaultTheme.StrokeColorPalettes}
|
||||
.hasTransparent=${!this.edgeless.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_color_picker')}
|
||||
@select=${(e: ColorEvent) =>
|
||||
this.onChange({ color: e.detail.value })}
|
||||
></edgeless-color-panel>
|
||||
</div>
|
||||
</edgeless-slide-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (props: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-brush-menu': EdgelessBrushMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { EdgelessPenDarkIcon, EdgelessPenLightIcon } from './icons.js';
|
||||
|
||||
export class EdgelessBrushToolButton extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.edgeless-brush-button {
|
||||
height: 100%;
|
||||
}
|
||||
.pen-wrapper {
|
||||
width: 35px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
#edgeless-pen-icon {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
.edgeless-brush-button:hover #edgeless-pen-icon,
|
||||
.pen-wrapper.active #edgeless-pen-icon {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _color$ = computed(() => {
|
||||
const theme = this.edgeless.std.get(ThemeProvider).theme$.value;
|
||||
return this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value.brush.color,
|
||||
undefined,
|
||||
theme
|
||||
);
|
||||
});
|
||||
|
||||
override enableActiveBackground = true;
|
||||
|
||||
override type = 'brush' as const;
|
||||
|
||||
private _toggleBrushMenu() {
|
||||
if (this.tryDisposePopper()) return;
|
||||
!this.active && this.setEdgelessTool(this.type);
|
||||
const menu = this.createPopper('edgeless-brush-menu', this);
|
||||
Object.assign(menu.element, {
|
||||
edgeless: this.edgeless,
|
||||
onChange: (props: Record<string, unknown>) => {
|
||||
this.edgeless.std.get(EditPropsStore).recordLastProps('brush', props);
|
||||
this.setEdgelessTool('brush');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { active } = this;
|
||||
const appTheme = this.edgeless.std.get(ThemeProvider).app$.value;
|
||||
const icon =
|
||||
appTheme === 'dark' ? EdgelessPenDarkIcon : EdgelessPenLightIcon;
|
||||
const color = this._color$.value;
|
||||
|
||||
return html`
|
||||
<edgeless-toolbar-button
|
||||
class="edgeless-brush-button"
|
||||
.tooltip=${this.popper ? '' : getTooltipWithShortcut('Pen', 'P')}
|
||||
.tooltipOffset=${4}
|
||||
.active=${active}
|
||||
.withHover=${true}
|
||||
@click=${() => this._toggleBrushMenu()}
|
||||
>
|
||||
<div style=${styleMap({ color })} class="pen-wrapper">${icon}</div>
|
||||
</edgeless-toolbar-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-brush-tool-button': EdgelessBrushToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const EdgelessPenLightIcon = html`
|
||||
<svg
|
||||
width="36"
|
||||
height="60"
|
||||
viewBox="0 0 36 60"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="edgeless-pen-icon"
|
||||
>
|
||||
<g filter="url(#filter0_d_5310_64454)">
|
||||
<path
|
||||
d="M8 38.8965L12.2828 37.4689V106.538H8V38.8965Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M8 38.8965L12.2828 37.4689V106.538H8V38.8965Z"
|
||||
fill="white"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M12.2832 36.993H17.5177V106.538H12.2832V36.993Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.5176 36.993H22.7521V106.538H17.5176V36.993Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M17.5176 36.993H22.7521V106.538H17.5176V36.993Z"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M22.752 30.9448L27.0347 38.8965V106.538H22.752V30.9448Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M22.752 30.9448L27.0347 38.8965V106.538H22.752V30.9448Z"
|
||||
fill="black"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M16.5909 2.88078C16.8233 1.90625 18.2099 1.90623 18.4423 2.88075L19.896 8.97414L22.2755 18.9483L27.0345 38.8965L23.9871 38.0231C23.1982 37.7969 22.3511 37.9039 21.6431 38.3189L18.023 40.4414C17.7107 40.6245 17.3238 40.6245 17.0115 40.4414L13.0218 38.1023C12.5499 37.8256 11.9851 37.7543 11.4592 37.905L8 38.8965L12.7583 18.9483L15.1374 8.97414L16.5909 2.88078Z"
|
||||
fill="#F1F1F1"
|
||||
/>
|
||||
<path
|
||||
d="M16.5909 2.88078C16.8233 1.90625 18.2099 1.90623 18.4423 2.88075L19.896 8.97414L22.2755 18.9483L27.0345 38.8965L23.9871 38.0231C23.1982 37.7969 22.3511 37.9039 21.6431 38.3189L18.023 40.4414C17.7107 40.6245 17.3238 40.6245 17.0115 40.4414L13.0218 38.1023C12.5499 37.8256 11.9851 37.7543 11.4592 37.905L8 38.8965L12.7583 18.9483L15.1374 8.97414L16.5909 2.88078Z"
|
||||
fill="url(#paint0_linear_5310_64454)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<g filter="url(#filter1_b_5310_64454)">
|
||||
<path
|
||||
d="M16.7391 2.26209C16.9345 1.44293 18.1 1.44293 18.2954 2.26209L20.3725 10.969H14.6621L16.7391 2.26209Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_5310_64454"
|
||||
x="0"
|
||||
y="-5"
|
||||
width="35.0352"
|
||||
height="124"
|
||||
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="4" />
|
||||
<feGaussianBlur stdDeviation="4" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_5310_64454"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5310_64454"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_b_5310_64454"
|
||||
x="12.7587"
|
||||
y="-0.255743"
|
||||
width="9.51686"
|
||||
height="13.1282"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="0.951724" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_5310_64454"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_5310_64454"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_5310_64454"
|
||||
x1="22.1949"
|
||||
y1="19.2552"
|
||||
x2="11.0983"
|
||||
y2="21.5941"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="0.3125" stop-opacity="0" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
export const EdgelessPenDarkIcon = html`<svg
|
||||
width="34"
|
||||
height="60"
|
||||
viewBox="0 0 34 60"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="edgeless-pen-icon"
|
||||
>
|
||||
<g filter="url(#filter0_d_5310_64464)">
|
||||
<path
|
||||
d="M7 38.8965L11.2828 37.4689V106.538H7V38.8965Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M7 38.8965L11.2828 37.4689V106.538H7V38.8965Z"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M11.2832 36.993H16.5177V106.538H11.2832V36.993Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M11.2832 36.993H16.5177V106.538H11.2832V36.993Z"
|
||||
fill="black"
|
||||
fill-opacity="0.26"
|
||||
/>
|
||||
<path
|
||||
d="M16.5176 36.993H21.7521V106.538H16.5176V36.993Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M16.5176 36.993H21.7521V106.538H16.5176V36.993Z"
|
||||
fill="black"
|
||||
fill-opacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M21.752 30.9448L26.0347 38.8965V106.538H21.752V30.9448Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M21.752 30.9448L26.0347 38.8965V106.538H21.752V30.9448Z"
|
||||
fill="black"
|
||||
fill-opacity="0.6"
|
||||
/>
|
||||
<path
|
||||
d="M15.5909 2.88078C15.8233 1.90625 17.2099 1.90623 17.4423 2.88075L18.896 8.97414L21.2755 18.9483L26.0345 38.8965L22.9871 38.0231C22.1982 37.7969 21.3511 37.9039 20.6431 38.3189L17.023 40.4414C16.7107 40.6245 16.3238 40.6245 16.0115 40.4414L12.0218 38.1023C11.5499 37.8256 10.9851 37.7543 10.4592 37.905L7 38.8965L11.7583 18.9483L14.1374 8.97414L15.5909 2.88078Z"
|
||||
fill="#C1C1C1"
|
||||
/>
|
||||
<path
|
||||
d="M15.5909 2.88078C15.8233 1.90625 17.2099 1.90623 17.4423 2.88075L18.896 8.97414L21.2755 18.9483L26.0345 38.8965L22.9871 38.0231C22.1982 37.7969 21.3511 37.9039 20.6431 38.3189L17.023 40.4414C16.7107 40.6245 16.3238 40.6245 16.0115 40.4414L12.0218 38.1023C11.5499 37.8256 10.9851 37.7543 10.4592 37.905L7 38.8965L11.7583 18.9483L14.1374 8.97414L15.5909 2.88078Z"
|
||||
fill="url(#paint0_linear_5310_64464)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<g filter="url(#filter1_b_5310_64464)">
|
||||
<path
|
||||
d="M15.7391 2.26209C15.9345 1.44293 17.1 1.44293 17.2954 2.26209L19.3725 10.969H13.6621L15.7391 2.26209Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M15.7391 2.26209C15.9345 1.44293 17.1 1.44293 17.2954 2.26209L19.3725 10.969H13.6621L15.7391 2.26209Z"
|
||||
fill="black"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_5310_64464"
|
||||
x="0"
|
||||
y="-6"
|
||||
width="33.0352"
|
||||
height="122"
|
||||
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="2" />
|
||||
<feGaussianBlur stdDeviation="3.5" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.78 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_5310_64464"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5310_64464"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_b_5310_64464"
|
||||
x="11.7587"
|
||||
y="-0.255743"
|
||||
width="9.51686"
|
||||
height="13.1282"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="0.951724" />
|
||||
<feComposite
|
||||
in2="SourceAlpha"
|
||||
operator="in"
|
||||
result="effect1_backgroundBlur_5310_64464"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_backgroundBlur_5310_64464"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_5310_64464"
|
||||
x1="21.1949"
|
||||
y1="19.2552"
|
||||
x2="11.5553"
|
||||
y2="21.8444"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="0.302413" stop-opacity="0" />
|
||||
<stop offset="0.557292" stop-opacity="0" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>`;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
|
||||
// more than 100% due to the shadow
|
||||
const leaveToPercent = `calc(100% + 10px)`;
|
||||
|
||||
export interface MenuPopper<T extends HTMLElement> {
|
||||
element: T;
|
||||
dispose: () => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
// store active poppers
|
||||
const popMap = new WeakMap<HTMLElement, Map<string, MenuPopper<HTMLElement>>>();
|
||||
|
||||
function animateEnter(el: HTMLElement) {
|
||||
el.style.transform = 'translateY(0)';
|
||||
}
|
||||
function animateLeave(el: HTMLElement) {
|
||||
el.style.transform = `translateY(${leaveToPercent})`;
|
||||
}
|
||||
|
||||
export function createPopper<T extends keyof HTMLElementTagNameMap>(
|
||||
tagName: T,
|
||||
reference: HTMLElement,
|
||||
options?: {
|
||||
/** transition duration in ms */
|
||||
duration?: number;
|
||||
onDispose?: () => void;
|
||||
setProps?: (ele: HTMLElementTagNameMap[T]) => void;
|
||||
}
|
||||
) {
|
||||
const duration = options?.duration ?? 230;
|
||||
|
||||
if (!popMap.has(reference)) popMap.set(reference, new Map());
|
||||
const elMap = popMap.get(reference);
|
||||
assertExists(elMap);
|
||||
// if there is already a popper, cancel leave transition and apply enter transition
|
||||
if (elMap.has(tagName)) {
|
||||
const popper = elMap.get(tagName);
|
||||
assertExists(popper);
|
||||
popper.cancel?.();
|
||||
requestAnimationFrame(() => animateEnter(popper.element));
|
||||
return popper as MenuPopper<HTMLElementTagNameMap[T]>;
|
||||
}
|
||||
|
||||
const clipWrapper = document.createElement('div');
|
||||
const menu = document.createElement(tagName);
|
||||
options?.setProps?.(menu);
|
||||
assertExists(reference.shadowRoot);
|
||||
clipWrapper.append(menu);
|
||||
reference.shadowRoot.append(clipWrapper);
|
||||
|
||||
// apply enter transition
|
||||
menu.style.transition = `all ${duration}ms ease`;
|
||||
animateLeave(menu);
|
||||
requestAnimationFrame(() => animateEnter(menu));
|
||||
|
||||
Object.assign(clipWrapper.style, {
|
||||
height: '100px',
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
boxSizing: 'border-box',
|
||||
left: '0px',
|
||||
bottom: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'end',
|
||||
});
|
||||
|
||||
Object.assign(menu.style, {
|
||||
width: '100%',
|
||||
marginLeft: '30px',
|
||||
maxWidth: 'calc(100% - 60px)',
|
||||
bottom: '0%',
|
||||
pointerEvents: 'auto',
|
||||
});
|
||||
const remove = () => {
|
||||
clipWrapper.remove();
|
||||
menu.remove();
|
||||
popMap.get(reference)?.delete(tagName);
|
||||
options?.onDispose?.();
|
||||
};
|
||||
|
||||
const popper: MenuPopper<HTMLElementTagNameMap[T]> = {
|
||||
element: menu,
|
||||
dispose: () => {
|
||||
// apply leave transition
|
||||
animateLeave(menu);
|
||||
menu.addEventListener('transitionend', remove, { once: true });
|
||||
popper.cancel = () => menu.removeEventListener('transitionend', remove);
|
||||
},
|
||||
};
|
||||
|
||||
popMap.get(reference)?.set(tagName, popper);
|
||||
return popper;
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { assertExists, Bound } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type ReactiveController,
|
||||
type ReactiveControllerHost,
|
||||
render,
|
||||
} from 'lit';
|
||||
|
||||
import type { DraggableShape } from '../../shape/utils.js';
|
||||
import {
|
||||
type ElementDragEvent,
|
||||
mouseResolver,
|
||||
touchResolver,
|
||||
} from './event-resolver.js';
|
||||
import {
|
||||
createShapeDraggingOverlay,
|
||||
defaultInfo,
|
||||
type DraggingInfo,
|
||||
} from './overlay-factory.js';
|
||||
import {
|
||||
defaultIsValidMove,
|
||||
type EdgelessDraggableElementHost,
|
||||
type EdgelessDraggableElementOptions,
|
||||
type ElementInfo,
|
||||
type OverlayLayer,
|
||||
} from './types.js';
|
||||
|
||||
interface ReactiveState<T> {
|
||||
cancelled: boolean;
|
||||
draggingElement: ElementInfo<T> | null;
|
||||
dragOut: boolean | null;
|
||||
}
|
||||
interface EventCache {
|
||||
onMouseUp?: (e: MouseEvent) => void;
|
||||
onMouseMove?: (e: MouseEvent) => void;
|
||||
onTouchMove?: (e: TouchEvent) => void;
|
||||
onTouchEnd?: (e: TouchEvent) => void;
|
||||
}
|
||||
|
||||
export class EdgelessDraggableElementController<T>
|
||||
implements ReactiveController
|
||||
{
|
||||
clearTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
events: EventCache = {};
|
||||
|
||||
info = defaultInfo as DraggingInfo<T>;
|
||||
|
||||
overlay: OverlayLayer | null = null;
|
||||
|
||||
states: ReactiveState<T> = {
|
||||
cancelled: false,
|
||||
draggingElement: null,
|
||||
dragOut: null,
|
||||
};
|
||||
|
||||
constructor(
|
||||
public host: EdgelessDraggableElementHost & ReactiveControllerHost,
|
||||
public options: EdgelessDraggableElementOptions<T>
|
||||
) {
|
||||
this.host = host;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* let overlay shape animate back to the original position
|
||||
*/
|
||||
private _animateCancelDrop(onFinished?: () => void, duration = 230) {
|
||||
const { overlay, info } = this;
|
||||
if (!overlay) return;
|
||||
this.options?.onCanceled?.(overlay, info.elementInfo);
|
||||
// unlock pointer events
|
||||
overlay.mask.style.pointerEvents = 'none';
|
||||
// clip bottom
|
||||
if (info.scopeRect) {
|
||||
overlay.mask.style.height =
|
||||
info.scopeRect.bottom - info.edgelessRect.top + 'px';
|
||||
}
|
||||
|
||||
const { element, elementRectOriginal } = info;
|
||||
|
||||
const newShapeRect = element.getBoundingClientRect();
|
||||
const x = newShapeRect.left - elementRectOriginal.left;
|
||||
const y = newShapeRect.top - elementRectOriginal.top;
|
||||
|
||||
// apply a transition
|
||||
overlay.element.style.transition = `transform ${duration}ms ease`;
|
||||
overlay.element.style.setProperty('--translate-x', `${x}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${y}px`);
|
||||
overlay.transitionWrapper.style.setProperty('--scale', '1');
|
||||
|
||||
this.clearTimeout = setTimeout(() => {
|
||||
if (onFinished) return onFinished();
|
||||
this.reset();
|
||||
this.removeAllEvents();
|
||||
this.clearTimeout = null;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
private _createOverlay({ x, y }: Pick<ElementDragEvent, 'x' | 'y'>) {
|
||||
const { edgeless } = this.options;
|
||||
const { elementInfo, elementRectOriginal, offsetPos, edgelessRect } =
|
||||
this.info;
|
||||
|
||||
this.reset();
|
||||
this._updateState('draggingElement', elementInfo);
|
||||
this.overlay = createShapeDraggingOverlay(this.info);
|
||||
|
||||
const { overlay } = this;
|
||||
// init shape position with 'left' and 'top';
|
||||
const { width, height, left, top } = elementRectOriginal;
|
||||
const relativeX = left - edgelessRect.left;
|
||||
const relativeY = top - edgelessRect.top;
|
||||
// make sure the transform origin is the same as the mouse position
|
||||
const ox = `${(((x - left) / width) * 100).toFixed(0)}%`;
|
||||
const oy = `${(((y - top) / height) * 100).toFixed(0)}%`;
|
||||
Object.assign(overlay.element.style, {
|
||||
left: `${relativeX}px`,
|
||||
top: `${relativeY}px`,
|
||||
});
|
||||
overlay.element.style.setProperty('--translate-x', `${offsetPos.x}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${offsetPos.y}px`);
|
||||
overlay.transitionWrapper.style.transformOrigin = `${ox} ${oy}`;
|
||||
|
||||
const shapeName = (elementInfo as ElementInfo<DraggableShape>).data.name;
|
||||
const { fillColor, strokeColor } =
|
||||
edgeless.host.std.get(EditPropsStore).lastProps$.value[
|
||||
`shape:${shapeName}`
|
||||
] || {};
|
||||
const color = edgeless.host.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(fillColor);
|
||||
const stroke = edgeless.host.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(strokeColor);
|
||||
overlay.element.style.setProperty('color', color);
|
||||
overlay.element.style.setProperty('stroke', stroke);
|
||||
// lifecycle hook
|
||||
this.options.onOverlayCreated?.(overlay, elementInfo);
|
||||
}
|
||||
|
||||
private _onDragEnd() {
|
||||
const { overlay, info, options } = this;
|
||||
const { startTime, elementInfo, edgelessRect, validMoved } = info;
|
||||
const { service, clickThreshold = 1500 } = options;
|
||||
const zoom = service.viewport.zoom;
|
||||
|
||||
if (!validMoved) {
|
||||
const duration = Date.now() - startTime;
|
||||
if (duration < clickThreshold) {
|
||||
options.onElementClick?.(info.elementInfo);
|
||||
if (options.clickToDrag) {
|
||||
this._createOverlay(info.startPos);
|
||||
this.info.moved = true;
|
||||
setTimeout(() => {
|
||||
this._updateOverlayScale(zoom);
|
||||
}, 50);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.states.dragOut && !this.states.cancelled && overlay) {
|
||||
const rect = overlay.transitionWrapper.getBoundingClientRect();
|
||||
const [modelX, modelY] = this.options.service.viewport.toModelCoord(
|
||||
rect.left - edgelessRect.left,
|
||||
rect.top - edgelessRect.top
|
||||
);
|
||||
const bound = new Bound(
|
||||
modelX,
|
||||
modelY,
|
||||
rect.width / zoom,
|
||||
rect.height / zoom
|
||||
);
|
||||
options?.onDrop?.(elementInfo, bound);
|
||||
|
||||
this.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.states.dragOut) this._animateCancelDrop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _onDragMove(e: ElementDragEvent) {
|
||||
if (this.states.cancelled) return;
|
||||
const { info, options } = this;
|
||||
|
||||
// first move
|
||||
if (!info.moved) {
|
||||
info.moved = true;
|
||||
this._createOverlay(e);
|
||||
}
|
||||
|
||||
const { overlay } = this;
|
||||
assertExists(overlay);
|
||||
|
||||
const { x, y } = e;
|
||||
const { startPos, scopeRect } = info;
|
||||
const offsetX = x - startPos.x;
|
||||
const offsetY = y - startPos.y;
|
||||
info.offsetPos = { x: offsetX, y: offsetY };
|
||||
|
||||
if (!info.validMoved) {
|
||||
const isValidMove = options.isValidMove ?? defaultIsValidMove;
|
||||
info.validMoved = isValidMove(info.offsetPos);
|
||||
}
|
||||
|
||||
// check if inside scopeElement
|
||||
const newDragOut =
|
||||
!scopeRect ||
|
||||
y < scopeRect.top ||
|
||||
y > scopeRect.bottom ||
|
||||
x < scopeRect.left ||
|
||||
x > scopeRect.right;
|
||||
if (newDragOut !== this.states.dragOut)
|
||||
options.onEnterOrLeaveScope?.(overlay, newDragOut);
|
||||
this._updateState('dragOut', newDragOut);
|
||||
|
||||
// apply transform
|
||||
// - move shape with translate
|
||||
overlay.element.style.setProperty('--translate-x', `${offsetX}px`);
|
||||
overlay.element.style.setProperty('--translate-y', `${offsetY}px`);
|
||||
// - scale shape with scale
|
||||
const zoom = options.service.viewport.zoom;
|
||||
this._updateOverlayScale(zoom);
|
||||
}
|
||||
|
||||
private _onDragStart(e: ElementDragEvent, elementInfo: ElementInfo<T>) {
|
||||
const { scopeElement, edgeless } = this.options;
|
||||
e.originalEvent.stopPropagation();
|
||||
e.originalEvent.preventDefault();
|
||||
|
||||
// Safari compatibility
|
||||
// Cannot get edgeless.host.getBoundingClientRect().width in Safari (Always 0)
|
||||
const edgelessRect = edgeless.host.getBoundingClientRect();
|
||||
if (edgelessRect.width === 0) {
|
||||
edgelessRect.width = edgeless.viewport.clientWidth;
|
||||
}
|
||||
|
||||
this.info = {
|
||||
startTime: Date.now(),
|
||||
startPos: { x: e.x, y: e.y },
|
||||
offsetPos: { x: 0, y: 0 },
|
||||
scopeRect: scopeElement?.getBoundingClientRect() ?? null,
|
||||
edgelessRect,
|
||||
elementRectOriginal: e.el.getBoundingClientRect(),
|
||||
element: e.el,
|
||||
elementInfo,
|
||||
moved: false,
|
||||
validMoved: false,
|
||||
parentToMount: edgeless.host,
|
||||
};
|
||||
|
||||
this.removeAllEvents();
|
||||
if (e.inputType === 'mouse') {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
this._onDragMove(mouseResolver(e));
|
||||
};
|
||||
const onMouseUp = (_: MouseEvent) => {
|
||||
const finished = this._onDragEnd();
|
||||
if (finished) {
|
||||
edgeless.host.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
};
|
||||
edgeless.host.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
this.events = { onMouseMove, onMouseUp };
|
||||
} else {
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
this._onDragMove(touchResolver(e));
|
||||
};
|
||||
const onTouchEnd = (_: TouchEvent) => {
|
||||
const finished = this._onDragEnd();
|
||||
if (finished) {
|
||||
edgeless.host.removeEventListener('touchmove', onTouchMove);
|
||||
window.removeEventListener('touchend', onTouchEnd);
|
||||
}
|
||||
};
|
||||
edgeless.host.addEventListener('touchmove', onTouchMove);
|
||||
window.addEventListener('touchend', onTouchEnd);
|
||||
this.events = { onTouchMove, onTouchEnd };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update overlay shape scale according to the current zoom level
|
||||
*/
|
||||
private _updateOverlayScale(zoom: number) {
|
||||
const transitionWrapper = this.overlay?.transitionWrapper;
|
||||
if (!transitionWrapper) return;
|
||||
|
||||
const standardWidth =
|
||||
this.info.elementInfo.standardWidth ?? this.options.standardWidth ?? 100;
|
||||
|
||||
const { elementRectOriginal } = this.info;
|
||||
const scale = (standardWidth * zoom) / elementRectOriginal.width;
|
||||
|
||||
const clickToDragScale = this.options.clickToDragScale ?? 1.2;
|
||||
|
||||
const finalScale = this.states.dragOut
|
||||
? scale
|
||||
: this.options.clickToDrag
|
||||
? clickToDragScale
|
||||
: 1;
|
||||
transitionWrapper.style.setProperty('--scale', finalScale.toFixed(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private _updateState<Key extends keyof ReactiveState<T>>(
|
||||
key: Key,
|
||||
value: ReactiveState<T>[Key]
|
||||
) {
|
||||
this.states[key] = value;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
private _updateStates(states: Partial<ReactiveState<T>>) {
|
||||
Object.assign(this.states, states);
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current dragging & animate even if dragOut
|
||||
*/
|
||||
cancel() {
|
||||
if (this.states.cancelled) return;
|
||||
this._updateState('cancelled', true);
|
||||
this._animateCancelDrop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link cancel} but without animation
|
||||
*/
|
||||
cancelWithoutAnimation() {
|
||||
if (this.states.cancelled) return;
|
||||
this._updateState('cancelled', true);
|
||||
this.reset();
|
||||
this.removeAllEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* A workaround to apply click event manually
|
||||
*/
|
||||
clickToDrag(target: HTMLElement, startPos: { x: number; y: number }) {
|
||||
if (!this.options.clickToDrag) {
|
||||
this.options.clickToDrag = true;
|
||||
console.warn(
|
||||
'clickToDrag is not enabled, it will be enabled automatically'
|
||||
);
|
||||
}
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const targetCenter = {
|
||||
x: targetRect.left + targetRect.width / 2,
|
||||
y: targetRect.top + targetRect.height / 2,
|
||||
};
|
||||
|
||||
const mouseDownEvent = new MouseEvent('mousedown', {
|
||||
clientX: targetCenter.x,
|
||||
clientY: targetCenter.y,
|
||||
});
|
||||
const mouseUpEvent = new MouseEvent('mouseup', {
|
||||
clientX: targetCenter.x,
|
||||
clientY: targetCenter.y,
|
||||
});
|
||||
target.dispatchEvent(mouseDownEvent);
|
||||
window.dispatchEvent(mouseUpEvent);
|
||||
|
||||
const mouseMoveEvent = new MouseEvent('mousemove', {
|
||||
clientX: startPos.x,
|
||||
clientY: startPos.y,
|
||||
});
|
||||
|
||||
this.options.edgeless.host.dispatchEvent(mouseMoveEvent);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.host.disposables.add(
|
||||
this.options.service.viewport.viewportUpdated.on(({ zoom }) => {
|
||||
this._updateOverlayScale(zoom);
|
||||
})
|
||||
);
|
||||
|
||||
this.host.disposables.addFromEvent(
|
||||
window,
|
||||
'keydown',
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && this.states.draggingElement) this.cancel();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.removeAllEvents();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onMouseDown(e: MouseEvent, elementInfo: ElementInfo<T>) {
|
||||
this._onDragStart(mouseResolver(e), elementInfo);
|
||||
}
|
||||
|
||||
onTouchStart(e: TouchEvent, elementInfo: ElementInfo<T>) {
|
||||
this._onDragStart(touchResolver(e), elementInfo);
|
||||
}
|
||||
|
||||
removeAllEvents() {
|
||||
const { events, options } = this;
|
||||
const host = options.edgeless.host;
|
||||
const { onMouseUp, onMouseMove, onTouchMove, onTouchEnd } = events;
|
||||
onMouseUp && window.removeEventListener('mouseup', onMouseUp);
|
||||
onMouseMove && host && host.removeEventListener('mousemove', onMouseMove);
|
||||
onTouchMove && host && host.removeEventListener('touchmove', onTouchMove);
|
||||
onTouchEnd && window.removeEventListener('touchend', onTouchEnd);
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this.clearTimeout) clearTimeout(this.clearTimeout);
|
||||
this.overlay?.mask.remove();
|
||||
this.overlay = null;
|
||||
this._updateStates({
|
||||
cancelled: false,
|
||||
draggingElement: null,
|
||||
dragOut: null,
|
||||
});
|
||||
}
|
||||
|
||||
updateElementInfo(elementInfo: Partial<ElementInfo<T>>) {
|
||||
this.info.elementInfo = {
|
||||
...this.info.elementInfo,
|
||||
...elementInfo,
|
||||
};
|
||||
|
||||
if (elementInfo.preview && this.overlay) {
|
||||
render(elementInfo.preview, this.overlay.transitionWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export type ElementDragEvent = {
|
||||
inputType: 'mouse' | 'touch';
|
||||
x: number;
|
||||
y: number;
|
||||
el: HTMLElement;
|
||||
originalEvent: MouseEvent | TouchEvent;
|
||||
};
|
||||
|
||||
export const touchResolver = (event: TouchEvent) =>
|
||||
({
|
||||
inputType: 'touch',
|
||||
x: event.touches[0].clientX,
|
||||
y: event.touches[0].clientY,
|
||||
el: event.currentTarget as HTMLElement,
|
||||
originalEvent: event,
|
||||
}) satisfies ElementDragEvent;
|
||||
|
||||
export const mouseResolver = (event: MouseEvent) =>
|
||||
({
|
||||
inputType: 'mouse',
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
el: event.currentTarget as HTMLElement,
|
||||
originalEvent: event,
|
||||
}) satisfies ElementDragEvent;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { render } from 'lit';
|
||||
|
||||
import type { ElementInfo, OverlayLayer } from './types.js';
|
||||
|
||||
export type DraggingInfo<T> = {
|
||||
startPos: { x: number; y: number };
|
||||
offsetPos: { x: number; y: number };
|
||||
startTime: number;
|
||||
scopeRect: DOMRect | null;
|
||||
edgelessRect: DOMRect;
|
||||
elementRectOriginal: DOMRect;
|
||||
element: HTMLElement;
|
||||
elementInfo: ElementInfo<T>;
|
||||
parentToMount: HTMLElement;
|
||||
moved: boolean;
|
||||
validMoved: boolean;
|
||||
};
|
||||
|
||||
export const defaultInfo = {
|
||||
startPos: { x: 0, y: 0 },
|
||||
offsetPos: { x: 0, y: 0 },
|
||||
startTime: 0,
|
||||
scopeRect: {} as DOMRect,
|
||||
edgelessRect: {} as DOMRect,
|
||||
elementRectOriginal: {} as DOMRect,
|
||||
element: null as unknown as HTMLElement,
|
||||
elementInfo: null as unknown as ElementInfo<unknown>,
|
||||
parentToMount: null as unknown as HTMLElement,
|
||||
moved: false,
|
||||
validMoved: false,
|
||||
} satisfies DraggingInfo<unknown>;
|
||||
|
||||
const className = (name: string) =>
|
||||
`edgeless-draggable-control-overlay-${name}`;
|
||||
const addClass = (node: HTMLElement, name: string) =>
|
||||
node.classList.add(className(name));
|
||||
|
||||
export const createShapeDraggingOverlay = <T>(
|
||||
info: DraggingInfo<T>
|
||||
): OverlayLayer => {
|
||||
const { edgelessRect, parentToMount, element: originalElement } = info;
|
||||
const elementStyle = getComputedStyle(originalElement);
|
||||
const mask = document.createElement('div');
|
||||
addClass(mask, 'mask');
|
||||
Object.assign(mask.style, {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
width: edgelessRect.width + 'px',
|
||||
height: edgelessRect.height + 'px',
|
||||
overflow: 'hidden',
|
||||
zIndex: '9999',
|
||||
|
||||
// for debug purpose
|
||||
// background: 'rgba(255, 0, 0, 0.1)',
|
||||
});
|
||||
|
||||
const element = document.createElement('div');
|
||||
addClass(element, 'element');
|
||||
const transitionWrapper = document.createElement('div');
|
||||
addClass(transitionWrapper, 'transition-wrapper');
|
||||
Object.assign(transitionWrapper.style, {
|
||||
transition: 'all 0.18s ease',
|
||||
transform: 'scale(var(--scale, 1)) rotate(var(--rotate, 0deg))',
|
||||
width: elementStyle.width,
|
||||
height: elementStyle.height,
|
||||
});
|
||||
transitionWrapper.style.setProperty('--rotate', '0deg');
|
||||
transitionWrapper.style.setProperty('--scale', '1');
|
||||
|
||||
render(info.elementInfo.preview, transitionWrapper);
|
||||
|
||||
Object.assign(element.style, {
|
||||
transform:
|
||||
'translate(var(--translate-x, 0), var(--translate-y, 0)) rotate(var(--rotate, 0deg)) scale(var(--scale, 1))',
|
||||
position: 'absolute',
|
||||
cursor: 'grabbing',
|
||||
transition: 'inherit',
|
||||
});
|
||||
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.textContent = `
|
||||
.${className('transition-wrapper')} > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
mask.append(styleTag);
|
||||
|
||||
element.append(transitionWrapper);
|
||||
mask.append(element);
|
||||
parentToMount.append(mask);
|
||||
|
||||
return { mask, element, transitionWrapper };
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { Bound, DisposableClass } from '@blocksuite/global/utils';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../../edgeless-root-block.js';
|
||||
import type { EdgelessRootService } from '../../../../edgeless-root-service.js';
|
||||
|
||||
export interface EdgelessDraggableElementHost extends DisposableClass {}
|
||||
|
||||
export interface OverlayLayer {
|
||||
/**
|
||||
* The root element of the overlay,
|
||||
* used to handle clip & prevent pointer events
|
||||
*/
|
||||
mask: HTMLElement;
|
||||
/**
|
||||
* The real preview element
|
||||
*/
|
||||
element: HTMLElement;
|
||||
/**
|
||||
* The wrapper that contains the preview element,
|
||||
* different from the element, this element has transition effect
|
||||
*/
|
||||
transitionWrapper: HTMLElement;
|
||||
}
|
||||
|
||||
export interface EdgelessDraggableElementOptions<T> {
|
||||
edgeless: EdgelessRootBlockComponent;
|
||||
service: EdgelessRootService;
|
||||
/**
|
||||
* In which element that the target should be dragged out
|
||||
* If not provided, recognized as the drag-out whenever dragging
|
||||
*/
|
||||
scopeElement?: HTMLElement;
|
||||
/**
|
||||
* The width of the element when placed to canvas
|
||||
* @default 100
|
||||
*/
|
||||
standardWidth?: number;
|
||||
|
||||
/**
|
||||
* the threshold of mousedown and mouseup duration in ms
|
||||
* if the duration is less than this value, it will be treated as a click
|
||||
* @default 1500
|
||||
*/
|
||||
clickThreshold?: number;
|
||||
|
||||
/**
|
||||
* if enabled, when clicked, will trigger drag, press ESC or reclick to cancel
|
||||
*/
|
||||
clickToDrag?: boolean;
|
||||
/**
|
||||
* the scale of the element inside {@link EdgelessDraggableElementController.scopeElement}
|
||||
* when {@link EdgelessDraggableElementOptions.clickToDrag} is enabled
|
||||
* @default 1.2
|
||||
*/
|
||||
clickToDragScale?: number;
|
||||
|
||||
/**
|
||||
* To verify if the move is valid
|
||||
*/
|
||||
isValidMove?: (offset: { x: number; y: number }) => boolean;
|
||||
|
||||
/**
|
||||
* when element is clicked - mouse down and up without moving
|
||||
*/
|
||||
onElementClick?: (element: ElementInfo<T>) => void;
|
||||
/**
|
||||
* when mouse down and moved, create overlay, customize overlay here
|
||||
*/
|
||||
onOverlayCreated?: (overlay: OverlayLayer, element: ElementInfo<T>) => void;
|
||||
/**
|
||||
* trigger when enter/leave the scope element
|
||||
*/
|
||||
onEnterOrLeaveScope?: (overlay: OverlayLayer, isOutside?: boolean) => void;
|
||||
/**
|
||||
* Drop the element on edgeless canvas
|
||||
*/
|
||||
onDrop?: (element: ElementInfo<T>, bound: Bound) => void;
|
||||
|
||||
/**
|
||||
* - ESC pressed
|
||||
* - or not dragged out and released
|
||||
*/
|
||||
onCanceled?: (overlay: OverlayLayer, element: ElementInfo<T>) => void;
|
||||
}
|
||||
|
||||
export type ElementInfo<T> = {
|
||||
// TODO: maybe make it optional, if not provided, clone event target
|
||||
preview: TemplateResult;
|
||||
data: T;
|
||||
/**
|
||||
* Override the value in {@link EdgelessDraggableElementOptions.standardWidth}
|
||||
*/
|
||||
standardWidth?: number;
|
||||
};
|
||||
|
||||
export const defaultIsValidMove = (offset: { x: number; y: number }) => {
|
||||
return Math.abs(offset.x) > 50 || Math.abs(offset.y) > 50;
|
||||
};
|
||||
@@ -1,182 +0,0 @@
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
} from '../context.js';
|
||||
|
||||
export class EdgelessSlideMenu extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
max-width: 100%;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.slide-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.menu-container {
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: none;
|
||||
position: relative;
|
||||
height: calc(var(--menu-height) + 1px);
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.slide-menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
transition: left 0.5s ease-in-out;
|
||||
}
|
||||
.next-slide-button,
|
||||
.previous-slide-button {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
color: var(--affine-icon-color);
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.5s ease-in-out;
|
||||
z-index: 12;
|
||||
}
|
||||
.next-slide-button {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%) scale(0.5);
|
||||
}
|
||||
.next-slide-button:hover {
|
||||
cursor: pointer;
|
||||
transform: translate(50%, -50%) scale(1);
|
||||
}
|
||||
.previous-slide-button {
|
||||
opacity: 0;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
}
|
||||
.previous-slide-button:hover {
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
.previous-slide-button svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleSlideButtonClick(direction: 'left' | 'right') {
|
||||
const totalWidth = this._slideMenuContent.clientWidth;
|
||||
const currentScrollLeft = this._menuContainer.scrollLeft;
|
||||
const menuWidth = this._menuContainer.clientWidth;
|
||||
const newLeft =
|
||||
currentScrollLeft + (direction === 'left' ? -menuWidth : menuWidth);
|
||||
this._menuContainer.scrollTo({
|
||||
left: Math.max(0, Math.min(newLeft, totalWidth)),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
private _handleWheel(event: WheelEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private _toggleSlideButton() {
|
||||
const scrollLeft = this._menuContainer.scrollLeft;
|
||||
const menuWidth = this._menuContainer.clientWidth;
|
||||
|
||||
const leftMin = 0;
|
||||
const leftMax = this._slideMenuContent.clientWidth - menuWidth + 2; // border is 2
|
||||
this.showPrevious = scrollLeft > leftMin;
|
||||
this.showNext = scrollLeft < leftMax;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
setTimeout(this._toggleSlideButton.bind(this), 0);
|
||||
this._disposables.addFromEvent(this._menuContainer, 'scrollend', () => {
|
||||
this._toggleSlideButton();
|
||||
});
|
||||
this._disposables.add(
|
||||
this.toolbarSlots.resize.on(() => this._toggleSlideButton())
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const iconSize = { width: '32px', height: '32px' };
|
||||
|
||||
return html`
|
||||
<div class="slide-menu-wrapper">
|
||||
<div
|
||||
class="previous-slide-button"
|
||||
@click=${() => this._handleSlideButtonClick('left')}
|
||||
style=${styleMap({ opacity: this.showPrevious ? '1' : '0' })}
|
||||
>
|
||||
${ArrowRightSmallIcon(iconSize)}
|
||||
</div>
|
||||
<div
|
||||
class="menu-container"
|
||||
style=${styleMap({ '--menu-height': this.height })}
|
||||
>
|
||||
<div class="slide-menu-content" @wheel=${this._handleWheel}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style=${styleMap({ opacity: this.showNext ? '1' : '0' })}
|
||||
class="next-slide-button"
|
||||
@click=${() => this._handleSlideButtonClick('right')}
|
||||
>
|
||||
${ArrowRightSmallIcon(iconSize)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.menu-container')
|
||||
private accessor _menuContainer!: HTMLDivElement;
|
||||
|
||||
@query('.slide-menu-content')
|
||||
private accessor _slideMenuContent!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor height = '40px';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showNext = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showPrevious = false;
|
||||
|
||||
@consume({ context: edgelessToolbarSlotsContext })
|
||||
accessor toolbarSlots!: EdgelessToolbarSlots;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-slide-menu': EdgelessSlideMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { ArrowUpSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
export class ToolbarArrowUpIcon extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
.arrow-up-icon {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<span class="arrow-up-icon"> ${ArrowUpSmallIcon()} </span>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { MenuConfig } from '@blocksuite/affine-components/context-menu';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
|
||||
/**
|
||||
* Helper function to build a menu configuration for a tool in dense mode
|
||||
*/
|
||||
export type DenseMenuBuilder = (
|
||||
edgeless: EdgelessRootBlockComponent
|
||||
) => MenuConfig;
|
||||
@@ -1,57 +0,0 @@
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { ConnectorMode } from '@blocksuite/affine-model';
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
ConnectorCIcon,
|
||||
ConnectorEIcon,
|
||||
ConnectorLIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
|
||||
import type { DenseMenuBuilder } from '../common/type.js';
|
||||
|
||||
export const buildConnectorDenseMenu: DenseMenuBuilder = edgeless => {
|
||||
const prevMode =
|
||||
edgeless.std.get(EditPropsStore).lastProps$.value.connector.mode;
|
||||
|
||||
const isSelected = edgeless.gfx.tool.currentToolName$.peek() === 'connector';
|
||||
|
||||
const createSelect =
|
||||
(mode: ConnectorMode, record = true) =>
|
||||
() => {
|
||||
edgeless.gfx.tool.setTool('connector', {
|
||||
mode,
|
||||
});
|
||||
record &&
|
||||
edgeless.std.get(EditPropsStore).recordLastProps('connector', { mode });
|
||||
};
|
||||
|
||||
const iconSize = { width: '20', height: '20' };
|
||||
return menu.subMenu({
|
||||
name: 'Connector',
|
||||
prefix: ConnectorCIcon(iconSize),
|
||||
select: createSelect(prevMode, false),
|
||||
isSelected,
|
||||
options: {
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Curve',
|
||||
prefix: ConnectorCIcon(iconSize),
|
||||
select: createSelect(ConnectorMode.Curve),
|
||||
isSelected: isSelected && prevMode === ConnectorMode.Curve,
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Elbowed',
|
||||
prefix: ConnectorEIcon(iconSize),
|
||||
select: createSelect(ConnectorMode.Orthogonal),
|
||||
isSelected: isSelected && prevMode === ConnectorMode.Orthogonal,
|
||||
}),
|
||||
menu.action({
|
||||
name: 'Straight',
|
||||
prefix: ConnectorLIcon(iconSize),
|
||||
select: createSelect(ConnectorMode.Straight),
|
||||
isSelected: isSelected && prevMode === ConnectorMode.Straight,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,156 +0,0 @@
|
||||
import { ConnectorMode, DefaultTheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
FeatureFlagService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ConnectorCIcon,
|
||||
ConnectorEIcon,
|
||||
ConnectorLIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import type { LineWidthEvent } from '../../panel/line-width-panel.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
|
||||
function ConnectorModeButtonGroup(
|
||||
mode: ConnectorMode,
|
||||
setConnectorMode: (props: Record<string, unknown>) => void
|
||||
) {
|
||||
/**
|
||||
* There is little hacky on rendering tooltip.
|
||||
* We don't want either tooltip overlap the top button or tooltip on left.
|
||||
* So we put the lower button's tooltip as the first element of the button group container
|
||||
*/
|
||||
return html`
|
||||
<div class="connector-mode-button-group">
|
||||
<edgeless-tool-icon-button
|
||||
.active=${mode === ConnectorMode.Curve}
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${'Curve'}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => setConnectorMode({ mode: ConnectorMode.Curve })}
|
||||
>
|
||||
${ConnectorCIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.active=${mode === ConnectorMode.Orthogonal}
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${'Elbowed'}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => setConnectorMode({ mode: ConnectorMode.Orthogonal })}
|
||||
>
|
||||
${ConnectorEIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.active=${mode === ConnectorMode.Straight}
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${'Straight'}
|
||||
.iconSize=${'20px'}
|
||||
@click=${() => setConnectorMode({ mode: ConnectorMode.Straight })}
|
||||
>
|
||||
${ConnectorLIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export class EdgelessConnectorMenu extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.connector-submenu-content {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.connector-mode-button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.connector-mode-button-group > edgeless-tool-icon-button svg {
|
||||
fill: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.submenu-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
margin: 0 16px;
|
||||
background-color: var(--affine-border-color);
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _props$ = computed(() => {
|
||||
const { mode, stroke, strokeWidth } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value.connector;
|
||||
return { mode, stroke, strokeWidth };
|
||||
});
|
||||
|
||||
private readonly _theme$ = computed(() => {
|
||||
return this.edgeless.std.get(ThemeProvider).theme$.value;
|
||||
});
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'connector';
|
||||
|
||||
override render() {
|
||||
const { stroke, strokeWidth, mode } = this._props$.value;
|
||||
const connectorModeButtonGroup = ConnectorModeButtonGroup(
|
||||
mode,
|
||||
this.onChange
|
||||
);
|
||||
|
||||
return html`
|
||||
<edgeless-slide-menu>
|
||||
<div class="connector-submenu-content">
|
||||
${connectorModeButtonGroup}
|
||||
<div class="submenu-divider"></div>
|
||||
<edgeless-line-width-panel
|
||||
.selectedSize=${strokeWidth}
|
||||
@select=${(e: LineWidthEvent) =>
|
||||
this.onChange({ strokeWidth: e.detail })}
|
||||
>
|
||||
</edgeless-line-width-panel>
|
||||
<div class="submenu-divider"></div>
|
||||
<edgeless-color-panel
|
||||
class="one-way"
|
||||
.value=${stroke}
|
||||
.theme=${this._theme$.value}
|
||||
.palettes=${DefaultTheme.StrokeColorPalettes}
|
||||
.hasTransparent=${!this.edgeless.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_color_picker')}
|
||||
@select=${(e: ColorEvent) =>
|
||||
this.onChange({ stroke: e.detail.value })}
|
||||
></edgeless-color-panel>
|
||||
</div>
|
||||
</edgeless-slide-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (props: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-connector-menu': EdgelessConnectorMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { ConnectorMode, getConnectorModeName } from '@blocksuite/affine-model';
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ConnectorCIcon,
|
||||
ConnectorEIcon,
|
||||
ConnectorLIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
|
||||
const IcomMap = {
|
||||
[ConnectorMode.Straight]: ConnectorLIcon(),
|
||||
[ConnectorMode.Orthogonal]: ConnectorEIcon(),
|
||||
[ConnectorMode.Curve]: ConnectorCIcon(),
|
||||
};
|
||||
|
||||
export class EdgelessConnectorToolButton extends QuickToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _mode$ = computed(() => {
|
||||
return this.edgeless.std.get(EditPropsStore).lastProps$.value.connector
|
||||
.mode;
|
||||
});
|
||||
|
||||
override type = 'connector' as const;
|
||||
|
||||
private _toggleMenu() {
|
||||
if (this.tryDisposePopper()) return;
|
||||
|
||||
const menu = this.createPopper('edgeless-connector-menu', this);
|
||||
menu.element.edgeless = this.edgeless;
|
||||
menu.element.onChange = (props: Record<string, unknown>) => {
|
||||
this.edgeless.std.get(EditPropsStore).recordLastProps('connector', props);
|
||||
this.setEdgelessTool(this.type, {
|
||||
mode: this._mode$.value,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { active } = this;
|
||||
const mode = this._mode$.value;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-connector-button"
|
||||
.tooltip=${this.popper
|
||||
? ''
|
||||
: getTooltipWithShortcut(getConnectorModeName(mode), 'C')}
|
||||
.tooltipOffset=${17}
|
||||
.active=${active}
|
||||
.iconContainerPadding=${6}
|
||||
.iconSize=${'24px'}
|
||||
@click=${() => {
|
||||
// don't update tool before toggling menu
|
||||
this._toggleMenu();
|
||||
this.edgeless.gfx.tool.setTool('connector', {
|
||||
mode,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${IcomMap[mode]}
|
||||
<toolbar-arrow-up-icon></toolbar-arrow-up-icon>
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-connector-tool-button': EdgelessConnectorToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { ColorScheme } from '@blocksuite/affine-model';
|
||||
import type { Slot } from '@blocksuite/global/utils';
|
||||
import { createContext } from '@lit/context';
|
||||
|
||||
import type { EdgelessToolbarWidget } from './edgeless-toolbar.js';
|
||||
|
||||
export interface EdgelessToolbarSlots {
|
||||
resize: Slot<{ w: number; h: number }>;
|
||||
}
|
||||
|
||||
export const edgelessToolbarSlotsContext = createContext<EdgelessToolbarSlots>(
|
||||
Symbol('edgelessToolbarSlotsContext')
|
||||
);
|
||||
|
||||
export const edgelessToolbarThemeContext = createContext<ColorScheme>(
|
||||
Symbol('edgelessToolbarThemeContext')
|
||||
);
|
||||
|
||||
export const edgelessToolbarContext = createContext<EdgelessToolbarWidget>(
|
||||
Symbol('edgelessToolbarContext')
|
||||
);
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { HandIcon, SelectIcon } from '@blocksuite/icons/lit';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
export class EdgelessDefaultToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
.current-icon {
|
||||
transition: 100ms;
|
||||
}
|
||||
.current-icon > svg {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'][] = ['default', 'pan'];
|
||||
|
||||
private _changeTool() {
|
||||
if (this.toolbar.activePopper) {
|
||||
// click manually always closes the popper
|
||||
this.toolbar.activePopper.dispose();
|
||||
}
|
||||
const type = this.edgelessTool?.type;
|
||||
if (type !== 'default' && type !== 'pan') {
|
||||
if (localStorage.defaultTool === 'default') {
|
||||
this.setEdgelessTool('default');
|
||||
} else if (localStorage.defaultTool === 'pan') {
|
||||
this.setEdgelessTool('pan', { panning: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._fadeOut();
|
||||
// wait for animation to finish
|
||||
setTimeout(() => {
|
||||
if (type === 'default') {
|
||||
this.setEdgelessTool('pan', { panning: false });
|
||||
} else if (type === 'pan') {
|
||||
this.setEdgelessTool('default');
|
||||
}
|
||||
this._fadeIn();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
private _fadeIn() {
|
||||
this.currentIcon.style.opacity = '1';
|
||||
this.currentIcon.style.transform = `translateY(0px)`;
|
||||
}
|
||||
|
||||
private _fadeOut() {
|
||||
this.currentIcon.style.opacity = '0';
|
||||
this.currentIcon.style.transform = `translateY(-5px)`;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (!localStorage.defaultTool) {
|
||||
localStorage.defaultTool = 'default';
|
||||
}
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const tool = this.edgeless.gfx.tool.currentToolName$.value;
|
||||
if (tool === 'default' || tool === 'pan') {
|
||||
localStorage.defaultTool = tool;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const type = this.edgelessTool?.type;
|
||||
const { active } = this;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-default-button ${type}"
|
||||
.tooltip=${type === 'pan'
|
||||
? getTooltipWithShortcut('Hand', 'H')
|
||||
: getTooltipWithShortcut('Select', 'V')}
|
||||
.tooltipOffset=${17}
|
||||
.active=${active}
|
||||
.iconContainerPadding=${6}
|
||||
.iconSize=${'24px'}
|
||||
@click=${this._changeTool}
|
||||
>
|
||||
<div class="current-icon">
|
||||
${localStorage.defaultTool === 'default' ? SelectIcon() : HandIcon()}
|
||||
</div>
|
||||
<toolbar-arrow-up-icon></toolbar-arrow-up-icon>
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.current-icon')
|
||||
accessor currentIcon!: HTMLInputElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-default-tool-button': EdgelessDefaultToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,696 +0,0 @@
|
||||
/* oxlint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import {
|
||||
type MenuHandler,
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import {
|
||||
darkToolbarStyles,
|
||||
lightToolbarStyles,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import { ColorScheme, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WidgetComponent } from '@blocksuite/block-std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import { debounce, Slot } from '@blocksuite/global/utils';
|
||||
import {
|
||||
ArrowLeftSmallIcon,
|
||||
ArrowRightSmallIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { autoPlacement, offset } from '@floating-ui/dom';
|
||||
import { ContextProvider } from '@lit/context';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { baseTheme, cssVar } from '@toeverything/theme';
|
||||
import { css, html, nothing, unsafeCSS } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { cache } from 'lit/directives/cache.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js';
|
||||
import type { MenuPopper } from './common/create-popper.js';
|
||||
import {
|
||||
edgelessToolbarContext,
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
edgelessToolbarThemeContext,
|
||||
} from './context.js';
|
||||
import { getQuickTools, getSeniorTools } from './tools.js';
|
||||
|
||||
const TOOLBAR_PADDING_X = 12;
|
||||
const TOOLBAR_HEIGHT = 64;
|
||||
const QUICK_TOOLS_GAP = 10;
|
||||
const QUICK_TOOL_SIZE = 36;
|
||||
const QUICK_TOOL_MORE_SIZE = 20;
|
||||
const SENIOR_TOOLS_GAP = 0;
|
||||
const SENIOR_TOOL_WIDTH = 96;
|
||||
const SENIOR_TOOL_NAV_SIZE = 20;
|
||||
const DIVIDER_WIDTH = 8;
|
||||
const DIVIDER_SPACE = 8;
|
||||
const SAFE_AREA_WIDTH = 64;
|
||||
|
||||
export const EDGELESS_TOOLBAR_WIDGET = 'edgeless-toolbar-widget';
|
||||
export class EdgelessToolbarWidget extends WidgetComponent<
|
||||
RootBlockModel,
|
||||
EdgelessRootBlockComponent
|
||||
> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: calc(50%);
|
||||
transform: translateX(-50%);
|
||||
bottom: 0;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
.edgeless-toolbar-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.edgeless-toolbar-wrapper[data-app-theme='light'] {
|
||||
${unsafeCSS(lightToolbarStyles.join('\n'))}
|
||||
}
|
||||
.edgeless-toolbar-wrapper[data-app-theme='dark'] {
|
||||
${unsafeCSS(darkToolbarStyles.join('\n'))}
|
||||
}
|
||||
.edgeless-toolbar-toggle-control {
|
||||
pointer-events: auto;
|
||||
padding-bottom: 16px;
|
||||
width: fit-content;
|
||||
max-width: calc(100% - ${unsafeCSS(SAFE_AREA_WIDTH)}px * 2);
|
||||
min-width: 264px;
|
||||
}
|
||||
.edgeless-toolbar-toggle-control[data-enable='true'] {
|
||||
transition: 0.23s ease;
|
||||
padding-top: 100px;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
.edgeless-toolbar-toggle-control[data-enable='true']:hover {
|
||||
padding-top: 0;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.edgeless-toolbar-smooth-corner {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
.edgeless-toolbar-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 ${unsafeCSS(TOOLBAR_PADDING_X)}px;
|
||||
height: ${unsafeCSS(TOOLBAR_HEIGHT)}px;
|
||||
}
|
||||
:host([disabled]) .edgeless-toolbar-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
.edgeless-toolbar-container[level='second'] {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
.edgeless-toolbar-container[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.quick-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: ${unsafeCSS(QUICK_TOOLS_GAP)}px;
|
||||
}
|
||||
.full-divider {
|
||||
width: ${unsafeCSS(DIVIDER_WIDTH)}px;
|
||||
height: 100%;
|
||||
margin: 0 ${unsafeCSS(DIVIDER_SPACE)}px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.full-divider::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--affine-border-color);
|
||||
}
|
||||
.brush-and-eraser {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
.senior-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: ${unsafeCSS(SENIOR_TOOLS_GAP)}px;
|
||||
height: 100%;
|
||||
min-width: ${unsafeCSS(SENIOR_TOOL_WIDTH)}px;
|
||||
}
|
||||
.quick-tool-item {
|
||||
width: ${unsafeCSS(QUICK_TOOL_SIZE)}px;
|
||||
height: ${unsafeCSS(QUICK_TOOL_SIZE)}px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.quick-tool-more {
|
||||
width: 0;
|
||||
height: ${unsafeCSS(QUICK_TOOL_SIZE)}px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.23s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-dense-quick='true'] .quick-tool-more {
|
||||
width: ${unsafeCSS(QUICK_TOOL_MORE_SIZE)}px;
|
||||
margin-left: ${unsafeCSS(DIVIDER_SPACE)}px;
|
||||
}
|
||||
.quick-tool-more-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.senior-tool-item {
|
||||
width: ${unsafeCSS(SENIOR_TOOL_WIDTH)}px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.senior-nav-button-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 0px;
|
||||
height: ${unsafeCSS(SENIOR_TOOL_NAV_SIZE)}px;
|
||||
transition: width 0.23s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.senior-nav-button {
|
||||
padding: 0;
|
||||
}
|
||||
.senior-nav-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
[data-dense-senior='true'] .senior-nav-button-wrapper {
|
||||
width: ${unsafeCSS(SENIOR_TOOL_NAV_SIZE)}px;
|
||||
}
|
||||
[data-dense-senior='true'] .senior-nav-button-wrapper.prev {
|
||||
margin-right: ${unsafeCSS(DIVIDER_SPACE)}px;
|
||||
}
|
||||
[data-dense-senior='true'] .senior-nav-button-wrapper.next {
|
||||
margin-left: ${unsafeCSS(DIVIDER_SPACE)}px;
|
||||
}
|
||||
.transform-button svg {
|
||||
transition: 0.3s ease-in-out;
|
||||
}
|
||||
.transform-button:hover svg {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _appTheme$ = computed(() => {
|
||||
return this.std.get(ThemeProvider).app$.value;
|
||||
});
|
||||
|
||||
private _moreQuickToolsMenu: MenuHandler | null = null;
|
||||
|
||||
private _moreQuickToolsMenuRef: HTMLElement | null = null;
|
||||
|
||||
@state()
|
||||
accessor containerWidth = 1920;
|
||||
|
||||
private readonly _onContainerResize = debounce(({ w }: { w: number }) => {
|
||||
if (!this.isConnected) return;
|
||||
|
||||
this.slots.resize.emit({ w, h: TOOLBAR_HEIGHT });
|
||||
this.containerWidth = w;
|
||||
|
||||
if (this._denseSeniorTools) {
|
||||
this.scrollSeniorToolIndex = Math.min(
|
||||
this._seniorTools.length - this.scrollSeniorToolSize,
|
||||
this.scrollSeniorToolIndex
|
||||
);
|
||||
} else {
|
||||
this.scrollSeniorToolIndex = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
this._denseQuickTools &&
|
||||
this._moreQuickToolsMenu &&
|
||||
this._moreQuickToolsMenuRef
|
||||
) {
|
||||
this._moreQuickToolsMenu.close();
|
||||
this._openMoreQuickToolsMenu({
|
||||
currentTarget: this._moreQuickToolsMenuRef,
|
||||
});
|
||||
}
|
||||
if (!this._denseQuickTools && this._moreQuickToolsMenu) {
|
||||
this._moreQuickToolsMenu.close();
|
||||
this._moreQuickToolsMenu = null;
|
||||
}
|
||||
}, 300);
|
||||
|
||||
private _resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
private readonly _slotsProvider = new ContextProvider(this, {
|
||||
context: edgelessToolbarSlotsContext,
|
||||
initialValue: { resize: new Slot() } satisfies EdgelessToolbarSlots,
|
||||
});
|
||||
|
||||
private readonly _themeProvider = new ContextProvider(this, {
|
||||
context: edgelessToolbarThemeContext,
|
||||
initialValue: ColorScheme.Light,
|
||||
});
|
||||
|
||||
private readonly _toolbarProvider = new ContextProvider(this, {
|
||||
context: edgelessToolbarContext,
|
||||
initialValue: this,
|
||||
});
|
||||
|
||||
activePopper: MenuPopper<HTMLElement> | null = null;
|
||||
|
||||
// calculate all the width manually
|
||||
private get _availableWidth() {
|
||||
return this.containerWidth - 2 * SAFE_AREA_WIDTH;
|
||||
}
|
||||
|
||||
private get _cachedPresentHideToolbar() {
|
||||
return !!this.std.get(EditPropsStore).getStorage('presentHideToolbar');
|
||||
}
|
||||
|
||||
private get _denseQuickTools() {
|
||||
return (
|
||||
this._availableWidth -
|
||||
this._seniorToolNavWidth -
|
||||
1 * SENIOR_TOOL_WIDTH -
|
||||
2 * TOOLBAR_PADDING_X <
|
||||
this._quickToolsWidthTotal
|
||||
);
|
||||
}
|
||||
|
||||
private get _denseSeniorTools() {
|
||||
return (
|
||||
this._availableWidth -
|
||||
this._quickToolsWidthTotal -
|
||||
this._spaceWidthTotal <
|
||||
this._seniorToolsWidthTotal
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When enabled, the toolbar will auto-hide when the mouse is not over it.
|
||||
*/
|
||||
private get _enableAutoHide() {
|
||||
return (
|
||||
this.isPresentMode &&
|
||||
this._cachedPresentHideToolbar &&
|
||||
!this.presentSettingMenuShow &&
|
||||
!this.presentFrameMenuShow
|
||||
);
|
||||
}
|
||||
|
||||
private get _hiddenQuickTools() {
|
||||
return this._quickTools
|
||||
.slice(this._visibleQuickToolSize)
|
||||
.filter(tool => !!tool.menu);
|
||||
}
|
||||
|
||||
private get _quickTools() {
|
||||
return getQuickTools({ edgeless: this.block });
|
||||
}
|
||||
|
||||
private get _quickToolsWidthTotal() {
|
||||
return (
|
||||
this._quickTools.length * (QUICK_TOOL_SIZE + QUICK_TOOLS_GAP) -
|
||||
QUICK_TOOLS_GAP
|
||||
);
|
||||
}
|
||||
|
||||
private get _seniorNextTooltip() {
|
||||
if (this._seniorScrollNextDisabled) return '';
|
||||
const nextTool =
|
||||
this._seniorTools[this.scrollSeniorToolIndex + this.scrollSeniorToolSize];
|
||||
return nextTool?.name ?? '';
|
||||
}
|
||||
|
||||
private get _seniorPrevTooltip() {
|
||||
if (this._seniorScrollPrevDisabled) return '';
|
||||
const prevTool = this._seniorTools[this.scrollSeniorToolIndex - 1];
|
||||
return prevTool?.name ?? '';
|
||||
}
|
||||
|
||||
private get _seniorScrollNextDisabled() {
|
||||
return (
|
||||
this.scrollSeniorToolIndex + this.scrollSeniorToolSize >=
|
||||
this._seniorTools.length
|
||||
);
|
||||
}
|
||||
|
||||
private get _seniorScrollPrevDisabled() {
|
||||
return this.scrollSeniorToolIndex === 0;
|
||||
}
|
||||
|
||||
private get _seniorToolNavWidth() {
|
||||
return this._denseSeniorTools
|
||||
? (SENIOR_TOOL_NAV_SIZE + DIVIDER_SPACE) * 2
|
||||
: 0;
|
||||
}
|
||||
|
||||
private get _seniorTools() {
|
||||
return getSeniorTools({
|
||||
edgeless: this.block,
|
||||
toolbarContainer: this.toolbarContainer,
|
||||
});
|
||||
}
|
||||
|
||||
private get _seniorToolsWidthTotal() {
|
||||
return (
|
||||
this._seniorTools.length * (SENIOR_TOOL_WIDTH + SENIOR_TOOLS_GAP) -
|
||||
SENIOR_TOOLS_GAP
|
||||
);
|
||||
}
|
||||
|
||||
private get _spaceWidthTotal() {
|
||||
return DIVIDER_WIDTH + DIVIDER_SPACE * 2 + TOOLBAR_PADDING_X * 2;
|
||||
}
|
||||
|
||||
private get _visibleQuickToolSize() {
|
||||
if (!this._denseQuickTools) return this._quickTools.length;
|
||||
const availableWidth =
|
||||
this._availableWidth -
|
||||
this._seniorToolNavWidth -
|
||||
this._spaceWidthTotal -
|
||||
SENIOR_TOOL_WIDTH;
|
||||
return Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
(availableWidth - QUICK_TOOL_MORE_SIZE - DIVIDER_SPACE) /
|
||||
(QUICK_TOOL_SIZE + QUICK_TOOLS_GAP)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get edgelessTool() {
|
||||
return this.gfx.tool.currentToolOption$.value;
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get isPresentMode() {
|
||||
return this.edgelessTool.type === 'frameNavigator';
|
||||
}
|
||||
|
||||
get scrollSeniorToolSize() {
|
||||
if (this._denseQuickTools) return 1;
|
||||
const seniorAvailableWidth =
|
||||
this._availableWidth - this._quickToolsWidthTotal - this._spaceWidthTotal;
|
||||
if (seniorAvailableWidth >= this._seniorToolsWidthTotal)
|
||||
return this._seniorTools.length;
|
||||
return (
|
||||
Math.floor(
|
||||
(seniorAvailableWidth - (SENIOR_TOOL_NAV_SIZE + DIVIDER_SPACE) * 2) /
|
||||
SENIOR_TOOL_WIDTH
|
||||
) || 1
|
||||
);
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return this._slotsProvider.value;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private _onSeniorNavNext() {
|
||||
if (this._seniorScrollNextDisabled) return;
|
||||
this.scrollSeniorToolIndex = Math.min(
|
||||
this._seniorTools.length - this.scrollSeniorToolSize,
|
||||
this.scrollSeniorToolIndex + this.scrollSeniorToolSize
|
||||
);
|
||||
}
|
||||
|
||||
private _onSeniorNavPrev() {
|
||||
if (this._seniorScrollPrevDisabled) return;
|
||||
this.scrollSeniorToolIndex = Math.max(
|
||||
0,
|
||||
this.scrollSeniorToolIndex - this.scrollSeniorToolSize
|
||||
);
|
||||
}
|
||||
|
||||
private _openMoreQuickToolsMenu(e: { currentTarget: HTMLElement }) {
|
||||
if (!this._hiddenQuickTools.length) return;
|
||||
|
||||
this._moreQuickToolsMenuRef = e.currentTarget;
|
||||
this._moreQuickToolsMenu = popMenu(
|
||||
popupTargetFromElement(e.currentTarget as HTMLElement),
|
||||
{
|
||||
middleware: [
|
||||
autoPlacement({
|
||||
allowedPlacements: ['top'],
|
||||
}),
|
||||
offset({
|
||||
mainAxis: (TOOLBAR_HEIGHT - QUICK_TOOL_MORE_SIZE) / 2 + 8,
|
||||
}),
|
||||
],
|
||||
options: {
|
||||
onClose: () => {
|
||||
this._moreQuickToolsMenu = null;
|
||||
this._moreQuickToolsMenuRef = null;
|
||||
},
|
||||
items: this._hiddenQuickTools.map(tool => tool.menu!),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _renderContent() {
|
||||
return html`
|
||||
<div class="quick-tools">
|
||||
${this._quickTools
|
||||
.slice(0, this._visibleQuickToolSize)
|
||||
.map(
|
||||
tool => html`<div class="quick-tool-item">${tool.content}</div>`
|
||||
)}
|
||||
</div>
|
||||
<div class="quick-tool-more">
|
||||
<icon-button
|
||||
?disabled=${!this._denseQuickTools}
|
||||
.size=${20}
|
||||
class="quick-tool-more-button"
|
||||
@click=${this._openMoreQuickToolsMenu}
|
||||
?active=${this._quickTools
|
||||
.slice(this._visibleQuickToolSize)
|
||||
.some(tool => tool.type === this.edgelessTool?.type)}
|
||||
>
|
||||
${MoreHorizontalIcon({ width: '20px', height: '20px' })}
|
||||
<affine-tooltip tip-position="top" .offset=${25}>
|
||||
More Tools
|
||||
</affine-tooltip>
|
||||
</icon-button>
|
||||
</div>
|
||||
<div class="full-divider"></div>
|
||||
<div class="senior-nav-button-wrapper prev">
|
||||
<icon-button
|
||||
.size=${20}
|
||||
class="senior-nav-button"
|
||||
?disabled=${this._seniorScrollPrevDisabled}
|
||||
@click=${this._onSeniorNavPrev}
|
||||
>
|
||||
${ArrowLeftSmallIcon({ width: '20px', height: '20px' })}
|
||||
${cache(
|
||||
this._seniorPrevTooltip
|
||||
? html` <affine-tooltip tip-position="top" .offset=${4}>
|
||||
${this._seniorPrevTooltip}
|
||||
</affine-tooltip>`
|
||||
: nothing
|
||||
)}
|
||||
</icon-button>
|
||||
</div>
|
||||
<div class="senior-tools">
|
||||
${this._seniorTools
|
||||
.slice(
|
||||
this.scrollSeniorToolIndex,
|
||||
this.scrollSeniorToolIndex + this.scrollSeniorToolSize
|
||||
)
|
||||
.map(
|
||||
tool => html`<div class="senior-tool-item">${tool.content}</div>`
|
||||
)}
|
||||
</div>
|
||||
<div class="senior-nav-button-wrapper next">
|
||||
<icon-button
|
||||
.size=${20}
|
||||
class="senior-nav-button"
|
||||
?disabled=${this._seniorScrollNextDisabled}
|
||||
@click=${this._onSeniorNavNext}
|
||||
>
|
||||
${ArrowRightSmallIcon({ width: '20px', height: '20px' })}
|
||||
${cache(
|
||||
this._seniorNextTooltip
|
||||
? html` <affine-tooltip tip-position="top" .offset=${4}>
|
||||
${this._seniorNextTooltip}
|
||||
</affine-tooltip>`
|
||||
: nothing
|
||||
)}
|
||||
</icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._toolbarProvider.setValue(this);
|
||||
this._resizeObserver = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const { width } = entry.contentRect;
|
||||
this._onContainerResize({ w: width });
|
||||
}
|
||||
});
|
||||
this._resizeObserver.observe(this);
|
||||
this.disposables.add(
|
||||
this.std
|
||||
.get(ThemeProvider)
|
||||
.theme$.subscribe(mode => this._themeProvider.setValue(mode))
|
||||
);
|
||||
this._disposables.add(
|
||||
this.block.bindHotKey(
|
||||
{
|
||||
Escape: () => {
|
||||
if (this.gfx.selection.editing) return;
|
||||
if (this.edgelessTool.type === 'frameNavigator') return;
|
||||
if (this.edgelessTool.type === 'default') {
|
||||
if (this.activePopper) {
|
||||
this.activePopper.dispose();
|
||||
this.activePopper = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.gfx.tool.setTool('default');
|
||||
},
|
||||
},
|
||||
{ global: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { _disposables, block, gfx } = this;
|
||||
|
||||
_disposables.add(
|
||||
gfx.viewport.viewportUpdated.on(() => this.requestUpdate())
|
||||
);
|
||||
_disposables.add(
|
||||
block.slots.readonlyUpdated.on(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
_disposables.add(
|
||||
block.slots.toolbarLocked.on(disabled => {
|
||||
this.toggleAttribute('disabled', disabled);
|
||||
})
|
||||
);
|
||||
// This state from `editPropsStore` is not reactive,
|
||||
// if the value is updated outside of this component, it will not be reflected.
|
||||
_disposables.add(
|
||||
this.std.get(EditPropsStore).slots.storageUpdated.on(({ key }) => {
|
||||
if (key === 'presentHideToolbar') {
|
||||
this.requestUpdate();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { type } = this.edgelessTool || {};
|
||||
if (this.doc.readonly && type !== 'frameNavigator') {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-toolbar-wrapper"
|
||||
data-app-theme=${this._appTheme$.value}
|
||||
>
|
||||
<div
|
||||
class="edgeless-toolbar-toggle-control"
|
||||
data-enable=${this._enableAutoHide}
|
||||
>
|
||||
<smooth-corner
|
||||
class="edgeless-toolbar-smooth-corner"
|
||||
.borderRadius=${16}
|
||||
.smooth=${0.7}
|
||||
.borderWidth=${1}
|
||||
.bgColor=${'var(--affine-background-overlay-panel-color)'}
|
||||
.borderColor=${'var(--affine-border-color)'}
|
||||
style="filter: drop-shadow(${cssVar('toolbarShadow')})"
|
||||
>
|
||||
<div
|
||||
class="edgeless-toolbar-container"
|
||||
data-dense-quick=${this._denseQuickTools &&
|
||||
this._hiddenQuickTools.length > 0}
|
||||
data-dense-senior=${this._denseSeniorTools}
|
||||
@dblclick=${stopPropagation}
|
||||
@mousedown=${stopPropagation}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
${this.isPresentMode
|
||||
? html`<presentation-toolbar
|
||||
.edgeless=${this.block}
|
||||
.settingMenuShow=${this.presentSettingMenuShow}
|
||||
.frameMenuShow=${this.presentFrameMenuShow}
|
||||
.setSettingMenuShow=${(show: boolean) =>
|
||||
(this.presentSettingMenuShow = show)}
|
||||
.setFrameMenuShow=${(show: boolean) =>
|
||||
(this.presentFrameMenuShow = show)}
|
||||
.containerWidth=${this.containerWidth}
|
||||
></presentation-toolbar>`
|
||||
: nothing}
|
||||
${this.isPresentMode ? nothing : this._renderContent()}
|
||||
</div>
|
||||
</smooth-corner>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor presentFrameMenuShow = false;
|
||||
|
||||
@state()
|
||||
accessor presentSettingMenuShow = false;
|
||||
|
||||
@state()
|
||||
accessor scrollSeniorToolIndex = 0;
|
||||
|
||||
@query('.edgeless-toolbar-container')
|
||||
accessor toolbarContainer!: HTMLElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-toolbar-widget': EdgelessToolbarWidget;
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { EdgelessEraserDarkIcon, EdgelessEraserLightIcon } from './icons.js';
|
||||
|
||||
export class EdgelessEraserToolButton extends EdgelessToolbarToolMixin(
|
||||
LitElement
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.eraser-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
width: 49px;
|
||||
height: 64px;
|
||||
}
|
||||
#edgeless-eraser-icon {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
.eraser-button:hover #edgeless-eraser-icon,
|
||||
.eraser-button.active #edgeless-eraser-icon {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
override enableActiveBackground = true;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'eraser';
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
this.edgeless.bindHotKey(
|
||||
{
|
||||
Escape: () => {
|
||||
if (this.edgelessTool.type === 'eraser') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
},
|
||||
},
|
||||
{ global: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const type = this.edgelessTool?.type;
|
||||
const appTheme = this.edgeless.std.get(ThemeProvider).app$.value;
|
||||
const icon =
|
||||
appTheme === 'dark' ? EdgelessEraserDarkIcon : EdgelessEraserLightIcon;
|
||||
|
||||
return html`
|
||||
<edgeless-toolbar-button
|
||||
class="edgeless-eraser-button"
|
||||
.tooltip=${getTooltipWithShortcut('Eraser', 'E')}
|
||||
.tooltipOffset=${4}
|
||||
.active=${type === 'eraser'}
|
||||
@click=${() => this.setEdgelessTool({ type: 'eraser' })}
|
||||
>
|
||||
<div class="eraser-button">${icon}</div>
|
||||
</edgeless-toolbar-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-eraser-tool-button': EdgelessEraserToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const EdgelessEraserLightIcon = html`<svg
|
||||
width="44"
|
||||
height="49"
|
||||
viewBox="0 0 44 49"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="edgeless-eraser-icon"
|
||||
>
|
||||
<g filter="url(#filter0_d_5310_64451)">
|
||||
<rect x="6" y="2" width="32" height="59" rx="5.75" fill="#F1F1F1" />
|
||||
<rect x="6.5" y="2.5" width="31" height="58" rx="5.25" stroke="#E3E2E4" />
|
||||
</g>
|
||||
<g filter="url(#filter1_f_5310_64451)">
|
||||
<rect
|
||||
x="36.2002"
|
||||
y="4.44995"
|
||||
width="18.4"
|
||||
height="3.45"
|
||||
rx="1.725"
|
||||
transform="rotate(90 36.2002 4.44995)"
|
||||
fill="white"
|
||||
fill-opacity="0.78"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_5310_64451)">
|
||||
<path
|
||||
d="M4 60H40V22C40 18.6863 37.3137 16 34 16H10C6.68629 16 4 18.6863 4 22V60Z"
|
||||
fill="#343434"
|
||||
fill-opacity="0.22"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
fill="#173654"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
fill="url(#paint0_linear_5310_64451)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
stroke="#E7E7E7"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
fill="#1E96EB"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
fill="url(#paint1_linear_5310_64451)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
stroke="#E7E7E7"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
fill="#EFFAFF"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
fill="url(#paint2_linear_5310_64451)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
stroke="#E7E7E7"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_5310_64451"
|
||||
x="2"
|
||||
y="2"
|
||||
width="40"
|
||||
height="67"
|
||||
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="4" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.258824 0 0 0 0 0.254902 0 0 0 0 0.286275 0 0 0 0.18 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_5310_64451"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5310_64451"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_f_5310_64451"
|
||||
x="30.45"
|
||||
y="2.14995"
|
||||
width="8.0502"
|
||||
height="23"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="1.15"
|
||||
result="effect1_foregroundBlur_5310_64451"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_f_5310_64451"
|
||||
x="0"
|
||||
y="12"
|
||||
width="44"
|
||||
height="52"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
result="effect1_foregroundBlur_5310_64451"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_5310_64451"
|
||||
x1="11"
|
||||
y1="16"
|
||||
x2="11"
|
||||
y2="62"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-opacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_5310_64451"
|
||||
x1="33"
|
||||
y1="16"
|
||||
x2="33"
|
||||
y2="62"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_5310_64451"
|
||||
x1="6"
|
||||
y1="0"
|
||||
x2="6"
|
||||
y2="46"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFF8F8" stop-opacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>`;
|
||||
export const EdgelessEraserDarkIcon = html`<svg
|
||||
width="44"
|
||||
height="49"
|
||||
viewBox="0 0 44 49"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="edgeless-eraser-icon"
|
||||
>
|
||||
<g filter="url(#filter0_d_5310_64471)">
|
||||
<path
|
||||
d="M6 7.75C6 4.57436 8.57436 2 11.75 2H32.25C35.4256 2 38 4.57436 38 7.75V61H6V7.75Z"
|
||||
fill="#C1C1C1"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 7.75C6.5 4.85051 8.85051 2.5 11.75 2.5H32.25C35.1495 2.5 37.5 4.85051 37.5 7.75V60.5H6.5V7.75Z"
|
||||
stroke="#DDDDDD"
|
||||
/>
|
||||
<path
|
||||
d="M6.5 7.75C6.5 4.85051 8.85051 2.5 11.75 2.5H32.25C35.1495 2.5 37.5 4.85051 37.5 7.75V60.5H6.5V7.75Z"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter1_f_5310_64471)">
|
||||
<rect
|
||||
x="36.2002"
|
||||
y="4.44995"
|
||||
width="18.4"
|
||||
height="3.45"
|
||||
rx="1.725"
|
||||
transform="rotate(90 36.2002 4.44995)"
|
||||
fill="white"
|
||||
fill-opacity="0.38"
|
||||
/>
|
||||
</g>
|
||||
<g filter="url(#filter2_f_5310_64471)">
|
||||
<path
|
||||
d="M4 62H40V23C40 19.6863 37.3137 17 34 17H10C6.68629 17 4 19.6863 4 23V62Z"
|
||||
fill="black"
|
||||
fill-opacity="0.44"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
fill="#0D2338"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
fill="url(#paint0_linear_5310_64471)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
stroke="#D3D3D3"
|
||||
/>
|
||||
<path
|
||||
d="M16.5 16.5H11H9.37046C8.79521 16.5 8.29403 16.8921 8.15566 17.4505L8.14174 17.5067C7.93847 18.3269 7.27583 18.9534 6.44553 19.1105C5.89708 19.2142 5.5 19.6934 5.5 20.2516V21.75V27.5V39V61.5H16.5V16.5Z"
|
||||
stroke="black"
|
||||
stroke-opacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
fill="#1A7CC1"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
fill="url(#paint1_linear_5310_64471)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
stroke="#D3D3D3"
|
||||
/>
|
||||
<path
|
||||
d="M27.5 16.5H33H34.6295C35.2048 16.5 35.706 16.8921 35.8443 17.4505L35.8583 17.5067C36.0615 18.3269 36.7242 18.9534 37.5545 19.1105C38.1029 19.2142 38.5 19.6934 38.5 20.2516V21.75V27.5V39V61.5H27.5V16.5Z"
|
||||
stroke="black"
|
||||
stroke-opacity="0.4"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
fill="#D0DFE5"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
fill="url(#paint2_linear_5310_64471)"
|
||||
fill-opacity="0.2"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
stroke="#D3D3D3"
|
||||
/>
|
||||
<rect
|
||||
x="-0.5"
|
||||
y="0.5"
|
||||
width="11"
|
||||
height="45"
|
||||
transform="matrix(-1 0 0 1 27 16)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.4"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_5310_64471"
|
||||
x="2"
|
||||
y="0"
|
||||
width="40"
|
||||
height="67"
|
||||
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="2" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0.195003 0 0 0 0 0.0133398 0 0 0 0 0.0133398 0 0 0 0.66 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_5310_64471"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_5310_64471"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter1_f_5310_64471"
|
||||
x="30.45"
|
||||
y="2.14995"
|
||||
width="8.0502"
|
||||
height="23"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="1.15"
|
||||
result="effect1_foregroundBlur_5310_64471"
|
||||
/>
|
||||
</filter>
|
||||
<filter
|
||||
id="filter2_f_5310_64471"
|
||||
x="0"
|
||||
y="13"
|
||||
width="44"
|
||||
height="53"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
/>
|
||||
<feGaussianBlur
|
||||
stdDeviation="2"
|
||||
result="effect1_foregroundBlur_5310_64471"
|
||||
/>
|
||||
</filter>
|
||||
<linearGradient
|
||||
id="paint0_linear_5310_64471"
|
||||
x1="11"
|
||||
y1="16"
|
||||
x2="11"
|
||||
y2="62"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-opacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_5310_64471"
|
||||
x1="33"
|
||||
y1="16"
|
||||
x2="33"
|
||||
y2="62"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="1" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_5310_64471"
|
||||
x1="6"
|
||||
y1="0"
|
||||
x2="6"
|
||||
y2="46"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FFF8F8" stop-opacity="0" />
|
||||
<stop offset="1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>`;
|
||||
@@ -1,9 +0,0 @@
|
||||
export const FrameConfig: {
|
||||
name: string;
|
||||
wh: [number, number];
|
||||
}[] = [
|
||||
{ name: '1:1', wh: [1200, 1200] },
|
||||
{ name: '4:3', wh: [1600, 1200] },
|
||||
{ name: '16:9', wh: [1600, 900] },
|
||||
{ name: '2:1', wh: [1600, 800] },
|
||||
];
|
||||
@@ -1,30 +0,0 @@
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { FrameIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
import type { DenseMenuBuilder } from '../common/type.js';
|
||||
import { FrameConfig } from './config.js';
|
||||
|
||||
export const buildFrameDenseMenu: DenseMenuBuilder = edgeless =>
|
||||
menu.subMenu({
|
||||
name: 'Frame',
|
||||
prefix: FrameIcon({ width: '20px', height: '20px' }),
|
||||
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }),
|
||||
isSelected: edgeless.gfx.tool.currentToolName$.peek() === 'frame',
|
||||
options: {
|
||||
items: [
|
||||
menu.action({
|
||||
name: 'Custom',
|
||||
select: () => edgeless.gfx.tool.setTool({ type: 'frame' }),
|
||||
}),
|
||||
...FrameConfig.map(config =>
|
||||
menu.action({
|
||||
name: `Slide ${config.name}`,
|
||||
select: () => {
|
||||
edgeless.gfx.tool.setTool('default');
|
||||
edgeless.service.frame.createFrameOnViewportCenter(config.wh);
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { FrameConfig } from './config.js';
|
||||
|
||||
export class EdgelessFrameMenu extends EdgelessToolbarToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: -1;
|
||||
}
|
||||
.menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.frame-add-button {
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
color: var(--affine-text-primary-color);
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-add-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
transition: background-color 0.23s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
.frame-add-button:hover::before {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.custom {
|
||||
width: 60px;
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--affine-border-color);
|
||||
transform: scaleX(0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'frame';
|
||||
|
||||
override render() {
|
||||
const { edgeless } = this;
|
||||
return html`
|
||||
<edgeless-slide-menu .showNext=${false}>
|
||||
<div class="menu-content">
|
||||
<div class="frame-add-button custom">Custom</div>
|
||||
<div class="divider"></div>
|
||||
${repeat(
|
||||
FrameConfig,
|
||||
item => item.name,
|
||||
(item, index) => html`
|
||||
<div
|
||||
@click=${() => {
|
||||
edgeless.gfx.tool.setTool('default');
|
||||
edgeless.service.frame.createFrameOnViewportCenter(item.wh);
|
||||
}}
|
||||
class="frame-add-button ${index}"
|
||||
data-name="${item.name}"
|
||||
data-w="${item.wh[0]}"
|
||||
data-h="${item.wh[1]}"
|
||||
>
|
||||
${item.name}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</edgeless-slide-menu>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-frame-menu': EdgelessFrameMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { FrameIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../../components/utils.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
|
||||
export class EdgelessFrameToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'frame';
|
||||
|
||||
private _toggleFrameMenu() {
|
||||
if (this.tryDisposePopper()) return;
|
||||
|
||||
const menu = this.createPopper('edgeless-frame-menu', this);
|
||||
menu.element.edgeless = this.edgeless;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const type = this.edgelessTool?.type;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-frame-button"
|
||||
.tooltip=${this.popper ? '' : getTooltipWithShortcut('Frame', 'F')}
|
||||
.tooltipOffset=${17}
|
||||
.iconSize=${'24px'}
|
||||
.active=${type === 'frame'}
|
||||
.iconContainerPadding=${6}
|
||||
@click=${() => {
|
||||
// don't update tool before toggling menu
|
||||
this._toggleFrameMenu();
|
||||
this.setEdgelessTool({ type: 'frame' });
|
||||
}}
|
||||
>
|
||||
${FrameIcon()}
|
||||
<toolbar-arrow-up-icon></toolbar-arrow-up-icon>
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-frame-tool-button': EdgelessFrameToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const LassoFreeHandIcon = html`
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.03152 18.9667C8.07902 18.9667 8.92819 18.1175 8.92819 17.07C8.92819 16.0225 8.07902 15.1733 7.03152 15.1733C5.98402 15.1733 5.13485 16.0225 5.13485 17.07C5.13485 18.1175 5.98402 18.9667 7.03152 18.9667ZM7.03152 20.2667C8.79699 20.2667 10.2282 18.8355 10.2282 17.07C10.2282 15.3045 8.79699 13.8733 7.03152 13.8733C5.26605 13.8733 3.83485 15.3045 3.83485 17.07C3.83485 18.8355 5.26605 20.2667 7.03152 20.2667Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.75714 19.6175C4.75714 19.3949 4.81661 19.145 4.93113 18.856C5.02405 18.6215 5.30218 18.5315 5.52776 18.6442C5.75334 18.757 5.83848 19.0293 5.75223 19.2663C5.68785 19.4432 5.67047 19.5529 5.67047 19.6175C5.67047 19.6727 5.67683 19.6824 5.71909 19.7205C5.74837 19.7468 5.77881 19.7695 5.82813 19.8062C5.8416 19.8162 5.85647 19.8273 5.87312 19.8398C5.94101 19.8907 6.03269 19.9615 6.119 20.0543C6.31218 20.2619 6.44109 20.5388 6.44109 20.9276C6.44109 21.3166 6.3093 21.6272 6.02789 21.8443C5.80114 22.0192 5.50339 22.1052 5.27045 22.1724C5.26524 22.1739 5.26006 22.1754 5.25492 22.1769C5.24264 22.1804 5.23031 22.184 5.21794 22.1876C4.8171 22.3032 4.37177 22.4316 3.91148 22.7773C3.70983 22.9288 3.42276 22.9326 3.24443 22.7542C3.06609 22.5759 3.06453 22.2842 3.26124 22.1263C3.89506 21.6177 4.50962 21.4409 4.94956 21.3144C4.96714 21.3093 4.98443 21.3044 5.00143 21.2994C5.28007 21.2189 5.40217 21.1735 5.47003 21.1211C5.471 21.1204 5.47202 21.1196 5.47311 21.1188C5.49238 21.1045 5.52776 21.0781 5.52776 20.9276C5.52776 20.7684 5.48543 20.7141 5.45027 20.6763C5.42241 20.6464 5.38566 20.6159 5.32511 20.5705C5.31683 20.5642 5.30753 20.5574 5.29743 20.5499C5.24552 20.5114 5.17247 20.4573 5.1081 20.3994C4.92203 20.2319 4.75714 19.9905 4.75714 19.6175Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.2086 12.6149C11.9968 11.7531 12.903 11.0458 13.6876 11.4606L22.0945 15.905C22.9737 16.3698 22.7465 17.6876 21.7625 17.8313L17.8897 18.3968C17.8383 18.4043 17.7946 18.4384 17.7749 18.4867L16.3978 21.8656C16.0219 22.7881 14.685 22.6903 14.4473 21.7229L12.2086 12.6149ZM13.2757 12.2397C13.1636 12.1804 13.0342 12.2815 13.0644 12.4046L15.3031 21.5126C15.3371 21.6508 15.528 21.6648 15.5817 21.533L16.9588 18.1541C17.0963 17.8167 17.402 17.5773 17.7624 17.5247L21.6352 16.9593C21.7758 16.9388 21.8082 16.7505 21.6826 16.6841L13.2757 12.2397Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M19.1489 14.9806C20.1975 13.7474 20.8015 12.2466 20.8015 10.6767C20.8015 6.775 17.0709 3.3 12.0549 3.3C7.03878 3.3 3.30818 6.775 3.30818 10.6767C3.30818 12.1636 3.85 13.5886 4.7983 14.7828C4.47298 15.1005 4.21518 15.4871 4.04843 15.9189C2.76805 14.4626 2.00818 12.6463 2.00818 10.6767C2.00818 5.88468 6.50623 2 12.0549 2C17.6035 2 22.1015 5.88468 22.1015 10.6767C22.1015 12.4784 21.4656 14.1519 20.3771 15.5389L19.1489 14.9806ZM13.9695 17.8785C13.3575 17.9927 12.7174 18.0534 12.0549 18.0534C11.3871 18.0534 10.7422 17.9918 10.1258 17.8758C10.0103 18.3206 9.80141 18.7278 9.52133 19.0751C10.3307 19.2567 11.1797 19.3533 12.0549 19.3533C12.8212 19.3533 13.5675 19.2793 14.2846 19.1389L13.9695 17.8785Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const LassoPolygonalIcon = html`
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.1538 12.3949C11.9419 11.533 12.8482 10.8257 13.6328 11.2405L22.0397 15.6849C22.9188 16.1497 22.6917 17.4676 21.7077 17.6112L17.8349 18.1767C17.7834 18.1842 17.7397 18.2184 17.7201 18.2666L16.343 21.6455C15.967 22.568 14.6302 22.4703 14.3924 21.5029L12.1538 12.3949ZM13.2209 12.0196C13.1088 11.9604 12.9793 12.0614 13.0096 12.1845L15.2482 21.2926C15.2822 21.4308 15.4732 21.4447 15.5269 21.3129L16.9039 17.934C17.0414 17.5967 17.3471 17.3573 17.7076 17.3047L21.5804 16.7393C21.7209 16.7187 21.7534 16.5304 21.6278 16.4641L13.2209 12.0196Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.11 18.9667C10.1575 18.9667 11.0067 18.1175 11.0067 17.07C11.0067 16.0225 10.1575 15.1733 9.11 15.1733C8.0625 15.1733 7.21333 16.0225 7.21333 17.07C7.21333 18.1175 8.0625 18.9667 9.11 18.9667ZM9.11 20.2667C10.8755 20.2667 12.3067 18.8355 12.3067 17.07C12.3067 15.3045 10.8755 13.8733 9.11 13.8733C7.34453 13.8733 5.91333 15.3045 5.91333 17.07C5.91333 18.8355 7.34453 20.2667 9.11 20.2667Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M20.15 1.78546V12.8479L18.2895 14.0882L17.0275 13.3671L18.85 12.1521V4.21453L10.565 9.7379L3.78505 6.77169L7.24943 14.4703C6.85977 14.7497 6.5355 15.1145 6.30404 15.5374L1.21497 4.22831L10.4351 8.26209L20.15 1.78546ZM12.0942 18.2184L13.8336 17.0588L13.443 15.7568L12.2635 16.5431C12.2919 16.7146 12.3067 16.8905 12.3067 17.07C12.3067 17.4748 12.2315 17.862 12.0942 18.2184Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M6.83562 19.6175C6.83562 19.3949 6.89509 19.145 7.00962 18.856C7.10254 18.6215 7.38066 18.5314 7.60625 18.6442C7.83183 18.757 7.91697 19.0293 7.83072 19.2663C7.76634 19.4432 7.74896 19.5529 7.74896 19.6175C7.74896 19.6726 7.75531 19.6824 7.79757 19.7204C7.82686 19.7468 7.8573 19.7695 7.90662 19.8062C7.92009 19.8162 7.93496 19.8273 7.9516 19.8398C8.0195 19.8907 8.11118 19.9615 8.19749 20.0542C8.39066 20.2619 8.51958 20.5387 8.51958 20.9276C8.51958 21.3165 8.38779 21.6272 8.10638 21.8443C7.87962 22.0192 7.58188 22.1051 7.34894 22.1724C7.34373 22.1739 7.33855 22.1754 7.33341 22.1769C7.32112 22.1804 7.3088 22.184 7.29643 22.1875C6.89559 22.3031 6.45026 22.4316 5.98997 22.7773C5.78832 22.9288 5.50125 22.9326 5.32291 22.7542C5.14457 22.5759 5.14302 22.2841 5.33973 22.1263C5.97355 21.6177 6.58811 21.4409 7.02805 21.3144C7.04562 21.3093 7.06292 21.3043 7.07992 21.2994C7.35856 21.2189 7.48065 21.1735 7.54851 21.1211C7.54948 21.1204 7.55051 21.1196 7.55159 21.1188C7.57087 21.1044 7.60625 21.0781 7.60625 20.9276C7.60625 20.7684 7.56391 20.7141 7.52876 20.6763C7.5009 20.6464 7.46415 20.6158 7.4036 20.5704C7.39532 20.5642 7.38602 20.5573 7.37592 20.5498C7.324 20.5114 7.25095 20.4573 7.18659 20.3993C7.00052 20.2319 6.83562 19.9905 6.83562 19.6175Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { LassoMode } from '@blocksuite/affine-shared/types';
|
||||
|
||||
import type { DenseMenuBuilder } from '../common/type.js';
|
||||
import { LassoFreeHandIcon, LassoPolygonalIcon } from './icons.js';
|
||||
|
||||
export const buildLassoDenseMenu: DenseMenuBuilder = edgeless => {
|
||||
// TODO: active state
|
||||
// const prevMode =
|
||||
// edgeless.service.editPropsStore.getLastProps('lasso').mode ??
|
||||
// LassoMode.FreeHand;
|
||||
|
||||
const isActive = edgeless.gfx.tool.currentToolName$.peek() === 'lasso';
|
||||
|
||||
const createSelect = (mode: LassoMode) => () => {
|
||||
edgeless.gfx.tool.setTool('lasso', { mode });
|
||||
};
|
||||
|
||||
return menu.subMenu({
|
||||
name: 'Lasso',
|
||||
prefix: LassoFreeHandIcon,
|
||||
select: createSelect(LassoMode.FreeHand),
|
||||
isSelected: isActive,
|
||||
options: {
|
||||
items: [
|
||||
menu.action({
|
||||
prefix: LassoFreeHandIcon,
|
||||
name: 'Free',
|
||||
select: createSelect(LassoMode.FreeHand),
|
||||
// isSelected: isActive && prevMode === LassoMode.FreeHand,
|
||||
}),
|
||||
menu.action({
|
||||
prefix: LassoPolygonalIcon,
|
||||
name: 'Polygonal',
|
||||
select: createSelect(LassoMode.Polygonal),
|
||||
// isSelected: isActive && prevMode === LassoMode.Polygonal,
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { LassoMode } from '@blocksuite/affine-shared/types';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
import { LassoFreeHandIcon, LassoPolygonalIcon } from './icons.js';
|
||||
|
||||
export class EdgelessLassoToolButton extends QuickToolMixin(
|
||||
WithDisposable(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.current-icon {
|
||||
transition: 100ms;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.current-icon > svg {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _changeTool = () => {
|
||||
const tool = this.edgelessTool;
|
||||
if (tool.type !== 'lasso') {
|
||||
this.setEdgelessTool({ type: 'lasso', mode: this.curMode });
|
||||
return;
|
||||
}
|
||||
|
||||
this._fadeOut();
|
||||
setTimeout(() => {
|
||||
this.curMode === LassoMode.FreeHand
|
||||
? this.setEdgelessTool({ type: 'lasso', mode: LassoMode.Polygonal })
|
||||
: this.setEdgelessTool({ type: 'lasso', mode: LassoMode.FreeHand });
|
||||
this._fadeIn();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
override type = 'lasso' as const;
|
||||
|
||||
private _fadeIn() {
|
||||
this.currentIcon.style.opacity = '1';
|
||||
this.currentIcon.style.transform = `translateY(0px)`;
|
||||
}
|
||||
|
||||
private _fadeOut() {
|
||||
this.currentIcon.style.opacity = '0';
|
||||
this.currentIcon.style.transform = `translateY(-5px)`;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const tool = this.edgeless.gfx.tool.currentToolOption$.value;
|
||||
|
||||
if (tool?.type === 'lasso') {
|
||||
const { mode } = tool;
|
||||
this.curMode = mode;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const type = this.edgelessTool?.type;
|
||||
const mode = this.curMode === LassoMode.FreeHand ? 'freehand' : 'polygonal';
|
||||
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-lasso-button ${mode}"
|
||||
.tooltip=${getTooltipWithShortcut('Lasso', 'L')}
|
||||
.tooltipOffset=${17}
|
||||
.active=${type === 'lasso'}
|
||||
.iconContainerPadding=${6}
|
||||
.iconSize=${'24px'}
|
||||
@click=${this._changeTool}
|
||||
>
|
||||
<span class="current-icon">
|
||||
${this.curMode === LassoMode.FreeHand
|
||||
? LassoFreeHandIcon
|
||||
: LassoPolygonalIcon}
|
||||
</span>
|
||||
<toolbar-arrow-up-icon></toolbar-arrow-up-icon>
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor curMode: LassoMode = LassoMode.FreeHand;
|
||||
|
||||
@query('.current-icon')
|
||||
accessor currentIcon!: HTMLInputElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-lasso-tool-button': EdgelessLassoToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import type { DenseMenuBuilder } from '../common/type.js';
|
||||
|
||||
export const buildLinkDenseMenu: DenseMenuBuilder = edgeless =>
|
||||
menu.action({
|
||||
name: 'Link',
|
||||
prefix: LinkIcon,
|
||||
select: () => {
|
||||
const [_, { insertedLinkType }] = edgeless.std.command.exec(
|
||||
insertLinkByQuickSearchCommand
|
||||
);
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
type: flavour.split(':')[1],
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
|
||||
export class EdgelessLinkToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
.link-icon,
|
||||
.link-icon > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
override type = 'default' as const;
|
||||
|
||||
private _onClick() {
|
||||
const [_, { insertedLinkType }] = this.edgeless.std.command.exec(
|
||||
insertLinkByQuickSearchCommand
|
||||
);
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: flavour.split(':')[1],
|
||||
});
|
||||
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('LinkedDocCreated', {
|
||||
control: 'links',
|
||||
page: 'whiteboard editor',
|
||||
module: 'edgeless toolbar',
|
||||
segment: 'whiteboard',
|
||||
type: flavour.split(':')[1],
|
||||
other: 'existing doc',
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<edgeless-tool-icon-button
|
||||
.iconContainerPadding="${6}"
|
||||
.tooltip="${getTooltipWithShortcut('Link', '@')}"
|
||||
.tooltipOffset=${17}
|
||||
class="edgeless-link-tool-button"
|
||||
@click=${this._onClick}
|
||||
>
|
||||
<span class="link-icon">${LinkIcon}</span>
|
||||
</edgeless-tool-icon-button>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-link-tool-button': EdgelessLinkToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ColorScheme, MindmapStyle } from '@blocksuite/affine-model';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { type DraggableTool, getMindmapRender } from './basket-elements.js';
|
||||
import {
|
||||
mindMapStyle1Dark,
|
||||
mindMapStyle1Light,
|
||||
mindMapStyle2Dark,
|
||||
mindMapStyle2Light,
|
||||
mindMapStyle3,
|
||||
mindMapStyle4,
|
||||
} from './icons.js';
|
||||
|
||||
export type ToolbarMindmapItem = {
|
||||
type: 'mindmap';
|
||||
icon: TemplateResult;
|
||||
style: MindmapStyle;
|
||||
render: DraggableTool['render'];
|
||||
};
|
||||
|
||||
export const getMindMaps = (theme: ColorScheme): ToolbarMindmapItem[] => [
|
||||
{
|
||||
type: 'mindmap',
|
||||
icon: theme === ColorScheme.Dark ? mindMapStyle1Dark : mindMapStyle1Light,
|
||||
style: MindmapStyle.ONE,
|
||||
render: getMindmapRender(MindmapStyle.ONE),
|
||||
},
|
||||
{
|
||||
type: 'mindmap',
|
||||
icon: mindMapStyle4,
|
||||
style: MindmapStyle.FOUR,
|
||||
render: getMindmapRender(MindmapStyle.FOUR),
|
||||
},
|
||||
{
|
||||
type: 'mindmap',
|
||||
icon: mindMapStyle3,
|
||||
style: MindmapStyle.THREE,
|
||||
render: getMindmapRender(MindmapStyle.THREE),
|
||||
},
|
||||
{
|
||||
type: 'mindmap',
|
||||
icon: theme === 'light' ? mindMapStyle2Light : mindMapStyle2Dark,
|
||||
style: MindmapStyle.TWO,
|
||||
render: getMindmapRender(MindmapStyle.TWO),
|
||||
},
|
||||
];
|
||||
@@ -1,214 +0,0 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||
import { addImages } from '@blocksuite/affine-block-image';
|
||||
import { CanvasElementType } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
MAX_IMAGE_WIDTH,
|
||||
type MindmapStyle,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
||||
import { assertInstanceOf, Bound } from '@blocksuite/global/utils';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
import type { EdgelessRootService } from '../../../edgeless-root-service.js';
|
||||
import { mountTextElementEditor } from '../../../utils/text.js';
|
||||
|
||||
export type ConfigProperty = 'x' | 'y' | 'r' | 's' | 'z' | 'o';
|
||||
export type ConfigState = 'default' | 'active' | 'hover' | 'next';
|
||||
export type ConfigStyle = Partial<Record<ConfigProperty, number | string>>;
|
||||
export type ToolConfig = Record<ConfigState, ConfigStyle>;
|
||||
|
||||
export type DraggableTool = {
|
||||
name: 'text' | 'mindmap' | 'media';
|
||||
icon: TemplateResult;
|
||||
config: ToolConfig;
|
||||
standardWidth?: number;
|
||||
render: (
|
||||
bound: Bound,
|
||||
edgelessService: EdgelessRootService,
|
||||
edgeless: EdgelessRootBlockComponent
|
||||
) => Promise<string | null>;
|
||||
};
|
||||
|
||||
const unitMap = { x: 'px', y: 'px', r: 'deg', s: '', z: '', o: '' };
|
||||
export const textConfig: ToolConfig = {
|
||||
default: { x: -20, y: -8, r: 7.74, s: 0.92, z: 3 },
|
||||
active: { x: -22, y: -9, r: -8, s: 0.92 },
|
||||
hover: { x: -22, y: -9, r: -8, s: 1, z: 3 },
|
||||
next: { x: -22, y: 64, r: 0 },
|
||||
};
|
||||
export const mindmapConfig: ToolConfig = {
|
||||
default: { x: 4, y: -4, s: 1, r: -7, z: 2 },
|
||||
active: { x: 11, y: -14, r: 9, s: 1 },
|
||||
hover: { x: 11, y: -14, r: 9, s: 1.16, z: 3 },
|
||||
next: { y: 64, r: 0 },
|
||||
};
|
||||
export const mediaConfig: ToolConfig = {
|
||||
default: { x: -20, y: -15, r: 23, s: 1.2, z: 1 },
|
||||
active: { x: -25, y: -20, r: -9, s: 1.2 },
|
||||
hover: { x: -25, y: -20, r: -9, s: 1.5, z: 3 },
|
||||
next: { y: 64, r: 0 },
|
||||
};
|
||||
|
||||
export const getMindmapRender =
|
||||
(mindmapStyle: MindmapStyle): DraggableTool['render'] =>
|
||||
async (bound, edgelessService) => {
|
||||
const [x, y, _, h] = bound.toXYWH();
|
||||
|
||||
const rootW = 145;
|
||||
const rootH = 50;
|
||||
|
||||
const nodeW = 80;
|
||||
const nodeH = 35;
|
||||
|
||||
const centerVertical = y + h / 2;
|
||||
const rootX = x;
|
||||
const rootY = centerVertical - rootH / 2;
|
||||
|
||||
type MindMapNode = {
|
||||
children: MindMapNode[];
|
||||
text: string;
|
||||
xywh: string;
|
||||
};
|
||||
|
||||
const root: MindMapNode = {
|
||||
children: [],
|
||||
text: 'Mind Map',
|
||||
xywh: `[${rootX},${rootY},${rootW},${rootH}]`,
|
||||
};
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const nodeX = x + rootW + 300;
|
||||
const nodeY = centerVertical - nodeH / 2 + (i - 1) * 50;
|
||||
root.children.push({
|
||||
children: [],
|
||||
text: 'Text',
|
||||
xywh: `[${nodeX},${nodeY},${nodeW},${nodeH}]`,
|
||||
});
|
||||
}
|
||||
|
||||
const mindmapId = edgelessService.crud.addElement('mindmap', {
|
||||
style: mindmapStyle,
|
||||
children: root,
|
||||
}) as string;
|
||||
|
||||
edgelessService.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:dnd', // for now we use toolbar:dnd for all mindmap creation here
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'mindmap',
|
||||
});
|
||||
|
||||
return mindmapId;
|
||||
};
|
||||
|
||||
export const textRender: DraggableTool['render'] = async (
|
||||
bound,
|
||||
service,
|
||||
edgeless
|
||||
) => {
|
||||
const vCenter = bound.y + bound.h / 2;
|
||||
const w = 100;
|
||||
const h = 32;
|
||||
|
||||
const flag = edgeless.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_edgeless_text');
|
||||
let id: string;
|
||||
if (flag) {
|
||||
const [_, { textId }] = edgeless.std.command.exec(
|
||||
insertEdgelessTextCommand,
|
||||
{
|
||||
x: bound.x,
|
||||
y: vCenter - h / 2,
|
||||
}
|
||||
);
|
||||
id = textId!;
|
||||
} else {
|
||||
id = service.crud.addElement(CanvasElementType.TEXT, {
|
||||
xywh: new Bound(bound.x, vCenter - h / 2, w, h).serialize(),
|
||||
text: new Y.Text(),
|
||||
}) as string;
|
||||
|
||||
edgeless.doc.captureSync();
|
||||
const textElement = edgeless.service.crud.getElementById(id);
|
||||
assertInstanceOf(textElement, TextElementModel);
|
||||
mountTextElementEditor(textElement, edgeless);
|
||||
}
|
||||
|
||||
service.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:dnd',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'text',
|
||||
});
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
export const mediaRender: DraggableTool['render'] = async (
|
||||
bound,
|
||||
_,
|
||||
edgeless
|
||||
) => {
|
||||
let file: File | null = null;
|
||||
try {
|
||||
file = await openFileOrFiles();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
if (!file) return null;
|
||||
|
||||
// image
|
||||
if (file.type.startsWith('image/')) {
|
||||
const [id] = await addImages(edgeless.std, [file], {
|
||||
point: [bound.x, bound.y],
|
||||
maxWidth: MAX_IMAGE_WIDTH,
|
||||
transformPoint: false,
|
||||
});
|
||||
if (id) return id;
|
||||
return null;
|
||||
}
|
||||
|
||||
// attachment
|
||||
const [id] = await addAttachments(
|
||||
edgeless.std,
|
||||
[file],
|
||||
[bound.x, bound.y],
|
||||
false
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
const toolStyle2StyleObj = (state: ConfigState, style: ConfigStyle = {}) => {
|
||||
const styleObj = {} as Record<string, string>;
|
||||
for (const [key, value] of Object.entries(style)) {
|
||||
styleObj[`--${state}-${key}`] = `${value}${unitMap[key as ConfigProperty]}`;
|
||||
}
|
||||
return styleObj;
|
||||
};
|
||||
export const toolConfig2StyleObj = (config: ToolConfig) => {
|
||||
const styleObj = {} as Record<string, string>;
|
||||
for (const [state, style] of Object.entries(config)) {
|
||||
Object.assign(
|
||||
styleObj,
|
||||
toolStyle2StyleObj(state as ConfigState, {
|
||||
...config.default,
|
||||
...style,
|
||||
})
|
||||
);
|
||||
}
|
||||
return styleObj;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,60 +0,0 @@
|
||||
import { LightLoadingIcon } from '@blocksuite/affine-components/icons';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
import { importMindMapIcon } from './icons.js';
|
||||
|
||||
export class MindMapPlaceholder extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
mindmap-import-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 28px 12px 12px;
|
||||
box-sizing: border-box;
|
||||
width: 200px;
|
||||
height: 122px;
|
||||
|
||||
border-radius: 12px;
|
||||
gap: 12px;
|
||||
|
||||
background-color: ${unsafeCSSVarV2('layer/background/secondary')};
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||
color: ${unsafeCSSVarV2('text/placeholder')};
|
||||
|
||||
box-shadow: 0px 0px 4px 0px rgba(66, 65, 73, 0.14);
|
||||
}
|
||||
|
||||
mindmap-import-placeholder .preview-icon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
mindmap-import-placeholder .description {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
color: ${unsafeCSSVarV2('text/placeholder')};
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<div class="placeholder-container">
|
||||
<div class="preview-icon">${importMindMapIcon}</div>
|
||||
<div class="description">
|
||||
${LightLoadingIcon}
|
||||
<span>Importing mind map...</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'mindmap-import-placeholder': MindMapPlaceholder;
|
||||
}
|
||||
}
|
||||
@@ -1,403 +0,0 @@
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type { MindmapStyle } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
FeatureFlagService,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import { modelContext, stdContext } from '@blocksuite/block-std';
|
||||
import { ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { Bound } from '@blocksuite/global/utils';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { consume } from '@lit/context';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../index.js';
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { getMindMaps, type ToolbarMindmapItem } from './assets.js';
|
||||
import { mediaRender, textRender } from './basket-elements.js';
|
||||
import { importMindMapIcon, mindmapMenuMediaIcon, textIcon } from './icons.js';
|
||||
import { MindMapPlaceholder } from './mindmap-importing-placeholder.js';
|
||||
|
||||
type TextItem = {
|
||||
type: 'text';
|
||||
icon: TemplateResult;
|
||||
render: typeof textRender;
|
||||
};
|
||||
|
||||
type MediaItem = {
|
||||
type: 'media';
|
||||
icon: TemplateResult;
|
||||
render: typeof mediaRender;
|
||||
};
|
||||
|
||||
type ImportItem = {
|
||||
type: 'import';
|
||||
icon: TemplateResult;
|
||||
};
|
||||
|
||||
const textItem: TextItem = { type: 'text', icon: textIcon, render: textRender };
|
||||
|
||||
const mediaItem: MediaItem = {
|
||||
type: 'media',
|
||||
icon: mindmapMenuMediaIcon,
|
||||
render: mediaRender,
|
||||
};
|
||||
|
||||
export class EdgelessMindmapMenu extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
z-index: -1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.text-and-mindmap {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 8px 0px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.thin-divider {
|
||||
width: 1px;
|
||||
transform: scaleX(0.5);
|
||||
height: 48px;
|
||||
background: var(--affine-border-color);
|
||||
}
|
||||
.text-item,
|
||||
.media-item {
|
||||
width: 60px;
|
||||
}
|
||||
.mindmap-item {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.text-item,
|
||||
.media-item,
|
||||
.mindmap-item {
|
||||
border-radius: 4px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.text-item > button,
|
||||
.media-item > button,
|
||||
.mindmap-item > button {
|
||||
position: absolute;
|
||||
border-radius: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: grab;
|
||||
padding: 0;
|
||||
}
|
||||
.text-item:hover,
|
||||
.media-item:hover,
|
||||
.mindmap-item[data-is-active='true'],
|
||||
.mindmap-item:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.text-item > button.next,
|
||||
.media-item > button.next,
|
||||
.mindmap-item > button.next {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _style$ = computed(() => {
|
||||
const { style } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value.mindmap;
|
||||
return style;
|
||||
});
|
||||
|
||||
draggableController!: EdgelessDraggableElementController<
|
||||
ToolbarMindmapItem | TextItem | ImportItem | MediaItem
|
||||
>;
|
||||
|
||||
override type = 'empty' as const;
|
||||
|
||||
private get _rootBlock(): EdgelessRootBlockComponent {
|
||||
return this.std.view.getBlock(this.model.id) as EdgelessRootBlockComponent;
|
||||
}
|
||||
|
||||
get mindMaps() {
|
||||
return getMindMaps(this.theme);
|
||||
}
|
||||
|
||||
private _importMindMapEntry() {
|
||||
const { draggingElement } = this.draggableController?.states || {};
|
||||
const isBeingDragged = draggingElement?.data.type === 'import';
|
||||
|
||||
return html`<div class="mindmap-item">
|
||||
<button
|
||||
style="opacity: ${isBeingDragged ? 0 : 1}"
|
||||
class="next"
|
||||
@mousedown=${(e: MouseEvent) => {
|
||||
this.draggableController.onMouseDown(e, {
|
||||
preview: importMindMapIcon,
|
||||
data: {
|
||||
type: 'import',
|
||||
icon: importMindMapIcon,
|
||||
},
|
||||
standardWidth: 350,
|
||||
});
|
||||
}}
|
||||
@touchstart=${(e: TouchEvent) => {
|
||||
this.draggableController.onTouchStart(e, {
|
||||
preview: importMindMapIcon,
|
||||
data: {
|
||||
type: 'import',
|
||||
icon: importMindMapIcon,
|
||||
},
|
||||
standardWidth: 350,
|
||||
});
|
||||
}}
|
||||
@click=${() => {
|
||||
this.draggableController.cancel();
|
||||
const viewportBound = this._rootBlock.service.viewport.viewportBounds;
|
||||
|
||||
viewportBound.x += viewportBound.w / 2;
|
||||
viewportBound.y += viewportBound.h / 2;
|
||||
|
||||
this._onImportMindMap(viewportBound);
|
||||
}}
|
||||
>
|
||||
${importMindMapIcon}
|
||||
</button>
|
||||
<affine-tooltip tip-position="top" .offset=${12}>
|
||||
${getTooltipWithShortcut('Support import of FreeMind,OPML.')}
|
||||
</affine-tooltip>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _onImportMindMap(bound: Bound) {
|
||||
const edgelessBlock = this._rootBlock;
|
||||
if (!edgelessBlock) return;
|
||||
|
||||
const placeholder = new MindMapPlaceholder();
|
||||
|
||||
placeholder.style.position = 'absolute';
|
||||
placeholder.style.left = `${bound.x}px`;
|
||||
placeholder.style.top = `${bound.y}px`;
|
||||
|
||||
edgelessBlock.gfxViewportElm.append(placeholder);
|
||||
|
||||
this.onImportMindMap?.(bound)
|
||||
.then(() => {
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
page: 'whiteboard editor',
|
||||
type: 'imported mind map',
|
||||
other: 'success',
|
||||
module: 'toolbar',
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.code === ErrorCode.UserAbortError) return;
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
page: 'whiteboard editor',
|
||||
type: 'imported mind map',
|
||||
other: 'failed',
|
||||
module: 'toolbar',
|
||||
});
|
||||
toast(this.edgeless.host, 'Import failed, please try again');
|
||||
console.error(e);
|
||||
})
|
||||
.finally(() => {
|
||||
placeholder.remove();
|
||||
});
|
||||
}
|
||||
|
||||
initDragController() {
|
||||
if (this.draggableController || !this.edgeless) return;
|
||||
this.draggableController = new EdgelessDraggableElementController(this, {
|
||||
service: this.edgeless.service,
|
||||
edgeless: this.edgeless,
|
||||
scopeElement: this,
|
||||
clickToDrag: true,
|
||||
onOverlayCreated: (_layer, element) => {
|
||||
if (element.data.type === 'mindmap') {
|
||||
this.onActiveStyleChange?.(element.data.style);
|
||||
}
|
||||
// a workaround to active mindmap, so that menu cannot be closed by `Escape`
|
||||
this.setEdgelessTool({ type: 'empty' });
|
||||
},
|
||||
onDrop: (element, bound) => {
|
||||
if ('render' in element.data) {
|
||||
element.data
|
||||
.render(bound, this.edgeless.service, this.edgeless)
|
||||
.then(id => {
|
||||
if (!id) return;
|
||||
if (element.data.type === 'mindmap') {
|
||||
this.onActiveStyleChange?.(element.data.style);
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
} else if (
|
||||
element.data.type === 'text' ||
|
||||
element.data.type === 'media'
|
||||
) {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else if (element.data.type === 'import') {
|
||||
this._onImportMindMap?.(bound);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { cancelled, draggingElement, dragOut } =
|
||||
this.draggableController?.states || {};
|
||||
|
||||
const isDraggingMedia = draggingElement?.data?.type === 'media';
|
||||
const isDraggingText = draggingElement?.data?.type === 'text';
|
||||
const showNextText = dragOut && !cancelled;
|
||||
return html`<edgeless-slide-menu .height=${'64px'}>
|
||||
<div class="text-and-mindmap">
|
||||
<div class="media-item">
|
||||
${isDraggingMedia
|
||||
? html`<button
|
||||
class="next"
|
||||
style="transform: translateY(${showNextText ? 0 : 64}px)"
|
||||
>
|
||||
${mediaItem.icon}
|
||||
</button>`
|
||||
: nothing}
|
||||
<button
|
||||
style="opacity: ${isDraggingMedia ? 0 : 1}"
|
||||
@mousedown=${(e: MouseEvent) =>
|
||||
this.draggableController.onMouseDown(e, {
|
||||
preview: mediaItem.icon,
|
||||
data: mediaItem,
|
||||
})}
|
||||
@touchstart=${(e: TouchEvent) =>
|
||||
this.draggableController.onTouchStart(e, {
|
||||
preview: mediaItem.icon,
|
||||
data: mediaItem,
|
||||
})}
|
||||
>
|
||||
${mediaItem.icon}
|
||||
</button>
|
||||
<affine-tooltip tip-position="top" .offset=${12}>
|
||||
${getTooltipWithShortcut('Add media')}
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
<div class="thin-divider"></div>
|
||||
<div class="text-item">
|
||||
${isDraggingText
|
||||
? html`<button
|
||||
class="next"
|
||||
style="transform: translateY(${showNextText ? 0 : 64}px)"
|
||||
>
|
||||
${textItem.icon}
|
||||
</button>`
|
||||
: nothing}
|
||||
<button
|
||||
style="opacity: ${isDraggingText ? 0 : 1}"
|
||||
@mousedown=${(e: MouseEvent) =>
|
||||
this.draggableController.onMouseDown(e, {
|
||||
preview: textItem.icon,
|
||||
data: textItem,
|
||||
})}
|
||||
@touchstart=${(e: TouchEvent) =>
|
||||
this.draggableController.onTouchStart(e, {
|
||||
preview: textItem.icon,
|
||||
data: textItem,
|
||||
})}
|
||||
>
|
||||
${textItem.icon}
|
||||
</button>
|
||||
<affine-tooltip tip-position="top" .offset=${12}>
|
||||
${getTooltipWithShortcut('Edgeless Text', 'T')}
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
<div class="thin-divider"></div>
|
||||
<!-- mind map -->
|
||||
${repeat(this.mindMaps, mindMap => {
|
||||
const isDraggingMindMap = draggingElement?.data?.type !== 'text';
|
||||
const draggingEle = draggingElement?.data as ToolbarMindmapItem;
|
||||
const isBeingDragged =
|
||||
isDraggingMindMap && draggingEle?.style === mindMap.style;
|
||||
const showNext = dragOut && !cancelled;
|
||||
const isActive = this._style$.value === mindMap.style;
|
||||
return html`
|
||||
<div class="mindmap-item" data-is-active=${isActive}>
|
||||
${isBeingDragged
|
||||
? html`<button
|
||||
style="transform: translateY(${showNext ? 0 : 64}px)"
|
||||
class="next"
|
||||
>
|
||||
${mindMap.icon}
|
||||
</button>`
|
||||
: nothing}
|
||||
<button
|
||||
style="opacity: ${isBeingDragged ? 0 : 1}"
|
||||
@mousedown=${(e: MouseEvent) => {
|
||||
this.draggableController.onMouseDown(e, {
|
||||
preview: mindMap.icon,
|
||||
data: mindMap,
|
||||
standardWidth: 350,
|
||||
});
|
||||
}}
|
||||
@touchstart=${(e: TouchEvent) => {
|
||||
this.draggableController.onTouchStart(e, {
|
||||
preview: mindMap.icon,
|
||||
data: mindMap,
|
||||
standardWidth: 350,
|
||||
});
|
||||
}}
|
||||
@click=${() => this.onActiveStyleChange?.(mindMap.style)}
|
||||
>
|
||||
${mindMap.icon}
|
||||
</button>
|
||||
<affine-tooltip tip-position="top" .offset=${12}>
|
||||
${getTooltipWithShortcut('Mind Map', 'M')}
|
||||
</affine-tooltip>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${this.std.store
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_mind_map_import')
|
||||
? this._importMindMapEntry()
|
||||
: nothing}
|
||||
</div>
|
||||
</edgeless-slide-menu>`;
|
||||
}
|
||||
|
||||
override updated(changedProperties: Map<PropertyKey, unknown>) {
|
||||
if (!changedProperties.has('edgeless')) return;
|
||||
this.initDragController();
|
||||
}
|
||||
|
||||
@consume({ context: modelContext })
|
||||
accessor model!: BlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onActiveStyleChange!: (style: MindmapStyle) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onImportMindMap!: (bound: Bound) => Promise<void>;
|
||||
|
||||
@consume({ context: stdContext })
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-mindmap-menu': EdgelessMindmapMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,454 +0,0 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
MindmapElementModel,
|
||||
MindmapStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import type { Bound } from '@blocksuite/global/utils';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { getMindMaps } from './assets.js';
|
||||
import {
|
||||
type DraggableTool,
|
||||
getMindmapRender,
|
||||
mediaConfig,
|
||||
mediaRender,
|
||||
mindmapConfig,
|
||||
textConfig,
|
||||
textRender,
|
||||
toolConfig2StyleObj,
|
||||
} from './basket-elements.js';
|
||||
import {
|
||||
basketIconDark,
|
||||
basketIconLight,
|
||||
mindmapMenuMediaIcon,
|
||||
textIcon,
|
||||
} from './icons.js';
|
||||
import { importMindmap } from './utils/import-mindmap.js';
|
||||
|
||||
export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.partial-clip {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% + 20px);
|
||||
pointer-events: none;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.basket-wrapper {
|
||||
pointer-events: auto;
|
||||
height: 64px;
|
||||
width: 96px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
}
|
||||
.basket,
|
||||
.basket-tool-item {
|
||||
transition: transform 0.3s ease-in-out;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.basket {
|
||||
bottom: 0;
|
||||
height: 17px;
|
||||
width: 76px;
|
||||
}
|
||||
.basket > div,
|
||||
.basket > svg {
|
||||
position: absolute;
|
||||
}
|
||||
.glass {
|
||||
width: 76px;
|
||||
height: 17px;
|
||||
border-radius: 2px;
|
||||
mask: url(#mindmap-basket-body-mask);
|
||||
}
|
||||
.glass.enabled {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
@-moz-document url-prefix() {
|
||||
.glass.enabled {
|
||||
backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
.basket {
|
||||
z-index: 3;
|
||||
}
|
||||
.basket-tool-item {
|
||||
cursor: grab;
|
||||
}
|
||||
.basket-tool-item svg {
|
||||
display: block;
|
||||
}
|
||||
.basket-tool-item {
|
||||
transform: translate(var(--default-x, 0), var(--default-y, 0))
|
||||
rotate(var(--default-r, 0)) scale(var(--default-s, 1));
|
||||
z-index: var(--default-z, 0);
|
||||
}
|
||||
|
||||
.basket-tool-item.next {
|
||||
transform: translate(var(--next-x, 0), var(--next-y, 0))
|
||||
rotate(var(--next-r, 0)) scale(var(--next-s, 1));
|
||||
z-index: var(--next-z, 0);
|
||||
}
|
||||
|
||||
/* active & hover */
|
||||
.basket-wrapper:hover .basket,
|
||||
.basket-wrapper.active .basket {
|
||||
z-index: 0;
|
||||
}
|
||||
.basket-wrapper:hover .basket-tool-item.current,
|
||||
.basket-wrapper.active .basket-tool-item.current {
|
||||
transform: translate(var(--active-x, 0), var(--active-y, 0))
|
||||
rotate(var(--active-r, 0)) scale(var(--active-s, 1));
|
||||
z-index: var(--active-z, 0);
|
||||
}
|
||||
|
||||
.basket-tool-item.next.coming,
|
||||
.basket-wrapper:hover .basket-tool-item.current:hover {
|
||||
transform: translate(var(--hover-x, 0), var(--hover-y, 0))
|
||||
rotate(var(--hover-r, 0)) scale(var(--hover-s, 1));
|
||||
z-index: var(--hover-z, 0);
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _style$ = computed(() => {
|
||||
const { style } =
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value.mindmap;
|
||||
return style;
|
||||
});
|
||||
|
||||
draggableController!: EdgelessDraggableElementController<DraggableTool>;
|
||||
|
||||
override enableActiveBackground = true;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'][] = ['empty', 'text'];
|
||||
|
||||
get draggableTools(): DraggableTool[] {
|
||||
const style = this._style$.value;
|
||||
const mindmap =
|
||||
this.mindmaps.find(m => m.style === style) || this.mindmaps[0];
|
||||
return [
|
||||
{
|
||||
name: 'media',
|
||||
icon: mindmapMenuMediaIcon,
|
||||
config: mediaConfig,
|
||||
standardWidth: 100,
|
||||
render: mediaRender,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
icon: textIcon,
|
||||
config: textConfig,
|
||||
standardWidth: 100,
|
||||
render: textRender,
|
||||
},
|
||||
{
|
||||
name: 'mindmap',
|
||||
icon: mindmap.icon,
|
||||
config: mindmapConfig,
|
||||
standardWidth: 350,
|
||||
render: getMindmapRender(style),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get mindmaps() {
|
||||
return getMindMaps(this.theme);
|
||||
}
|
||||
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
private _toggleMenu() {
|
||||
if (this.tryDisposePopper()) return;
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
|
||||
const menu = this.createPopper('edgeless-mindmap-menu', this);
|
||||
Object.assign(menu.element, {
|
||||
edgeless: this.edgeless,
|
||||
onActiveStyleChange: (style: MindmapStyle) => {
|
||||
this.edgeless.std.get(EditPropsStore).recordLastProps('mindmap', {
|
||||
style,
|
||||
});
|
||||
},
|
||||
onImportMindMap: (bound: Bound) => {
|
||||
return importMindmap(bound).then(mindmap => {
|
||||
const id = this.crud.addElement('mindmap', {
|
||||
children: mindmap,
|
||||
layoutType: mindmap?.layoutType === 'left' ? 1 : 0,
|
||||
});
|
||||
if (!id) return;
|
||||
const element = this.crud.getElementById(id) as MindmapElementModel;
|
||||
|
||||
this.tryDisposePopper();
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({
|
||||
elements: [element.tree.id],
|
||||
editing: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
initDragController() {
|
||||
if (!this.edgeless || !this.toolbarContainer) return;
|
||||
if (this.draggableController) return;
|
||||
this.draggableController = new EdgelessDraggableElementController(this, {
|
||||
service: this.edgeless.service,
|
||||
edgeless: this.edgeless,
|
||||
scopeElement: this.toolbarContainer,
|
||||
standardWidth: 100,
|
||||
clickToDrag: true,
|
||||
onOverlayCreated: (overlay, { data }) => {
|
||||
const tool = this.draggableTools.find(t => t.name === data.name);
|
||||
if (!tool) return;
|
||||
|
||||
// recover the rotation
|
||||
const rotate = tool.config?.hover?.r ?? tool.config?.default?.r ?? 0;
|
||||
overlay.element.style.setProperty('--rotate', rotate + 'deg');
|
||||
setTimeout(() => {
|
||||
overlay.transitionWrapper.style.setProperty(
|
||||
'--rotate',
|
||||
-rotate + 'deg'
|
||||
);
|
||||
}, 50);
|
||||
|
||||
// set the scale (without transition)
|
||||
const scale = tool.config?.hover?.s ?? tool.config?.default?.s ?? 1;
|
||||
overlay.element.style.setProperty('--scale', `${scale}`);
|
||||
|
||||
// a workaround to handle getBoundingClientRect() when the element is rotated
|
||||
const _left = parseInt(overlay.element.style.left);
|
||||
const _top = parseInt(overlay.element.style.top);
|
||||
if (data.name === 'mindmap') {
|
||||
overlay.element.style.left = _left + 3 + 'px';
|
||||
overlay.element.style.top = _top + 5 + 'px';
|
||||
} else if (data.name === 'text') {
|
||||
overlay.element.style.left = _left + 0 + 'px';
|
||||
overlay.element.style.top = _top + 3 + 'px';
|
||||
}
|
||||
this.readyToDrop = true;
|
||||
},
|
||||
onCanceled: overlay => {
|
||||
overlay.transitionWrapper.style.transformOrigin = 'unset';
|
||||
overlay.transitionWrapper.style.setProperty('--rotate', '0deg');
|
||||
this.readyToDrop = false;
|
||||
},
|
||||
onDrop: (el, bound) => {
|
||||
el.data
|
||||
.render(bound, this.edgeless.service, this.edgeless)
|
||||
.then(id => {
|
||||
if (!id) return;
|
||||
this.readyToDrop = false;
|
||||
if (el.data.name === 'mindmap') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
this.edgeless.gfx.selection.set({
|
||||
elements: [id],
|
||||
editing: false,
|
||||
});
|
||||
} else if (el.data.name === 'text') {
|
||||
this.setEdgelessTool({ type: 'default' });
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
});
|
||||
|
||||
this.edgeless.bindHotKey(
|
||||
{
|
||||
m: () => {
|
||||
const service = this.edgeless.service;
|
||||
if (service.locked) return;
|
||||
if (service.selection.editing) return;
|
||||
|
||||
if (this.readyToDrop) {
|
||||
// change the style
|
||||
const activeIndex = this.mindmaps.findIndex(
|
||||
m => m.style === this._style$.value
|
||||
);
|
||||
const nextIndex = (activeIndex + 1) % this.mindmaps.length;
|
||||
const next = this.mindmaps[nextIndex];
|
||||
this.edgeless.std.get(EditPropsStore).recordLastProps('mindmap', {
|
||||
style: next.style,
|
||||
});
|
||||
const tool = this.draggableTools.find(t => t.name === 'mindmap');
|
||||
this.draggableController.updateElementInfo({
|
||||
data: tool,
|
||||
preview: next.icon,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setEdgelessTool({ type: 'empty' });
|
||||
const icon = this.mindmapElement;
|
||||
const { x, y } = service.gfx.tool.lastMousePos$.peek();
|
||||
const { left, top } = this.edgeless.viewport;
|
||||
const clientPos = { x: x + left, y: y + top };
|
||||
this.draggableController.clickToDrag(icon, clientPos);
|
||||
},
|
||||
},
|
||||
{ global: true }
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { popper } = this;
|
||||
const appTheme = this.edgeless.std.get(ThemeProvider).app$.value;
|
||||
const basketIcon = appTheme === 'light' ? basketIconLight : basketIconDark;
|
||||
const glassBg =
|
||||
appTheme === 'light' ? 'rgba(255,255,255,0.5)' : 'rgba(74, 74, 74, 0.6)';
|
||||
|
||||
const { cancelled, dragOut, draggingElement } =
|
||||
this.draggableController?.states || {};
|
||||
|
||||
const active = popper || draggingElement;
|
||||
|
||||
return html`<edgeless-toolbar-button
|
||||
class="edgeless-mindmap-button"
|
||||
?withHover=${true}
|
||||
.tooltip=${popper ? '' : 'Others'}
|
||||
.tooltipOffset=${4}
|
||||
@click=${this._toggleMenu}
|
||||
style="width: 100%; height: 100%; display: inline-block"
|
||||
>
|
||||
<div class="partial-clip">
|
||||
<div class="basket-wrapper ${active ? 'active' : ''}">
|
||||
${repeat(
|
||||
this.draggableTools,
|
||||
t => t.name,
|
||||
tool => {
|
||||
const isBeingDragged = draggingElement?.data.name === tool.name;
|
||||
const variables = toolConfig2StyleObj(tool.config);
|
||||
|
||||
const nextStyle = styleMap({
|
||||
...variables,
|
||||
});
|
||||
const currentStyle = styleMap({
|
||||
...variables,
|
||||
opacity: isBeingDragged ? 0 : 1,
|
||||
pointerEvents: draggingElement ? 'none' : 'auto',
|
||||
});
|
||||
|
||||
return html`${isBeingDragged
|
||||
? html`<div
|
||||
class=${classMap({
|
||||
'basket-tool-item': true,
|
||||
next: true,
|
||||
coming: !!dragOut && !cancelled,
|
||||
})}
|
||||
style=${nextStyle}
|
||||
>
|
||||
${tool.icon}
|
||||
</div>`
|
||||
: nothing}
|
||||
|
||||
<div
|
||||
style=${currentStyle}
|
||||
@mousedown=${(e: MouseEvent) =>
|
||||
this.draggableController.onMouseDown(e, {
|
||||
data: tool,
|
||||
preview: tool.icon,
|
||||
standardWidth: tool.standardWidth,
|
||||
})}
|
||||
@touchstart=${(e: TouchEvent) =>
|
||||
this.draggableController.onTouchStart(e, {
|
||||
data: tool,
|
||||
preview: tool.icon,
|
||||
standardWidth: tool.standardWidth,
|
||||
})}
|
||||
class="basket-tool-item current ${tool.name}"
|
||||
>
|
||||
${tool.icon}
|
||||
</div>`;
|
||||
}
|
||||
)}
|
||||
|
||||
<div class="basket">
|
||||
<div
|
||||
class="glass ${this.enableBlur ? 'enabled' : ''}"
|
||||
style="background: ${glassBg}"
|
||||
></div>
|
||||
${basketIcon}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg width="0" height="0" style="opacity: 0; pointer-events: none">
|
||||
<defs>
|
||||
<mask id="mindmap-basket-body-mask">
|
||||
<rect
|
||||
x="2"
|
||||
width="71.8"
|
||||
y="2"
|
||||
height="15"
|
||||
rx="1.5"
|
||||
ry="1.5"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
width="32"
|
||||
height="6"
|
||||
x="22"
|
||||
y="5.9"
|
||||
fill="black"
|
||||
rx="3"
|
||||
ry="3"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
</svg>
|
||||
</edgeless-toolbar-button>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: Map<PropertyKey, unknown>) {
|
||||
const controllerRequiredProps = ['edgeless', 'toolbarContainer'] as const;
|
||||
if (
|
||||
controllerRequiredProps.some(p => _changedProperties.has(p)) &&
|
||||
!this.draggableController
|
||||
) {
|
||||
this.initDragController();
|
||||
}
|
||||
}
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor enableBlur = true;
|
||||
|
||||
@query('.basket-tool-item.mindmap')
|
||||
accessor mindmapElement!: HTMLElement;
|
||||
|
||||
@state()
|
||||
accessor readyToDrop = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-mindmap-tool-button': EdgelessMindmapToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { Bound } from '@blocksuite/global/utils';
|
||||
import c from 'simple-xml-to-json';
|
||||
|
||||
type MindMapNode = {
|
||||
children: MindMapNode[];
|
||||
text: string;
|
||||
xywh?: string;
|
||||
title?: string;
|
||||
layoutType?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export async function importMindmap(bound: Bound): Promise<MindMapNode> {
|
||||
const file = await openFileOrFiles({
|
||||
acceptType: 'MindMap',
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
throw new BlockSuiteError(ErrorCode.UserAbortError, 'Aborted by user');
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
if (file.name.endsWith('.mm')) {
|
||||
result = await parseMmFile(file);
|
||||
} else if (file.name.endsWith('.opml') || file.name.endsWith('.xml')) {
|
||||
result = await parseOPMLFile(file);
|
||||
} else {
|
||||
throw new BlockSuiteError(ErrorCode.ParsingError, 'Unsupported file type');
|
||||
}
|
||||
|
||||
if (result) {
|
||||
result.xywh = bound.serialize();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function readAsText(file: File) {
|
||||
return file.text();
|
||||
}
|
||||
|
||||
type RawMmNode = {
|
||||
node?: {
|
||||
TEXT: string;
|
||||
POSITION: 'left' | 'right';
|
||||
children?: RawMmNode[];
|
||||
};
|
||||
};
|
||||
|
||||
async function parseMmFile(file: File): Promise<MindMapNode> {
|
||||
const content = await readAsText(file);
|
||||
|
||||
try {
|
||||
const parsed = c.convertXML(content);
|
||||
const map = parsed.map.children[0];
|
||||
|
||||
const traverse = (node: RawMmNode): MindMapNode | null => {
|
||||
if (!node.node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return node.node.POSITION
|
||||
? {
|
||||
layoutType: node.node.POSITION,
|
||||
text: node.node.TEXT ?? 'MINDMAP',
|
||||
children:
|
||||
(node.node.children
|
||||
?.map(traverse)
|
||||
.filter(node => node) as MindMapNode[]) ?? [],
|
||||
}
|
||||
: {
|
||||
text: node.node.TEXT ?? 'MINDMAP',
|
||||
children:
|
||||
(node.node.children
|
||||
?.map(traverse)
|
||||
.filter(node => node) as MindMapNode[]) ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
const result = traverse(map);
|
||||
|
||||
if (!result) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ParsingError,
|
||||
'Failed to parse mm file'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ParsingError,
|
||||
'Failed to parse mm file'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type RawOPMLOutline = {
|
||||
outline: {
|
||||
text: string;
|
||||
children: RawOPMLOutline[];
|
||||
};
|
||||
};
|
||||
|
||||
async function parseOPMLFile(file: File): Promise<MindMapNode> {
|
||||
const content = await readAsText(file);
|
||||
|
||||
try {
|
||||
const parsed = c.convertXML(content);
|
||||
const outline = parsed.opml?.children[1].body?.children?.[0];
|
||||
|
||||
const traverse = (node: RawOPMLOutline): MindMapNode | null => {
|
||||
if (!node.outline?.text && !node.outline?.children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
text: node.outline?.text ?? 'MINDMAP',
|
||||
children: node.outline.children
|
||||
? (node.outline.children.map(traverse) as MindMapNode[])
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
const result = traverse(outline);
|
||||
|
||||
if (!result) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ParsingError,
|
||||
'Failed to parse OPML file'
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ParsingError,
|
||||
'Failed to parse OPML file'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Constructor } from '@blocksuite/global/utils';
|
||||
import type { LitElement } from 'lit';
|
||||
|
||||
import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type EdgelessToolbarToolClass,
|
||||
EdgelessToolbarToolMixin,
|
||||
} from './tool.mixin.js';
|
||||
|
||||
export declare abstract class QuickToolMixinClass extends EdgelessToolbarToolClass {}
|
||||
|
||||
/**
|
||||
* Mixin for quick tool item.
|
||||
*/
|
||||
export const QuickToolMixin = <T extends Constructor<LitElement>>(
|
||||
SuperClass: T
|
||||
) => {
|
||||
abstract class DerivedClass extends EdgelessToolbarToolMixin(SuperClass) {}
|
||||
|
||||
return DerivedClass as unknown as T & Constructor<QuickToolMixinClass>;
|
||||
};
|
||||
@@ -1,174 +0,0 @@
|
||||
import type { ColorScheme } from '@blocksuite/affine-model';
|
||||
import type {
|
||||
GfxToolsFullOption,
|
||||
GfxToolsFullOptionValue,
|
||||
ToolController,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
type Constructor,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type DisposableClass,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { consume } from '@lit/context';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import type { LitElement } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
import { createPopper, type MenuPopper } from '../common/create-popper.js';
|
||||
import {
|
||||
edgelessToolbarContext,
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
edgelessToolbarThemeContext,
|
||||
} from '../context.js';
|
||||
import type { EdgelessToolbarWidget } from '../edgeless-toolbar.js';
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export declare abstract class EdgelessToolbarToolClass extends DisposableClass {
|
||||
active: boolean;
|
||||
|
||||
createPopper: typeof createPopper;
|
||||
|
||||
edgeless: EdgelessRootBlockComponent;
|
||||
|
||||
edgelessTool: GfxToolsFullOptionValue;
|
||||
|
||||
enableActiveBackground?: boolean;
|
||||
|
||||
popper: MenuPopper<HTMLElement> | null;
|
||||
|
||||
setEdgelessTool: ToolController['setTool'];
|
||||
|
||||
theme: ColorScheme;
|
||||
|
||||
toolbarContainer: HTMLElement | null;
|
||||
|
||||
toolbarSlots: EdgelessToolbarSlots;
|
||||
|
||||
/**
|
||||
* @return true if operation was successful
|
||||
*/
|
||||
tryDisposePopper: () => boolean;
|
||||
|
||||
abstract type:
|
||||
| GfxToolsFullOptionValue['type']
|
||||
| GfxToolsFullOptionValue['type'][];
|
||||
|
||||
accessor toolbar: EdgelessToolbarWidget;
|
||||
}
|
||||
|
||||
export const EdgelessToolbarToolMixin = <T extends Constructor<LitElement>>(
|
||||
SuperClass: T
|
||||
) => {
|
||||
abstract class DerivedClass extends WithDisposable(SuperClass) {
|
||||
enableActiveBackground = false;
|
||||
|
||||
abstract type:
|
||||
| GfxToolsFullOptionValue['type']
|
||||
| GfxToolsFullOptionValue['type'][];
|
||||
|
||||
get active() {
|
||||
const { type } = this;
|
||||
const activeType = this.edgelessTool?.type;
|
||||
|
||||
return activeType
|
||||
? Array.isArray(type)
|
||||
? type.includes(activeType)
|
||||
: activeType === type
|
||||
: false;
|
||||
}
|
||||
|
||||
get setEdgelessTool() {
|
||||
return (...args: Parameters<ToolController['setTool']>) => {
|
||||
this.edgeless.gfx.tool.setTool(
|
||||
// @ts-expect-error FIXME: ts error
|
||||
...args
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private _applyActiveStyle() {
|
||||
if (!this.enableActiveBackground) return;
|
||||
this.style.background = this.active
|
||||
? cssVar('hoverColor')
|
||||
: 'transparent';
|
||||
}
|
||||
|
||||
private _updateActiveEdgelessTool() {
|
||||
this.edgelessTool = this.edgeless.gfx.tool.currentToolOption$.value;
|
||||
this._applyActiveStyle();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.edgeless) return;
|
||||
this._updateActiveEdgelessTool();
|
||||
this._applyActiveStyle();
|
||||
|
||||
this._disposables.add(
|
||||
effect(() => {
|
||||
this._updateActiveEdgelessTool();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: move to toolbar-tool-with-menu.mixin
|
||||
createPopper(...args: Parameters<typeof createPopper>) {
|
||||
if (this.toolbar.activePopper) {
|
||||
this.toolbar.activePopper.dispose();
|
||||
this.toolbar.activePopper = null;
|
||||
}
|
||||
this.popper = createPopper(args[0], args[1], {
|
||||
...args[2],
|
||||
onDispose: () => {
|
||||
args[2]?.onDispose?.();
|
||||
this.popper = null;
|
||||
},
|
||||
}) as MenuPopper<HTMLElement>;
|
||||
this.toolbar.activePopper = this.popper;
|
||||
return this.popper;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.popper?.dispose();
|
||||
}
|
||||
|
||||
tryDisposePopper() {
|
||||
if (!this.active) return false;
|
||||
if (this.popper) {
|
||||
this.popper.dispose();
|
||||
this.popper = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@state()
|
||||
accessor edgelessTool!: ValueOf<GfxToolsFullOption> | null;
|
||||
|
||||
@state()
|
||||
public accessor popper: MenuPopper<HTMLElement> | null = null;
|
||||
|
||||
@consume({ context: edgelessToolbarThemeContext, subscribe: true })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@consume({ context: edgelessToolbarContext })
|
||||
accessor toolbar!: EdgelessToolbarWidget;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor toolbarContainer: HTMLElement | null = null;
|
||||
|
||||
@consume({ context: edgelessToolbarSlotsContext })
|
||||
accessor toolbarSlots!: EdgelessToolbarSlots;
|
||||
}
|
||||
|
||||
return DerivedClass as unknown as T & Constructor<EdgelessToolbarToolClass>;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Constructor } from '@blocksuite/global/utils';
|
||||
import type { LitElement } from 'lit';
|
||||
|
||||
import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
type EdgelessToolbarToolClass,
|
||||
EdgelessToolbarToolMixin,
|
||||
} from './tool.mixin.js';
|
||||
|
||||
export declare abstract class ToolbarButtonWithMenuClass extends EdgelessToolbarToolClass {}
|
||||
|
||||
export const ToolbarButtonWithMenuMixin = <
|
||||
T extends Constructor<LitElement> = Constructor<LitElement>,
|
||||
>(
|
||||
SuperClass: T
|
||||
) => {
|
||||
abstract class DerivedClass extends EdgelessToolbarToolMixin(SuperClass) {}
|
||||
|
||||
return DerivedClass as unknown as T & Constructor<ToolbarButtonWithMenuClass>;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { svg } from 'lit';
|
||||
|
||||
export const toShapeNotToAdapt = svg`<svg width="44" height="5" viewBox="0 0 44 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.9013 1.45752V1.94112H42.5034V1.45752H43.9013ZM42.8208 0.901367H43.4646V3.06551C43.4646 3.12496 43.4737 3.1713 43.4918 3.20455C43.5099 3.23679 43.5351 3.25946 43.5674 3.27256C43.6006 3.28565 43.6389 3.2922 43.6822 3.2922C43.7124 3.2922 43.7427 3.28968 43.7729 3.28465C43.8031 3.2786 43.8263 3.27407 43.8424 3.27105L43.9437 3.75012C43.9114 3.76019 43.8661 3.77178 43.8076 3.78488C43.7492 3.79898 43.6782 3.80755 43.5946 3.81057C43.4394 3.81662 43.3034 3.79596 43.1865 3.74861C43.0706 3.70126 42.9805 3.62771 42.916 3.52796C42.8515 3.42822 42.8198 3.30228 42.8208 3.15014V0.901367Z" fill="currentColor"/>
|
||||
<path d="M39.9741 4.64977V1.45796H40.6089V1.84787H40.6376C40.6658 1.7854 40.7066 1.72193 40.76 1.65745C40.8144 1.59196 40.8849 1.53755 40.9716 1.49423C41.0592 1.4499 41.168 1.42773 41.298 1.42773C41.4673 1.42773 41.6234 1.47207 41.7665 1.56073C41.9096 1.64838 42.0239 1.78087 42.1096 1.95819C42.1952 2.13451 42.238 2.35566 42.238 2.62164C42.238 2.88057 42.1962 3.0992 42.1126 3.27753C42.03 3.45486 41.9171 3.58936 41.774 3.68104C41.632 3.77172 41.4728 3.81706 41.2965 3.81706C41.1716 3.81706 41.0653 3.79641 40.9776 3.7551C40.891 3.71379 40.8199 3.6619 40.7645 3.59944C40.7091 3.53596 40.6668 3.47199 40.6376 3.4075H40.6179V4.64977H39.9741ZM40.6043 2.61862C40.6043 2.75665 40.6235 2.87705 40.6618 2.97981C40.7 3.08258 40.7555 3.16268 40.828 3.22011C40.9005 3.27653 40.9887 3.30474 41.0925 3.30474C41.1972 3.30474 41.2859 3.27602 41.3584 3.21859C41.431 3.16016 41.4859 3.07956 41.5232 2.97679C41.5615 2.87302 41.5806 2.75363 41.5806 2.61862C41.5806 2.48462 41.562 2.36674 41.5247 2.26498C41.4874 2.16322 41.4325 2.08363 41.36 2.0262C41.2874 1.96877 41.1983 1.94006 41.0925 1.94006C40.9877 1.94006 40.899 1.96776 40.8265 2.02318C40.7549 2.07859 40.7 2.15718 40.6618 2.25894C40.6235 2.36069 40.6043 2.48059 40.6043 2.61862Z" fill="currentColor"/>
|
||||
<path d="M38.1667 3.8231C38.0186 3.8231 37.8867 3.79741 37.7708 3.74603C37.6549 3.69364 37.5632 3.61656 37.4957 3.5148C37.4292 3.41204 37.396 3.28408 37.396 3.13094C37.396 3.00198 37.4197 2.89367 37.467 2.80602C37.5144 2.71836 37.5789 2.64784 37.6605 2.59444C37.7421 2.54104 37.8348 2.50074 37.9385 2.47354C38.0433 2.44633 38.1531 2.42719 38.268 2.41611C38.403 2.402 38.5118 2.38891 38.5944 2.37681C38.6771 2.36372 38.737 2.34457 38.7743 2.31939C38.8116 2.2942 38.8302 2.25692 38.8302 2.20755V2.19848C38.8302 2.10277 38.8 2.02872 38.7395 1.97633C38.6801 1.92394 38.5954 1.89774 38.4856 1.89774C38.3698 1.89774 38.2776 1.92343 38.2091 1.97482C38.1406 2.02519 38.0952 2.08867 38.073 2.16524L37.4776 2.11688C37.5078 1.97582 37.5673 1.85391 37.6559 1.75115C37.7446 1.64737 37.8589 1.56778 37.999 1.51237C38.14 1.45594 38.3033 1.42773 38.4886 1.42773C38.6176 1.42773 38.741 1.44285 38.8589 1.47307C38.9778 1.5033 39.0831 1.55015 39.1748 1.61362C39.2675 1.67709 39.3405 1.7587 39.3939 1.85845C39.4473 1.95718 39.474 2.07557 39.474 2.2136V3.77928H38.8634V3.45738H38.8453C38.808 3.52992 38.7582 3.59389 38.6957 3.64931C38.6332 3.70371 38.5582 3.74653 38.4705 3.77777C38.3829 3.80799 38.2816 3.8231 38.1667 3.8231ZM38.3511 3.37879C38.4458 3.37879 38.5295 3.36015 38.602 3.32287C38.6745 3.28459 38.7315 3.2332 38.7728 3.16872C38.8141 3.10424 38.8347 3.0312 38.8347 2.94959V2.70325C38.8146 2.71635 38.7869 2.72844 38.7516 2.73952C38.7174 2.7496 38.6786 2.75917 38.6352 2.76823C38.5919 2.7763 38.5486 2.78385 38.5053 2.7909C38.4619 2.79695 38.4227 2.80249 38.3874 2.80753C38.3118 2.81861 38.2458 2.83624 38.1894 2.86042C38.133 2.8846 38.0892 2.91735 38.0579 2.95866C38.0267 2.99896 38.0111 3.04933 38.0111 3.10978C38.0111 3.19744 38.0428 3.26444 38.1063 3.31078C38.1708 3.35612 38.2524 3.37879 38.3511 3.37879Z" fill="currentColor"/>
|
||||
<path d="M35.6544 3.81647C35.4781 3.81647 35.3184 3.77113 35.1753 3.68045C35.0333 3.58877 34.9204 3.45426 34.8368 3.27694C34.7542 3.09861 34.7129 2.87998 34.7129 2.62105C34.7129 2.35506 34.7557 2.13391 34.8413 1.9576C34.927 1.78028 35.0408 1.64779 35.1829 1.56013C35.326 1.47147 35.4826 1.42714 35.6529 1.42714C35.7829 1.42714 35.8912 1.44931 35.9778 1.49364C36.0655 1.53696 36.136 1.59137 36.1894 1.65685C36.2438 1.72134 36.2851 1.78481 36.3133 1.84728H36.333V0.683594H36.9753V3.77868H36.3405V3.40691H36.3133C36.2831 3.47139 36.2403 3.53537 36.1849 3.59884C36.1305 3.66131 36.0594 3.7132 35.9718 3.7545C35.8851 3.79581 35.7793 3.81647 35.6544 3.81647ZM35.8584 3.30414C35.9622 3.30414 36.0499 3.27593 36.1214 3.21951C36.1939 3.16208 36.2494 3.08199 36.2876 2.97922C36.3269 2.87645 36.3466 2.75605 36.3466 2.61803C36.3466 2.48 36.3274 2.3601 36.2891 2.25834C36.2509 2.15658 36.1955 2.078 36.1229 2.02258C36.0504 1.96717 35.9622 1.93946 35.8584 1.93946C35.7526 1.93946 35.6635 1.96818 35.5909 2.02561C35.5184 2.08303 35.4635 2.16263 35.4262 2.26439C35.3889 2.36615 35.3703 2.48403 35.3703 2.61803C35.3703 2.75303 35.3889 2.87242 35.4262 2.9762C35.4645 3.07896 35.5194 3.15957 35.5909 3.218C35.6635 3.27543 35.7526 3.30414 35.8584 3.30414Z" fill="currentColor"/>
|
||||
<path d="M32.9929 3.82213C32.8448 3.82213 32.7128 3.79644 32.597 3.74505C32.4811 3.69266 32.3894 3.61559 32.3219 3.51383C32.2554 3.41106 32.2222 3.28311 32.2222 3.12996C32.2222 3.001 32.2458 2.89269 32.2932 2.80504C32.3406 2.71739 32.405 2.64686 32.4866 2.59346C32.5682 2.54006 32.6609 2.49976 32.7647 2.47256C32.8695 2.44536 32.9793 2.42621 33.0942 2.41513C33.2292 2.40103 33.338 2.38793 33.4206 2.37584C33.5032 2.36274 33.5632 2.3436 33.6005 2.31841C33.6377 2.29322 33.6564 2.25594 33.6564 2.20658V2.19751C33.6564 2.10179 33.6261 2.02774 33.5657 1.97535C33.5062 1.92296 33.4216 1.89676 33.3118 1.89676C33.1959 1.89676 33.1037 1.92246 33.0352 1.97384C32.9667 2.02422 32.9214 2.08769 32.8992 2.16426L32.3038 2.1159C32.334 1.97485 32.3934 1.85294 32.4821 1.75017C32.5708 1.6464 32.6851 1.5668 32.8252 1.51139C32.9662 1.45497 33.1294 1.42676 33.3148 1.42676C33.4438 1.42676 33.5672 1.44187 33.6851 1.4721C33.804 1.50232 33.9093 1.54917 34.0009 1.61264C34.0936 1.67612 34.1667 1.75773 34.2201 1.85747C34.2735 1.95621 34.3002 2.07459 34.3002 2.21262V3.7783H33.6896V3.4564H33.6715C33.6342 3.52894 33.5843 3.59292 33.5219 3.64833C33.4594 3.70274 33.3843 3.74556 33.2967 3.77679C33.209 3.80702 33.1078 3.82213 32.9929 3.82213ZM33.1773 3.37781C33.272 3.37781 33.3556 3.35917 33.4282 3.3219C33.5007 3.28361 33.5576 3.23223 33.5989 3.16775C33.6402 3.10327 33.6609 3.03022 33.6609 2.94861V2.70227C33.6408 2.71537 33.613 2.72746 33.5778 2.73854C33.5435 2.74862 33.5047 2.75819 33.4614 2.76726C33.4181 2.77532 33.3748 2.78287 33.3314 2.78993C33.2881 2.79597 33.2488 2.80151 33.2136 2.80655C33.138 2.81763 33.072 2.83527 33.0156 2.85945C32.9592 2.88363 32.9153 2.91637 32.8841 2.95768C32.8529 2.99798 32.8373 3.04836 32.8373 3.10881C32.8373 3.19646 32.869 3.26346 32.9325 3.30981C32.9969 3.35514 33.0786 3.37781 33.1773 3.37781Z" fill="currentColor"/>
|
||||
<path d="M29.7856 3.82364C29.5508 3.82364 29.3478 3.77377 29.1765 3.67402C29.0063 3.57327 28.8748 3.43323 28.7821 3.25389C28.6894 3.07354 28.6431 2.86448 28.6431 2.62671C28.6431 2.38692 28.6894 2.17736 28.7821 1.99802C28.8748 1.81767 29.0063 1.67763 29.1765 1.57789C29.3478 1.47713 29.5508 1.42676 29.7856 1.42676C30.0203 1.42676 30.2229 1.47713 30.3931 1.57789C30.5644 1.67763 30.6964 1.81767 30.7891 1.99802C30.8818 2.17736 30.9281 2.38692 30.9281 2.62671C30.9281 2.86448 30.8818 3.07354 30.7891 3.25389C30.6964 3.43323 30.5644 3.57327 30.3931 3.67402C30.2229 3.77377 30.0203 3.82364 29.7856 3.82364ZM29.7886 3.32492C29.8954 3.32492 29.9846 3.29469 30.0561 3.23424C30.1276 3.17278 30.1815 3.08916 30.2178 2.98337C30.2551 2.87758 30.2737 2.75718 30.2737 2.62218C30.2737 2.48717 30.2551 2.36677 30.2178 2.26098C30.1815 2.15519 30.1276 2.07157 30.0561 2.01011C29.9846 1.94865 29.8954 1.91792 29.7886 1.91792C29.6808 1.91792 29.5901 1.94865 29.5166 2.01011C29.444 2.07157 29.3891 2.15519 29.3519 2.26098C29.3156 2.36677 29.2974 2.48717 29.2974 2.62218C29.2974 2.75718 29.3156 2.87758 29.3519 2.98337C29.3891 3.08916 29.444 3.17278 29.5166 3.23424C29.5901 3.29469 29.6808 3.32492 29.7886 3.32492Z" fill="currentColor"/>
|
||||
<path d="M28.3413 1.45752V1.94112H26.9434V1.45752H28.3413ZM27.2607 0.901367H27.9045V3.06551C27.9045 3.12496 27.9136 3.1713 27.9317 3.20455C27.9499 3.23679 27.9751 3.25946 28.0073 3.27256C28.0405 3.28565 28.0788 3.2922 28.1222 3.2922C28.1524 3.2922 28.1826 3.28968 28.2128 3.28465C28.2431 3.2786 28.2662 3.27407 28.2823 3.27105L28.3836 3.75012C28.3514 3.76019 28.306 3.77178 28.2476 3.78488C28.1892 3.79898 28.1181 3.80755 28.0345 3.81057C27.8793 3.81662 27.7433 3.79596 27.6265 3.74861C27.5106 3.70126 27.4204 3.62771 27.3559 3.52796C27.2915 3.42822 27.2597 3.30228 27.2607 3.15014V0.901367Z" fill="currentColor"/>
|
||||
<path d="M25.7007 1.45752V1.94112H24.3027V1.45752H25.7007ZM24.6201 0.901367H25.2639V3.06551C25.2639 3.12496 25.273 3.1713 25.2911 3.20455C25.3092 3.23679 25.3344 3.25946 25.3667 3.27256C25.3999 3.28565 25.4382 3.2922 25.4815 3.2922C25.5118 3.2922 25.542 3.28968 25.5722 3.28465C25.6024 3.2786 25.6256 3.27407 25.6417 3.27105L25.743 3.75012C25.7107 3.76019 25.6654 3.77178 25.607 3.78488C25.5485 3.79898 25.4775 3.80755 25.3939 3.81057C25.2387 3.81662 25.1027 3.79596 24.9858 3.74861C24.87 3.70126 24.7798 3.62771 24.7153 3.52796C24.6508 3.42822 24.6191 3.30228 24.6201 3.15014V0.901367Z" fill="currentColor"/>
|
||||
<path d="M22.9062 3.82462C22.6714 3.82462 22.4684 3.77474 22.2972 3.675C22.1269 3.57425 21.9954 3.4342 21.9027 3.25487C21.81 3.07452 21.7637 2.86546 21.7637 2.62769C21.7637 2.3879 21.81 2.17833 21.9027 1.999C21.9954 1.81865 22.1269 1.67861 22.2972 1.57886C22.4684 1.47811 22.6714 1.42773 22.9062 1.42773C23.1409 1.42773 23.3435 1.47811 23.5137 1.57886C23.685 1.67861 23.817 1.81865 23.9097 1.999C24.0024 2.17833 24.0487 2.3879 24.0487 2.62769C24.0487 2.86546 24.0024 3.07452 23.9097 3.25487C23.817 3.4342 23.685 3.57425 23.5137 3.675C23.3435 3.77474 23.1409 3.82462 22.9062 3.82462ZM22.9092 3.3259C23.016 3.3259 23.1052 3.29567 23.1767 3.23522C23.2482 3.17376 23.3021 3.09014 23.3384 2.98435C23.3757 2.87856 23.3943 2.75816 23.3943 2.62315C23.3943 2.48815 23.3757 2.36775 23.3384 2.26196C23.3021 2.15617 23.2482 2.07254 23.1767 2.01109C23.1052 1.94963 23.016 1.9189 22.9092 1.9189C22.8014 1.9189 22.7107 1.94963 22.6372 2.01109C22.5646 2.07254 22.5097 2.15617 22.4725 2.26196C22.4362 2.36775 22.4181 2.48815 22.4181 2.62315C22.4181 2.75816 22.4362 2.87856 22.4725 2.98435C22.5097 3.09014 22.5646 3.17376 22.6372 3.23522C22.7107 3.29567 22.8014 3.3259 22.9092 3.3259Z" fill="currentColor"/>
|
||||
<path d="M19.8538 2.43727V3.77928H19.21V1.45796H19.8235V1.86752H19.8507C19.9021 1.73251 19.9883 1.62571 20.1092 1.54712C20.2301 1.46753 20.3767 1.42773 20.549 1.42773C20.7102 1.42773 20.8507 1.463 20.9706 1.53352C21.0905 1.60405 21.1837 1.7048 21.2502 1.83578C21.3167 1.96575 21.3499 2.12091 21.3499 2.30125V3.77928H20.7061V2.41611C20.7071 2.27405 20.6709 2.16322 20.5973 2.08363C20.5238 2.00303 20.4225 1.96273 20.2935 1.96273C20.2069 1.96273 20.1303 1.98136 20.0638 2.01864C19.9983 2.05592 19.947 2.11033 19.9097 2.18186C19.8734 2.25239 19.8548 2.33752 19.8538 2.43727Z" fill="currentColor"/>
|
||||
<path d="M16.7385 3.82364C16.4997 3.82364 16.2942 3.77528 16.1219 3.67856C15.9506 3.58083 15.8186 3.4428 15.726 3.26447C15.6333 3.08513 15.5869 2.87305 15.5869 2.62822C15.5869 2.38944 15.6333 2.17988 15.726 1.99953C15.8186 1.81919 15.9491 1.67864 16.1174 1.57789C16.2866 1.47713 16.4851 1.42676 16.7128 1.42676C16.866 1.42676 17.0085 1.45144 17.1405 1.50081C17.2735 1.54917 17.3894 1.62222 17.4881 1.71995C17.5878 1.81767 17.6654 1.94059 17.7208 2.0887C17.7762 2.23579 17.804 2.40808 17.804 2.60555V2.78237H15.8438V2.38339H17.1979C17.1979 2.2907 17.1778 2.20859 17.1375 2.13706C17.0972 2.06552 17.0413 2.00961 16.9697 1.96931C16.8992 1.928 16.8171 1.90734 16.7234 1.90734C16.6257 1.90734 16.539 1.93001 16.4635 1.97535C16.3889 2.01968 16.3305 2.07963 16.2881 2.15519C16.2458 2.22975 16.2242 2.31287 16.2232 2.40455V2.78388C16.2232 2.89874 16.2443 2.99798 16.2866 3.0816C16.33 3.16523 16.3909 3.22971 16.4695 3.27505C16.5481 3.32038 16.6413 3.34305 16.7491 3.34305C16.8206 3.34305 16.8861 3.33298 16.9455 3.31283C17.005 3.29268 17.0559 3.26245 17.0982 3.22215C17.1405 3.18185 17.1727 3.13248 17.1949 3.07405L17.7904 3.11334C17.7601 3.25641 17.6982 3.38134 17.6045 3.48814C17.5118 3.59393 17.3919 3.67654 17.2448 3.73599C17.0987 3.79442 16.9299 3.82364 16.7385 3.82364Z" fill="currentColor"/>
|
||||
<path d="M12.9878 4.64977V1.45796H13.6225V1.84787H13.6512C13.6795 1.7854 13.7203 1.72193 13.7737 1.65745C13.8281 1.59196 13.8986 1.53755 13.9852 1.49423C14.0729 1.4499 14.1817 1.42773 14.3117 1.42773C14.4809 1.42773 14.6371 1.47207 14.7802 1.56073C14.9232 1.64838 15.0376 1.78087 15.1232 1.95819C15.2089 2.13451 15.2517 2.35566 15.2517 2.62164C15.2517 2.88057 15.2099 3.0992 15.1262 3.27753C15.0436 3.45486 14.9308 3.58936 14.7877 3.68104C14.6457 3.77172 14.4865 3.81706 14.3102 3.81706C14.1852 3.81706 14.0789 3.79641 13.9913 3.7551C13.9046 3.71379 13.8336 3.6619 13.7782 3.59944C13.7228 3.53596 13.6805 3.47199 13.6512 3.4075H13.6316V4.64977H12.9878ZM13.618 2.61862C13.618 2.75665 13.6371 2.87705 13.6754 2.97981C13.7137 3.08258 13.7691 3.16268 13.8417 3.22011C13.9142 3.27653 14.0024 3.30474 14.1061 3.30474C14.2109 3.30474 14.2996 3.27602 14.3721 3.21859C14.4447 3.16016 14.4996 3.07956 14.5368 2.97679C14.5751 2.87302 14.5943 2.75363 14.5943 2.61862C14.5943 2.48462 14.5756 2.36674 14.5384 2.26498C14.5011 2.16322 14.4462 2.08363 14.3736 2.0262C14.3011 1.96877 14.2119 1.94006 14.1061 1.94006C14.0014 1.94006 13.9127 1.96776 13.8402 2.02318C13.7686 2.07859 13.7137 2.15718 13.6754 2.25894C13.6371 2.36069 13.618 2.48059 13.618 2.61862Z" fill="currentColor"/>
|
||||
<path d="M11.1814 3.82213C11.0333 3.82213 10.9013 3.79644 10.7854 3.74505C10.6696 3.69266 10.5779 3.61559 10.5104 3.51383C10.4439 3.41106 10.4106 3.28311 10.4106 3.12996C10.4106 3.001 10.4343 2.89269 10.4817 2.80504C10.529 2.71739 10.5935 2.64686 10.6751 2.59346C10.7567 2.54006 10.8494 2.49976 10.9532 2.47256C11.058 2.44536 11.1678 2.42621 11.2827 2.41513C11.4177 2.40103 11.5265 2.38793 11.6091 2.37584C11.6917 2.36274 11.7516 2.3436 11.7889 2.31841C11.8262 2.29322 11.8448 2.25594 11.8448 2.20658V2.19751C11.8448 2.10179 11.8146 2.02774 11.7542 1.97535C11.6947 1.92296 11.6101 1.89676 11.5003 1.89676C11.3844 1.89676 11.2922 1.92246 11.2237 1.97384C11.1552 2.02422 11.1099 2.08769 11.0877 2.16426L10.4923 2.1159C10.5225 1.97485 10.5819 1.85294 10.6706 1.75017C10.7592 1.6464 10.8736 1.5668 11.0136 1.51139C11.1547 1.45497 11.3179 1.42676 11.5033 1.42676C11.6323 1.42676 11.7557 1.44187 11.8736 1.4721C11.9924 1.50232 12.0977 1.54917 12.1894 1.61264C12.2821 1.67612 12.3552 1.75773 12.4085 1.85747C12.4619 1.95621 12.4886 2.07459 12.4886 2.21262V3.7783H11.8781V3.4564H11.86C11.8227 3.52894 11.7728 3.59292 11.7103 3.64833C11.6479 3.70274 11.5728 3.74556 11.4852 3.77679C11.3975 3.80702 11.2963 3.82213 11.1814 3.82213ZM11.3658 3.37781C11.4605 3.37781 11.5441 3.35917 11.6166 3.3219C11.6892 3.28361 11.7461 3.23223 11.7874 3.16775C11.8287 3.10327 11.8494 3.03022 11.8494 2.94861V2.70227C11.8292 2.71537 11.8015 2.72746 11.7663 2.73854C11.732 2.74862 11.6932 2.75819 11.6499 2.76726C11.6066 2.77532 11.5632 2.78287 11.5199 2.78993C11.4766 2.79597 11.4373 2.80151 11.402 2.80655C11.3265 2.81763 11.2605 2.83527 11.2041 2.85945C11.1476 2.88363 11.1038 2.91637 11.0726 2.95768C11.0413 2.99798 11.0257 3.04836 11.0257 3.10881C11.0257 3.19646 11.0575 3.26346 11.1209 3.30981C11.1854 3.35514 11.267 3.37781 11.3658 3.37781Z" fill="currentColor"/>
|
||||
<path d="M8.50757 2.43667V3.77868H7.86377V0.683594H8.48944V1.86692H8.51664C8.56903 1.7299 8.65366 1.6226 8.77053 1.54502C8.88741 1.46643 9.034 1.42714 9.21032 1.42714C9.37152 1.42714 9.51207 1.4624 9.63196 1.53293C9.75286 1.60245 9.84656 1.7027 9.91306 1.83367C9.98056 1.96364 10.0138 2.1193 10.0128 2.30066V3.77868H9.369V2.41551C9.37001 2.27245 9.33374 2.16112 9.26019 2.08152C9.18765 2.00193 9.08589 1.96213 8.95491 1.96213C8.86726 1.96213 8.78968 1.98077 8.72217 2.01805C8.65568 2.05533 8.60329 2.10973 8.565 2.18127C8.52772 2.25179 8.50858 2.33693 8.50757 2.43667Z" fill="currentColor"/>
|
||||
<path d="M7.40576 2.1199L6.81636 2.15617C6.80629 2.10579 6.78462 2.06045 6.75138 2.02015C6.71813 1.97885 6.6743 1.9461 6.61989 1.92192C6.5665 1.89673 6.50252 1.88414 6.42796 1.88414C6.32822 1.88414 6.24409 1.9053 6.17558 1.94761C6.10707 1.98892 6.07281 2.04433 6.07281 2.11385C6.07281 2.16927 6.09498 2.21612 6.13931 2.2544C6.18364 2.29269 6.25971 2.32342 6.36751 2.34659L6.78765 2.43122C7.01333 2.47757 7.18159 2.55212 7.29241 2.65489C7.40324 2.75766 7.45865 2.89266 7.45865 3.05991C7.45865 3.21205 7.41382 3.34554 7.32415 3.4604C7.23549 3.57526 7.11358 3.66492 6.95842 3.72941C6.80427 3.79288 6.62644 3.82462 6.42494 3.82462C6.11765 3.82462 5.87282 3.76064 5.69046 3.63268C5.50911 3.50372 5.40282 3.32841 5.37158 3.10676L6.00481 3.07351C6.02395 3.16721 6.07029 3.23875 6.14384 3.28811C6.21739 3.33647 6.31159 3.36065 6.42645 3.36065C6.53929 3.36065 6.62997 3.33899 6.69848 3.29567C6.768 3.25134 6.80326 3.19441 6.80427 3.1249C6.80326 3.06646 6.77858 3.0186 6.73022 2.98132C6.68186 2.94304 6.6073 2.91382 6.50655 2.89367L6.10455 2.81357C5.87786 2.76823 5.7091 2.68965 5.59827 2.57781C5.48845 2.46598 5.43354 2.32342 5.43354 2.15012C5.43354 2.00101 5.47384 1.87255 5.55445 1.76475C5.63606 1.65694 5.75041 1.57382 5.89751 1.51539C6.04561 1.45695 6.2189 1.42773 6.41738 1.42773C6.71057 1.42773 6.94129 1.4897 7.10955 1.61362C7.27881 1.73755 7.37755 1.9063 7.40576 2.1199Z" fill="currentColor"/>
|
||||
<path d="M2.92866 3.82364C2.69391 3.82364 2.49089 3.77377 2.31961 3.67402C2.14934 3.57327 2.01786 3.43323 1.92517 3.25389C1.83248 3.07354 1.78613 2.86448 1.78613 2.62671C1.78613 2.38692 1.83248 2.17736 1.92517 1.99802C2.01786 1.81767 2.14934 1.67763 2.31961 1.57789C2.49089 1.47713 2.69391 1.42676 2.92866 1.42676C3.16341 1.42676 3.36592 1.47713 3.53619 1.57789C3.70747 1.67763 3.83945 1.81767 3.93214 1.99802C4.02483 2.17736 4.07118 2.38692 4.07118 2.62671C4.07118 2.86448 4.02483 3.07354 3.93214 3.25389C3.83945 3.43323 3.70747 3.57327 3.53619 3.67402C3.36592 3.77377 3.16341 3.82364 2.92866 3.82364ZM2.93168 3.32492C3.03848 3.32492 3.12764 3.29469 3.19917 3.23424C3.27071 3.17278 3.32461 3.08916 3.36088 2.98337C3.39816 2.87758 3.4168 2.75718 3.4168 2.62218C3.4168 2.48717 3.39816 2.36677 3.36088 2.26098C3.32461 2.15519 3.27071 2.07157 3.19917 2.01011C3.12764 1.94865 3.03848 1.91792 2.93168 1.91792C2.82387 1.91792 2.7332 1.94865 2.65965 2.01011C2.58711 2.07157 2.5322 2.15519 2.49492 2.26098C2.45865 2.36677 2.44051 2.48717 2.44051 2.62218C2.44051 2.75718 2.45865 2.87758 2.49492 2.98337C2.5322 3.08916 2.58711 3.17278 2.65965 3.23424C2.7332 3.29469 2.82387 3.32492 2.93168 3.32492Z" fill="currentColor"/>
|
||||
<path d="M1.48533 1.45752V1.94112H0.0874023L0.0874023 1.45752H1.48533ZM0.40477 0.901367H1.04857V3.06551C1.04857 3.12496 1.05764 3.1713 1.07578 3.20455C1.09391 3.23679 1.1191 3.25946 1.15134 3.27256C1.18459 3.28565 1.22287 3.2922 1.2662 3.2922C1.29642 3.2922 1.32665 3.28968 1.35687 3.28465C1.3871 3.2786 1.41027 3.27407 1.42639 3.27105L1.52765 3.75012C1.49541 3.76019 1.45007 3.77178 1.39163 3.78488C1.3332 3.79898 1.26217 3.80755 1.17854 3.81057C1.02339 3.81662 0.88737 3.79596 0.770499 3.74861C0.654634 3.70126 0.564461 3.62771 0.49998 3.52796C0.435499 3.42822 0.403763 3.30228 0.40477 3.15014V0.901367Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
`;
|
||||
@@ -1,151 +0,0 @@
|
||||
import {
|
||||
BulletedListIcon,
|
||||
CheckBoxIcon,
|
||||
CodeBlockIcon,
|
||||
DividerIcon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
Heading4Icon,
|
||||
Heading5Icon,
|
||||
Heading6Icon,
|
||||
NumberedListIcon,
|
||||
QuoteIcon,
|
||||
TextIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export const BUTTON_GROUP_LENGTH = 10;
|
||||
|
||||
export type NoteMenuItem = {
|
||||
icon: TemplateResult<1>;
|
||||
tooltip: string;
|
||||
childFlavour: NoteChildrenFlavour;
|
||||
childType: string | null;
|
||||
};
|
||||
|
||||
const LIST_ITEMS = [
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'bulleted',
|
||||
name: 'Bulleted List',
|
||||
description: 'A simple bulleted list.',
|
||||
icon: BulletedListIcon,
|
||||
tooltip: 'Drag/Click to insert Bulleted List',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'numbered',
|
||||
name: 'Numbered List',
|
||||
description: 'A list with numbering.',
|
||||
icon: NumberedListIcon,
|
||||
tooltip: 'Drag/Click to insert Numbered List',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:list',
|
||||
type: 'todo',
|
||||
name: 'To-do List',
|
||||
description: 'Track tasks with a to-do list.',
|
||||
icon: CheckBoxIcon,
|
||||
tooltip: 'Drag/Click to insert To-do List',
|
||||
},
|
||||
];
|
||||
|
||||
const TEXT_ITEMS = [
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'text',
|
||||
name: 'Text',
|
||||
description: 'Start typing with plain text.',
|
||||
icon: TextIcon,
|
||||
tooltip: 'Drag/Click to insert Text block',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h1',
|
||||
name: 'Heading 1',
|
||||
description: 'Headings in the largest font.',
|
||||
icon: Heading1Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 1',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h2',
|
||||
name: 'Heading 2',
|
||||
description: 'Headings in the 2nd font size.',
|
||||
icon: Heading2Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 2',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h3',
|
||||
name: 'Heading 3',
|
||||
description: 'Headings in the 3rd font size.',
|
||||
icon: Heading3Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 3',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h4',
|
||||
name: 'Heading 4',
|
||||
description: 'Heading in the 4th font size.',
|
||||
icon: Heading4Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 4',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h5',
|
||||
name: 'Heading 5',
|
||||
description: 'Heading in the 5th font size.',
|
||||
icon: Heading5Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 5',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'h6',
|
||||
name: 'Heading 6',
|
||||
description: 'Heading in the 6th font size.',
|
||||
icon: Heading6Icon,
|
||||
tooltip: 'Drag/Click to insert Heading 6',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:code',
|
||||
type: 'code',
|
||||
name: 'Code Block',
|
||||
description: 'Capture a code snippet.',
|
||||
icon: CodeBlockIcon,
|
||||
tooltip: 'Drag/Click to insert Code Block',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:paragraph',
|
||||
type: 'quote',
|
||||
name: 'Quote',
|
||||
description: 'Capture a quote.',
|
||||
icon: QuoteIcon,
|
||||
tooltip: 'Drag/Click to insert Quote',
|
||||
},
|
||||
{
|
||||
flavour: 'affine:divider',
|
||||
type: null,
|
||||
name: 'Divider',
|
||||
description: 'A visual divider.',
|
||||
icon: DividerIcon,
|
||||
tooltip: 'A visual divider',
|
||||
},
|
||||
];
|
||||
|
||||
// TODO: add image, bookmark, database blocks
|
||||
export const NOTE_MENU_ITEMS = TEXT_ITEMS.concat(LIST_ITEMS)
|
||||
.filter(item => item.name !== 'Divider')
|
||||
.map(item => {
|
||||
return {
|
||||
icon: item.icon,
|
||||
tooltip:
|
||||
item.type !== 'text'
|
||||
? item.tooltip.replace('Drag/Click to insert ', '')
|
||||
: 'Text',
|
||||
childFlavour: item.flavour as NoteChildrenFlavour,
|
||||
childType: item.type,
|
||||
} as NoteMenuItem;
|
||||
});
|
||||
@@ -1,212 +0,0 @@
|
||||
import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { addImages, LoadedImageIcon } from '@blocksuite/affine-block-image';
|
||||
import { AttachmentIcon, LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { MAX_IMAGE_WIDTH } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { NoteChildrenFlavour } from '@blocksuite/affine-shared/types';
|
||||
import {
|
||||
getImageFilesFromLocal,
|
||||
openFileOrFiles,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { NoteToolOption } from '../../../gfx-tool/note-tool.js';
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { NOTE_MENU_ITEMS } from './note-menu-config.js';
|
||||
|
||||
export class EdgelessNoteMenu extends EdgelessToolbarToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
z-index: -1;
|
||||
}
|
||||
.menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.button-group-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
fill: var(--affine-icon-color);
|
||||
}
|
||||
.button-group-container svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--affine-border-color);
|
||||
transform: scaleX(0.5);
|
||||
margin: 0 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'affine:note';
|
||||
|
||||
private async _addImages() {
|
||||
this._imageLoading = true;
|
||||
const imageFiles = await getImageFilesFromLocal();
|
||||
const ids = await addImages(this.edgeless.std, imageFiles, {
|
||||
maxWidth: MAX_IMAGE_WIDTH,
|
||||
});
|
||||
this._imageLoading = false;
|
||||
this.edgeless.gfx.tool.setTool('default');
|
||||
this.edgeless.gfx.selection.set({ elements: ids });
|
||||
}
|
||||
|
||||
private _onHandleLinkButtonClick() {
|
||||
const [_, { insertedLinkType }] = this.edgeless.service.std.command.exec(
|
||||
insertLinkByQuickSearchCommand
|
||||
);
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
type: flavour.split(':')[1],
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const tool = this.edgeless.gfx.tool.currentToolOption$.value;
|
||||
|
||||
if (tool?.type !== 'affine:note') return;
|
||||
this.childFlavour = tool.childFlavour;
|
||||
this.childType = tool.childType;
|
||||
this.tip = tool.tip;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { childType } = this;
|
||||
|
||||
return html`
|
||||
<edgeless-slide-menu>
|
||||
<div class="menu-content">
|
||||
<!-- add to edgeless -->
|
||||
<div class="button-group-container">
|
||||
<edgeless-tool-icon-button
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${'Image'}
|
||||
@click=${this._addImages}
|
||||
.disabled=${this._imageLoading}
|
||||
>
|
||||
${LoadedImageIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${getTooltipWithShortcut('Link', '@')}
|
||||
@click=${() => {
|
||||
this._onHandleLinkButtonClick();
|
||||
}}
|
||||
>
|
||||
${LinkIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<edgeless-tool-icon-button
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${'File'}
|
||||
@click=${async () => {
|
||||
const file = await openFileOrFiles();
|
||||
if (!file) return;
|
||||
await addAttachments(this.edgeless.std, [file]);
|
||||
this.edgeless.gfx.tool.setTool('default');
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: 'attachment',
|
||||
});
|
||||
}}
|
||||
>
|
||||
${AttachmentIcon}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- add to note -->
|
||||
<div class="button-group-container">
|
||||
${repeat(
|
||||
NOTE_MENU_ITEMS,
|
||||
item => item.childFlavour,
|
||||
item => html`
|
||||
<edgeless-tool-icon-button
|
||||
.active=${childType === item.childType}
|
||||
.activeMode=${'background'}
|
||||
.tooltip=${item.tooltip}
|
||||
@click=${() =>
|
||||
this.onChange({
|
||||
childFlavour: item.childFlavour,
|
||||
childType: item.childType,
|
||||
tip: item.tooltip,
|
||||
})}
|
||||
>
|
||||
${item.icon}
|
||||
</edgeless-tool-icon-button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</edgeless-slide-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _imageLoading = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor childFlavour!: NoteChildrenFlavour;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor childType!: string | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onChange!: (
|
||||
props: Partial<{
|
||||
childFlavour: NoteToolOption['childFlavour'];
|
||||
childType: string | null;
|
||||
tip: string;
|
||||
}>
|
||||
) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor tip!: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-menu': EdgelessNoteMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import {
|
||||
Heading1Icon,
|
||||
LinkIcon,
|
||||
TextIcon,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { SignalWatcher } from '@blocksuite/global/utils';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
|
||||
import type { NoteToolOption } from '../../../gfx-tool/note-tool.js';
|
||||
import { getTooltipWithShortcut } from '../../utils.js';
|
||||
import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js';
|
||||
import { toShapeNotToAdapt } from './icon.js';
|
||||
|
||||
export class EdgelessNoteSeniorButton extends EdgelessToolbarToolMixin(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host,
|
||||
.edgeless-note-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:host * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.note-root[data-app-theme='light'] {
|
||||
--paper-border-color: var(--affine-pure-white);
|
||||
--paper-foriegn-color: rgba(0, 0, 0, 0.1);
|
||||
--paper-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
|
||||
--icon-card-bg: #fff;
|
||||
--icon-card-shadow:
|
||||
0px 2px 4px rgba(0, 0, 0, 0.22), inset 0px -2px 1px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.note-root[data-app-theme='dark'] {
|
||||
--paper-border-color: var(--affine-divider-color);
|
||||
--paper-foriegn-color: rgba(255, 255, 255, 0.12);
|
||||
--paper-shadow: 0px 2px 6px rgba(0, 0, 0, 0.8);
|
||||
--icon-card-bg: #343434;
|
||||
--icon-card-shadow:
|
||||
0px 2px 4px rgba(0, 0, 0, 0.6),
|
||||
inset 0px -2px 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.note-root {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
.paper {
|
||||
--y: 20px;
|
||||
--r: 4.42deg;
|
||||
width: 60px;
|
||||
height: 72px;
|
||||
background: var(--paper-bg);
|
||||
border: 1px solid var(--paper-border-color);
|
||||
position: absolute;
|
||||
transform: translateY(var(--y)) rotate(var(--r));
|
||||
color: var(--paper-foriegn-color);
|
||||
box-shadow: var(--paper-shadow);
|
||||
padding-top: 32px;
|
||||
padding-left: 3px;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.edgeless-toolbar-note-icon {
|
||||
position: absolute;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--affine-icon-secondary);
|
||||
background: var(--icon-card-bg);
|
||||
box-shadow: var(--icon-card-shadow);
|
||||
bottom: 12px;
|
||||
transition: transform 0.4s ease;
|
||||
transform: translateX(var(--x)) translateY(var(--y)) rotate(var(--r));
|
||||
}
|
||||
.edgeless-toolbar-note-icon.link {
|
||||
--x: -22px;
|
||||
--y: -5px;
|
||||
--r: -6deg;
|
||||
transform-origin: 0% 100%;
|
||||
}
|
||||
.edgeless-toolbar-note-icon.text {
|
||||
--r: 4deg;
|
||||
--x: 0px;
|
||||
--y: 0px;
|
||||
}
|
||||
.edgeless-toolbar-note-icon.heading {
|
||||
--x: 21px;
|
||||
--y: -7px;
|
||||
--r: 8deg;
|
||||
transform-origin: 0% 100%;
|
||||
}
|
||||
|
||||
.note-root:hover .paper {
|
||||
--y: 15px;
|
||||
}
|
||||
.note-root:hover .link {
|
||||
--x: -25px;
|
||||
--y: -5px;
|
||||
--r: -9.5deg;
|
||||
}
|
||||
.note-root:hover .text {
|
||||
--y: -10px;
|
||||
}
|
||||
.note-root:hover .heading {
|
||||
--x: 23px;
|
||||
--y: -8px;
|
||||
--r: 15deg;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _noteBg$ = computed(() => {
|
||||
return this.edgeless.std
|
||||
.get(ThemeProvider)
|
||||
.generateColorProperty(
|
||||
this.edgeless.std.get(EditPropsStore).lastProps$.value['affine:note']
|
||||
.background
|
||||
);
|
||||
});
|
||||
|
||||
private readonly _states = ['childFlavour', 'childType', 'tip'] as const;
|
||||
|
||||
override enableActiveBackground = true;
|
||||
|
||||
override type = 'affine:note' as const;
|
||||
|
||||
private _toggleNoteMenu() {
|
||||
if (this.tryDisposePopper()) return;
|
||||
|
||||
const { edgeless, childFlavour, childType, tip } = this;
|
||||
|
||||
this.setEdgelessTool({
|
||||
type: 'affine:note',
|
||||
childFlavour,
|
||||
childType,
|
||||
tip,
|
||||
});
|
||||
const menu = this.createPopper('edgeless-note-menu', this);
|
||||
|
||||
Object.assign(menu.element, {
|
||||
edgeless,
|
||||
childFlavour,
|
||||
childType,
|
||||
tip,
|
||||
onChange: (
|
||||
props: Partial<{
|
||||
childFlavour: NoteToolOption['childFlavour'];
|
||||
childType: string | null;
|
||||
tip: string;
|
||||
}>
|
||||
) => {
|
||||
this._states.forEach(key => {
|
||||
// oxlint-disable-next-line eqeqeq
|
||||
if (props[key] != undefined) {
|
||||
Object.assign(this, { [key]: props[key] });
|
||||
}
|
||||
});
|
||||
this.setEdgelessTool({
|
||||
type: 'affine:note',
|
||||
childFlavour: this.childFlavour,
|
||||
childType: this.childType,
|
||||
tip: this.tip,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const appTheme = this.edgeless.std.get(ThemeProvider).app$.value;
|
||||
|
||||
return html`<edgeless-toolbar-button
|
||||
class="edgeless-note-button"
|
||||
.tooltip=${this.popper ? '' : getTooltipWithShortcut('Note', 'N')}
|
||||
.tooltipOffset=${5}
|
||||
>
|
||||
<div
|
||||
class="note-root"
|
||||
data-app-theme=${appTheme}
|
||||
@click=${this._toggleNoteMenu}
|
||||
style="--paper-bg: ${this._noteBg$.value}"
|
||||
>
|
||||
<div class="paper">${toShapeNotToAdapt}</div>
|
||||
<div class="edgeless-toolbar-note-icon link">${LinkIcon}</div>
|
||||
<div class="edgeless-toolbar-note-icon heading">${Heading1Icon}</div>
|
||||
<div class="edgeless-toolbar-note-icon text">${TextIcon}</div>
|
||||
</div>
|
||||
</edgeless-toolbar-button>`;
|
||||
}
|
||||
|
||||
// TODO: better to extract these states outside of component?
|
||||
@state()
|
||||
accessor childFlavour: NoteToolOption['childFlavour'] = 'affine:paragraph';
|
||||
|
||||
@state()
|
||||
accessor childType = 'text';
|
||||
|
||||
@state()
|
||||
accessor tip = 'Note';
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/block-std/gfx';
|
||||
import { PageIcon } from '@blocksuite/icons/lit';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
|
||||
import { getTooltipWithShortcut } from '../../../components/utils.js';
|
||||
import type { NoteToolOption } from '../../../gfx-tool/note-tool.js';
|
||||
import { createPopper, type MenuPopper } from '../common/create-popper.js';
|
||||
import { QuickToolMixin } from '../mixins/quick-tool.mixin.js';
|
||||
import type { EdgelessNoteMenu } from './note-menu.js';
|
||||
|
||||
export class EdgelessNoteToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
private _noteMenu: MenuPopper<EdgelessNoteMenu> | null = null;
|
||||
|
||||
private readonly _states = ['childFlavour', 'childType', 'tip'] as const;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'affine:note';
|
||||
|
||||
private _disposeMenu() {
|
||||
this._noteMenu?.dispose();
|
||||
this._noteMenu = null;
|
||||
}
|
||||
|
||||
private _toggleNoteMenu() {
|
||||
if (this._noteMenu) {
|
||||
this._disposeMenu();
|
||||
this.requestUpdate();
|
||||
} else {
|
||||
this.edgeless.gfx.tool.setTool('affine:note', {
|
||||
childFlavour: this.childFlavour,
|
||||
childType: this.childType,
|
||||
tip: this.tip,
|
||||
});
|
||||
this._noteMenu = createPopper('edgeless-note-menu', this);
|
||||
|
||||
this._noteMenu.element.edgeless = this.edgeless;
|
||||
this._noteMenu.element.childFlavour = this.childFlavour;
|
||||
this._noteMenu.element.childType = this.childType;
|
||||
this._noteMenu.element.tip = this.tip;
|
||||
this._noteMenu.element.onChange = (
|
||||
props: Partial<{
|
||||
childFlavour: NoteToolOption['childFlavour'];
|
||||
childType: string | null;
|
||||
tip: string;
|
||||
}>
|
||||
) => {
|
||||
this._states.forEach(key => {
|
||||
// oxlint-disable-next-line eqeqeq
|
||||
if (props[key] != undefined) {
|
||||
Object.assign(this, { [key]: props[key] });
|
||||
}
|
||||
});
|
||||
this.edgeless.gfx.tool.setTool('affine:note', {
|
||||
childFlavour: this.childFlavour,
|
||||
childType: this.childType,
|
||||
tip: this.tip,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._disposables.add(
|
||||
effect(() => {
|
||||
const value = this.edgeless.gfx.tool.currentToolName$.value;
|
||||
if (value !== 'affine:note') {
|
||||
this._disposeMenu();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
this._disposeMenu();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { active } = this;
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-note-button"
|
||||
.tooltip=${this._noteMenu ? '' : getTooltipWithShortcut('Note', 'N')}
|
||||
.tooltipOffset=${17}
|
||||
.active=${active}
|
||||
.iconContainerPadding=${6}
|
||||
.iconSize=${'24px'}
|
||||
@click=${() => {
|
||||
this._toggleNoteMenu();
|
||||
}}
|
||||
>
|
||||
${PageIcon()}
|
||||
<toolbar-arrow-up-icon></toolbar-arrow-up-icon>
|
||||
</edgeless-tool-icon-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor childFlavour: NoteToolOption['childFlavour'] = 'affine:paragraph';
|
||||
|
||||
@state()
|
||||
accessor childType = 'text';
|
||||
|
||||
@state()
|
||||
accessor tip = 'Text';
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-tool-button': EdgelessNoteToolButton;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { LayerIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
import type { EdgelessFrameOrderMenu } from './frame-order-menu.js';
|
||||
|
||||
export class EdgelessFrameOrderButton extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
edgeless-frame-order-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
edgeless-frame-order-menu[data-show] {
|
||||
display: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
private _edgelessFrameOrderPopper: ReturnType<
|
||||
typeof createButtonPopper
|
||||
> | null = null;
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._edgelessFrameOrderPopper?.dispose();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this._edgelessFrameOrderPopper = createButtonPopper(
|
||||
this._edgelessFrameOrderButton,
|
||||
this._edgelessFrameOrderMenu,
|
||||
({ display }) => this.setPopperShow(display === 'show'),
|
||||
{
|
||||
mainAxis: 22,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const { readonly } = this.edgeless.doc;
|
||||
return html`
|
||||
<style>
|
||||
.edgeless-frame-order-button svg {
|
||||
color: ${readonly ? 'var(--affine-text-disable-color)' : 'inherit'};
|
||||
}
|
||||
</style>
|
||||
<edgeless-tool-icon-button
|
||||
class="edgeless-frame-order-button"
|
||||
.iconSize=${'24px'}
|
||||
.tooltip=${this.popperShow ? '' : 'Frame Order'}
|
||||
@click=${() => {
|
||||
if (readonly) return;
|
||||
this._edgelessFrameOrderPopper?.toggle();
|
||||
}}
|
||||
.iconContainerPadding=${0}
|
||||
>
|
||||
${LayerIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-frame-order-menu .edgeless=${this.edgeless}>
|
||||
</edgeless-frame-order-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.edgeless-frame-order-button')
|
||||
private accessor _edgelessFrameOrderButton!: HTMLElement;
|
||||
|
||||
@query('edgeless-frame-order-menu')
|
||||
private accessor _edgelessFrameOrderMenu!: EdgelessFrameOrderMenu;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frames!: FrameBlockModel[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor popperShow = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor setPopperShow: (show: boolean) => void = () => {};
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { generateKeyBetweenV2 } from '@blocksuite/block-std/gfx';
|
||||
import {
|
||||
DisposableGroup,
|
||||
SignalWatcher,
|
||||
WithDisposable,
|
||||
} from '@blocksuite/global/utils';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessFrameOrderMenu extends SignalWatcher(
|
||||
WithDisposable(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
.edgeless-frame-order-items-container {
|
||||
max-height: 281px;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-menu-shadow);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.edgeless-frame-order-items-container.embed {
|
||||
padding: 0;
|
||||
background: unset;
|
||||
box-shadow: unset;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
width: 256px;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.draggable:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.item:hover .drag-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
cursor: pointer;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
border-radius: 1px;
|
||||
opacity: 0.2;
|
||||
background: var(--affine-placeholder-color);
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
color: var(--affine-text-primary-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.clone {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 8px;
|
||||
height: 30px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
box-shadow: var(--affine-menu-shadow);
|
||||
background-color: var(--affine-white);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.indicator-line {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 8px;
|
||||
background-color: var(--affine-primary-color);
|
||||
height: 1px;
|
||||
width: 90%;
|
||||
}
|
||||
`;
|
||||
|
||||
get crud() {
|
||||
return this.edgeless.std.get(EdgelessCRUDIdentifier);
|
||||
}
|
||||
|
||||
private get _frames() {
|
||||
return this.edgeless.service.frames;
|
||||
}
|
||||
|
||||
private _bindEvent() {
|
||||
const { _disposables } = this;
|
||||
|
||||
_disposables.addFromEvent(this._container, 'wheel', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
_disposables.addFromEvent(this._container, 'pointerdown', e => {
|
||||
const ele = e.target as HTMLElement;
|
||||
const draggable = ele.closest('.draggable');
|
||||
if (!draggable) return;
|
||||
const clone = this._clone;
|
||||
const indicatorLine = this._indicatorLine;
|
||||
clone.style.visibility = 'visible';
|
||||
|
||||
const rect = draggable.getBoundingClientRect();
|
||||
|
||||
const index = Number(draggable.getAttribute('index'));
|
||||
this._curIndex = index;
|
||||
let newIndex = -1;
|
||||
|
||||
const containerRect = this._container.getBoundingClientRect();
|
||||
const start = containerRect.top + 8;
|
||||
const end = containerRect.bottom;
|
||||
|
||||
const shiftX = e.clientX - rect.left;
|
||||
const shiftY = e.clientY - rect.top;
|
||||
function moveAt(x: number, y: number) {
|
||||
clone.style.left = x - containerRect.left - shiftX + 'px';
|
||||
clone.style.top = y - containerRect.top - shiftY + 'px';
|
||||
}
|
||||
|
||||
function isInsideContainer(e: PointerEvent) {
|
||||
return e.clientY >= start && e.clientY <= end;
|
||||
}
|
||||
moveAt(e.clientX, e.clientY);
|
||||
|
||||
this._disposables.addFromEvent(document, 'pointermove', e => {
|
||||
indicatorLine.style.visibility = 'visible';
|
||||
moveAt(e.clientX, e.clientY);
|
||||
if (isInsideContainer(e)) {
|
||||
const relativeY = e.pageY + this._container.scrollTop - start;
|
||||
let top = 0;
|
||||
if (relativeY < rect.height / 2) {
|
||||
newIndex = 0;
|
||||
top = this.embed ? -2 : 4;
|
||||
} else {
|
||||
newIndex = Math.ceil(
|
||||
(relativeY - rect.height / 2) / (rect.height + 10)
|
||||
);
|
||||
top =
|
||||
(this.embed ? -2 : 7.5) +
|
||||
newIndex * rect.height +
|
||||
(newIndex - 0.5) * 4;
|
||||
}
|
||||
|
||||
indicatorLine.style.top = top - this._container.scrollTop + 'px';
|
||||
return;
|
||||
}
|
||||
newIndex = -1;
|
||||
});
|
||||
|
||||
this._disposables.addFromEvent(document, 'pointerup', () => {
|
||||
clone.style.visibility = 'hidden';
|
||||
indicatorLine.style.visibility = 'hidden';
|
||||
if (
|
||||
newIndex >= 0 &&
|
||||
newIndex <= this._frames.length &&
|
||||
newIndex !== index &&
|
||||
newIndex !== index + 1
|
||||
) {
|
||||
const frameMgr = this.edgeless.service.frame;
|
||||
// Legacy compatibility
|
||||
frameMgr.refreshLegacyFrameOrder();
|
||||
|
||||
const before = this._frames[newIndex - 1]?.presentationIndex || null;
|
||||
const after = this._frames[newIndex]?.presentationIndex || null;
|
||||
|
||||
const frame = this._frames[index];
|
||||
|
||||
this.crud.updateElement(frame.id, {
|
||||
presentationIndex: generateKeyBetweenV2(before, after),
|
||||
});
|
||||
this.edgeless.doc.captureSync();
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
this._disposables.dispose();
|
||||
this._disposables = new DisposableGroup();
|
||||
this._bindEvent();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this._bindEvent();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const frame = this._frames[this._curIndex];
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-frame-order-items-container ${this.embed
|
||||
? 'embed'
|
||||
: ''}"
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
${repeat(
|
||||
this._frames,
|
||||
frame => frame.id,
|
||||
(frame, index) => html`
|
||||
<div class="item draggable" id=${frame.id} index=${index}>
|
||||
<div class="drag-indicator"></div>
|
||||
<div class="title">${frame.title.toString()}</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div class="indicator-line"></div>
|
||||
<div class="clone item">
|
||||
${frame
|
||||
? html`<div class="drag-indicator"></div>
|
||||
<div class="index">${this._curIndex + 1}</div>
|
||||
<div class="title">${frame.title.toString()}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.clone')
|
||||
private accessor _clone!: HTMLDivElement;
|
||||
|
||||
@query('.edgeless-frame-order-items-container')
|
||||
private accessor _container!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
private accessor _curIndex = -1;
|
||||
|
||||
@query('.indicator-line')
|
||||
private accessor _indicatorLine!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor embed = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-frame-order-menu': EdgelessFrameOrderMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { EditPropsStore } from '@blocksuite/affine-shared/services';
|
||||
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import { SettingsIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js';
|
||||
|
||||
export class EdgelessNavigatorSettingButton extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
.navigator-setting-menu {
|
||||
display: none;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
background-color: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-menu-shadow);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.navigator-setting-menu[data-show] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
padding: 4px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-width: 264px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.item-container.header {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 0px 4px;
|
||||
line-height: 22px;
|
||||
font-size: var(--affine-font-sm);
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.text.title {
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
font-size: var(--affine-font-xs);
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.divider::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--affine-border-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _navigatorSettingPopper?: ReturnType<
|
||||
typeof createButtonPopper
|
||||
> | null = null;
|
||||
|
||||
private readonly _onBlackBackgroundChange = (checked: boolean) => {
|
||||
this.blackBackground = checked;
|
||||
this.edgeless.slots.navigatorSettingUpdated.emit({
|
||||
blackBackground: this.blackBackground,
|
||||
});
|
||||
};
|
||||
|
||||
private _tryRestoreSettings() {
|
||||
const blackBackground = this.edgeless.std
|
||||
.get(EditPropsStore)
|
||||
.getStorage('presentBlackBackground');
|
||||
this.blackBackground = blackBackground ?? true;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._tryRestoreSettings();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this._navigatorSettingPopper?.dispose();
|
||||
this._navigatorSettingPopper = null;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this._navigatorSettingPopper = createButtonPopper(
|
||||
this._navigatorSettingButton,
|
||||
this._navigatorSettingMenu,
|
||||
({ display }) => this.setPopperShow(display === 'show'),
|
||||
{
|
||||
mainAxis: 22,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<edgeless-tool-icon-button
|
||||
class="navigator-setting-button"
|
||||
.tooltip=${this.popperShow ? '' : 'Settings'}
|
||||
.iconSize=${'24px'}
|
||||
@click=${() => {
|
||||
this._navigatorSettingPopper?.toggle();
|
||||
}}
|
||||
.iconContainerPadding=${0}
|
||||
>
|
||||
${SettingsIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
|
||||
<div
|
||||
class="navigator-setting-menu"
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="item-container header">
|
||||
<div class="text title">Playback Settings</div>
|
||||
</div>
|
||||
|
||||
<div class="item-container">
|
||||
<div class="text">Black background</div>
|
||||
|
||||
<toggle-switch
|
||||
.on=${this.blackBackground}
|
||||
.onChange=${this._onBlackBackgroundChange}
|
||||
>
|
||||
</toggle-switch>
|
||||
</div>
|
||||
|
||||
<div class="item-container">
|
||||
<div class="text">Hide toolbar</div>
|
||||
|
||||
<toggle-switch
|
||||
.on=${this.hideToolbar}
|
||||
.onChange=${(checked: boolean) => {
|
||||
this.onHideToolbarChange && this.onHideToolbarChange(checked);
|
||||
}}
|
||||
>
|
||||
</toggle-switch>
|
||||
</div>
|
||||
|
||||
${this.includeFrameOrder
|
||||
? html` <div class="divider"></div>
|
||||
<div class="item-container header">
|
||||
<div class="text title">Frame Order</div>
|
||||
</div>
|
||||
|
||||
<edgeless-frame-order-menu
|
||||
.edgeless=${this.edgeless}
|
||||
.embed=${true}
|
||||
></edgeless-frame-order-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.navigator-setting-button')
|
||||
private accessor _navigatorSettingButton!: HTMLElement;
|
||||
|
||||
@query('.navigator-setting-menu')
|
||||
private accessor _navigatorSettingMenu!: HTMLElement;
|
||||
|
||||
@state()
|
||||
accessor blackBackground = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: EdgelessRootBlockComponent;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hideToolbar = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor includeFrameOrder = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onHideToolbarChange: undefined | ((hideToolbar: boolean) => void) =
|
||||
undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor popperShow = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor setPopperShow: (show: boolean) => void = () => {};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user