chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,431 @@
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
import type { NoteBlockModel, RootBlockModel } from '@blocksuite/affine-model';
import { NoteDisplayMode } from '@blocksuite/affine-model';
import type { Viewport } from '@blocksuite/affine-shared/types';
import {
focusTitle,
getDocTitleInlineEditor,
getScrollContainer,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/block-std';
import { BlockComponent } from '@blocksuite/block-std';
import type { BlockModel, Text } from '@blocksuite/store';
import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { buildPath } from '../../_common/utils/index.js';
import { PageClipboard } from '../clipboard/index.js';
import type { PageRootBlockWidgetName } from '../index.js';
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
import type { PageRootService } from './page-root-service.js';
const DOC_BLOCK_CHILD_PADDING = 24;
const DOC_BOTTOM_PADDING = 32;
function testClickOnBlankArea(
state: PointerEventState,
viewportLeft: number,
viewportWidth: number,
pageWidth: number,
paddingLeft: number,
paddingRight: number
) {
const blankLeft =
viewportLeft + (viewportWidth - pageWidth) / 2 + paddingLeft;
const blankRight =
viewportLeft + (viewportWidth - pageWidth) / 2 + pageWidth - paddingRight;
if (state.raw.clientX < blankLeft || state.raw.clientX > blankRight) {
return true;
}
return false;
}
export class PageRootBlockComponent extends BlockComponent<
RootBlockModel,
PageRootService,
PageRootBlockWidgetName
> {
static override styles = css`
editor-host:has(> affine-page-root, * > affine-page-root) {
display: block;
height: 100%;
}
affine-page-root {
display: block;
height: 100%;
cursor: default;
}
.affine-page-root-block-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
font-family: var(--affine-font-family);
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
max-width: var(--affine-editor-width);
margin: 0 auto;
/* Leave a place for drag-handle */
/* Do not use prettier format this style, or it will be broken */
/* prettier-ignore */
padding-left: var(--affine-editor-side-padding, ${DOC_BLOCK_CHILD_PADDING}px);
/* prettier-ignore */
padding-right: var(--affine-editor-side-padding, ${DOC_BLOCK_CHILD_PADDING}px);
/* prettier-ignore */
padding-bottom: var(--affine-editor-bottom-padding, ${DOC_BOTTOM_PADDING}px);
}
/* Extra small devices (phones, 640px and down) */
@container viewport (width <= 640px) {
.affine-page-root-block-container {
padding-left: ${DOC_BLOCK_CHILD_PADDING}px;
padding-right: ${DOC_BLOCK_CHILD_PADDING}px;
}
}
.affine-block-element {
display: block;
}
@media print {
.selected {
background-color: transparent !important;
}
}
`;
private _viewportElement: HTMLDivElement | null = null;
clipboardController = new PageClipboard(this);
focusFirstParagraph = () => {
const defaultNote = this._getDefaultNoteBlock();
const firstText = defaultNote?.children.find(block =>
matchFlavours(block, ['affine:paragraph', 'affine:list', 'affine:code'])
);
if (firstText) {
focusTextModel(this.std, firstText.id);
} else {
const newFirstParagraphId = this.doc.addBlock(
'affine:paragraph',
{},
defaultNote,
0
);
focusTextModel(this.std, newFirstParagraphId);
}
};
keyboardManager: PageKeyboardManager | null = null;
prependParagraphWithText = (text: Text) => {
const newFirstParagraphId = this.doc.addBlock(
'affine:paragraph',
{ text },
this._getDefaultNoteBlock(),
0
);
focusTextModel(this.std, newFirstParagraphId);
};
get rootScrollContainer() {
return getScrollContainer(this);
}
get slots() {
return this.service.slots;
}
get viewport(): Viewport | null {
if (!this.viewportElement) {
return null;
}
const {
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
} = this.viewportElement;
const { top, left } = this.viewportElement.getBoundingClientRect();
return {
top,
left,
scrollLeft,
scrollTop,
scrollWidth,
scrollHeight,
clientWidth,
clientHeight,
};
}
get viewportElement(): HTMLDivElement | null {
if (this._viewportElement) return this._viewportElement;
this._viewportElement = this.host.closest(
'.affine-page-viewport'
) as HTMLDivElement | null;
return this._viewportElement;
}
private _createDefaultNoteBlock() {
const { doc } = this;
const noteId = doc.addBlock('affine:note', {}, doc.root?.id);
return doc.getBlockById(noteId) as NoteBlockModel;
}
private _getDefaultNoteBlock() {
return (
this.doc.root?.children.find(child => child.flavour === 'affine:note') ??
this._createDefaultNoteBlock()
);
}
private _initViewportResizeEffect() {
const viewport = this.viewport;
const viewportElement = this.viewportElement;
if (!viewport || !viewportElement) {
return;
}
// when observe viewportElement resize, emit viewport update event
const resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
for (const { target } of entries) {
if (target === viewportElement) {
this.slots.viewportUpdated.emit(viewport);
break;
}
}
}
);
resizeObserver.observe(viewportElement);
this.disposables.add(() => {
resizeObserver.unobserve(viewportElement);
resizeObserver.disconnect();
});
}
override connectedCallback() {
super.connectedCallback();
this.clipboardController.hostConnected();
this.keyboardManager = new PageKeyboardManager(this);
this.bindHotKey({
'Mod-a': () => {
const blocks = this.model.children
.filter(model => {
if (matchFlavours(model, ['affine:note'])) {
const note = model as NoteBlockModel;
if (note.displayMode === NoteDisplayMode.EdgelessOnly)
return false;
return true;
}
return false;
})
.flatMap(model => {
return model.children.map(child => {
return this.std.selection.create('block', {
blockId: child.id,
});
});
});
this.std.selection.setGroup('note', blocks);
return true;
},
ArrowUp: () => {
const selection = this.host.selection;
const sel = selection.value.find(
sel => sel.is('text') || sel.is('block')
);
if (!sel) return;
let model: BlockModel | null = null;
let path: string[] = buildPath(this.doc.getBlockById(sel.blockId));
while (path.length > 0 && !model) {
const m = this.doc.getBlockById(path[path.length - 1]);
if (m && m.flavour === 'affine:note') {
model = m;
}
path = path.slice(0, -1);
}
if (!model) return;
const prevNote = this.doc.getPrev(model);
if (!prevNote || prevNote.flavour !== 'affine:note') {
const isFirstText = sel.is('text') && sel.start.index === 0;
const isBlock = sel.is('block');
if (isBlock || isFirstText) {
focusTitle(this.host);
}
return;
}
const notes = this.doc.getBlockByFlavour('affine:note');
const index = notes.indexOf(prevNote);
if (index !== 0) return;
const range = this.std.range.value;
requestAnimationFrame(() => {
const currentRange = this.std.range.value;
if (!range || !currentRange) return;
// If the range has not changed, it means we need to manually move the cursor to the title.
if (
range.startContainer === currentRange.startContainer &&
range.startOffset === currentRange.startOffset &&
range.endContainer === currentRange.endContainer &&
range.endOffset === currentRange.endOffset
) {
const titleInlineEditor = getDocTitleInlineEditor(this.host);
if (titleInlineEditor) {
titleInlineEditor.focusEnd();
}
}
});
},
});
this.handleEvent('click', ctx => {
const event = ctx.get('pointerState');
if (
event.raw.target !== this &&
event.raw.target !== this.viewportElement &&
event.raw.target !== this.rootElementContainer
) {
return;
}
const { paddingLeft, paddingRight } = window.getComputedStyle(
this.rootElementContainer
);
if (!this.viewport) return;
const isClickOnBlankArea = testClickOnBlankArea(
event,
this.viewport.left,
this.viewport.clientWidth,
this.rootElementContainer.clientWidth,
parseFloat(paddingLeft),
parseFloat(paddingRight)
);
if (isClickOnBlankArea) {
this.host.selection.clear(['block']);
return;
}
let newTextSelectionId: string | null = null;
const readonly = this.doc.readonly;
const lastNote = this.model.children
.slice()
.reverse()
.find(child => {
const isNote = matchFlavours(child, ['affine:note']);
if (!isNote) return false;
const note = child as NoteBlockModel;
const displayOnDoc =
!!note.displayMode &&
note.displayMode !== NoteDisplayMode.EdgelessOnly;
return displayOnDoc;
});
if (!lastNote) {
if (readonly) return;
const noteId = this.doc.addBlock('affine:note', {}, this.model.id);
const paragraphId = this.doc.addBlock('affine:paragraph', {}, noteId);
newTextSelectionId = paragraphId;
} else {
const last = lastNote.children.at(-1);
if (
!last ||
!(matchFlavours(last, ['affine:paragraph']) && last.text.length === 0)
) {
if (readonly) return;
const paragraphId = this.doc.addBlock(
'affine:paragraph',
{},
lastNote.id
);
newTextSelectionId = paragraphId;
}
}
this.updateComplete
.then(() => {
if (!newTextSelectionId) return;
this.host.selection.setGroup('note', [
this.host.selection.create('text', {
from: {
blockId: newTextSelectionId,
index: 0,
length: 0,
},
to: null,
}),
]);
})
.catch(console.error);
});
}
override disconnectedCallback() {
super.disconnectedCallback();
this.clipboardController.hostDisconnected();
this._disposables.dispose();
this.keyboardManager = null;
}
override firstUpdated() {
this._initViewportResizeEffect();
const noteModels = this.model.children.filter(model =>
matchFlavours(model, ['affine:note'])
);
noteModels.forEach(note => {
this.disposables.add(
note.propsUpdated.on(({ key }) => {
if (key === 'displayMode') {
this.requestUpdate();
}
})
);
});
}
override renderBlock() {
const widgets = html`${repeat(
Object.entries(this.widgets),
([id]) => id,
([_, widget]) => widget
)}`;
const children = this.renderChildren(this.model, child => {
const isNote = matchFlavours(child, ['affine:note']);
const note = child as NoteBlockModel;
const displayOnEdgeless =
!!note.displayMode && note.displayMode === NoteDisplayMode.EdgelessOnly;
// Should remove deprecated `hidden` property in the future
return !(isNote && displayOnEdgeless);
});
return html`
<div class="affine-page-root-block-container">${children} ${widgets}</div>
`;
}
@query('.affine-page-root-block-container')
accessor rootElementContainer!: HTMLDivElement;
}
declare global {
interface HTMLElementTagNameMap {
'affine-page-root': PageRootBlockComponent;
}
}
@@ -0,0 +1,13 @@
import { RootBlockSchema } from '@blocksuite/affine-model';
import { Slot } from '@blocksuite/store';
import type { Viewport } from '../../_common/utils/index.js';
import { RootService } from '../root-service.js';
export class PageRootService extends RootService {
static override readonly flavour = RootBlockSchema.model.flavour;
slots = {
viewportUpdated: new Slot<Viewport>(),
};
}
@@ -0,0 +1,76 @@
import {
DNDAPIExtension,
DocDisplayMetaService,
DocModeService,
EmbedOptionService,
ThemeService,
} from '@blocksuite/affine-shared/services';
import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring';
import {
BlockViewExtension,
CommandExtension,
type ExtensionType,
FlavourExtension,
WidgetViewMapExtension,
} from '@blocksuite/block-std';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { ExportManagerExtension } from '../../_common/export-manager/export-manager.js';
import { commands } from '../commands/index.js';
import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '../widgets/doc-remote-selection/doc-remote-selection.js';
import { AFFINE_DRAG_HANDLE_WIDGET } from '../widgets/drag-handle/consts.js';
import { AFFINE_EMBED_CARD_TOOLBAR_WIDGET } from '../widgets/embed-card-toolbar/embed-card-toolbar.js';
import { AFFINE_FORMAT_BAR_WIDGET } from '../widgets/format-bar/format-bar.js';
import { AFFINE_INNER_MODAL_WIDGET } from '../widgets/inner-modal/inner-modal.js';
import { AFFINE_KEYBOARD_TOOLBAR_WIDGET } from '../widgets/keyboard-toolbar/index.js';
import { AFFINE_LINKED_DOC_WIDGET } from '../widgets/linked-doc/index.js';
import { AFFINE_MODAL_WIDGET } from '../widgets/modal/modal.js';
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from '../widgets/page-dragging-area/page-dragging-area.js';
import { AFFINE_SLASH_MENU_WIDGET } from '../widgets/slash-menu/index.js';
import { AFFINE_VIEWPORT_OVERLAY_WIDGET } from '../widgets/viewport-overlay/viewport-overlay.js';
import { PageRootService } from './page-root-service.js';
export const pageRootWidgetViewMap = {
[AFFINE_KEYBOARD_TOOLBAR_WIDGET]: literal`${unsafeStatic(AFFINE_KEYBOARD_TOOLBAR_WIDGET)}`,
[AFFINE_MODAL_WIDGET]: literal`${unsafeStatic(AFFINE_MODAL_WIDGET)}`,
[AFFINE_INNER_MODAL_WIDGET]: literal`${unsafeStatic(AFFINE_INNER_MODAL_WIDGET)}`,
[AFFINE_SLASH_MENU_WIDGET]: literal`${unsafeStatic(
AFFINE_SLASH_MENU_WIDGET
)}`,
[AFFINE_LINKED_DOC_WIDGET]: literal`${unsafeStatic(
AFFINE_LINKED_DOC_WIDGET
)}`,
[AFFINE_DRAG_HANDLE_WIDGET]: literal`${unsafeStatic(
AFFINE_DRAG_HANDLE_WIDGET
)}`,
[AFFINE_EMBED_CARD_TOOLBAR_WIDGET]: literal`${unsafeStatic(
AFFINE_EMBED_CARD_TOOLBAR_WIDGET
)}`,
[AFFINE_FORMAT_BAR_WIDGET]: literal`${unsafeStatic(
AFFINE_FORMAT_BAR_WIDGET
)}`,
[AFFINE_DOC_REMOTE_SELECTION_WIDGET]: literal`${unsafeStatic(
AFFINE_DOC_REMOTE_SELECTION_WIDGET
)}`,
[AFFINE_PAGE_DRAGGING_AREA_WIDGET]: literal`${unsafeStatic(
AFFINE_PAGE_DRAGGING_AREA_WIDGET
)}`,
[AFFINE_VIEWPORT_OVERLAY_WIDGET]: literal`${unsafeStatic(
AFFINE_VIEWPORT_OVERLAY_WIDGET
)}`,
[AFFINE_SCROLL_ANCHORING_WIDGET]: literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}`,
};
export const PageRootBlockSpec: ExtensionType[] = [
FlavourExtension('affine:page'),
PageRootService,
DocModeService,
ThemeService,
EmbedOptionService,
CommandExtension(commands),
BlockViewExtension('affine:page', literal`affine-page-root`),
WidgetViewMapExtension('affine:page', pageRootWidgetViewMap),
ExportManagerExtension,
DNDAPIExtension,
DocDisplayMetaService,
];