mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
180
blocksuite/affine/components/src/caption/block-caption.ts
Normal file
180
blocksuite/affine/components/src/caption/block-caption.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { DocMode } from '@blocksuite/affine-model';
|
||||
import { stopPropagation } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import {
|
||||
docContext,
|
||||
modelContext,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/block-std';
|
||||
import { WithDisposable } from '@blocksuite/global/utils';
|
||||
import type { BlockModel, Doc } from '@blocksuite/store';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
|
||||
import { focusTextModel } from '../rich-text/index.js';
|
||||
|
||||
export interface BlockCaptionProps {
|
||||
caption: string | null | undefined;
|
||||
}
|
||||
|
||||
export class BlockCaptionEditor<
|
||||
Model extends BlockModel<BlockCaptionProps> = BlockModel<BlockCaptionProps>,
|
||||
> extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
.block-caption-editor {
|
||||
display: inline-table;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--affine-icon-color);
|
||||
font-size: var(--affine-font-sm);
|
||||
font-family: inherit;
|
||||
text-align: center;
|
||||
field-sizing: content;
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.block-caption-editor::placeholder {
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private _focus = false;
|
||||
|
||||
show = () => {
|
||||
this.display = true;
|
||||
this.updateComplete.then(() => this.input.focus()).catch(console.error);
|
||||
};
|
||||
|
||||
get mode(): DocMode {
|
||||
return this.doc.getParent(this.model)?.flavour === 'affine:surface'
|
||||
? 'edgeless'
|
||||
: 'page';
|
||||
}
|
||||
|
||||
private _onCaptionKeydown(event: KeyboardEvent) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.mode === 'edgeless' || event.isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const doc = this.doc;
|
||||
const target = event.target as HTMLInputElement;
|
||||
const start = target.selectionStart;
|
||||
if (start === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.model;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = target.value;
|
||||
const caption = value.slice(0, start);
|
||||
doc.updateBlock(model, { caption });
|
||||
|
||||
const nextBlockText = value.slice(start);
|
||||
const index = parent.children.indexOf(model);
|
||||
const id = doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ text: new Text(nextBlockText) },
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
|
||||
focusTextModel(this.std, id);
|
||||
}
|
||||
}
|
||||
|
||||
private _onInputBlur() {
|
||||
this._focus = false;
|
||||
this.display = !!this.caption?.length;
|
||||
}
|
||||
|
||||
private _onInputChange(e: InputEvent) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.caption = target.value;
|
||||
this.doc.updateBlock(this.model, {
|
||||
caption: this.caption,
|
||||
});
|
||||
}
|
||||
|
||||
private _onInputFocus() {
|
||||
this._focus = true;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.caption = this.model.caption;
|
||||
|
||||
this.disposables.add(
|
||||
this.model.propsUpdated.on(({ key }) => {
|
||||
if (key === 'caption') {
|
||||
this.caption = this.model.caption;
|
||||
if (!this._focus) {
|
||||
this.display = !!this.caption?.length;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.display && !this.caption) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<textarea
|
||||
.disabled=${this.doc.readonly}
|
||||
placeholder="Write a caption"
|
||||
class="block-caption-editor"
|
||||
.value=${this.caption ?? ''}
|
||||
@input=${this._onInputChange}
|
||||
@focus=${this._onInputFocus}
|
||||
@blur=${this._onInputBlur}
|
||||
@pointerdown=${stopPropagation}
|
||||
@click=${stopPropagation}
|
||||
@dblclick=${stopPropagation}
|
||||
@cut=${stopPropagation}
|
||||
@copy=${stopPropagation}
|
||||
@paste=${stopPropagation}
|
||||
@keydown=${this._onCaptionKeydown}
|
||||
@keyup=${stopPropagation}
|
||||
></textarea>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor caption: string | null | undefined = undefined;
|
||||
|
||||
@state()
|
||||
accessor display = false;
|
||||
|
||||
@consume({ context: docContext })
|
||||
accessor doc!: Doc;
|
||||
|
||||
@query('.block-caption-editor')
|
||||
accessor input!: HTMLInputElement;
|
||||
|
||||
@consume({ context: modelContext })
|
||||
accessor model!: Model;
|
||||
|
||||
@consume({ context: stdContext })
|
||||
accessor std!: BlockStdScope;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'block-caption-editor': BlockCaptionEditor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { BlockComponent, type BlockService } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { html, nothing } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import type { BlockCaptionEditor } from './block-caption.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export enum SelectedStyle {
|
||||
Background = 'Background',
|
||||
Border = 'Border',
|
||||
}
|
||||
|
||||
export class CaptionedBlockComponent<
|
||||
Model extends BlockModel = BlockModel,
|
||||
Service extends BlockService = BlockService,
|
||||
WidgetName extends string = string,
|
||||
> extends BlockComponent<Model, Service, WidgetName> {
|
||||
static override styles = styles;
|
||||
|
||||
get captionEditor() {
|
||||
if (!this.useCaptionEditor || !this._captionEditorRef.value) {
|
||||
console.error(
|
||||
'Oops! Please enable useCaptionEditor before accessing captionEditor'
|
||||
);
|
||||
}
|
||||
return this._captionEditorRef.value;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addRenderer(this._renderWithWidget);
|
||||
}
|
||||
|
||||
private _renderWithWidget(content: unknown) {
|
||||
const style = styleMap({
|
||||
position: 'relative',
|
||||
...this.blockContainerStyles,
|
||||
});
|
||||
const theme = this.std.get(ThemeProvider).theme;
|
||||
const isBorder = this.selectedStyle === SelectedStyle.Border;
|
||||
|
||||
return html`<div
|
||||
style=${style}
|
||||
class=${classMap({
|
||||
'affine-block-component': true,
|
||||
[theme]: true,
|
||||
border: isBorder,
|
||||
})}
|
||||
>
|
||||
${content}
|
||||
${this.useCaptionEditor
|
||||
? html`<block-caption-editor
|
||||
${ref(this._captionEditorRef)}
|
||||
></block-caption-editor>`
|
||||
: nothing}
|
||||
${this.selectedStyle === SelectedStyle.Background
|
||||
? html`<affine-block-selection .block=${this}></affine-block-selection>`
|
||||
: null}
|
||||
${this.useZeroWidth && !this.doc.readonly
|
||||
? html`<block-zero-width .block=${this}></block-zero-width>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// There may be multiple block-caption-editors in a nested structure.
|
||||
private accessor _captionEditorRef: Ref<BlockCaptionEditor> =
|
||||
createRef<BlockCaptionEditor>();
|
||||
|
||||
protected accessor blockContainerStyles: StyleInfo | undefined = undefined;
|
||||
|
||||
protected accessor selectedStyle = SelectedStyle.Background;
|
||||
|
||||
protected accessor useCaptionEditor = false;
|
||||
|
||||
protected accessor useZeroWidth = false;
|
||||
}
|
||||
10
blocksuite/affine/components/src/caption/index.ts
Normal file
10
blocksuite/affine/components/src/caption/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { BlockCaptionEditor } from './block-caption.js';
|
||||
export { BlockCaptionEditor, type BlockCaptionProps } from './block-caption.js';
|
||||
export {
|
||||
CaptionedBlockComponent,
|
||||
SelectedStyle,
|
||||
} from './captioned-block-component.js';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('block-caption-editor', BlockCaptionEditor);
|
||||
}
|
||||
18
blocksuite/affine/components/src/caption/styles.ts
Normal file
18
blocksuite/affine/components/src/caption/styles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
export const styles = css`
|
||||
.affine-block-component.border.light .selected-style {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 0px 0px 1px var(--affine-brand-color);
|
||||
}
|
||||
.affine-block-component.border.dark .selected-style {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 0px 0px 1px var(--affine-brand-color);
|
||||
}
|
||||
@media print {
|
||||
.affine-block-component.border.light .selected-style,
|
||||
.affine-block-component.border.dark .selected-style {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user