mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 00:56:26 +08:00
refactor(editor): edgeless note toolbar config extension (#10719)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-components/edgeless-line-width-panel';
|
||||
@@ -25,6 +25,8 @@ import { effects as componentColorPickerEffects } from '@blocksuite/affine-compo
|
||||
import { effects as componentContextMenuEffects } from '@blocksuite/affine-components/context-menu';
|
||||
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
|
||||
import { effects as componentDropIndicatorEffects } from '@blocksuite/affine-components/drop-indicator';
|
||||
import { effects as componentEdgelessLineStylesEffects } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||
import { effects as componentEdgelessLineWidthEffects } from '@blocksuite/affine-components/edgeless-line-width-panel';
|
||||
import { effects as componentEmbedCardModalEffects } from '@blocksuite/affine-components/embed-card-modal';
|
||||
import { FilterableListComponent } from '@blocksuite/affine-components/filterable-list';
|
||||
import { effects as componentHighlightDropdownMenuEffects } from '@blocksuite/affine-components/highlight-dropdown-menu';
|
||||
@@ -149,6 +151,8 @@ export function effects() {
|
||||
componentViewDropdownMenuEffects();
|
||||
componentTooltipContentWithShortcutEffects();
|
||||
componentSizeDropdownMenuEffects();
|
||||
componentEdgelessLineWidthEffects();
|
||||
componentEdgelessLineStylesEffects();
|
||||
|
||||
widgetScrollAnchoringEffects();
|
||||
widgetFrameTitleEffects();
|
||||
|
||||
@@ -26,9 +26,11 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { LineWidth, type StrokeStyle } from '@blocksuite/affine-model';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { ArrowDownSmallIcon, LineStyleIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class EdgelessNoteBorderDropdownMenu extends ShadowlessElement {
|
||||
override render() {
|
||||
const { lineSize, lineStyle } = this;
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Border style"
|
||||
.tooltip="${'Border style'}"
|
||||
>
|
||||
${LineStyleIcon()} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<affine-edgeless-line-styles-panel
|
||||
.lineSize=${lineSize}
|
||||
.lineStyle=${lineStyle}
|
||||
></affine-edgeless-line-styles-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineStyle!: StrokeStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineSize: LineWidth = LineWidth.Two;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-border-dropdown-menu': EdgelessNoteBorderDropdownMenu;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
const DisplayModeMap = {
|
||||
[NoteDisplayMode.DocAndEdgeless]: 'Both',
|
||||
[NoteDisplayMode.EdgelessOnly]: 'Edgeless',
|
||||
[NoteDisplayMode.DocOnly]: 'Page',
|
||||
} as const satisfies Record<NoteDisplayMode, string>;
|
||||
|
||||
export class EdgelessNoteDisplayModeDropdownMenu extends ShadowlessElement {
|
||||
get mode() {
|
||||
return DisplayModeMap[this.displayMode];
|
||||
}
|
||||
|
||||
select(detail: NoteDisplayMode) {
|
||||
this.dispatchEvent(new CustomEvent('select', { detail }));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { displayMode, mode } = this;
|
||||
|
||||
return html`
|
||||
<span class="display-mode-button-label">Show in</span>
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Mode"
|
||||
.tooltip="${'Display mode'}"
|
||||
.justify="${'space-between'}"
|
||||
.labelHeight="${'20px'}"
|
||||
>
|
||||
<span class="label">${mode}</span>
|
||||
${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<note-display-mode-panel
|
||||
.displayMode=${displayMode}
|
||||
.onSelect=${(newMode: NoteDisplayMode) => this.select(newMode)}
|
||||
>
|
||||
</note-display-mode-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor displayMode!: NoteDisplayMode;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-display-mode-dropdown-menu': EdgelessNoteDisplayModeDropdownMenu;
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { ColorScheme, NoteShadow } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
NoteShadowDuotoneIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
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 EdgelessNoteShadowDropdownMenu extends 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-icon svg rect:first-of-type {
|
||||
fill: var(--background);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.item[data-selected] {
|
||||
border: 1px solid var(--affine-brand-color);
|
||||
}
|
||||
`;
|
||||
|
||||
select(value: NoteShadow) {
|
||||
this.dispatchEvent(new CustomEvent('select', { detail: value }));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { value, background, theme } = this;
|
||||
const isDark = theme === ColorScheme.Dark;
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Shadow style"
|
||||
.tooltip="${'Shadow style'}"
|
||||
>
|
||||
${NoteShadowDuotoneIcon()} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div
|
||||
data-orientation="horizontal"
|
||||
style=${styleMap({
|
||||
'--background': background.startsWith('--')
|
||||
? `var(${background})`
|
||||
: background,
|
||||
})}
|
||||
>
|
||||
${repeat(
|
||||
SHADOWS,
|
||||
shadow => shadow.type,
|
||||
({ type, tooltip, styles: { dark, light } }, index) =>
|
||||
html`<div
|
||||
class="item"
|
||||
?data-selected="${value === type}"
|
||||
@click=${() => this.select(type)}
|
||||
>
|
||||
<editor-icon-button
|
||||
class="item-icon"
|
||||
data-testid="${type.replace('--', '')}"
|
||||
.tooltip=${tooltip}
|
||||
.tipPosition="${'bottom'}"
|
||||
.iconContainerPadding=${0}
|
||||
.hover=${false}
|
||||
style=${styleMap({
|
||||
boxShadow: `${isDark ? dark : light}`,
|
||||
})}
|
||||
>
|
||||
${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon}
|
||||
</editor-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor background!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value!: NoteShadow;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-shadow-dropdown-menu': EdgelessNoteShadowDropdownMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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>
|
||||
`;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const viewInPageNotifyFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const viewInPageNotifyFooterButton = style({
|
||||
padding: '0px 6px',
|
||||
borderRadius: '4px',
|
||||
color: cssVarV2('text/primary'),
|
||||
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
|
||||
':hover': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,599 @@
|
||||
import {
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
packColor,
|
||||
type PickColorEvent,
|
||||
} from '@blocksuite/affine-components/color-picker';
|
||||
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||
import {
|
||||
DefaultTheme,
|
||||
NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
type NoteShadow,
|
||||
resolveColor,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
NotificationProvider,
|
||||
SidebarExtensionIdentifier,
|
||||
type ToolbarAction,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonResolvedValue } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
BlockFlavourIdentifier,
|
||||
EditorLifeCycleExtension,
|
||||
} from '@blocksuite/block-std';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
AutoHeightIcon,
|
||||
CornerIcon,
|
||||
CustomizedHeightIcon,
|
||||
LinkedPageIcon,
|
||||
ScissorsIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
|
||||
import { changeNoteDisplayMode } from '../commands';
|
||||
import * as styles from '../components/view-in-page-notify.css';
|
||||
import { NoteConfigExtension } from '../config';
|
||||
|
||||
const trackBaseProps = {
|
||||
category: 'note',
|
||||
};
|
||||
|
||||
const CORNER_LIST = [
|
||||
{ key: 'None', value: 0 },
|
||||
{ key: 'Small', value: 8 },
|
||||
{ key: 'Medium', value: 16 },
|
||||
{ key: 'Large', value: 24 },
|
||||
{ key: 'Huge', value: 32 },
|
||||
] as const;
|
||||
|
||||
const builtinSurfaceToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.show-in',
|
||||
when(ctx) {
|
||||
return (
|
||||
ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 &&
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { displayMode } = firstModel.props;
|
||||
const onSelect = (e: CustomEvent<NoteDisplayMode>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const newMode = e.detail;
|
||||
setDisplayMode(ctx, firstModel, newMode);
|
||||
};
|
||||
|
||||
return html`
|
||||
<edgeless-note-display-mode-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.displayMode="${displayMode}"
|
||||
>
|
||||
</edgeless-note-display-mode-dropdown-menu>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.display-in-page',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length === 1 &&
|
||||
!elements[0].isPageBlock() &&
|
||||
!ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
generate(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const shouldShowTooltip$ = computed(
|
||||
() =>
|
||||
firstModel.props.displayMode$.value ===
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
const label$ = computed(() =>
|
||||
firstModel.props.displayMode$.value === NoteDisplayMode.EdgelessOnly
|
||||
? 'Display In Page'
|
||||
: 'Displayed In Page'
|
||||
);
|
||||
const onSelect = () => {
|
||||
const newMode =
|
||||
firstModel.props.displayMode === NoteDisplayMode.EdgelessOnly
|
||||
? NoteDisplayMode.DocAndEdgeless
|
||||
: NoteDisplayMode.EdgelessOnly;
|
||||
setDisplayMode(ctx, firstModel, newMode);
|
||||
};
|
||||
|
||||
return {
|
||||
content: html`<editor-icon-button
|
||||
aria-label="${label$.value}"
|
||||
.showTooltip="${shouldShowTooltip$.value}"
|
||||
.tooltip="${'This note is part of Page Mode. Click to remove it from the page.'}"
|
||||
data-testid="display-in-page"
|
||||
@click=${() => onSelect()}
|
||||
>
|
||||
${LinkedPageIcon()}
|
||||
<span class="label">${label$.value}</span>
|
||||
</editor-icon-button>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.color-picker',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
elements[0].props.displayMode !== NoteDisplayMode.DocOnly
|
||||
);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const enableCustomColor = ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_color_picker');
|
||||
const theme = ctx.themeProvider.edgelessTheme;
|
||||
|
||||
const firstModel = models[0];
|
||||
const background =
|
||||
getMostCommonResolvedValue(
|
||||
models.map(model => model.props),
|
||||
'background',
|
||||
background => resolveColor(background, theme)
|
||||
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
|
||||
const onPick = (e: PickColorEvent) => {
|
||||
const field = 'background';
|
||||
|
||||
if (e.type === 'pick') {
|
||||
const color = e.detail.value;
|
||||
for (const model of models) {
|
||||
const props = packColor(field, color);
|
||||
ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, props);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
model[e.type === 'start' ? 'stash' : 'pop'](field);
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<edgeless-color-picker-button
|
||||
class="background"
|
||||
.label="${'Background'}"
|
||||
.pick=${onPick}
|
||||
.color=${background}
|
||||
.colorPanelClass="${'small'}"
|
||||
.theme=${theme}
|
||||
.palettes=${DefaultTheme.NoteBackgroundColorPalettes}
|
||||
.originalColor=${firstModel.props.background}
|
||||
.enableCustomColor=${enableCustomColor}
|
||||
>
|
||||
</edgeless-color-picker-button>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'd.style',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
elements[0].props.displayMode !== NoteDisplayMode.DocOnly
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'a.shadow-style',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const theme = ctx.themeProvider.edgelessTheme;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { shadowType } = firstModel.props.edgeless.style;
|
||||
const background =
|
||||
getMostCommonResolvedValue(
|
||||
models.map(model => model.props),
|
||||
'background',
|
||||
background => resolveColor(background, theme)
|
||||
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
|
||||
const onSelect = (e: CustomEvent<NoteShadow>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const shadowType = e.detail;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
shadowType,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<edgeless-note-shadow-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.value="${shadowType}"
|
||||
.background="${background}"
|
||||
.theme="${theme}"
|
||||
></edgeless-note-shadow-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
{
|
||||
id: 'b.border-style',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { borderSize, borderStyle } = firstModel.props.edgeless.style;
|
||||
const onSelect = (e: CustomEvent<LineDetailType>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const { type, value } = e.detail;
|
||||
|
||||
if (type === 'size') {
|
||||
const borderSize = value;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const borderStyle = value;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderStyle,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`
|
||||
<edgeless-note-border-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.lineSize=${borderSize}
|
||||
.lineStyle=${borderStyle}
|
||||
></edgeless-note-border-dropdown-menu>
|
||||
`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
{
|
||||
id: 'c.corners',
|
||||
label: 'Corners',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const label = this.label;
|
||||
const firstModel = models[0];
|
||||
const borderRadius$ = computed(
|
||||
() => firstModel.props.edgeless$.value.style.borderRadius
|
||||
);
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const borderRadius = e.detail;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDExtension).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderRadius,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.label="${label}"
|
||||
.icon=${CornerIcon()}
|
||||
.sizes=${CORNER_LIST}
|
||||
.size$=${borderRadius$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'e.slicer',
|
||||
icon: ScissorsIcon(),
|
||||
tooltip: html`<affine-tooltip-content-with-shortcut
|
||||
data-tip="${'Cutting mode'}"
|
||||
data-shortcut="${'-'}"
|
||||
></affine-tooltip-content-with-shortcut>`,
|
||||
active: false,
|
||||
when(ctx) {
|
||||
return (
|
||||
ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 &&
|
||||
ctx.std
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
run(ctx) {
|
||||
ctx.std.get(EdgelessLegacySlotIdentifier).toggleNoteSlicer.next();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'f.auto-height',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
(!elements[0].isPageBlock() ||
|
||||
!ctx.std.getOptional(NoteConfigExtension.identifier)
|
||||
?.edgelessNoteHeader)
|
||||
);
|
||||
},
|
||||
generate(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { collapse } = firstModel.props.edgeless$.value;
|
||||
const options: Pick<ToolbarAction, 'tooltip' | 'icon'> = collapse
|
||||
? {
|
||||
tooltip: 'Auto height',
|
||||
icon: AutoHeightIcon(),
|
||||
}
|
||||
: {
|
||||
tooltip: 'Customized height',
|
||||
icon: CustomizedHeightIcon(),
|
||||
};
|
||||
|
||||
return {
|
||||
...options,
|
||||
run(ctx) {
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
|
||||
if (edgeless.collapse) {
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.props.edgeless.collapse = false;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (edgeless.collapsedHeight) {
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
bounds.h = edgeless.collapsedHeight * (edgeless.scale ?? 1);
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.xywh = xywh;
|
||||
model.props.edgeless.collapse = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'g.scale',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const scale$ = computed(() => {
|
||||
const scale = firstModel.props.edgeless$.value.scale ?? 1;
|
||||
return Math.round(100 * scale);
|
||||
});
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const scale = e.detail / 100;
|
||||
|
||||
models.forEach(model => {
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
const oldScale = model.props.edgeless.scale ?? 1;
|
||||
const ratio = scale / oldScale;
|
||||
bounds.w *= ratio;
|
||||
bounds.h *= ratio;
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.xywh = xywh;
|
||||
model.props.edgeless.scale = scale;
|
||||
});
|
||||
});
|
||||
|
||||
ctx.track('SelectedCardScale', {
|
||||
...trackBaseProps,
|
||||
control: 'select card scale',
|
||||
});
|
||||
};
|
||||
const onToggle = (e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track('OpenedCardScaleSelector', {
|
||||
...trackBaseProps,
|
||||
control: 'switch card scale',
|
||||
});
|
||||
};
|
||||
const format = (value: number) => `${value}%`;
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(NoteBlockModel).length > 0,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
function setDisplayMode(
|
||||
ctx: ToolbarContext,
|
||||
model: NoteBlockModel,
|
||||
newMode: NoteDisplayMode
|
||||
) {
|
||||
const displayMode = model.props.displayMode;
|
||||
|
||||
ctx.command.exec(changeNoteDisplayMode, {
|
||||
noteId: model.id,
|
||||
mode: newMode,
|
||||
stopCapture: true,
|
||||
});
|
||||
|
||||
// if change note to page only, should clear the selection
|
||||
if (newMode === NoteDisplayMode.DocOnly) {
|
||||
ctx.selection.clear();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const clear = () => {
|
||||
ctx.history.off('stack-item-added', addHandler);
|
||||
ctx.history.off('stack-item-popped', popHandler);
|
||||
disposable.unsubscribe();
|
||||
};
|
||||
const closeNotify = () => {
|
||||
abortController.abort();
|
||||
clear();
|
||||
};
|
||||
|
||||
const addHandler = ctx.history.on('stack-item-added', closeNotify);
|
||||
const popHandler = ctx.history.on('stack-item-popped', closeNotify);
|
||||
const disposable = ctx.std
|
||||
.get(EditorLifeCycleExtension)
|
||||
.slots.unmounted.subscribe(closeNotify);
|
||||
|
||||
const undo = () => {
|
||||
ctx.store.undo();
|
||||
closeNotify();
|
||||
};
|
||||
|
||||
const viewInToc = () => {
|
||||
const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier);
|
||||
sidebar?.open('outline');
|
||||
closeNotify();
|
||||
};
|
||||
|
||||
const data =
|
||||
newMode === NoteDisplayMode.EdgelessOnly
|
||||
? {
|
||||
title: 'Note removed from Page Mode',
|
||||
message: 'Content removed from your page.',
|
||||
}
|
||||
: {
|
||||
title: 'Note displayed in Page Mode',
|
||||
message: 'Content added to your page.',
|
||||
};
|
||||
|
||||
const notification = ctx.std.getOptional(NotificationProvider);
|
||||
notification?.notify({
|
||||
title: data.title,
|
||||
message: `${data.message}. Find it in the TOC for quick navigation.`,
|
||||
accent: 'success',
|
||||
duration: 5 * 1000,
|
||||
footer: html`<div class=${styles.viewInPageNotifyFooter}>
|
||||
<button
|
||||
class=${styles.viewInPageNotifyFooterButton}
|
||||
@click=${undo}
|
||||
data-testid="undo-display-in-page"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<button
|
||||
class=${styles.viewInPageNotifyFooterButton}
|
||||
@click=${viewInToc}
|
||||
data-testid="view-in-toc"
|
||||
>
|
||||
View in Toc
|
||||
</button>
|
||||
</div>`,
|
||||
abort: abortController.signal,
|
||||
onClose: () => {
|
||||
clear();
|
||||
},
|
||||
});
|
||||
|
||||
ctx.track('NoteDisplayModeChanged', {
|
||||
...trackBaseProps,
|
||||
control: 'display mode',
|
||||
other: `from ${displayMode} to ${newMode}`,
|
||||
});
|
||||
}
|
||||
|
||||
export const createBuiltinToolbarConfigExtension = (
|
||||
flavour: string
|
||||
): ExtensionType[] => {
|
||||
const name = flavour.split(':').pop();
|
||||
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(`affine:surface:${name}`),
|
||||
config: builtinSurfaceToolbarConfig,
|
||||
}),
|
||||
];
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
import { EdgelessNoteBackground } from './components/edgeless-note-background';
|
||||
import { EdgelessNoteBorderDropdownMenu } from './components/edgeless-note-border-dropdown-menu';
|
||||
import { EdgelessNoteDisplayModeDropdownMenu } from './components/edgeless-note-display-mode-dropdown-menu';
|
||||
import { EdgelessNoteMask } from './components/edgeless-note-mask';
|
||||
import { EdgelessNoteShadowDropdownMenu } from './components/edgeless-note-shadow-dropdown-menu';
|
||||
import { EdgelessPageBlockTitle } from './components/edgeless-page-block-title';
|
||||
import { NoteBlockComponent } from './note-block';
|
||||
import {
|
||||
@@ -13,4 +16,16 @@ export function effects() {
|
||||
customElements.define('edgeless-note-mask', EdgelessNoteMask);
|
||||
customElements.define('edgeless-note-background', EdgelessNoteBackground);
|
||||
customElements.define('edgeless-page-block-title', EdgelessPageBlockTitle);
|
||||
customElements.define(
|
||||
'edgeless-note-shadow-dropdown-menu',
|
||||
EdgelessNoteShadowDropdownMenu
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-note-border-dropdown-menu',
|
||||
EdgelessNoteBorderDropdownMenu
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-note-display-mode-dropdown-menu',
|
||||
EdgelessNoteDisplayModeDropdownMenu
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { literal } from 'lit/static-html.js';
|
||||
import {
|
||||
DocNoteBlockAdapterExtensions,
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
} from './adapters/index.js';
|
||||
import { NoteSlashMenuConfigExtension } from './configs/slash-menu.js';
|
||||
import { NoteBlockService } from './note-service.js';
|
||||
} from './adapters/index';
|
||||
import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
import { NoteBlockService } from './note-service';
|
||||
|
||||
const flavour = NoteBlockSchema.model.flavour;
|
||||
|
||||
@@ -26,4 +27,5 @@ export const EdgelessNoteBlockSpec: ExtensionType[] = [
|
||||
BlockViewExtension(flavour, literal`affine-edgeless-note`),
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
NoteSlashMenuConfigExtension,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
].flat();
|
||||
|
||||
@@ -92,7 +92,7 @@ const conversionsActionGroup = {
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Conversions"
|
||||
.tooltip="${'Turn Into'}"
|
||||
.tooltip="${'Turn into'}"
|
||||
>
|
||||
${conversion.icon} ${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { builtinFrameToolbarConfig } from './frame';
|
||||
import { builtinGroupToolbarConfig } from './group';
|
||||
import { builtinMindmapToolbarConfig } from './mindmap';
|
||||
import { builtinMiscToolbarConfig } from './misc';
|
||||
import { builtinNoteToolbarConfig } from './note';
|
||||
import { builtinShapeToolbarConfig } from './shape';
|
||||
import { builtinTextToolbarConfig } from './text';
|
||||
|
||||
@@ -38,11 +37,6 @@ export const EdgelessElementToolbarExtension: ExtensionType[] = [
|
||||
config: builtinMindmapToolbarConfig,
|
||||
}),
|
||||
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('affine:surface:note'),
|
||||
config: builtinNoteToolbarConfig,
|
||||
}),
|
||||
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('affine:surface:shape'),
|
||||
config: builtinShapeToolbarConfig,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { ToolbarModuleConfig } from '@blocksuite/affine-shared/services';
|
||||
|
||||
export const builtinNoteToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.test',
|
||||
label: 'Note',
|
||||
run() {},
|
||||
},
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
@@ -66,7 +66,9 @@
|
||||
"./card-style-dropdown-menu": "./src/card-style-dropdown-menu/index.ts",
|
||||
"./highlight-dropdown-menu": "./src/highlight-dropdown-menu/index.ts",
|
||||
"./tooltip-content-with-shortcut": "./src/tooltip-content-with-shortcut/index.ts",
|
||||
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts"
|
||||
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",
|
||||
"./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts",
|
||||
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -205,12 +205,13 @@ export class EdgelessColorPanel extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
onSelect(palette: Palette) {
|
||||
select(palette: Palette) {
|
||||
this.dispatchEvent(
|
||||
new ColorEvent('select', {
|
||||
detail: palette,
|
||||
composed: true,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -236,7 +237,7 @@ export class EdgelessColorPanel extends LitElement {
|
||||
.hollowCircle=${this.hollowCircle}
|
||||
?active=${activated}
|
||||
@click=${() => {
|
||||
this.onSelect(palette);
|
||||
this.select(palette);
|
||||
this.value = resolvedColor;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EdgelessLineStylesPanel } from './line-styles-panel';
|
||||
|
||||
export * from './line-styles-panel';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(
|
||||
'affine-edgeless-line-styles-panel',
|
||||
EdgelessLineStylesPanel
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { LineWidth, StrokeStyle } from '@blocksuite/affine-model';
|
||||
import { BanIcon, DashLineIcon, StraightLineIcon } from '@blocksuite/icons/lit';
|
||||
import { html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
export type LineDetailType =
|
||||
| {
|
||||
type: 'size';
|
||||
value: LineWidth;
|
||||
}
|
||||
| {
|
||||
type: 'style';
|
||||
value: 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 class EdgelessLineStylesPanel extends LitElement {
|
||||
select(detail: LineDetailType) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('select', {
|
||||
detail,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { lineSize, lineStyle, lineStyles } = this;
|
||||
|
||||
return html`
|
||||
<affine-edgeless-line-width-panel
|
||||
?disabled=${lineStyle === StrokeStyle.None}
|
||||
.selectedSize=${lineSize}
|
||||
@select=${(e: CustomEvent<LineWidth>) => {
|
||||
e.stopPropagation();
|
||||
this.select({ type: 'size', value: e.detail });
|
||||
}}
|
||||
></affine-edgeless-line-width-panel>
|
||||
|
||||
<editor-toolbar-separator></editor-toolbar-separator>
|
||||
|
||||
${repeat(
|
||||
LINE_STYLE_LIST.filter(item => lineStyles.includes(item.value)),
|
||||
item => item.value,
|
||||
({ name, icon, value }) => {
|
||||
const active = lineStyle === value;
|
||||
const classInfo = {
|
||||
'line-style-button': true,
|
||||
[`mode-${value}`]: true,
|
||||
};
|
||||
if (active) classInfo['active'] = true;
|
||||
|
||||
return html`
|
||||
<editor-icon-button
|
||||
class=${classMap(classInfo)}
|
||||
.tooltip="${name}"
|
||||
.withHover=${active}
|
||||
@click=${() => this.select({ type: 'style', value })}
|
||||
>
|
||||
${icon}
|
||||
</editor-icon-button>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineStyle!: StrokeStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineSize: LineWidth = LineWidth.Two;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineStyles: StrokeStyle[] = [
|
||||
StrokeStyle.Solid,
|
||||
StrokeStyle.Dash,
|
||||
StrokeStyle.None,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-line-styles-panel': EdgelessLineStylesPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EdgelessLineWidthPanel } from './line-width-panel';
|
||||
|
||||
export * from './line-width-panel';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(
|
||||
'affine-edgeless-line-width-panel',
|
||||
EdgelessLineWidthPanel
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import { LINE_WIDTHS, LineWidth } from '@blocksuite/affine-model';
|
||||
import { on, once } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
interface Config {
|
||||
width: number;
|
||||
itemSize: number;
|
||||
itemIconSize: number;
|
||||
dragHandleSize: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
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 CustomEvent('select', {
|
||||
detail: lineWidth,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
cancelable: 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 {
|
||||
'affine-edgeless-line-width-panel': EdgelessLineWidthPanel;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { PropTypes, requiredProperties } from '@blocksuite/block-std';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { ArrowDownSmallIcon, DoneIcon } from '@blocksuite/icons/lit';
|
||||
import type { ReadonlySignal, Signal } from '@preact/signals-core';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { css, html, LitElement, type TemplateResult } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit-html/directives/repeat.js';
|
||||
import { when } from 'lit-html/directives/when.js';
|
||||
@@ -39,7 +39,7 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
|
||||
:host([data-type='check']) editor-menu-action[data-selected] {
|
||||
color: var(--affine-primary-color);
|
||||
background-color: none;
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -76,6 +76,12 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
@property({ attribute: false })
|
||||
accessor format: ((e: number) => string) | undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor label: string = 'Scale';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor icon: TemplateResult | undefined;
|
||||
|
||||
@property({ attribute: 'data-type' })
|
||||
accessor type: 'normal' | 'check' = 'normal';
|
||||
|
||||
@@ -118,6 +124,8 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
sizes,
|
||||
format,
|
||||
type,
|
||||
icon,
|
||||
label,
|
||||
size$: { value: size },
|
||||
} = this;
|
||||
const isCheckType = type === 'check';
|
||||
@@ -128,13 +136,14 @@ export class SizeDropdownMenu extends SignalWatcher(LitElement) {
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Scale"
|
||||
.tooltip="${'Scale'}"
|
||||
aria-label="${label}"
|
||||
.tooltip="${label}"
|
||||
.justify="${'space-between'}"
|
||||
.labelHeight="${'20px'}"
|
||||
.iconContainerWidth="${'65px'}"
|
||||
.iconContainerWidth="${icon ? 'unset' : '65px'}"
|
||||
>
|
||||
<span class="label">${format?.(size) ?? size}</span>
|
||||
${icon ??
|
||||
html`<span class="label">${format?.(size) ?? size}</span>`}
|
||||
${ArrowDownSmallIcon()}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
|
||||
@@ -20,7 +20,7 @@ type ActionBase = {
|
||||
export type ToolbarAction = ActionBase & {
|
||||
label?: string;
|
||||
icon?: TemplateResult;
|
||||
tooltip?: string;
|
||||
tooltip?: string | TemplateResult;
|
||||
variant?: 'destructive';
|
||||
disabled?: ((cx: ToolbarContext) => boolean) | boolean;
|
||||
content?:
|
||||
|
||||
@@ -56,6 +56,10 @@ abstract class ToolbarContextBase {
|
||||
return this.std.store;
|
||||
}
|
||||
|
||||
get history() {
|
||||
return this.store.history;
|
||||
}
|
||||
|
||||
get view() {
|
||||
return this.std.view;
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ export function ToolbarModuleExtension(module: ToolbarModule): ExtensionType {
|
||||
export class ToolbarRegistryExtension extends Extension {
|
||||
flavour$ = signal<string>('affine:note');
|
||||
|
||||
elementsMap$ = signal<Map<string, GfxModel[]>>(new Map());
|
||||
|
||||
message$ = signal<{
|
||||
flavour: string;
|
||||
element: Element;
|
||||
setFloating: (element?: Element) => void;
|
||||
} | null>(null);
|
||||
|
||||
elementsMap$ = signal<Map<string, GfxModel[]>>(new Map());
|
||||
|
||||
flags = new Flags();
|
||||
|
||||
constructor(readonly std: BlockStdScope) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
import maxBy from 'lodash-es/maxBy';
|
||||
|
||||
export function getMostCommonValue<T, F extends keyof T>(
|
||||
records: T[],
|
||||
field: F
|
||||
) {
|
||||
const grouped = groupBy(records, record => record[field]);
|
||||
const values = Object.values(grouped);
|
||||
const record = maxBy(values, records => records.length)?.[0];
|
||||
return record?.[field];
|
||||
}
|
||||
|
||||
export function getMostCommonResolvedValue<
|
||||
T,
|
||||
F extends Exclude<keyof T, symbol>,
|
||||
U,
|
||||
>(records: T[], field: F, resolve: (value: T[F]) => U) {
|
||||
return getMostCommonValue(
|
||||
records.map(record => ({ [field]: resolve(record[field]) })),
|
||||
field
|
||||
);
|
||||
}
|
||||
@@ -359,18 +359,4 @@ export const createKeydownObserver = ({
|
||||
target.addEventListener('compositionend', () => onInput?.(true), { signal });
|
||||
};
|
||||
|
||||
export class ColorEvent extends Event {
|
||||
detail: Palette;
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
{
|
||||
detail,
|
||||
composed,
|
||||
bubbles,
|
||||
}: { detail: Palette; composed: boolean; bubbles: boolean }
|
||||
) {
|
||||
super(type, { bubbles, composed });
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
export class ColorEvent extends CustomEvent<Palette> {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './auto-scroll';
|
||||
export * from './button-popper';
|
||||
export * from './collapsed';
|
||||
export * from './computing';
|
||||
export * from './dnd';
|
||||
export * from './dom';
|
||||
export * from './drag-helper';
|
||||
|
||||
@@ -147,7 +147,7 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
host,
|
||||
std,
|
||||
} = this;
|
||||
const { flags, elementsMap$, flavour$, message$ } = toolbarRegistry;
|
||||
const { flags, flavour$, message$, elementsMap$ } = toolbarRegistry;
|
||||
const context = new ToolbarContext(std);
|
||||
|
||||
// TODO(@fundon): fix toolbar position shaking when the wheel scrolls
|
||||
@@ -392,14 +392,14 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
})
|
||||
);
|
||||
|
||||
// Handles blocks when adding
|
||||
// TODO(@fundon): improve these cases
|
||||
// Waits until the view is created when switching the view mode.
|
||||
// `card view` or `embed view`
|
||||
disposables.add(
|
||||
std.view.viewUpdated.subscribe(record => {
|
||||
const hasAddedBlock =
|
||||
record.type === 'block' && record.method === 'add';
|
||||
if (!hasAddedBlock) return;
|
||||
const hasAdded = record.type === 'block' && record.method === 'add';
|
||||
if (!hasAdded) return;
|
||||
|
||||
if (flags.isBlock()) {
|
||||
const blockIds = std.selection
|
||||
@@ -426,16 +426,53 @@ export class AffineToolbarWidget extends WidgetComponent {
|
||||
})
|
||||
);
|
||||
|
||||
// Handles blocks when updating
|
||||
disposables.add(
|
||||
// TODO(@fundon): use rxjs' filter
|
||||
std.store.slots.blockUpdated.subscribe(record => {
|
||||
if (
|
||||
flags.isBlock() &&
|
||||
record.type === 'update' &&
|
||||
record.props.key === 'text'
|
||||
) {
|
||||
flags.refresh(Flag.Block);
|
||||
const hasUpdated = record.type === 'update';
|
||||
if (!hasUpdated) return;
|
||||
|
||||
if (flags.isBlock()) {
|
||||
const blockIds = std.selection
|
||||
.filter$(BlockSelection)
|
||||
.peek()
|
||||
.map(s => s.blockId);
|
||||
if (blockIds.includes(record.id)) {
|
||||
batch(() => {
|
||||
this.setReferenceElementWithBlocks(
|
||||
blockIds
|
||||
.map(id => std.view.getBlock(id))
|
||||
.filter(block => block !== null)
|
||||
);
|
||||
flags.refresh(Flag.Block);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.isSurface()) {
|
||||
flags.refresh(Flag.Surface);
|
||||
return;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Handles elemets when updating
|
||||
disposables.add(
|
||||
effect(() => {
|
||||
const surface = context.gfx.surface$.value;
|
||||
if (!surface) return;
|
||||
|
||||
const subscription = surface.elementUpdated.subscribe(() => {
|
||||
if (!flags.isSurface()) return;
|
||||
|
||||
flags.refresh(Flag.Surface);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -2678,9 +2678,11 @@ __metadata:
|
||||
"@lit/context": "npm:^1.1.2"
|
||||
"@preact/signals-core": "npm:^1.8.0"
|
||||
"@toeverything/theme": "npm:^1.1.12"
|
||||
"@types/lodash-es": "npm:^4.17.12"
|
||||
"@types/mdast": "npm:^4.0.4"
|
||||
"@vanilla-extract/css": "npm:^1.17.0"
|
||||
lit: "npm:^3.2.0"
|
||||
lodash-es: "npm:^4.17.21"
|
||||
minimatch: "npm:^10.0.1"
|
||||
rxjs: "npm:^7.8.1"
|
||||
zod: "npm:^3.23.8"
|
||||
|
||||
Reference in New Issue
Block a user