mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +08:00
feat(editor): add shortcut to highlighter tool (#11604)
Closes: [BS-3092](https://linear.app/affine-design/issue/BS-3092/highlighter-快捷键) ### What's Changed! * Added shortcut `⇧ P` to highlighter tool [Screen Recording 2025-04-10 at 16.33.30.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/38aadc08-ed18-4b48-9d91-b4876d14a2d3.mov" />](https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/38aadc08-ed18-4b48-9d91-b4876d14a2d3.mov)
This commit is contained in:
@@ -88,6 +88,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
|||||||
p: () => {
|
p: () => {
|
||||||
this._setEdgelessTool('brush');
|
this._setEdgelessTool('brush');
|
||||||
},
|
},
|
||||||
|
'Shift-p': () => {
|
||||||
|
this._setEdgelessTool('highlighter');
|
||||||
|
},
|
||||||
e: () => {
|
e: () => {
|
||||||
this._setEdgelessTool('eraser');
|
this._setEdgelessTool('eraser');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { css, html, LitElement } from 'lit';
|
import { css, html, LitElement } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
|
import { repeat } from 'lit-html/directives/repeat.js';
|
||||||
|
|
||||||
export class TooltipContentWithShortcut extends LitElement {
|
export class TooltipContentWithShortcut extends LitElement {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
@@ -9,6 +10,10 @@ export class TooltipContentWithShortcut extends LitElement {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
.tooltip__shortcuts {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
.tooltip__shortcut {
|
.tooltip__shortcut {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -28,19 +33,30 @@ export class TooltipContentWithShortcut extends LitElement {
|
|||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
}
|
}
|
||||||
.tooltip__label {
|
.tooltip__label {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
get shortcuts() {
|
||||||
|
let shortcut = this.shortcut;
|
||||||
|
if (!shortcut) return [];
|
||||||
|
return shortcut.split(' ');
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { tip, shortcut, postfix } = this;
|
const { tip, shortcuts, postfix } = this;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="tooltip-with-shortcut">
|
<div class="tooltip-with-shortcut">
|
||||||
<span class="tooltip__label">${tip}</span>
|
<span class="tooltip__label">${tip}</span>
|
||||||
${shortcut
|
<div class="tooltip__shortcuts">
|
||||||
? html`<span class="tooltip__shortcut">${shortcut}</span>`
|
${repeat(
|
||||||
: ''}
|
shortcuts,
|
||||||
|
shortcut => html`<span class="tooltip__shortcut">${shortcut}</span>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
${postfix ? html`<span class="tooltip__postfix">${postfix}</span>` : ''}
|
${postfix ? html`<span class="tooltip__postfix">${postfix}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
EdgelessBrushDarkIcon,
|
||||||
|
EdgelessBrushLightIcon,
|
||||||
|
EdgelessHighlighterDarkIcon,
|
||||||
|
EdgelessHighlighterLightIcon,
|
||||||
|
} from './icons';
|
||||||
|
import type { Pen } from './types';
|
||||||
|
|
||||||
|
export const penIconMap = {
|
||||||
|
dark: {
|
||||||
|
brush: EdgelessBrushDarkIcon,
|
||||||
|
highlighter: EdgelessHighlighterDarkIcon,
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
brush: EdgelessBrushLightIcon,
|
||||||
|
highlighter: EdgelessHighlighterLightIcon,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const penInfoMap: { [k in Pen]: { tip: string; shortcut: string } } = {
|
||||||
|
brush: {
|
||||||
|
tip: 'Pen',
|
||||||
|
shortcut: 'P',
|
||||||
|
},
|
||||||
|
highlighter: {
|
||||||
|
tip: 'Highlighter',
|
||||||
|
shortcut: '⇧ P',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,11 +7,16 @@ import {
|
|||||||
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
|
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
|
||||||
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||||
import { computed, type Signal } from '@preact/signals-core';
|
import {
|
||||||
|
computed,
|
||||||
|
type ReadonlySignal,
|
||||||
|
type Signal,
|
||||||
|
} from '@preact/signals-core';
|
||||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
|
import { penInfoMap } from './consts';
|
||||||
import type { Pen, PenMap } from './types';
|
import type { Pen, PenMap } from './types';
|
||||||
|
|
||||||
export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
||||||
@@ -26,8 +31,14 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
|
|
||||||
.pens {
|
.pens {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
|
|
||||||
|
edgeless-tool-icon-button {
|
||||||
|
display: flex;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.pen-wrapper {
|
.pen-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -36,7 +47,7 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
transform: translateY(10px);
|
transform: translateY(-2px);
|
||||||
transition-property: color, transform;
|
transition-property: color, transform;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
@@ -46,7 +57,7 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
.pen-wrapper:hover,
|
.pen-wrapper:hover,
|
||||||
.pen-wrapper:active,
|
.pen-wrapper:active,
|
||||||
.pen-wrapper[data-active] {
|
.pen-wrapper[data-active] {
|
||||||
transform: translateY(-10px);
|
transform: translateY(-22px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +67,8 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu-divider {
|
menu-divider {
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin: 0 9px;
|
margin: 0 9px;
|
||||||
}
|
}
|
||||||
@@ -83,42 +96,64 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
override render() {
|
override render() {
|
||||||
const {
|
const {
|
||||||
_theme$: { value: theme },
|
_theme$: { value: theme },
|
||||||
color$: { value: currentColor },
|
|
||||||
colors$: {
|
colors$: {
|
||||||
value: { brush: brushColor, highlighter: highlighterColor },
|
value: { brush: brushColor, highlighter: highlighterColor },
|
||||||
},
|
},
|
||||||
pen$: { value: pen },
|
|
||||||
penIconMap$: {
|
penIconMap$: {
|
||||||
value: { brush: brushIcon, highlighter: highlighterIcon },
|
value: { brush: brushIcon, highlighter: highlighterIcon },
|
||||||
},
|
},
|
||||||
|
penInfo$: {
|
||||||
|
value: { type, color },
|
||||||
|
},
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<edgeless-slide-menu>
|
<edgeless-slide-menu>
|
||||||
<div class="pens" slot="prefix">
|
<div class="pens" slot="prefix">
|
||||||
<div
|
<edgeless-tool-icon-button
|
||||||
class="pen-wrapper edgeless-brush-button"
|
class="edgeless-brush-button"
|
||||||
?data-active="${pen === 'brush'}"
|
.tooltip=${html`<affine-tooltip-content-with-shortcut
|
||||||
style=${styleMap({ color: brushColor })}
|
data-tip="${penInfoMap.brush.tip}"
|
||||||
|
data-shortcut="${penInfoMap.brush.shortcut}"
|
||||||
|
></affine-tooltip-content-with-shortcut>`}
|
||||||
|
.tooltipOffset=${20}
|
||||||
|
.hover=${false}
|
||||||
@click=${() => this._onPickPen('brush')}
|
@click=${() => this._onPickPen('brush')}
|
||||||
>
|
>
|
||||||
${brushIcon}
|
<div
|
||||||
</div>
|
class="pen-wrapper"
|
||||||
<div
|
style=${styleMap({ color: brushColor })}
|
||||||
class="pen-wrapper edgeless-highlighter-button"
|
?data-active="${type === 'brush'}"
|
||||||
?data-active="${pen === 'highlighter'}"
|
>
|
||||||
style=${styleMap({ color: highlighterColor })}
|
${brushIcon}
|
||||||
|
</div>
|
||||||
|
</edgeless-tool-icon-button>
|
||||||
|
|
||||||
|
<edgeless-tool-icon-button
|
||||||
|
class="edgeless-highlighter-button"
|
||||||
|
.tooltip=${html`<affine-tooltip-content-with-shortcut
|
||||||
|
data-tip="${penInfoMap.highlighter.tip}"
|
||||||
|
data-shortcut="${penInfoMap.highlighter.shortcut}"
|
||||||
|
></affine-tooltip-content-with-shortcut>`}
|
||||||
|
.tooltipOffset=${20}
|
||||||
|
.hover=${false}
|
||||||
@click=${() => this._onPickPen('highlighter')}
|
@click=${() => this._onPickPen('highlighter')}
|
||||||
>
|
>
|
||||||
${highlighterIcon}
|
<div
|
||||||
</div>
|
class="pen-wrapper"
|
||||||
|
style=${styleMap({ color: highlighterColor })}
|
||||||
|
?data-active="${type === 'highlighter'}"
|
||||||
|
>
|
||||||
|
${highlighterIcon}
|
||||||
|
</div>
|
||||||
|
</edgeless-tool-icon-button>
|
||||||
<menu-divider .vertical=${true}></menu-divider>
|
<menu-divider .vertical=${true}></menu-divider>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-content">
|
<div class="menu-content">
|
||||||
<edgeless-color-panel
|
<edgeless-color-panel
|
||||||
class="one-way"
|
class="one-way"
|
||||||
@select=${this._onPickColor}
|
@select=${this._onPickColor}
|
||||||
.value=${currentColor}
|
.value=${color}
|
||||||
.theme=${theme}
|
.theme=${theme}
|
||||||
.palettes=${DefaultTheme.StrokeColorShortPalettes}
|
.palettes=${DefaultTheme.StrokeColorShortPalettes}
|
||||||
.shouldKeepColor=${true}
|
.shouldKeepColor=${true}
|
||||||
@@ -135,14 +170,20 @@ export class EdgelessPenMenu extends EdgelessToolbarToolMixin(
|
|||||||
accessor onChange!: (props: Record<string, unknown>) => void;
|
accessor onChange!: (props: Record<string, unknown>) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor colors$!: Signal<PenMap<string>>;
|
accessor colors$!: ReadonlySignal<PenMap<string>>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor color$!: Signal<string>;
|
accessor penIconMap$!: ReadonlySignal<PenMap<TemplateResult>>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor pen$!: Signal<Pen>;
|
accessor pen$!: Signal<Pen>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor penIconMap$!: Signal<PenMap<TemplateResult>>;
|
accessor penInfo$!: ReadonlySignal<{
|
||||||
|
type: Pen;
|
||||||
|
color: string;
|
||||||
|
icon: TemplateResult<1>;
|
||||||
|
tip: string;
|
||||||
|
shortcut: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { css, html, LitElement, nothing } from 'lit';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { when } from 'lit/directives/when.js';
|
import { when } from 'lit/directives/when.js';
|
||||||
|
|
||||||
import { penIconMap } from './icons';
|
import { penIconMap, penInfoMap } from './consts';
|
||||||
import type { Pen } from './types';
|
import type { Pen } from './types';
|
||||||
|
|
||||||
export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
||||||
@@ -81,6 +81,18 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
|||||||
return this.penIconMap$.value[pen];
|
return this.penIconMap$.value[pen];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private readonly penInfo$ = computed(() => {
|
||||||
|
const type = this.pen$.value;
|
||||||
|
const icon = this.penIcon$.value;
|
||||||
|
const color = this.color$.value;
|
||||||
|
return {
|
||||||
|
...penInfoMap[type],
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
private readonly pen$ = signal<Pen>('brush');
|
private readonly pen$ = signal<Pen>('brush');
|
||||||
|
|
||||||
override enableActiveBackground = true;
|
override enableActiveBackground = true;
|
||||||
@@ -89,9 +101,20 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
|||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.gfx.tool.currentToolName$.subscribe(tool => {
|
this.gfx.tool.currentToolName$.subscribe(name => {
|
||||||
if (this.type.map(String).includes(tool)) return;
|
const tool = this.type.find(t => t === name);
|
||||||
this.tryDisposePopper();
|
if (!tool) {
|
||||||
|
this.tryDisposePopper();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool !== this.pen$.peek()) {
|
||||||
|
this.pen$.value = tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.active) return;
|
||||||
|
|
||||||
|
this._togglePenMenu();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,10 +124,10 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
|||||||
!this.active && this.setEdgelessTool(this.pen$.peek());
|
!this.active && this.setEdgelessTool(this.pen$.peek());
|
||||||
const menu = this.createPopper('edgeless-pen-menu', this);
|
const menu = this.createPopper('edgeless-pen-menu', this);
|
||||||
Object.assign(menu.element, {
|
Object.assign(menu.element, {
|
||||||
color$: this.color$,
|
|
||||||
colors$: this.colors$,
|
colors$: this.colors$,
|
||||||
pen$: this.pen$,
|
|
||||||
penIconMap$: this.penIconMap$,
|
penIconMap$: this.penIconMap$,
|
||||||
|
pen$: this.pen$,
|
||||||
|
penInfo$: this.penInfo$,
|
||||||
edgeless: this.edgeless,
|
edgeless: this.edgeless,
|
||||||
onChange: (props: Record<string, unknown>) => {
|
onChange: (props: Record<string, unknown>) => {
|
||||||
const pen = this.pen$.peek();
|
const pen = this.pen$.peek();
|
||||||
@@ -117,20 +140,22 @@ export class EdgelessPenToolButton extends EdgelessToolbarToolMixin(
|
|||||||
override render() {
|
override render() {
|
||||||
const {
|
const {
|
||||||
active,
|
active,
|
||||||
penIcon$: { value: icon },
|
penInfo$: {
|
||||||
color$: { value: color },
|
value: { type, color, icon, tip, shortcut },
|
||||||
|
},
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<edgeless-toolbar-button
|
<edgeless-toolbar-button
|
||||||
class="edgeless-pen-button"
|
class="edgeless-pen-button"
|
||||||
|
data-drawing-tool="${type}"
|
||||||
.tooltip=${when(
|
.tooltip=${when(
|
||||||
this.popper,
|
this.popper,
|
||||||
() => nothing,
|
() => nothing,
|
||||||
() =>
|
() =>
|
||||||
html`<affine-tooltip-content-with-shortcut
|
html`<affine-tooltip-content-with-shortcut
|
||||||
data-tip="${'Pen'}"
|
data-tip="${tip}"
|
||||||
data-shortcut="${'P'}"
|
data-shortcut="${shortcut}"
|
||||||
></affine-tooltip-content-with-shortcut>`
|
></affine-tooltip-content-with-shortcut>`
|
||||||
)}
|
)}
|
||||||
.tooltipOffset=${4}
|
.tooltipOffset=${4}
|
||||||
|
|||||||
@@ -57,3 +57,28 @@ test('should exit drawing tools menu when Escape is pressed', async ({
|
|||||||
|
|
||||||
await expect(drawingToolsMenu).toBeHidden();
|
await expect(drawingToolsMenu).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should enter highlighter tool when `Shift + P` is pressed', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const drawingToolButton = page.locator('.edgeless-pen-button');
|
||||||
|
const drawingToolsMenu = page.locator('edgeless-pen-menu');
|
||||||
|
|
||||||
|
await expect(drawingToolButton).toHaveAttribute('data-drawing-tool', 'brush');
|
||||||
|
await expect(drawingToolsMenu).toBeHidden();
|
||||||
|
|
||||||
|
await page.keyboard.press('Shift+P');
|
||||||
|
|
||||||
|
await expect(drawingToolButton).toHaveAttribute(
|
||||||
|
'data-drawing-tool',
|
||||||
|
'highlighter'
|
||||||
|
);
|
||||||
|
await expect(drawingToolsMenu).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
await expect(drawingToolsMenu).toBeHidden();
|
||||||
|
await expect(drawingToolButton).toHaveAttribute(
|
||||||
|
'data-drawing-tool',
|
||||||
|
'highlighter'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export async function locatorEdgelessToolButton(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'brush':
|
case 'brush':
|
||||||
case 'highlighter':
|
case 'highlighter':
|
||||||
buttonType = 'div';
|
buttonType = 'edgeless-tool-icon-button';
|
||||||
break;
|
break;
|
||||||
case 'pen':
|
case 'pen':
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export async function locateEdgelessToolButton(
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'brush':
|
case 'brush':
|
||||||
case 'highlighter':
|
case 'highlighter':
|
||||||
buttonType = 'div';
|
buttonType = 'edgeless-tool-icon-button';
|
||||||
break;
|
break;
|
||||||
case 'pen':
|
case 'pen':
|
||||||
case 'text':
|
case 'text':
|
||||||
|
|||||||
Reference in New Issue
Block a user