feat(editor): edgeless zoom toolbar widget extension (#12037)
Closes: BS-3363
This commit is contained in:
41
blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json
Normal file
41
blocksuite/affine/widgets/edgeless-zoom-toolbar/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-widget-edgeless-zoom-toolbar",
|
||||
"description": "Affine edgeless zoom toolbar widget.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
AffineEdgelessZoomToolbarWidget,
|
||||
} from '.';
|
||||
import { ZoomBarToggleButton } from './zoom-bar-toggle-button';
|
||||
import { EdgelessZoomToolbar } from './zoom-toolbar';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
|
||||
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
|
||||
customElements.define(
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
AffineEdgelessZoomToolbarWidget
|
||||
);
|
||||
}
|
||||
94
blocksuite/affine/widgets/edgeless-zoom-toolbar/src/index.ts
Normal file
94
blocksuite/affine/widgets/edgeless-zoom-toolbar/src/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
export const AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET =
|
||||
'affine-edgeless-zoom-toolbar-widget';
|
||||
|
||||
export class AffineEdgelessZoomToolbarWidget extends WidgetComponent<RootBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 12px;
|
||||
z-index: var(--affine-z-index-popover);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@container viewport (width <= 1200px) {
|
||||
edgeless-zoom-toolbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@container viewport (width > 1200px) {
|
||||
zoom-bar-toggle-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
get edgeless() {
|
||||
return this.block;
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const currentTool = this.gfx.tool.currentToolName$.value;
|
||||
|
||||
if (currentTool !== 'frameNavigator') {
|
||||
this._hide = false;
|
||||
}
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { disposables, std } = this;
|
||||
const slots = std.get(EdgelessLegacySlotIdentifier);
|
||||
|
||||
disposables.add(
|
||||
slots.navigatorSettingUpdated.subscribe(({ hideToolbar }) => {
|
||||
if (hideToolbar !== undefined) {
|
||||
this._hide = hideToolbar;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this._hide || !this.edgeless) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<edgeless-zoom-toolbar .std=${this.std}></edgeless-zoom-toolbar>
|
||||
<zoom-bar-toggle-button .std=${this.std}></zoom-bar-toggle-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _hide = false;
|
||||
}
|
||||
|
||||
export const edgelessZoomToolbarWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}`
|
||||
);
|
||||
23
blocksuite/affine/widgets/edgeless-zoom-toolbar/src/view.ts
Normal file
23
blocksuite/affine/widgets/edgeless-zoom-toolbar/src/view.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { effects } from './effects';
|
||||
import { edgelessZoomToolbarWidget } from './index';
|
||||
|
||||
export class EdgelessZoomToolbarViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-edgeless-zoom-toolbar-widget';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
|
||||
override setup(context: ViewExtensionContext) {
|
||||
super.setup(context);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
context.register(edgelessZoomToolbarWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { offset } from '@floating-ui/dom';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
export class ZoomBarToggleButton extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
.toggle-button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
edgeless-zoom-toolbar {
|
||||
position: absolute;
|
||||
bottom: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
private _abortController: AbortController | null = null;
|
||||
|
||||
private _closeZoomMenu() {
|
||||
if (this._abortController && !this._abortController.signal.aborted) {
|
||||
this._abortController.abort();
|
||||
this._abortController = null;
|
||||
this._showPopper = false;
|
||||
}
|
||||
}
|
||||
|
||||
private get _slots() {
|
||||
return this.std.get(EdgelessLegacySlotIdentifier);
|
||||
}
|
||||
|
||||
private _toggleZoomMenu() {
|
||||
if (this._abortController && !this._abortController.signal.aborted) {
|
||||
this._closeZoomMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
this._abortController = new AbortController();
|
||||
this._abortController.signal.addEventListener('abort', () => {
|
||||
this._showPopper = false;
|
||||
});
|
||||
createLitPortal({
|
||||
template: html`<edgeless-zoom-toolbar
|
||||
.std=${this.std}
|
||||
.layout=${'vertical'}
|
||||
></edgeless-zoom-toolbar>`,
|
||||
container: this._toggleButton,
|
||||
computePosition: {
|
||||
referenceElement: this._toggleButton,
|
||||
placement: 'top',
|
||||
middleware: [offset(4)],
|
||||
autoUpdate: true,
|
||||
},
|
||||
abortController: this._abortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
this._showPopper = true;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._closeZoomMenu();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { disposables } = this;
|
||||
disposables.add(
|
||||
this._slots.readonlyUpdated.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.std.store.readonly) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="toggle-button" @pointerdown=${stopPropagation}>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Toggle Zoom Tool Bar'}
|
||||
.tipPosition=${'right'}
|
||||
.active=${this._showPopper}
|
||||
.arrow=${false}
|
||||
.activeMode=${'background'}
|
||||
.iconContainerPadding=${6}
|
||||
.iconSize=${'24px'}
|
||||
@click=${() => this._toggleZoomMenu()}
|
||||
>
|
||||
${MoreHorizontalIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _showPopper = false;
|
||||
|
||||
@query('.toggle-button')
|
||||
private accessor _toggleButton!: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { MinusIcon, PlusIcon, ViewBarIcon } from '@blocksuite/icons/lit';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import {
|
||||
GfxControllerIdentifier,
|
||||
ZOOM_MAX,
|
||||
ZOOM_MIN,
|
||||
ZOOM_STEP,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
export class EdgelessZoomToolbar extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.edgeless-zoom-toolbar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
fill: currentcolor;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.edgeless-zoom-toolbar-container.horizantal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.edgeless-zoom-toolbar-container.vertical {
|
||||
flex-direction: column;
|
||||
width: 40px;
|
||||
background-color: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.edgeless-zoom-toolbar-container[level='second'] {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.edgeless-zoom-toolbar-container[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zoom-percent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
color: var(--affine-icon-color);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
}
|
||||
|
||||
.zoom-percent:hover {
|
||||
color: var(--affine-primary-color);
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.zoom-percent[disabled] {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
color: var(--affine-text-disable-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get slots() {
|
||||
return this.std.get(EdgelessLegacySlotIdentifier);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get edgelessTool() {
|
||||
return this.gfx.tool.currentToolOption$.peek();
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return this.viewport.locked;
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
return this.gfx.viewport;
|
||||
}
|
||||
|
||||
setZoomByStep = (step: number) => {
|
||||
this.viewport.smoothZoom(clamp(this.zoom + step, ZOOM_MIN, ZOOM_MAX));
|
||||
};
|
||||
|
||||
get zoom() {
|
||||
if (!this.viewport) {
|
||||
console.error('Something went wrong, viewport is not available');
|
||||
return 1;
|
||||
}
|
||||
return this.viewport.zoom;
|
||||
}
|
||||
|
||||
private _isVerticalBar() {
|
||||
return this.layout === 'vertical';
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
this.gfx.tool.currentToolName$.value;
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { disposables } = this;
|
||||
disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => this.requestUpdate())
|
||||
);
|
||||
disposables.add(
|
||||
this.slots.readonlyUpdated.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.std.store.readonly) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const formattedZoom = `${Math.round(this.zoom * 100)}%`;
|
||||
const classes = `edgeless-zoom-toolbar-container ${this.layout}`;
|
||||
const locked = this.locked;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classes}
|
||||
@dblclick=${stopPropagation}
|
||||
@mousedown=${stopPropagation}
|
||||
@mouseup=${stopPropagation}
|
||||
@pointerdown=${stopPropagation}
|
||||
>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Fit to screen'}
|
||||
.tipPosition=${this._isVerticalBar() ? 'right' : 'top-end'}
|
||||
.arrow=${!this._isVerticalBar()}
|
||||
@click=${() => this.gfx.fitToScreen()}
|
||||
.iconContainerPadding=${4}
|
||||
.iconSize=${'24px'}
|
||||
.disabled=${locked}
|
||||
>
|
||||
${ViewBarIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Zoom out'}
|
||||
.tipPosition=${this._isVerticalBar() ? 'right' : 'top'}
|
||||
.arrow=${!this._isVerticalBar()}
|
||||
@click=${() => this.setZoomByStep(-ZOOM_STEP)}
|
||||
.iconContainerPadding=${4}
|
||||
.iconSize=${'24px'}
|
||||
.disabled=${locked}
|
||||
>
|
||||
${MinusIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
<button
|
||||
class="zoom-percent"
|
||||
@click=${() => this.viewport.smoothZoom(1)}
|
||||
.disabled=${locked}
|
||||
>
|
||||
${formattedZoom}
|
||||
</button>
|
||||
<edgeless-tool-icon-button
|
||||
.tooltip=${'Zoom in'}
|
||||
.tipPosition=${this._isVerticalBar() ? 'right' : 'top'}
|
||||
.arrow=${!this._isVerticalBar()}
|
||||
@click=${() => this.setZoomByStep(ZOOM_STEP)}
|
||||
.iconContainerPadding=${4}
|
||||
.iconSize=${'24px'}
|
||||
.disabled=${locked}
|
||||
>
|
||||
${PlusIcon()}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor layout: 'horizontal' | 'vertical' = 'horizontal';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../blocks/surface" },
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user