mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 16:19:43 +08:00
chore: merge blocksuite source code (#9213)
This commit is contained in:
@@ -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,
|
||||
];
|
||||
Reference in New Issue
Block a user