mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 19:15:33 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06b84330a9 | |||
| 5147e2c62d | |||
| 03e8e7143d | |||
| 5e8691367d | |||
| e3d88ab3f2 | |||
| 61e40c7523 | |||
| cdb721d6a6 | |||
| c89680cb55 | |||
| 0256fdb2af | |||
| a4711aad61 | |||
| 6d97c5a393 |
@@ -10,6 +10,7 @@
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||
|
||||
@@ -1,19 +1,54 @@
|
||||
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset';
|
||||
import { type CalloutBlockModel } from '@blocksuite/affine-model';
|
||||
import { type CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
DocModeProvider,
|
||||
type IconData,
|
||||
IconPickerServiceIdentifier,
|
||||
IconType,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { UniComponent } from '@blocksuite/affine-shared/types';
|
||||
import * as icons from '@blocksuite/icons/lit';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { type Signal, signal } from '@preact/signals-core';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { css, html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
// Copy of renderUniLit and UniLit from affine-data-view
|
||||
export const renderUniLit = <Props, Expose extends NonNullable<unknown>>(
|
||||
uni: UniComponent<Props, Expose> | undefined,
|
||||
props?: Props,
|
||||
options?: {
|
||||
ref?: Signal<Expose | undefined>;
|
||||
style?: Readonly<StyleInfo>;
|
||||
class?: string;
|
||||
}
|
||||
): TemplateResult => {
|
||||
return html` <uni-lit
|
||||
.uni="${uni}"
|
||||
.props="${props}"
|
||||
.ref="${options?.ref}"
|
||||
style=${options?.style ? styleMap(options?.style) : ''}
|
||||
></uni-lit>`;
|
||||
};
|
||||
const getIcon = (icon?: IconData) => {
|
||||
console.log(icon);
|
||||
if (!icon) {
|
||||
return '💡';
|
||||
}
|
||||
if (icon.type === IconType.Emoji) {
|
||||
return icon.unicode;
|
||||
}
|
||||
if (icon.type === IconType.AffineIcon) {
|
||||
return (
|
||||
icons as Record<string, (props: { style: string }) => TemplateResult>
|
||||
)[`${icon.name}Icon`]?.({ style: `color:${icon.color}` });
|
||||
}
|
||||
return '💡';
|
||||
};
|
||||
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
@@ -26,7 +61,6 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
align-items: flex-start;
|
||||
padding: 5px 10px;
|
||||
border-radius: 8px;
|
||||
background-color: ${unsafeCSSVarV2('block/callout/background/grey')};
|
||||
}
|
||||
|
||||
.affine-callout-emoji-container {
|
||||
@@ -40,6 +74,12 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.affine-callout-emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.affine-callout-emoji:hover {
|
||||
cursor: pointer;
|
||||
@@ -51,38 +91,68 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
min-width: 0;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.icon-picker-container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
}
|
||||
`;
|
||||
|
||||
private _emojiMenuAbortController: AbortController | null = null;
|
||||
private readonly _toggleEmojiMenu = () => {
|
||||
if (this._emojiMenuAbortController) {
|
||||
this._emojiMenuAbortController.abort();
|
||||
private readonly showIconPicker$ = signal(false);
|
||||
|
||||
private _closeEmojiMenu() {
|
||||
this.showIconPicker$.value = false;
|
||||
}
|
||||
|
||||
private _toggleIconPicker() {
|
||||
this.showIconPicker$.value = !this.showIconPicker$.value;
|
||||
}
|
||||
|
||||
private _renderIconPicker() {
|
||||
if (!this.showIconPicker$.value) {
|
||||
return html``;
|
||||
}
|
||||
this._emojiMenuAbortController = new AbortController();
|
||||
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
// Get IconPickerService from the framework
|
||||
const iconPickerService = this.std.getOptional(IconPickerServiceIdentifier);
|
||||
if (!iconPickerService) {
|
||||
console.warn('IconPickerService not found');
|
||||
return html``;
|
||||
}
|
||||
|
||||
createLitPortal({
|
||||
template: html`<affine-emoji-menu
|
||||
.theme=${theme}
|
||||
.onEmojiSelect=${(data: { native: string }) => {
|
||||
this.model.props.emoji = data.native;
|
||||
// Get the uni-component from the service
|
||||
const iconPickerComponent = iconPickerService.iconPickerComponent;
|
||||
|
||||
// Create props for the icon picker
|
||||
const props = {
|
||||
onSelect: (iconData?: IconData) => {
|
||||
this.model.props.icon$.value = iconData;
|
||||
this._closeEmojiMenu(); // Close the picker after selection
|
||||
},
|
||||
onClose: () => {
|
||||
this._closeEmojiMenu();
|
||||
},
|
||||
};
|
||||
|
||||
return html`
|
||||
<div
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
></affine-emoji-menu>`,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
container: this.host,
|
||||
computePosition: {
|
||||
referenceElement: this._emojiButton,
|
||||
placement: 'bottom-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._emojiMenuAbortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
};
|
||||
class="icon-picker-container"
|
||||
>
|
||||
${renderUniLit(iconPickerComponent, props)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly _handleBlockClick = (event: MouseEvent) => {
|
||||
// Check if the click target is emoji related element
|
||||
@@ -125,9 +195,6 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
return this.std.get(DefaultInlineManagerExtension.identifier);
|
||||
}
|
||||
|
||||
@query('.affine-callout-emoji')
|
||||
private accessor _emojiButton!: HTMLElement;
|
||||
|
||||
override get topContenteditableElement() {
|
||||
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
||||
return this.closest<BlockComponent>(
|
||||
@@ -138,21 +205,32 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const emoji = this.model.props.emoji$.value;
|
||||
const icon = this.model.props.icon$.value;
|
||||
const background = this.model.props.background$.value;
|
||||
|
||||
const themeProvider = this.std.get(ThemeProvider);
|
||||
const theme = themeProvider.theme$.value;
|
||||
const backgroundColor = themeProvider.generateColorProperty(
|
||||
background || DefaultTheme.NoteBackgroundColorMap.White,
|
||||
DefaultTheme.NoteBackgroundColorMap.White,
|
||||
theme
|
||||
);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="affine-callout-block-container"
|
||||
@click=${this._handleBlockClick}
|
||||
style=${styleMap({
|
||||
backgroundColor: backgroundColor,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@click=${this._toggleEmojiMenu}
|
||||
@click=${this._toggleIconPicker}
|
||||
contenteditable="false"
|
||||
class="affine-callout-emoji-container"
|
||||
style=${styleMap({
|
||||
display: emoji.length === 0 ? 'none' : undefined,
|
||||
})}
|
||||
>
|
||||
<span class="affine-callout-emoji">${emoji}</span>
|
||||
<span class="affine-callout-emoji">${getIcon(icon)}</span>
|
||||
${this._renderIconPicker()}
|
||||
</div>
|
||||
<div class="affine-callout-children">
|
||||
${this.renderChildren(this.model)}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import { CalloutBlockModel, DefaultTheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
type ToolbarAction,
|
||||
type ToolbarActionGroup,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { PaletteIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
const colors = [
|
||||
'default',
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'teal',
|
||||
'blue',
|
||||
'purple',
|
||||
'grey',
|
||||
] as const;
|
||||
|
||||
const backgroundColorAction = {
|
||||
id: 'background-color',
|
||||
label: 'Background Color',
|
||||
tooltip: 'Change background color',
|
||||
icon: PaletteIcon(),
|
||||
run() {
|
||||
// This will be handled by the content function
|
||||
},
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(CalloutBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const updateBackground = (color: string) => {
|
||||
// Map text highlight colors to note background colors
|
||||
const colorMap: Record<
|
||||
string,
|
||||
keyof typeof DefaultTheme.NoteBackgroundColorMap | null
|
||||
> = {
|
||||
default: null,
|
||||
red: 'Red',
|
||||
orange: 'Orange',
|
||||
yellow: 'Yellow',
|
||||
green: 'Green',
|
||||
teal: 'Green', // Map teal to green as it's not available in NoteBackgroundColorMap
|
||||
blue: 'Blue',
|
||||
purple: 'Purple',
|
||||
grey: 'White', // Map grey to white as it's the closest available
|
||||
};
|
||||
|
||||
const mappedColor = colorMap[color];
|
||||
const backgroundValue = mappedColor
|
||||
? DefaultTheme.NoteBackgroundColorMap[mappedColor]
|
||||
: null;
|
||||
ctx.store.updateBlock(model, { background: backgroundValue });
|
||||
};
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="background"
|
||||
.tooltip=${'Background Color'}
|
||||
>
|
||||
${PaletteIcon()} ${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
<div class="highlight-heading">Background</div>
|
||||
${repeat(colors, color => {
|
||||
const isDefault = color === 'default';
|
||||
const value = isDefault
|
||||
? null
|
||||
: `var(--affine-text-highlight-${color})`;
|
||||
const displayName = `${color} Background`;
|
||||
|
||||
return html`
|
||||
<editor-menu-action
|
||||
data-testid="background-${color}"
|
||||
@click=${() => updateBackground(color)}
|
||||
>
|
||||
<affine-text-duotone-icon
|
||||
style=${styleMap({
|
||||
'--color': 'var(--affine-text-primary-color)',
|
||||
'--background': value ?? 'transparent',
|
||||
})}
|
||||
></affine-text-duotone-icon>
|
||||
<span class="label capitalize">${displayName}</span>
|
||||
</editor-menu-action>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
},
|
||||
} satisfies ToolbarAction;
|
||||
|
||||
const builtinToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'style',
|
||||
actions: [backgroundColorAction],
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
],
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
export const createBuiltinToolbarConfigExtension = (
|
||||
flavour: string
|
||||
): ExtensionType[] => {
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(flavour),
|
||||
config: builtinToolbarConfig,
|
||||
}),
|
||||
];
|
||||
};
|
||||
@@ -1,14 +1,11 @@
|
||||
import { CalloutBlockComponent } from './callout-block';
|
||||
import { EmojiMenu } from './emoji-menu';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-callout', CalloutBlockComponent);
|
||||
customElements.define('affine-emoji-menu', EmojiMenu);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-callout': CalloutBlockComponent;
|
||||
'affine-emoji-menu': EmojiMenu;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import data from '@emoji-mart/data';
|
||||
import { Picker } from 'emoji-mart';
|
||||
import { html, LitElement, type PropertyValues } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
|
||||
export class EmojiMenu extends WithDisposable(LitElement) {
|
||||
override firstUpdated(props: PropertyValues) {
|
||||
const result = super.firstUpdated(props);
|
||||
|
||||
const picker = new Picker({
|
||||
data,
|
||||
onEmojiSelect: this.onEmojiSelect,
|
||||
autoFocus: true,
|
||||
theme: this.theme,
|
||||
});
|
||||
this.emojiMenu.append(picker as unknown as Node);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor onEmojiSelect: (data: any) => void = () => {};
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme: 'light' | 'dark' = 'light';
|
||||
|
||||
@query('.affine-emoji-menu')
|
||||
accessor emojiMenu!: HTMLElement;
|
||||
|
||||
override render() {
|
||||
return html`<div class="affine-emoji-menu"></div>`;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { CalloutKeymapExtension } from './callout-keymap';
|
||||
import { calloutSlashMenuConfig } from './configs/slash-menu';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
import { effects } from './effects';
|
||||
|
||||
export class CalloutViewExtension extends ViewExtensionProvider {
|
||||
@@ -25,6 +26,7 @@ export class CalloutViewExtension extends ViewExtensionProvider {
|
||||
BlockViewExtension('affine:callout', literal`affine-callout`),
|
||||
CalloutKeymapExtension,
|
||||
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
|
||||
...createBuiltinToolbarConfigExtension('affine:callout'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { updateBlockAlign } from '@blocksuite/affine-block-note';
|
||||
import { ImageBlockModel, TextAlign } from '@blocksuite/affine-model';
|
||||
import { ImageBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
@@ -13,9 +12,6 @@ import {
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
DuplicateIcon,
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
@@ -55,55 +51,7 @@ const builtinToolbarConfig = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.1.align-left',
|
||||
tooltip: 'Align left',
|
||||
icon: TextAlignLeftIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.chain
|
||||
.pipe(updateBlockAlign, {
|
||||
textAlign: TextAlign.Left,
|
||||
selectedBlocks: [block],
|
||||
})
|
||||
.run();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.2.align-center',
|
||||
tooltip: 'Align center',
|
||||
icon: TextAlignCenterIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.chain
|
||||
.pipe(updateBlockAlign, {
|
||||
textAlign: TextAlign.Center,
|
||||
selectedBlocks: [block],
|
||||
})
|
||||
.run();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.3.align-right',
|
||||
tooltip: 'Align right',
|
||||
icon: TextAlignRightIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(ImageBlockComponent);
|
||||
if (block) {
|
||||
ctx.chain
|
||||
.pipe(updateBlockAlign, {
|
||||
textAlign: TextAlign.Right,
|
||||
selectedBlocks: [block],
|
||||
})
|
||||
.run();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'd.comment',
|
||||
id: 'c.comment',
|
||||
...blockCommentToolbarButton,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -143,15 +143,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
const alignItemsStyleMap = styleMap({
|
||||
alignItems:
|
||||
this.model.props.textAlign$.value === 'left'
|
||||
? 'flex-start'
|
||||
: this.model.props.textAlign$.value === 'right'
|
||||
? 'flex-end'
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const resovledState = this.resourceController.resolveStateWith({
|
||||
loadingIcon: LoadingIcon({
|
||||
strokeColor: cssVarV2('button/pureWhiteText'),
|
||||
@@ -171,7 +162,6 @@ export class ImageBlockComponent extends CaptionedBlockComponent<ImageBlockModel
|
||||
html`<affine-page-image
|
||||
.block=${this}
|
||||
.state=${resovledState}
|
||||
style="${alignItemsStyleMap}"
|
||||
></affine-page-image>`,
|
||||
() =>
|
||||
html`<affine-image-fallback-card
|
||||
|
||||
@@ -150,10 +150,6 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
|
||||
const listIcon = getListIcon(model, !collapsed, _onClickIcon);
|
||||
|
||||
const textAlignStyle = styleMap({
|
||||
textAlign: this.model.props.textAlign$?.value,
|
||||
});
|
||||
|
||||
const children = html`<div
|
||||
class="affine-block-children-container"
|
||||
style=${styleMap({
|
||||
@@ -165,7 +161,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
||||
</div>`;
|
||||
|
||||
return html`
|
||||
<div class=${'affine-list-block-container'} style="${textAlignStyle}">
|
||||
<div class=${'affine-list-block-container'}>
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-list-rich-text-wrapper': true,
|
||||
|
||||
@@ -8,4 +8,3 @@ export { indentBlock } from './indent-block.js';
|
||||
export { indentBlocks } from './indent-blocks.js';
|
||||
export { selectBlock } from './select-block.js';
|
||||
export { selectBlocksBetween } from './select-blocks-between.js';
|
||||
export { updateBlockAlign } from './update-block-align.js';
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
getBlockSelectionsCommand,
|
||||
getImageSelectionsCommand,
|
||||
getSelectedBlocksCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type Command,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
type UpdateBlockAlignConfig = {
|
||||
textAlign: TextAlign;
|
||||
selectedBlocks?: BlockComponent[];
|
||||
};
|
||||
|
||||
export const updateBlockAlign: Command<UpdateBlockAlignConfig> = (
|
||||
ctx,
|
||||
next
|
||||
) => {
|
||||
let { std, textAlign, selectedBlocks } = ctx;
|
||||
|
||||
if (selectedBlocks === null) {
|
||||
const [result, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
chain.pipe(getImageSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedBlocksCommand, { types: ['text', 'block', 'image'] })
|
||||
.run();
|
||||
if (result) {
|
||||
selectedBlocks = ctx.selectedBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) return false;
|
||||
|
||||
selectedBlocks.forEach(block => {
|
||||
std.store.updateBlock(block.model, { textAlign });
|
||||
});
|
||||
|
||||
const selectionManager = std.host.selection;
|
||||
const textSelection = selectionManager.find(TextSelection);
|
||||
if (!textSelection) {
|
||||
return false;
|
||||
}
|
||||
selectionManager.setGroup('note', [textSelection]);
|
||||
return next();
|
||||
};
|
||||
@@ -4,15 +4,9 @@ import {
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-inline-preset';
|
||||
import {
|
||||
type TextAlignConfig,
|
||||
textAlignConfigs,
|
||||
type TextConversionConfig,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
getSelectedModelsCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type SlashMenuActionItem,
|
||||
@@ -23,7 +17,7 @@ import {
|
||||
import { HeadingsIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
|
||||
import { updateBlockAlign, updateBlockType } from '../commands';
|
||||
import { updateBlockType } from '../commands';
|
||||
import { tooltips } from './tooltips';
|
||||
|
||||
let basicIndex = 0;
|
||||
@@ -66,10 +60,6 @@ const noteSlashMenuConfig: SlashMenuConfig = {
|
||||
createConversionItem(config, `1_List@${index++}`)
|
||||
),
|
||||
|
||||
...textAlignConfigs.map((config, index) =>
|
||||
createAlignItem(config, `2_Align@${index++}`)
|
||||
),
|
||||
|
||||
...textFormatConfigs
|
||||
.filter(i => !['Code', 'Link'].includes(i.name))
|
||||
.map((config, index) =>
|
||||
@@ -99,26 +89,6 @@ function createConversionItem(
|
||||
};
|
||||
}
|
||||
|
||||
function createAlignItem(
|
||||
config: TextAlignConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
): SlashMenuActionItem {
|
||||
const { textAlign, name, icon } = config;
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
icon,
|
||||
action: ({ std }) => {
|
||||
std.command
|
||||
.chain()
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe(getSelectedModelsCommand, { types: ['text'] })
|
||||
.pipe(updateBlockAlign, { textAlign })
|
||||
.run();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTextFormatItem(
|
||||
config: TextFormatConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
NoteBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
textAlignConfigs,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
focusBlockEnd,
|
||||
focusBlockStart,
|
||||
@@ -39,7 +36,6 @@ import {
|
||||
indentBlocks,
|
||||
selectBlock,
|
||||
selectBlocksBetween,
|
||||
updateBlockAlign,
|
||||
updateBlockType,
|
||||
} from './commands';
|
||||
import { moveBlockConfigs } from './move-block';
|
||||
@@ -161,36 +157,6 @@ class NoteKeymap {
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextAlignHotKey = () => {
|
||||
return textAlignConfigs.reduce(
|
||||
(acc, item) => {
|
||||
const keymap = item.hotkey!.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(updateBlockAlign, { textAlign: item.textAlign })
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private _focusBlock: BlockComponent | null = null;
|
||||
|
||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||
@@ -602,7 +568,6 @@ class NoteKeymap {
|
||||
...this._bindMoveBlockHotKey(),
|
||||
...this._bindQuickActionHotKey(),
|
||||
...this._bindTextConversionHotKey(),
|
||||
...this._bindTextAlignHotKey(),
|
||||
Tab: ctx => {
|
||||
const [success] = this.std.command.exec(indentBlocks);
|
||||
|
||||
|
||||
@@ -264,10 +264,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
`;
|
||||
}
|
||||
|
||||
const textAlignStyle = styleMap({
|
||||
textAlign: this.model.props.textAlign$?.value,
|
||||
});
|
||||
|
||||
const children = html`<div
|
||||
class="affine-block-children-container"
|
||||
style=${styleMap({
|
||||
@@ -292,7 +288,6 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
'affine-paragraph-block-container': true,
|
||||
'highlight-comment': this.isCommentHighlighted,
|
||||
})}
|
||||
style="${textAlignStyle}"
|
||||
data-has-collapsed-siblings="${collapsedSiblings.length > 0}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
updateBlockAlign,
|
||||
updateBlockType,
|
||||
} from '@blocksuite/affine-block-note';
|
||||
import { updateBlockType } from '@blocksuite/affine-block-note';
|
||||
import type { HighlightType } from '@blocksuite/affine-components/highlight-dropdown-menu';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
@@ -26,12 +23,8 @@ import {
|
||||
import {
|
||||
EmbedLinkedDocBlockSchema,
|
||||
EmbedSyncedDocBlockSchema,
|
||||
type TextAlign,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
textAlignConfigs,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
copySelectedModelsCommand,
|
||||
deleteSelectedModelsCommand,
|
||||
@@ -53,7 +46,6 @@ import {
|
||||
ActionPlacement,
|
||||
blockCommentToolbarButton,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||
import { tableViewMeta } from '@blocksuite/data-view/view-presets';
|
||||
import {
|
||||
CopyIcon,
|
||||
@@ -138,64 +130,6 @@ const conversionsActionGroup = {
|
||||
},
|
||||
} as const satisfies ToolbarActionGenerator;
|
||||
|
||||
const alignActionGroup = {
|
||||
id: 'b.align',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
generate({ chain }) {
|
||||
const [ok, { selectedModels = [] }] = chain
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedModelsCommand, { types: ['text', 'block'] })
|
||||
.run();
|
||||
if (!ok) return null;
|
||||
|
||||
const alignment =
|
||||
textAlignConfigs.find(
|
||||
({ textAlign }) =>
|
||||
textAlign ===
|
||||
getMostCommonValue(
|
||||
selectedModels.map(
|
||||
({ props }) => props as { textAlign?: TextAlign }
|
||||
),
|
||||
'textAlign'
|
||||
)
|
||||
) ?? textAlignConfigs[0];
|
||||
const update = (textAlign: TextAlign) => {
|
||||
chain.pipe(updateBlockAlign, { textAlign }).run();
|
||||
};
|
||||
|
||||
return {
|
||||
content: html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button aria-label="Align" .tooltip="${'Align'}">
|
||||
${alignment.icon} ${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div data-size="large" data-orientation="vertical">
|
||||
${repeat(
|
||||
textAlignConfigs,
|
||||
item => item.name,
|
||||
({ textAlign, name, icon }) => html`
|
||||
<editor-menu-action
|
||||
aria-label=${name}
|
||||
@click=${() => update(textAlign)}
|
||||
>
|
||||
${icon}<span class="label">${name}</span>
|
||||
</editor-menu-action>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`,
|
||||
};
|
||||
},
|
||||
} as const satisfies ToolbarActionGenerator;
|
||||
|
||||
const inlineTextActionGroup = {
|
||||
id: 'b.inline-text',
|
||||
when: ({ chain }) => isFormatSupported(chain).run()[0],
|
||||
@@ -357,7 +291,6 @@ const turnIntoLinkedDoc = {
|
||||
export const builtinToolbarConfig = {
|
||||
actions: [
|
||||
conversionsActionGroup,
|
||||
alignActionGroup,
|
||||
inlineTextActionGroup,
|
||||
highlightActionGroup,
|
||||
turnIntoDatabase,
|
||||
|
||||
@@ -144,16 +144,6 @@ export class TableBlockComponent extends CaptionedBlockComponent<TableBlockModel
|
||||
style=${styleMap({
|
||||
paddingLeft: `${virtualPadding}px`,
|
||||
paddingRight: `${virtualPadding}px`,
|
||||
marginLeft:
|
||||
!this.model.props.textAlign$.value ||
|
||||
this.model.props.textAlign$?.value === 'left'
|
||||
? undefined
|
||||
: 'auto',
|
||||
marginRight:
|
||||
!this.model.props.textAlign$.value ||
|
||||
this.model.props.textAlign$?.value === 'right'
|
||||
? undefined
|
||||
: 'auto',
|
||||
width: 'max-content',
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IconData } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
BlockModel,
|
||||
BlockSchemaExtension,
|
||||
@@ -5,18 +6,22 @@ import {
|
||||
type Text,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { Color } from '../../themes/index.js';
|
||||
import { DefaultTheme } from '../../themes/index.js';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
export type CalloutProps = {
|
||||
emoji: string;
|
||||
icon?: IconData;
|
||||
text: Text;
|
||||
background: Color;
|
||||
} & BlockMeta;
|
||||
|
||||
export const CalloutBlockSchema = defineBlockSchema({
|
||||
flavour: 'affine:callout',
|
||||
props: (internal): CalloutProps => ({
|
||||
emoji: '😀',
|
||||
icon: undefined,
|
||||
text: internal.Text(),
|
||||
background: DefaultTheme.NoteBackgroundColorMap.White,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:updatedAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types.js';
|
||||
import { ImageBlockTransformer } from './image-transformer.js';
|
||||
|
||||
@@ -21,7 +20,6 @@ export type ImageBlockProps = {
|
||||
rotate: number;
|
||||
size?: number;
|
||||
comments?: Record<string, boolean>;
|
||||
textAlign?: TextAlign;
|
||||
} & Omit<GfxCommonBlockProps, 'scale'> &
|
||||
BlockMeta;
|
||||
|
||||
@@ -36,7 +34,6 @@ const defaultImageProps: ImageBlockProps = {
|
||||
rotate: 0,
|
||||
size: -1,
|
||||
comments: undefined,
|
||||
textAlign: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedAt': undefined,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
// `toggle` type has been deprecated, do not use it
|
||||
@@ -14,7 +13,6 @@ export type ListType = 'bulleted' | 'numbered' | 'todo' | 'toggle';
|
||||
export type ListProps = {
|
||||
type: ListType;
|
||||
text: Text;
|
||||
textAlign?: TextAlign;
|
||||
checked: boolean;
|
||||
collapsed: boolean;
|
||||
order: number | null;
|
||||
@@ -27,7 +25,6 @@ export const ListBlockSchema = defineBlockSchema({
|
||||
({
|
||||
type: 'bulleted',
|
||||
text: internal.Text(),
|
||||
textAlign: undefined,
|
||||
checked: false,
|
||||
collapsed: false,
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type Text,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
export type ParagraphType =
|
||||
@@ -20,7 +19,6 @@ export type ParagraphType =
|
||||
|
||||
export type ParagraphProps = {
|
||||
type: ParagraphType;
|
||||
textAlign?: TextAlign;
|
||||
text: Text;
|
||||
collapsed: boolean;
|
||||
comments?: Record<string, boolean>;
|
||||
@@ -31,7 +29,6 @@ export const ParagraphBlockSchema = defineBlockSchema({
|
||||
props: (internal): ParagraphProps => ({
|
||||
type: 'text',
|
||||
text: internal.Text(),
|
||||
textAlign: undefined,
|
||||
collapsed: false,
|
||||
comments: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
defineBlockSchema,
|
||||
} from '@blocksuite/store';
|
||||
|
||||
import type { TextAlign } from '../../consts';
|
||||
import type { BlockMeta } from '../../utils/types';
|
||||
|
||||
export type TableCell = {
|
||||
@@ -31,7 +30,6 @@ export interface TableBlockProps extends BlockMeta {
|
||||
// key = `${rowId}:${columnId}`
|
||||
cells: Record<string, TableCell>;
|
||||
comments?: Record<string, boolean>;
|
||||
textAlign?: TextAlign;
|
||||
}
|
||||
|
||||
export interface TableCellSerialized {
|
||||
@@ -55,7 +53,6 @@ export const TableBlockSchema = defineBlockSchema({
|
||||
columns: {},
|
||||
cells: {},
|
||||
comments: undefined,
|
||||
textAlign: undefined,
|
||||
'meta:createdAt': undefined,
|
||||
'meta:createdBy': undefined,
|
||||
'meta:updatedAt': undefined,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { TextAlign } from '@blocksuite/affine-model';
|
||||
import {
|
||||
TextAlignCenterIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
export interface TextAlignConfig {
|
||||
textAlign: TextAlign;
|
||||
name: string;
|
||||
hotkey: string[] | null;
|
||||
icon: TemplateResult<1>;
|
||||
}
|
||||
|
||||
export const textAlignConfigs: TextAlignConfig[] = [
|
||||
{
|
||||
textAlign: TextAlign.Left,
|
||||
name: 'Align left',
|
||||
hotkey: [`Mod-Shift-L`],
|
||||
icon: TextAlignLeftIcon(),
|
||||
},
|
||||
{
|
||||
textAlign: TextAlign.Center,
|
||||
name: 'Align center',
|
||||
hotkey: [`Mod-Shift-E`],
|
||||
icon: TextAlignCenterIcon(),
|
||||
},
|
||||
{
|
||||
textAlign: TextAlign.Right,
|
||||
name: 'Align right',
|
||||
hotkey: [`Mod-Shift-R`],
|
||||
icon: TextAlignRightIcon(),
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,3 @@
|
||||
export { type TextAlignConfig, textAlignConfigs } from './align';
|
||||
export { type TextConversionConfig, textConversionConfigs } from './conversion';
|
||||
export {
|
||||
asyncGetRichText,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './icon-picker-service/index.js';
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { UniComponent } from '@blocksuite/affine-shared/types';
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { TemplateResult } from 'lit';
|
||||
export enum IconType {
|
||||
Emoji = 'emoji',
|
||||
AffineIcon = 'affine-icon',
|
||||
Blob = 'blob',
|
||||
}
|
||||
|
||||
export type IconData =
|
||||
| {
|
||||
type: IconType.Emoji;
|
||||
unicode: string;
|
||||
}
|
||||
| {
|
||||
type: IconType.AffineIcon;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
| {
|
||||
type: IconType.Blob;
|
||||
blob: Blob;
|
||||
};
|
||||
|
||||
export interface IconPickerOptions {
|
||||
onSelect?: (icon: IconData) => void;
|
||||
onClose?: () => void;
|
||||
currentIcon?: IconData;
|
||||
}
|
||||
|
||||
export interface IconPickerService {
|
||||
iconPickerComponent: UniComponent<{ onSelect?: (data?: IconData) => void }>;
|
||||
renderIconPicker(options: IconPickerOptions): TemplateResult;
|
||||
}
|
||||
|
||||
export const IconPickerServiceIdentifier =
|
||||
createIdentifier<IconPickerService>('IconPickerService');
|
||||
@@ -13,6 +13,7 @@ export * from './feature-flag-service';
|
||||
export * from './file-size-limit-service';
|
||||
export * from './font-loader';
|
||||
export * from './generate-url-service';
|
||||
export * from './icon-picker-service';
|
||||
export * from './link-preview-service';
|
||||
export * from './native-clipboard-service';
|
||||
export * from './notification-service';
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"lit": "^3.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lottie-react": "^2.4.0",
|
||||
"mermaid": "^11.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"mp4-muxer": "^5.2.1",
|
||||
"nanoid": "^5.0.9",
|
||||
"next-themes": "^0.4.4",
|
||||
|
||||
@@ -175,6 +175,7 @@ const usePreviewExtensions = () => {
|
||||
.ai(enableAI, framework)
|
||||
.theme(framework)
|
||||
.database(framework)
|
||||
.iconPicker(framework)
|
||||
.linkedDoc(framework)
|
||||
.paragraph(enableAI)
|
||||
.linkPreview(framework)
|
||||
|
||||
@@ -117,6 +117,7 @@ const usePatchSpecs = (mode: DocMode, shared?: boolean) => {
|
||||
.electron(framework)
|
||||
.linkPreview(framework)
|
||||
.codeBlockPreview(framework)
|
||||
.iconPicker(framework)
|
||||
.comment(enableComment, framework).value;
|
||||
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type AffineEditorViewOptions,
|
||||
} from '@affine/core/blocksuite/view-extensions/editor-view/editor-view';
|
||||
import { ElectronViewExtension } from '@affine/core/blocksuite/view-extensions/electron';
|
||||
import { AffineIconPickerExtension } from '@affine/core/blocksuite/view-extensions/icon-picker';
|
||||
import { AffineLinkPreviewExtension } from '@affine/core/blocksuite/view-extensions/link-preview-service';
|
||||
import { MobileViewExtension } from '@affine/core/blocksuite/view-extensions/mobile';
|
||||
import { PdfViewExtension } from '@affine/core/blocksuite/view-extensions/pdf';
|
||||
@@ -58,6 +59,7 @@ type Configure = {
|
||||
electron: (framework?: FrameworkProvider) => Configure;
|
||||
linkPreview: (framework?: FrameworkProvider) => Configure;
|
||||
codeBlockPreview: (framework?: FrameworkProvider) => Configure;
|
||||
iconPicker: (framework?: FrameworkProvider) => Configure;
|
||||
comment: (
|
||||
enableComment?: boolean,
|
||||
framework?: FrameworkProvider
|
||||
@@ -86,6 +88,7 @@ class ViewProvider {
|
||||
AffineThemeViewExtension,
|
||||
AffineEditorViewExtension,
|
||||
AffineEditorConfigViewExtension,
|
||||
AffineIconPickerExtension,
|
||||
CodeBlockPreviewViewExtension,
|
||||
EdgelessBlockHeaderConfigViewExtension,
|
||||
TurboRendererViewExtension,
|
||||
@@ -123,6 +126,7 @@ class ViewProvider {
|
||||
electron: this._configureElectron,
|
||||
linkPreview: this._configureLinkPreview,
|
||||
codeBlockPreview: this._configureCodeBlockHtmlPreview,
|
||||
iconPicker: this._configureIconPicker,
|
||||
comment: this._configureComment,
|
||||
value: this._manager,
|
||||
};
|
||||
@@ -146,6 +150,7 @@ class ViewProvider {
|
||||
.electron()
|
||||
.linkPreview()
|
||||
.codeBlockPreview()
|
||||
.iconPicker()
|
||||
.comment();
|
||||
|
||||
return this.config;
|
||||
@@ -333,6 +338,11 @@ class ViewProvider {
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureIconPicker = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(AffineIconPickerExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureComment = (
|
||||
enableComment?: boolean,
|
||||
framework?: FrameworkProvider
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { IconPickerServiceIdentifier } from '@blocksuite/affine/shared/services';
|
||||
import { type ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { IconPickerService } from '../../../modules/icon-picker/services/icon-picker';
|
||||
|
||||
/**
|
||||
* Patch the icon picker service to make it available in BlockSuite
|
||||
* @param framework
|
||||
* @returns
|
||||
*/
|
||||
export function patchIconPickerService(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: (di: Container) => {
|
||||
di.override(IconPickerServiceIdentifier, () => {
|
||||
return framework.get(IconPickerService);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { patchIconPickerService } from './icon-picker-service';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
type AffineIconPickerViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class AffineIconPickerExtension extends ViewExtensionProvider<AffineIconPickerViewOptions> {
|
||||
override name = 'affine-icon-picker-extension';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: AffineIconPickerViewOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
if (!options?.framework) {
|
||||
return;
|
||||
}
|
||||
const { framework } = options;
|
||||
context.register(patchIconPickerService(framework));
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,8 @@ export function useAsyncCallback<T extends any[]>(
|
||||
const handleAsyncError = React.useContext(AsyncCallbackContext);
|
||||
return React.useCallback(
|
||||
(...args: any) => {
|
||||
// oxlint-disable-next-line exhaustive-deps
|
||||
callback(...args).catch(e => handleAsyncError(e));
|
||||
},
|
||||
[...deps] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[callback, handleAsyncError, ...deps] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export const useAISpecs = () => {
|
||||
.mobile(framework)
|
||||
.electron(framework)
|
||||
.linkPreview(framework)
|
||||
.iconPicker(framework)
|
||||
.codeBlockPreview(framework).value;
|
||||
|
||||
return manager.get('page');
|
||||
|
||||
@@ -38,9 +38,6 @@ type KeyboardShortcutsI18NKeys =
|
||||
| 'bodyText'
|
||||
| 'increaseIndent'
|
||||
| 'reduceIndent'
|
||||
| 'alignLeft'
|
||||
| 'alignCenter'
|
||||
| 'alignRight'
|
||||
| 'groupDatabase'
|
||||
| 'moveUp'
|
||||
| 'moveDown'
|
||||
@@ -188,9 +185,6 @@ export const useMacPageKeyboardShortcuts = (): ShortcutMap => {
|
||||
[tH('6')]: ['⌘', '⌥', '6'],
|
||||
[t('increaseIndent')]: ['Tab'],
|
||||
[t('reduceIndent')]: ['⇧', 'Tab'],
|
||||
[t('alignLeft')]: ['⌘', '⇧', 'L'],
|
||||
[t('alignCenter')]: ['⌘', '⇧', 'E'],
|
||||
[t('alignRight')]: ['⌘', '⇧', 'R'],
|
||||
[t('groupDatabase')]: ['⌘', 'G'],
|
||||
[t('switch')]: ['⌥', 'S'],
|
||||
// not implement yet
|
||||
@@ -248,9 +242,6 @@ export const useWinPageKeyboardShortcuts = (): ShortcutMap => {
|
||||
[tH('6')]: ['Ctrl', 'Shift', '6'],
|
||||
[t('increaseIndent')]: ['Tab'],
|
||||
[t('reduceIndent')]: ['Shift+Tab'],
|
||||
[t('alignLeft')]: ['Ctrl', 'Shift', 'L'],
|
||||
[t('alignCenter')]: ['Ctrl', 'Shift', 'E'],
|
||||
[t('alignRight')]: ['Ctrl', 'Shift', 'R'],
|
||||
[t('groupDatabase')]: ['Ctrl + G'],
|
||||
['Switch']: ['Alt + S'],
|
||||
// not implement yet
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type Framework } from '@toeverything/infra';
|
||||
|
||||
import { IconPickerService } from './services/icon-picker';
|
||||
|
||||
export { IconPickerService } from './services/icon-picker';
|
||||
|
||||
export function configureIconPickerModule(framework: Framework) {
|
||||
framework.service(IconPickerService);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
type IconData as ComponentIconData,
|
||||
IconPicker,
|
||||
IconType,
|
||||
uniReactRoot,
|
||||
} from '@affine/component';
|
||||
// Import the identifier for internal use
|
||||
import {
|
||||
type IconData,
|
||||
type IconPickerOptions,
|
||||
type IconPickerService as IIconPickerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { Service } from '@toeverything/infra';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
|
||||
// Re-export types from BlockSuite shared services
|
||||
export type {
|
||||
IconData,
|
||||
IconPickerOptions,
|
||||
IconPickerService as IIconPickerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
export { IconPickerServiceIdentifier } from '@blocksuite/affine-shared/services';
|
||||
|
||||
// Convert between BlockSuite IconData and Component IconData
|
||||
function convertToBlockSuiteIconData(
|
||||
componentIconData: ComponentIconData
|
||||
): IconData {
|
||||
if (componentIconData.type === IconType.Emoji) {
|
||||
return {
|
||||
type: 'emoji',
|
||||
value: componentIconData.unicode,
|
||||
};
|
||||
} else if (componentIconData.type === IconType.AffineIcon) {
|
||||
return {
|
||||
type: 'icon',
|
||||
value: componentIconData.name,
|
||||
};
|
||||
}
|
||||
// For other types, default to icon type
|
||||
return {
|
||||
type: 'icon',
|
||||
value: 'default',
|
||||
};
|
||||
}
|
||||
|
||||
export class IconPickerService extends Service implements IIconPickerService {
|
||||
public readonly iconPickerComponent =
|
||||
uniReactRoot.createUniComponent(IconPicker);
|
||||
|
||||
renderIconPicker(options: IconPickerOptions): TemplateResult {
|
||||
const element = document.createElement('div');
|
||||
|
||||
// Adapt the options to match IconPicker component's expected interface
|
||||
const adaptedOptions = {
|
||||
onSelect: options.onSelect
|
||||
? (data?: ComponentIconData) => {
|
||||
if (data && options.onSelect) {
|
||||
const blockSuiteIconData = convertToBlockSuiteIconData(data);
|
||||
options.onSelect(blockSuiteIconData);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
onClose: options.onClose,
|
||||
};
|
||||
|
||||
this.iconPickerComponent(element, adaptedOptions, () => {});
|
||||
return html`${element}`;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { configureFavoriteModule } from './favorite';
|
||||
import { configureFeatureFlagModule } from './feature-flag';
|
||||
import { configureGlobalContextModule } from './global-context';
|
||||
import { configureI18nModule } from './i18n';
|
||||
import { configureIconPickerModule } from './icon-picker';
|
||||
import { configureImportClipperModule } from './import-clipper';
|
||||
import { configureImportTemplateModule } from './import-template';
|
||||
import { configureIntegrationModule } from './integration';
|
||||
@@ -132,4 +133,5 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureCommentModule(framework);
|
||||
configureDocSummaryModule(framework);
|
||||
configurePaywallModule(framework);
|
||||
configureIconPickerModule(framework);
|
||||
}
|
||||
|
||||
@@ -2509,18 +2509,6 @@ export function useAFFiNEI18N(): {
|
||||
* `Just now`
|
||||
*/
|
||||
["com.affine.just-now"](): string;
|
||||
/**
|
||||
* `Align center`
|
||||
*/
|
||||
["com.affine.keyboardShortcuts.alignCenter"](): string;
|
||||
/**
|
||||
* `Align left`
|
||||
*/
|
||||
["com.affine.keyboardShortcuts.alignLeft"](): string;
|
||||
/**
|
||||
* `Align right`
|
||||
*/
|
||||
["com.affine.keyboardShortcuts.alignRight"](): string;
|
||||
/**
|
||||
* `Append to daily note`
|
||||
*/
|
||||
|
||||
@@ -626,9 +626,6 @@
|
||||
"com.affine.journal.placeholder.title": "No Journal",
|
||||
"com.affine.journal.placeholder.create": "Create Daily Journal",
|
||||
"com.affine.just-now": "Just now",
|
||||
"com.affine.keyboardShortcuts.alignCenter": "Align center",
|
||||
"com.affine.keyboardShortcuts.alignLeft": "Align left",
|
||||
"com.affine.keyboardShortcuts.alignRight": "Align right",
|
||||
"com.affine.keyboardShortcuts.appendDailyNote": "Append to daily note",
|
||||
"com.affine.keyboardShortcuts.bodyText": "Body text",
|
||||
"com.affine.keyboardShortcuts.bold": "Bold",
|
||||
|
||||
@@ -623,9 +623,6 @@
|
||||
"com.affine.journal.daily-count-updated-empty-tips": "你还没有任何更新",
|
||||
"com.affine.journal.updated-today": "更新",
|
||||
"com.affine.just-now": "就是现在",
|
||||
"com.affine.keyboardShortcuts.alignCenter": "居中对齐",
|
||||
"com.affine.keyboardShortcuts.alignLeft": "左对齐",
|
||||
"com.affine.keyboardShortcuts.alignRight": "右对齐",
|
||||
"com.affine.keyboardShortcuts.appendDailyNote": "添加日常笔记快捷键",
|
||||
"com.affine.keyboardShortcuts.bodyText": "正文",
|
||||
"com.affine.keyboardShortcuts.bold": "粗体",
|
||||
|
||||
@@ -623,9 +623,6 @@
|
||||
"com.affine.journal.daily-count-updated-empty-tips": "你還沒有任何更新",
|
||||
"com.affine.journal.updated-today": "更新",
|
||||
"com.affine.just-now": "就是現在",
|
||||
"com.affine.keyboardShortcuts.alignCenter": "置中對齊",
|
||||
"com.affine.keyboardShortcuts.alignLeft": "靠左對齊",
|
||||
"com.affine.keyboardShortcuts.alignRight": "靠右對齊",
|
||||
"com.affine.keyboardShortcuts.appendDailyNote": "附加到隨筆",
|
||||
"com.affine.keyboardShortcuts.bodyText": "正文",
|
||||
"com.affine.keyboardShortcuts.bold": "粗體",
|
||||
|
||||
@@ -92,6 +92,13 @@ test('should format quick bar show when clicking drag handle', async ({
|
||||
|
||||
const { formatBar } = getFormatBar(page);
|
||||
await expect(formatBar).toBeVisible();
|
||||
|
||||
const box = await formatBar.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error("formatBar doesn't exist");
|
||||
}
|
||||
assertAlmostEqual(box.x, 251, 5);
|
||||
assertAlmostEqual(box.y - dragHandleRect.y, -55.5, 5);
|
||||
});
|
||||
|
||||
test('should format quick bar show when select text by keyboard', async ({
|
||||
@@ -541,6 +548,17 @@ test('should format quick bar work in single block selection', async ({
|
||||
const { formatBar } = getFormatBar(page);
|
||||
await expect(formatBar).toBeVisible();
|
||||
|
||||
const formatRect = await formatBar.boundingBox();
|
||||
const selectionRect = await blockSelections.boundingBox();
|
||||
if (!formatRect) {
|
||||
throw new Error('formatRect is not found');
|
||||
}
|
||||
if (!selectionRect) {
|
||||
throw new Error('selectionRect is not found');
|
||||
}
|
||||
assertAlmostEqual(formatRect.x - selectionRect.x, 147.5, 10);
|
||||
assertAlmostEqual(formatRect.y - selectionRect.y, -48, 10);
|
||||
|
||||
const boldBtn = formatBar.getByTestId('bold');
|
||||
await boldBtn.click();
|
||||
const italicBtn = formatBar.getByTestId('italic');
|
||||
@@ -585,6 +603,17 @@ test('should format quick bar work in multiple block selection', async ({
|
||||
const formatBarController = getFormatBar(page);
|
||||
await expect(formatBarController.formatBar).toBeVisible();
|
||||
|
||||
const box = await formatBarController.formatBar.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error("formatBar doesn't exist");
|
||||
}
|
||||
const rect = await blockSelections.first().boundingBox();
|
||||
if (!rect) {
|
||||
throw new Error('rect is not found');
|
||||
}
|
||||
assertAlmostEqual(box.x - rect.x, 147.5, 10);
|
||||
assertAlmostEqual(box.y - rect.y, -48, 10);
|
||||
|
||||
await formatBarController.boldBtn.click();
|
||||
await formatBarController.italicBtn.click();
|
||||
await formatBarController.underlineBtn.click();
|
||||
|
||||
@@ -606,7 +606,7 @@ test.describe('slash search', () => {
|
||||
await expect(slashMenu).toBeVisible();
|
||||
|
||||
await type(page, 'c');
|
||||
await expect(slashItems).toHaveCount(9);
|
||||
await expect(slashItems).toHaveCount(8);
|
||||
await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']);
|
||||
await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']);
|
||||
await expect(slashItems.nth(2).locator('.text')).toHaveText(['New Doc']);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@affine-tools/cli": "workspace:*",
|
||||
"@affine-tools/utils": "workspace:*",
|
||||
"@googleapis/androidpublisher": "^31.0.0",
|
||||
"@googleapis/androidpublisher": "^28.0.0",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Reference in New Issue
Block a user