refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
45
blocksuite/affine/fragments/frame-panel/package.json
Normal file
45
blocksuite/affine/fragments/frame-panel/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-fragment-frame-panel",
|
||||
"description": "Frame panel fragment for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-block-frame": "workspace:*",
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.10",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import { EdgelessFrameManager } from '@blocksuite/affine-block-frame';
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditPropsStore,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/std';
|
||||
import {
|
||||
generateKeyBetweenV2,
|
||||
GfxControllerIdentifier,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import type { Store } from '@blocksuite/store';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import type {
|
||||
DragEvent,
|
||||
FitViewEvent,
|
||||
FrameCard,
|
||||
SelectEvent,
|
||||
} from '../card/frame-card.js';
|
||||
import { startDragging } from '../utils/drag.js';
|
||||
|
||||
const compare = EdgelessFrameManager.framePresentationComparator;
|
||||
|
||||
type FrameListItem = {
|
||||
frame: FrameBlockModel;
|
||||
|
||||
// frame index
|
||||
frameIndex: string;
|
||||
|
||||
// card index
|
||||
cardIndex: number;
|
||||
};
|
||||
|
||||
const styles = css`
|
||||
.frame-list-container {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.no-frame-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.no-frame-placeholder {
|
||||
margin-top: 240px;
|
||||
align-self: center;
|
||||
width: 230px;
|
||||
height: 48px;
|
||||
color: var(--affine-text-secondary-color, #8e8d91);
|
||||
text-align: center;
|
||||
|
||||
/* light/base */
|
||||
font-size: 15px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.insert-indicator {
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background-color: var(--affine-blue-600);
|
||||
position: absolute;
|
||||
contain: layout size;
|
||||
width: 284px;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_PANEL_BODY = 'affine-frame-panel-body';
|
||||
|
||||
export class FramePanelBody extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _clearDocDisposables = () => {
|
||||
this._docDisposables?.dispose();
|
||||
this._docDisposables = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* click at blank area to clear selection
|
||||
*/
|
||||
private readonly _clickBlank = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
// check if click at frame-card, if not, set this._selected to empty
|
||||
if (
|
||||
(e.target as HTMLElement).closest('frame-card') ||
|
||||
this._selected.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selected = [];
|
||||
this._gfx.selection.set({
|
||||
elements: this._selected,
|
||||
editing: false,
|
||||
});
|
||||
};
|
||||
|
||||
private _docDisposables: DisposableGroup | null = null;
|
||||
|
||||
private _frameElementHeight = 0;
|
||||
|
||||
private _frameItems: FrameListItem[] = [];
|
||||
|
||||
private _indicatorTranslateY = 0;
|
||||
|
||||
private _lastEdgelessRootId = '';
|
||||
|
||||
private get _gfx() {
|
||||
return this.editorHost.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private readonly _selectFrame = (e: SelectEvent) => {
|
||||
const { selected, id, multiselect } = e.detail;
|
||||
|
||||
if (!selected) {
|
||||
// de-select frame
|
||||
this._selected = this._selected.filter(frameId => frameId !== id);
|
||||
} else if (multiselect) {
|
||||
this._selected = [...this._selected, id];
|
||||
} else {
|
||||
this._selected = [id];
|
||||
}
|
||||
|
||||
this._gfx.selection.set({
|
||||
elements: this._selected,
|
||||
editing: false,
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _updateFrameItems = () => {
|
||||
this._frameItems = this.frames.map((frame, idx) => ({
|
||||
frame,
|
||||
frameIndex: frame.props.presentationIndex ?? frame.props.index,
|
||||
cardIndex: idx,
|
||||
}));
|
||||
};
|
||||
|
||||
get frames() {
|
||||
const frames = this.editorHost.doc
|
||||
.getBlocksByFlavour('affine:frame')
|
||||
.map(block => block.model as FrameBlockModel);
|
||||
return frames.sort(compare);
|
||||
}
|
||||
|
||||
get viewportPadding(): [number, number, number, number] {
|
||||
return this.fitPadding
|
||||
? ([0, 0, 0, 0].map((val, idx) =>
|
||||
Number.isFinite(this.fitPadding[idx]) ? this.fitPadding[idx] : val
|
||||
) as [number, number, number, number])
|
||||
: [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
private _drag(e: DragEvent) {
|
||||
if (!this._selected.length) return;
|
||||
|
||||
this._dragging = true;
|
||||
|
||||
const framesMap = this._frameItems.reduce((map, frame) => {
|
||||
map.set(frame.frame.id, {
|
||||
...frame,
|
||||
});
|
||||
return map;
|
||||
}, new Map<string, FrameListItem>());
|
||||
const selected = this._selected.slice();
|
||||
|
||||
const draggedFramesInfo = selected.map(id => {
|
||||
const frame = framesMap.get(id) as FrameListItem;
|
||||
|
||||
return {
|
||||
frame: frame.frame,
|
||||
element: this.renderRoot.querySelector(
|
||||
`[data-frame-id="${frame.frame.id}"]`
|
||||
) as FrameCard,
|
||||
cardIndex: frame.cardIndex,
|
||||
frameIndex: frame.frameIndex,
|
||||
};
|
||||
});
|
||||
const width = draggedFramesInfo[0].element.clientWidth;
|
||||
|
||||
this._frameElementHeight = draggedFramesInfo[0].element.offsetHeight;
|
||||
|
||||
startDragging(draggedFramesInfo, {
|
||||
width,
|
||||
container: this,
|
||||
document: this.ownerDocument,
|
||||
domHost: this.domHost ?? this.ownerDocument,
|
||||
start: {
|
||||
x: e.detail.clientX,
|
||||
y: e.detail.clientY,
|
||||
},
|
||||
framePanelBody: this,
|
||||
frameListContainer: this.frameListContainer,
|
||||
frameElementHeight: this._frameElementHeight,
|
||||
onDragEnd: insertIdx => {
|
||||
this._dragging = false;
|
||||
this.insertIndex = undefined;
|
||||
|
||||
if (insertIdx === undefined || this._frameItems.length <= 1) return;
|
||||
this._reorderFrames(selected, framesMap, insertIdx);
|
||||
},
|
||||
onDragMove: (idx, indicatorTranslateY) => {
|
||||
this.insertIndex = idx;
|
||||
this._indicatorTranslateY = indicatorTranslateY ?? 0;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _fitToElement(e: FitViewEvent) {
|
||||
const { block } = e.detail;
|
||||
const bound = Bound.deserialize(block.xywh);
|
||||
const docModeProvider = this.editorHost.std.get(DocModeProvider);
|
||||
|
||||
if (docModeProvider.getEditorMode() !== 'edgeless') {
|
||||
// When click frame card in page mode
|
||||
// Should switch to edgeless mode and set viewport to the frame
|
||||
const viewport = {
|
||||
xywh: block.xywh,
|
||||
referenceId: block.id,
|
||||
padding: this.viewportPadding as [number, number, number, number],
|
||||
};
|
||||
|
||||
this.editorHost.std.get(EditPropsStore).setStorage('viewport', viewport);
|
||||
this.editorHost.std.get(DocModeProvider).setEditorMode('edgeless');
|
||||
} else {
|
||||
this._gfx.viewport.setViewportByBound(bound, this.viewportPadding, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderEmptyContent() {
|
||||
const emptyContent = html` <div class="no-frame-container">
|
||||
<div class="no-frame-placeholder">
|
||||
Add frames to organize and present your Edgeless
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return emptyContent;
|
||||
}
|
||||
|
||||
private _renderFrameList() {
|
||||
const selectedFrames = new Set(this._selected);
|
||||
const frameCards = html`${repeat(this._frameItems, frameItem => {
|
||||
const { frame, frameIndex, cardIndex } = frameItem;
|
||||
return keyed(
|
||||
frame,
|
||||
html`<affine-frame-card
|
||||
data-frame-id=${frame.id}
|
||||
.frame=${frame}
|
||||
.std=${this.editorHost.std}
|
||||
.cardIndex=${cardIndex}
|
||||
.frameIndex=${frameIndex}
|
||||
.status=${selectedFrames.has(frame.id)
|
||||
? this._dragging
|
||||
? 'placeholder'
|
||||
: 'selected'
|
||||
: 'none'}
|
||||
@select=${this._selectFrame}
|
||||
@fitview=${this._fitToElement}
|
||||
@drag=${this._drag}
|
||||
></affine-frame-card>`
|
||||
);
|
||||
})}`;
|
||||
|
||||
const frameList = html` <div class="frame-list-container">
|
||||
${this.insertIndex !== undefined
|
||||
? html`<div
|
||||
class="insert-indicator"
|
||||
style=${`transform: translateY(${this._indicatorTranslateY}px)`}
|
||||
></div>`
|
||||
: nothing}
|
||||
${frameCards}
|
||||
</div>`;
|
||||
return frameList;
|
||||
}
|
||||
|
||||
private _reorderFrames(
|
||||
selected: string[],
|
||||
framesMap: Map<string, FrameListItem>,
|
||||
insertIndex: number
|
||||
) {
|
||||
if (insertIndex >= 0 && insertIndex <= this._frameItems.length) {
|
||||
const frames = Array.from(framesMap.values()).map(
|
||||
frameItem => frameItem.frame
|
||||
);
|
||||
const selectedFrames = selected
|
||||
.map(id => framesMap.get(id) as FrameListItem)
|
||||
.map(frameItem => frameItem.frame)
|
||||
.sort(compare);
|
||||
|
||||
// update selected frames index
|
||||
// make the indexes larger than the frame before and smaller than the frame after
|
||||
let before = frames[insertIndex - 1]?.props.presentationIndex || null;
|
||||
const after = frames[insertIndex]?.props.presentationIndex || null;
|
||||
selectedFrames.forEach(frame => {
|
||||
const newIndex = generateKeyBetweenV2(before, after);
|
||||
frame.doc.updateBlock(frame, {
|
||||
presentationIndex: newIndex,
|
||||
});
|
||||
before = newIndex;
|
||||
});
|
||||
|
||||
this.editorHost.doc.captureSync();
|
||||
this._updateFrames();
|
||||
}
|
||||
}
|
||||
|
||||
private _setDocDisposables(doc: Store) {
|
||||
this._clearDocDisposables();
|
||||
this._docDisposables = new DisposableGroup();
|
||||
this._docDisposables.add(
|
||||
doc.slots.blockUpdated.subscribe(({ type, flavour }) => {
|
||||
if (flavour === 'affine:frame' && type !== 'update') {
|
||||
requestAnimationFrame(() => {
|
||||
this._updateFrames();
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _updateFrames() {
|
||||
if (this._dragging) return;
|
||||
|
||||
if (!this.frames.length) {
|
||||
this._selected = [];
|
||||
this._frameItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const frameItems: FramePanelBody['_frameItems'] = [];
|
||||
const oldSelectedSet = new Set(this._selected);
|
||||
const newSelected: string[] = [];
|
||||
const frames = this.frames.sort(compare);
|
||||
frames.forEach((frame, idx) => {
|
||||
const frameItem = {
|
||||
frame,
|
||||
frameIndex: frame.props.presentationIndex ?? frame.props.index,
|
||||
cardIndex: idx,
|
||||
};
|
||||
|
||||
frameItems.push(frameItem);
|
||||
if (oldSelectedSet.has(frame.id)) {
|
||||
newSelected.push(frame.id);
|
||||
}
|
||||
});
|
||||
|
||||
this._frameItems = frameItems;
|
||||
this._selected = newSelected;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._updateFrameItems();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearDocDisposables();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const disposables = this.disposables;
|
||||
disposables.addFromEvent(this, 'click', this._clickBlank);
|
||||
}
|
||||
|
||||
override render() {
|
||||
this._updateFrameItems();
|
||||
return html` ${this._frameItems.length
|
||||
? this._renderFrameList()
|
||||
: this._renderEmptyContent()}`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editorHost') && this.editorHost) {
|
||||
this._setDocDisposables(this.editorHost.doc);
|
||||
// after switch to edgeless mode, should update the selection
|
||||
if (this.editorHost.doc.id === this._lastEdgelessRootId) {
|
||||
this._gfx.selection.set({
|
||||
elements: this._selected,
|
||||
editing: false,
|
||||
});
|
||||
} else {
|
||||
this._selected = this._selected.length ? [] : this._selected;
|
||||
}
|
||||
this._lastEdgelessRootId = this.editorHost.doc.id;
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _dragging = false;
|
||||
|
||||
// Store the ids of the selected frames
|
||||
@state()
|
||||
private accessor _selected: string[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor domHost!: Document | HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editorHost!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fitPadding!: number[];
|
||||
|
||||
@query('.frame-list-container')
|
||||
accessor frameListContainer!: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor insertIndex: number | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_PANEL_BODY]: FramePanelBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import type { RichText } from '@blocksuite/affine-rich-text';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
const styles = css`
|
||||
frame-card-title-editor rich-text .nowrap-lines::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_TITLE_EDITOR = 'affine-frame-card-title-editor';
|
||||
|
||||
export class FrameCardTitleEditor extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _isComposing = false;
|
||||
|
||||
get inlineEditor() {
|
||||
return this.richText.inlineEditor;
|
||||
}
|
||||
|
||||
private _unmount() {
|
||||
// dispose in advance to avoid execute `this.remove()` twice
|
||||
this.disposables.dispose();
|
||||
this.remove();
|
||||
this.titleContentElement.style.display = 'block';
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (this.inlineEditor === null) return;
|
||||
|
||||
this.titleContentElement.style.display = 'none';
|
||||
|
||||
this.inlineEditor.selectAll();
|
||||
|
||||
this.inlineEditor.slots.renderComplete.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
|
||||
const inlineEditorContainer = this.inlineEditor.rootElement;
|
||||
if (!inlineEditorContainer) return;
|
||||
|
||||
this.disposables.addFromEvent(inlineEditorContainer, 'blur', () => {
|
||||
this._unmount();
|
||||
});
|
||||
this.disposables.addFromEvent(inlineEditorContainer, 'click', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
this.disposables.addFromEvent(inlineEditorContainer, 'dblclick', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
this.disposables.addFromEvent(inlineEditorContainer, 'keydown', e => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter' && !this._isComposing) {
|
||||
this._unmount();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override async getUpdateComplete(): Promise<boolean> {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this.richText?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const inlineEditorStyle = styleMap({
|
||||
transformOrigin: 'top left',
|
||||
borderRadius: '4px',
|
||||
maxWidth: `${this.maxWidth}px`,
|
||||
maxHeight: '20px',
|
||||
width: 'fit-content',
|
||||
height: '20px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
lineHeight: '20px',
|
||||
position: 'absolute',
|
||||
left: `${this.left}px`,
|
||||
top: '0px',
|
||||
minWidth: '8px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
border: '1px solid var(--affine-primary-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30)',
|
||||
zIndex: '1',
|
||||
display: 'block',
|
||||
});
|
||||
return html`<rich-text
|
||||
.yText=${this.frameModel.props.title.yText}
|
||||
.enableFormat=${false}
|
||||
.enableAutoScrollHorizontally=${true}
|
||||
.enableUndoRedo=${false}
|
||||
.wrapText=${false}
|
||||
style=${inlineEditorStyle}
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frameModel!: FrameBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor left!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor maxWidth!: number;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor titleContentElement!: HTMLElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_TITLE_EDITOR]: FrameCardTitleEditor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import { FrameCardTitleEditor } from './frame-card-title-editor.js';
|
||||
|
||||
const styles = css`
|
||||
.frame-card-title-container {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
box-sizing: border-box;
|
||||
gap: 6px;
|
||||
font-size: var(--affine-font-sm);
|
||||
cursor: default;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-card-title-container .card-index {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
background: var(--affine-black);
|
||||
margin-left: 2px;
|
||||
|
||||
color: var(--affine-white);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.frame-card-title-container .card-title {
|
||||
height: 20px;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_CARD_TITLE = 'affine-frame-card-title';
|
||||
|
||||
export class FrameCardTitle extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _clearTitleDisposables = () => {
|
||||
this._titleDisposables?.dispose();
|
||||
this._titleDisposables = null;
|
||||
};
|
||||
|
||||
private readonly _mountTitleEditor = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const titleEditor = new FrameCardTitleEditor();
|
||||
titleEditor.frameModel = this.frame;
|
||||
titleEditor.titleContentElement = this.titleContentElement;
|
||||
const left = this.titleIndexElement.offsetWidth + 6;
|
||||
titleEditor.left = left;
|
||||
titleEditor.maxWidth = this.titleContainer.offsetWidth - left - 6;
|
||||
this.titleContainer.append(titleEditor);
|
||||
};
|
||||
|
||||
private _titleDisposables: DisposableGroup | null = null;
|
||||
|
||||
private readonly _updateElement = () => {
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private _setFrameDisposables(title: Y.Text) {
|
||||
this._clearTitleDisposables();
|
||||
title.observe(this._updateElement);
|
||||
this._titleDisposables = new DisposableGroup();
|
||||
this._titleDisposables.add({
|
||||
dispose: () => {
|
||||
title.unobserve(this._updateElement);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearTitleDisposables();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div class="frame-card-title-container">
|
||||
<div
|
||||
class="card-index"
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@dblclick=${(e: MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
${this.cardIndex + 1}
|
||||
</div>
|
||||
<div class="card-title">
|
||||
<span
|
||||
@click=${(e: MouseEvent) => e.stopPropagation()}
|
||||
@dblclick=${this._mountTitleEditor}
|
||||
>${this.frame.props.title}</span
|
||||
>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('frame')) {
|
||||
this._setFrameDisposables(this.frame.props.title.yText);
|
||||
}
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor cardIndex!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frame!: FrameBlockModel;
|
||||
|
||||
@query('.frame-card-title-container')
|
||||
accessor titleContainer!: HTMLElement;
|
||||
|
||||
@query('.frame-card-title-container .card-title')
|
||||
accessor titleContentElement!: HTMLElement;
|
||||
|
||||
@query('.frame-card-title-container .card-index')
|
||||
accessor titleIndexElement!: HTMLElement;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_CARD_TITLE]: FrameCardTitle;
|
||||
}
|
||||
}
|
||||
254
blocksuite/affine/fragments/frame-panel/src/card/frame-card.ts
Normal file
254
blocksuite/affine/fragments/frame-panel/src/card/frame-card.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { on, once } from '@blocksuite/affine-shared/utils';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { type BlockStdScope, ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export type ReorderEvent = CustomEvent<{
|
||||
currentNumber: number;
|
||||
targetNumber: number;
|
||||
realIndex: number;
|
||||
}>;
|
||||
|
||||
export type SelectEvent = CustomEvent<{
|
||||
id: string;
|
||||
selected: boolean;
|
||||
index: number;
|
||||
multiselect: boolean;
|
||||
}>;
|
||||
|
||||
export type DragEvent = CustomEvent<{
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}>;
|
||||
|
||||
export type FitViewEvent = CustomEvent<{
|
||||
block: FrameBlockModel;
|
||||
}>;
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.frame-card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 284px;
|
||||
height: 198px;
|
||||
gap: 8px;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-card-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 170px;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
background: var(--affine-background-primary-color);
|
||||
box-shadow: 0px 0px 12px 0px rgba(66, 65, 73, 0.18);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-card-container.selected .frame-card-body {
|
||||
border: 2px solid var(--light-brand-color, #1e96eb);
|
||||
}
|
||||
|
||||
.frame-card-container.dragging {
|
||||
pointer-events: none;
|
||||
transform-origin: 16px 8px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: calc(var(--affine-z-index-popover, 0) + 3);
|
||||
}
|
||||
|
||||
.frame-card-container.dragging frame-card-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.frame-card-container.dragging .dragging-card-number {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(-30%, 30%);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--affine-black);
|
||||
color: var(--affine-white);
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.frame-card-container.placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_CARD = 'affine-frame-card';
|
||||
|
||||
export class FrameCard extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private _dispatchDragEvent(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const { clientX: startX, clientY: startY } = e;
|
||||
const disposeDragStart = on(this.ownerDocument, 'mousemove', e => {
|
||||
if (
|
||||
Math.abs(startX - e.clientX) < 5 &&
|
||||
Math.abs(startY - e.clientY) < 5
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.status !== 'selected') {
|
||||
this._dispatchSelectEvent(e);
|
||||
}
|
||||
|
||||
const event = new CustomEvent('drag', {
|
||||
detail: {
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
pageX: e.pageX,
|
||||
pageY: e.pageY,
|
||||
},
|
||||
});
|
||||
|
||||
this.dispatchEvent(event);
|
||||
disposeDragStart();
|
||||
});
|
||||
|
||||
once(this.ownerDocument, 'mouseup', () => {
|
||||
disposeDragStart();
|
||||
});
|
||||
}
|
||||
|
||||
private _dispatchFitViewEvent(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
|
||||
const event = new CustomEvent('fitview', {
|
||||
detail: {
|
||||
block: this.frame,
|
||||
},
|
||||
});
|
||||
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
private _dispatchSelectEvent(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
const event = new CustomEvent('select', {
|
||||
detail: {
|
||||
id: this.frame.id,
|
||||
selected: this.status !== 'selected',
|
||||
index: this.cardIndex,
|
||||
multiselect: e.shiftKey,
|
||||
},
|
||||
}) as SelectEvent;
|
||||
|
||||
this.dispatchEvent(event);
|
||||
}
|
||||
|
||||
private _DraggingCardNumber() {
|
||||
if (this.draggingCardNumber === undefined) return nothing;
|
||||
|
||||
return html`<div class="dragging-card-number">
|
||||
${this.draggingCardNumber}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { pos, stackOrder, width } = this;
|
||||
const containerStyle = styleMap(
|
||||
this.status === 'dragging'
|
||||
? {
|
||||
transform: `${
|
||||
stackOrder === 0
|
||||
? `translate(${pos.x - 16}px, ${pos.y - 8}px)`
|
||||
: `translate(${pos.x - 10}px, ${pos.y - 16}px) scale(0.96)`
|
||||
}`,
|
||||
width: width ? `${width}px` : undefined,
|
||||
}
|
||||
: {}
|
||||
);
|
||||
return html`<div
|
||||
class="frame-card-container ${this.status ?? ''}"
|
||||
style=${containerStyle}
|
||||
>
|
||||
${this.status === 'dragging'
|
||||
? nothing
|
||||
: html`<affine-frame-card-title
|
||||
.cardIndex=${this.cardIndex}
|
||||
.frame=${this.frame}
|
||||
></affine-frame-card-title>`}
|
||||
<div
|
||||
class="frame-card-body"
|
||||
@click=${this._dispatchSelectEvent}
|
||||
@dblclick=${this._dispatchFitViewEvent}
|
||||
@mousedown=${this._dispatchDragEvent}
|
||||
>
|
||||
${this.status === 'dragging' && stackOrder !== 0
|
||||
? nothing
|
||||
: html`<frame-preview
|
||||
.frame=${this.frame}
|
||||
.std=${this.std}
|
||||
></frame-preview>`}
|
||||
${this._DraggingCardNumber()}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor cardIndex!: number;
|
||||
|
||||
@query('.frame-card-container')
|
||||
accessor containerElement!: HTMLElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor draggingCardNumber: number | undefined = undefined;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frame!: FrameBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frameIndex!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor pos!: { x: number; y: number };
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor stackOrder!: number;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor status: 'selected' | 'dragging' | 'placeholder' | 'none' = 'none';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor width: number | undefined = undefined;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_CARD]: FrameCard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeExtension,
|
||||
DocModeProvider,
|
||||
ViewportElementExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { SpecProvider } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { Bound, deserializeXYWH } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import {
|
||||
BlockStdScope,
|
||||
type EditorHost,
|
||||
LifeCycleWatcher,
|
||||
ShadowlessElement,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { type Query, type Store } from '@blocksuite/store';
|
||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
const DEFAULT_PREVIEW_CONTAINER_WIDTH = 280;
|
||||
const DEFAULT_PREVIEW_CONTAINER_HEIGHT = 166;
|
||||
|
||||
const styles = css`
|
||||
.frame-preview-container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-preview-surface-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.frame-preview-viewport {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.frame-preview-viewport
|
||||
> editor-host
|
||||
> affine-edgeless-root-preview
|
||||
> .edgeless-background {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_PREVIEW = 'frame-preview';
|
||||
|
||||
export class FramePreview extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _clearFrameDisposables = () => {
|
||||
this._frameDisposables?.dispose();
|
||||
this._frameDisposables = null;
|
||||
};
|
||||
|
||||
private readonly _docFilter: Query = {
|
||||
mode: 'loose',
|
||||
match: [
|
||||
{
|
||||
flavour: 'affine:frame',
|
||||
viewType: 'hidden',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private _frameDisposables: DisposableGroup | null = null;
|
||||
|
||||
private _previewDoc: Store | null = null;
|
||||
|
||||
private readonly _previewSpec = SpecProvider._.getSpec('preview:edgeless');
|
||||
|
||||
private readonly _updateFrameViewportWH = () => {
|
||||
const [, , w, h] = deserializeXYWH(this.frame.xywh);
|
||||
|
||||
let scale = 1;
|
||||
if (this.fillScreen) {
|
||||
scale = Math.max(this.surfaceWidth / w, this.surfaceHeight / h);
|
||||
} else {
|
||||
scale = Math.min(this.surfaceWidth / w, this.surfaceHeight / h);
|
||||
}
|
||||
|
||||
this.frameViewportWH = {
|
||||
width: w * scale,
|
||||
height: h * scale,
|
||||
};
|
||||
};
|
||||
|
||||
get _originalDoc() {
|
||||
return this.frame.doc;
|
||||
}
|
||||
|
||||
private _initPreviewDoc() {
|
||||
const doc = this._originalDoc.workspace.getDoc(this._originalDoc.id);
|
||||
this._previewDoc =
|
||||
doc?.getStore({ readonly: true, query: this._docFilter }) ?? null;
|
||||
this.disposables.add(() => {
|
||||
this._originalDoc.doc.clearQuery(this._docFilter);
|
||||
});
|
||||
}
|
||||
|
||||
private _initSpec() {
|
||||
const refreshViewport = this._refreshViewport.bind(this);
|
||||
class FramePreviewWatcher extends LifeCycleWatcher {
|
||||
static override key = 'frame-preview-watcher';
|
||||
|
||||
override mounted() {
|
||||
const std = this.std;
|
||||
const { view } = std;
|
||||
view.viewUpdated.subscribe(payload => {
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.method !== 'add' ||
|
||||
payload.view.model.flavour !== 'affine:page'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const viewport = std.get(GfxControllerIdentifier).viewport;
|
||||
const subscription = viewport.sizeUpdated.subscribe(() => {
|
||||
subscription.unsubscribe();
|
||||
refreshViewport();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const docModeService = this.std.get(DocModeProvider);
|
||||
this._previewSpec.extend([
|
||||
ViewportElementExtension('.frame-preview-viewport'),
|
||||
FramePreviewWatcher,
|
||||
DocModeExtension(docModeService),
|
||||
]);
|
||||
}
|
||||
|
||||
private _refreshViewport() {
|
||||
const previewEditorHost = this.previewEditor;
|
||||
|
||||
if (!previewEditorHost) return;
|
||||
|
||||
const { viewport } = previewEditorHost.std.get(GfxControllerIdentifier);
|
||||
const frameBound = Bound.deserialize(this.frame.xywh);
|
||||
viewport.setViewportByBound(frameBound);
|
||||
}
|
||||
|
||||
private _renderSurfaceContent() {
|
||||
const { width, height } = this.frameViewportWH;
|
||||
|
||||
const _previewSpec = this._previewSpec.value;
|
||||
return html`<div
|
||||
class="frame-preview-surface-container"
|
||||
style=${styleMap({
|
||||
width: `${this.surfaceWidth}px`,
|
||||
height: `${this.surfaceHeight}px`,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="frame-preview-viewport"
|
||||
style=${styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
})}
|
||||
>
|
||||
${guard([this._previewDoc, this.frame], () => {
|
||||
if (!this._previewDoc || !this.frame) return nothing;
|
||||
return new BlockStdScope({
|
||||
store: this._previewDoc,
|
||||
extensions: _previewSpec,
|
||||
}).render();
|
||||
})}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _setFrameDisposables(frame: FrameBlockModel) {
|
||||
this._clearFrameDisposables();
|
||||
this._frameDisposables = new DisposableGroup();
|
||||
this._frameDisposables.add(
|
||||
frame.propsUpdated.subscribe(
|
||||
debounce(this._updateFrameViewportWH, 10, { leading: true })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._initSpec();
|
||||
this._initPreviewDoc();
|
||||
this._updateFrameViewportWH();
|
||||
this._setFrameDisposables(this.frame);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearFrameDisposables();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { frame } = this;
|
||||
const noContent = !frame || !frame.xywh;
|
||||
|
||||
return html`<div class="frame-preview-container">
|
||||
${noContent ? nothing : this._renderSurfaceContent()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('frame')) {
|
||||
this._setFrameDisposables(this.frame);
|
||||
}
|
||||
if (_changedProperties.has('frameViewportWH')) {
|
||||
this._refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@state()
|
||||
accessor fillScreen = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor frame!: FrameBlockModel;
|
||||
|
||||
@state()
|
||||
accessor frameViewportWH = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
@query('editor-host')
|
||||
accessor previewEditor: EditorHost | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor surfaceHeight: number = DEFAULT_PREVIEW_CONTAINER_HEIGHT;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor surfaceWidth: number = DEFAULT_PREVIEW_CONTAINER_WIDTH;
|
||||
}
|
||||
34
blocksuite/affine/fragments/frame-panel/src/effects.ts
Normal file
34
blocksuite/affine/fragments/frame-panel/src/effects.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
AFFINE_FRAME_PANEL_BODY,
|
||||
FramePanelBody,
|
||||
} from './body/frame-panel-body';
|
||||
import { AFFINE_FRAME_CARD, FrameCard } from './card/frame-card';
|
||||
import {
|
||||
AFFINE_FRAME_CARD_TITLE,
|
||||
FrameCardTitle,
|
||||
} from './card/frame-card-title';
|
||||
import {
|
||||
AFFINE_FRAME_TITLE_EDITOR,
|
||||
FrameCardTitleEditor,
|
||||
} from './card/frame-card-title-editor';
|
||||
import { AFFINE_FRAME_PREVIEW, FramePreview } from './card/frame-preview';
|
||||
import { AFFINE_FRAME_PANEL, FramePanel } from './frame-panel';
|
||||
import {
|
||||
AFFINE_FRAME_PANEL_HEADER,
|
||||
FramePanelHeader,
|
||||
} from './header/frame-panel-header';
|
||||
import {
|
||||
AFFINE_FRAMES_SETTING_MENU,
|
||||
FramesSettingMenu,
|
||||
} from './header/frames-setting-menu';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_FRAME_PANEL, FramePanel);
|
||||
customElements.define(AFFINE_FRAME_TITLE_EDITOR, FrameCardTitleEditor);
|
||||
customElements.define(AFFINE_FRAME_CARD, FrameCard);
|
||||
customElements.define(AFFINE_FRAME_CARD_TITLE, FrameCardTitle);
|
||||
customElements.define(AFFINE_FRAME_PANEL_BODY, FramePanelBody);
|
||||
customElements.define(AFFINE_FRAME_PANEL_HEADER, FramePanelHeader);
|
||||
customElements.define(AFFINE_FRAMES_SETTING_MENU, FramesSettingMenu);
|
||||
customElements.define(AFFINE_FRAME_PREVIEW, FramePreview);
|
||||
}
|
||||
92
blocksuite/affine/fragments/frame-panel/src/frame-panel.ts
Normal file
92
blocksuite/affine/fragments/frame-panel/src/frame-panel.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/std';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
const styles = css`
|
||||
affine-frame-panel {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
frame-panel {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.frame-panel-container {
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
height: 100%;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.frame-panel-body {
|
||||
padding-top: 12px;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin; /* For Firefox */
|
||||
scrollbar-color: transparent transparent; /* For Firefox */
|
||||
}
|
||||
|
||||
.frame-panel-body::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.frame-panel-body::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.frame-panel-body:hover::-webkit-scrollbar-thumb {
|
||||
background-color: var(--affine-black-30);
|
||||
}
|
||||
|
||||
.frame-panel-body::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.frame-panel-body::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_PANEL = 'affine-frame-panel';
|
||||
|
||||
export class FramePanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = styles;
|
||||
|
||||
override render() {
|
||||
return html`<div class="frame-panel-container">
|
||||
<affine-frame-panel-header
|
||||
.editorHost=${this.host}
|
||||
></affine-frame-panel-header>
|
||||
<affine-frame-panel-body
|
||||
class="frame-panel-body"
|
||||
.editorHost=${this.host}
|
||||
.fitPadding=${this.fitPadding}
|
||||
></affine-frame-panel-body>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor fitPadding: number[] = [50, 380, 50, 50];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_PANEL]: FramePanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import type { NavigatorMode } from '@blocksuite/affine-block-frame';
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditPropsStore,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { createButtonPopper } from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { PresentationIcon, SettingsIcon } from '@blocksuite/icons/lit';
|
||||
import type { EditorHost } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { css, html, LitElement, type PropertyValues } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.frame-panel-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.all-frames-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100px;
|
||||
height: 24px;
|
||||
margin: 8px 4px;
|
||||
}
|
||||
|
||||
.all-frames-setting-button svg {
|
||||
color: var(--affine-icon-secondary);
|
||||
}
|
||||
|
||||
.all-frames-setting-button:hover svg,
|
||||
.all-frames-setting-button.active svg {
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
|
||||
.all-frames-setting-label {
|
||||
width: 68px;
|
||||
height: 22px;
|
||||
font-size: var(--affine-font-sm);
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: var(--light-text-color-text-secondary-color, #8e8d91);
|
||||
}
|
||||
|
||||
.frames-setting-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.frames-setting-container[data-show] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.presentation-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
box-sizing: border-box;
|
||||
width: 117px;
|
||||
height: 28px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
margin: 4px 0;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
background: var(--affine-white);
|
||||
}
|
||||
|
||||
.presentation-button:hover {
|
||||
background: var(--affine-hover-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.presentation-button svg {
|
||||
fill: var(--affine-icon-color);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.presentation-button-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAME_PANEL_HEADER = 'affine-frame-panel-header';
|
||||
|
||||
export class FramePanelHeader extends WithDisposable(LitElement) {
|
||||
static override styles = styles;
|
||||
|
||||
private readonly _clearEdgelessDisposables = () => {
|
||||
this._edgelessDisposables?.dispose();
|
||||
this._edgelessDisposables = null;
|
||||
};
|
||||
|
||||
private _edgelessDisposables: DisposableGroup | null = null;
|
||||
|
||||
private get _gfx() {
|
||||
return this.editorHost.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private readonly _enterPresentationMode = () => {
|
||||
const docModeProvider = this.editorHost.std.get(DocModeProvider);
|
||||
if (docModeProvider.getEditorMode() !== 'edgeless') {
|
||||
this.editorHost.std.get(DocModeProvider).setEditorMode('edgeless');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this._gfx.tool.setTool({
|
||||
type: 'frameNavigator',
|
||||
mode: this._navigatorMode,
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
private _framesSettingMenuPopper: ReturnType<
|
||||
typeof createButtonPopper
|
||||
> | null = null;
|
||||
|
||||
private _navigatorMode: NavigatorMode = 'fit';
|
||||
|
||||
private readonly _setEdgelessDisposables = () => {
|
||||
const slots = this.editorHost.std.get(EdgelessLegacySlotIdentifier);
|
||||
|
||||
this._clearEdgelessDisposables();
|
||||
this._edgelessDisposables = new DisposableGroup();
|
||||
this._edgelessDisposables.add(
|
||||
slots.navigatorSettingUpdated.subscribe(({ fillScreen }) => {
|
||||
this._navigatorMode = fillScreen ? 'fill' : 'fit';
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
private _tryLoadNavigatorStateLocalRecord() {
|
||||
this._navigatorMode = this.editorHost.std
|
||||
.get(EditPropsStore)
|
||||
.getStorage('presentFillScreen')
|
||||
? 'fill'
|
||||
: 'fit';
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._tryLoadNavigatorStateLocalRecord();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._edgelessDisposables) {
|
||||
this._clearEdgelessDisposables();
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const disposables = this.disposables;
|
||||
|
||||
this._framesSettingMenuPopper = createButtonPopper({
|
||||
reference: this._frameSettingButton,
|
||||
popperElement: this._frameSettingMenu,
|
||||
stateUpdated: ({ display }) => {
|
||||
this._settingPopperShow = display === 'show';
|
||||
},
|
||||
mainAxis: 14,
|
||||
crossAxis: -100,
|
||||
});
|
||||
disposables.add(this._framesSettingMenuPopper);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div class="frame-panel-header">
|
||||
<div class="all-frames-setting">
|
||||
<span class="all-frames-setting-label">All frames</span>
|
||||
<edgeless-tool-icon-button
|
||||
class="all-frames-setting-button ${this._settingPopperShow
|
||||
? 'active'
|
||||
: ''}"
|
||||
.tooltip=${this._settingPopperShow ? '' : 'All Frames Settings'}
|
||||
.tipPosition=${'top'}
|
||||
.active=${this._settingPopperShow}
|
||||
.activeMode=${'background'}
|
||||
@click=${() => this._framesSettingMenuPopper?.toggle()}
|
||||
>
|
||||
${SettingsIcon({ width: '20px', height: '20px' })}
|
||||
</edgeless-tool-icon-button>
|
||||
</div>
|
||||
<div class="frames-setting-container">
|
||||
<affine-frames-setting-menu
|
||||
.editorHost=${this.editorHost}
|
||||
></affine-frames-setting-menu>
|
||||
</div>
|
||||
<div class="presentation-button" @click=${this._enterPresentationMode}>
|
||||
${PresentationIcon({ width: '16px', height: '16px' })}<span
|
||||
class="presentation-button-label"
|
||||
>Presentation</span
|
||||
>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editorHost')) {
|
||||
const docModeProvider = this.editorHost.std.get(DocModeProvider);
|
||||
if (docModeProvider.getEditorMode() === 'edgeless') {
|
||||
this._setEdgelessDisposables();
|
||||
} else {
|
||||
this._clearEdgelessDisposables();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@query('.all-frames-setting-button')
|
||||
private accessor _frameSettingButton!: HTMLDivElement;
|
||||
|
||||
@query('.frames-setting-container')
|
||||
private accessor _frameSettingMenu!: HTMLDivElement;
|
||||
|
||||
@state()
|
||||
private accessor _settingPopperShow = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editorHost!: EditorHost;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAME_PANEL_HEADER]: FramePanelHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditPropsStore,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import type { EditorHost } from '@blocksuite/std';
|
||||
import { css, html, LitElement, type PropertyValues } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.frames-setting-menu-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.frames-setting-menu-item {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 4px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.frames-setting-menu-item .setting-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.frames-setting-menu-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
box-sizing: border-box;
|
||||
background: var(--affine-border-color);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.frames-setting-menu-item.action {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.frames-setting-menu-item .action-label {
|
||||
width: 138px;
|
||||
height: 20px;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--affine-text-primary-color);
|
||||
}
|
||||
|
||||
.frames-setting-menu-item .toggle-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
menu-divider {
|
||||
height: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AFFINE_FRAMES_SETTING_MENU = 'affine-frames-setting-menu';
|
||||
|
||||
export class FramesSettingMenu extends WithDisposable(LitElement) {
|
||||
static override styles = styles;
|
||||
|
||||
get slots() {
|
||||
return this.editorHost.std.get(EdgelessLegacySlotIdentifier);
|
||||
}
|
||||
|
||||
private readonly _onBlackBackgroundChange = (checked: boolean) => {
|
||||
this.blackBackground = checked;
|
||||
this.slots.navigatorSettingUpdated.next({
|
||||
blackBackground: this.blackBackground,
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onFillScreenChange = (checked: boolean) => {
|
||||
this.fillScreen = checked;
|
||||
this.slots.navigatorSettingUpdated.next({
|
||||
fillScreen: this.fillScreen,
|
||||
});
|
||||
this._editPropsStore.setStorage('presentFillScreen', this.fillScreen);
|
||||
};
|
||||
|
||||
private readonly _onHideToolBarChange = (checked: boolean) => {
|
||||
this.hideToolbar = checked;
|
||||
this.slots.navigatorSettingUpdated.next({
|
||||
hideToolbar: this.hideToolbar,
|
||||
});
|
||||
this._editPropsStore.setStorage('presentHideToolbar', this.hideToolbar);
|
||||
};
|
||||
|
||||
private get _editPropsStore() {
|
||||
return this.editorHost.std.get(EditPropsStore);
|
||||
}
|
||||
|
||||
private _tryRestoreSettings() {
|
||||
const blackBackground = this._editPropsStore.getStorage(
|
||||
'presentBlackBackground'
|
||||
);
|
||||
|
||||
this.blackBackground = blackBackground ?? true;
|
||||
this.fillScreen =
|
||||
this._editPropsStore.getStorage('presentFillScreen') ?? false;
|
||||
this.hideToolbar =
|
||||
this._editPropsStore.getStorage('presentHideToolbar') ?? false;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._tryRestoreSettings();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div
|
||||
class="frames-setting-menu-container"
|
||||
@click=${(e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="frames-setting-menu-item">
|
||||
<div class="setting-label">Preview Settings</div>
|
||||
</div>
|
||||
<div class="frames-setting-menu-item action">
|
||||
<div class="action-label">Fill Screen</div>
|
||||
<div class="toggle-button">
|
||||
<toggle-switch
|
||||
.subscribe=${this.fillScreen}
|
||||
.onChange=${this._onFillScreenChange}
|
||||
></toggle-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<menu-divider></menu-divider>
|
||||
|
||||
<div class="frames-setting-menu-item">
|
||||
<div class="setting-label">Playback Settings</div>
|
||||
</div>
|
||||
<div class="frames-setting-menu-item action">
|
||||
<div class="action-label">Dark background</div>
|
||||
<div class="toggle-button">
|
||||
<toggle-switch
|
||||
.subscribe=${this.blackBackground}
|
||||
.onChange=${this._onBlackBackgroundChange}
|
||||
></toggle-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frames-setting-menu-item action">
|
||||
<div class="action-label">Hide toolbar</div>
|
||||
<div class="toggle-button">
|
||||
<toggle-switch
|
||||
.subscribe=${this.hideToolbar}
|
||||
.onChange=${this._onHideToolBarChange}
|
||||
></toggle-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
override updated(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editorHost')) {
|
||||
const docModeProvider = this.editorHost.std.get(DocModeProvider);
|
||||
if (docModeProvider.getEditorMode() === 'edgeless') {
|
||||
this.disposables.add(
|
||||
this.slots.navigatorSettingUpdated.subscribe(
|
||||
({ blackBackground, hideToolbar }) => {
|
||||
if (
|
||||
blackBackground !== undefined &&
|
||||
blackBackground !== this.blackBackground
|
||||
) {
|
||||
this.blackBackground = blackBackground;
|
||||
}
|
||||
|
||||
if (
|
||||
hideToolbar !== undefined &&
|
||||
hideToolbar !== this.hideToolbar
|
||||
) {
|
||||
this.hideToolbar = hideToolbar;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
accessor blackBackground = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editorHost!: EditorHost;
|
||||
|
||||
@state()
|
||||
accessor fillScreen = false;
|
||||
|
||||
@state()
|
||||
accessor hideToolbar = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_FRAMES_SETTING_MENU]: FramesSettingMenu;
|
||||
}
|
||||
}
|
||||
1
blocksuite/affine/fragments/frame-panel/src/index.ts
Normal file
1
blocksuite/affine/fragments/frame-panel/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './frame-panel';
|
||||
156
blocksuite/affine/fragments/frame-panel/src/utils/drag.ts
Normal file
156
blocksuite/affine/fragments/frame-panel/src/utils/drag.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { on, once } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
import type { FramePanelBody } from '../body/frame-panel-body.js';
|
||||
import { FrameCard } from '../card/frame-card.js';
|
||||
|
||||
/**
|
||||
* start drag frame cards
|
||||
* @param frames frames to drag
|
||||
*/
|
||||
export function startDragging(
|
||||
frames: {
|
||||
frame: FrameBlockModel;
|
||||
element: FrameCard;
|
||||
cardIndex: number;
|
||||
frameIndex: string;
|
||||
}[],
|
||||
options: {
|
||||
width: number;
|
||||
onDragEnd?: (insertIndex?: number) => void;
|
||||
onDragMove?: (insertIdx?: number, indicatorTranslateY?: number) => void;
|
||||
framePanelBody: HTMLElement;
|
||||
frameListContainer: HTMLElement;
|
||||
frameElementHeight: number;
|
||||
document: Document;
|
||||
domHost: Document | HTMLElement;
|
||||
container: FramePanelBody;
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const {
|
||||
document,
|
||||
domHost,
|
||||
container,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
frameElementHeight,
|
||||
framePanelBody,
|
||||
frameListContainer,
|
||||
start,
|
||||
} = options;
|
||||
const cardElements = frames
|
||||
.slice(frames.length - 2, frames.length)
|
||||
.map((frame, idx, arr) => {
|
||||
const el = new FrameCard();
|
||||
|
||||
el.frame = frame.frame;
|
||||
|
||||
el.cardIndex = frame.cardIndex;
|
||||
el.frameIndex = frame.frameIndex;
|
||||
el.status = 'dragging';
|
||||
el.stackOrder = arr.length - 1 - idx;
|
||||
el.pos = start;
|
||||
el.width = options.width;
|
||||
el.std = container.editorHost.std;
|
||||
if (frames.length > 1 && el.stackOrder === 0)
|
||||
el.draggingCardNumber = frames.length;
|
||||
|
||||
return el;
|
||||
});
|
||||
const maskElement = createMaskElement(document);
|
||||
const listContainerRect = framePanelBody.getBoundingClientRect();
|
||||
const children = Array.from(frameListContainer.children) as FrameCard[];
|
||||
const computedStyle = getComputedStyle(frameListContainer);
|
||||
const frameListContainerGap =
|
||||
parseInt(computedStyle.getPropertyValue('gap')) ?? 16;
|
||||
let idx: undefined | number;
|
||||
let indicatorTranslateY: undefined | number;
|
||||
|
||||
container.renderRoot.append(maskElement);
|
||||
container.renderRoot.append(...cardElements);
|
||||
|
||||
const insideListContainer = (e: MouseEvent) => {
|
||||
return (
|
||||
e.clientX >= listContainerRect.left &&
|
||||
e.clientX <= listContainerRect.right &&
|
||||
e.clientY >= listContainerRect.top &&
|
||||
e.clientY <= listContainerRect.bottom
|
||||
);
|
||||
};
|
||||
|
||||
const disposeMove = on(container, 'mousemove', e => {
|
||||
cardElements.forEach(el => {
|
||||
el.pos = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
};
|
||||
});
|
||||
|
||||
if (!insideListContainer(e)) {
|
||||
idx = undefined;
|
||||
onDragMove?.(idx, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
idx = 0;
|
||||
for (const card of children) {
|
||||
if (!card.frame) break;
|
||||
|
||||
const topBoundary =
|
||||
listContainerRect.top +
|
||||
card.offsetTop -
|
||||
framePanelBody.scrollTop -
|
||||
frameListContainerGap / 2;
|
||||
const midBoundary = topBoundary + card.offsetHeight / 2;
|
||||
const bottomBoundary =
|
||||
topBoundary + card.offsetHeight + frameListContainerGap;
|
||||
|
||||
if (e.clientY >= topBoundary && e.clientY <= bottomBoundary) {
|
||||
idx = e.clientY > midBoundary ? idx + 1 : idx;
|
||||
|
||||
indicatorTranslateY =
|
||||
idx * (frameElementHeight + frameListContainerGap) -
|
||||
frameListContainerGap / 2;
|
||||
|
||||
onDragMove?.(idx, indicatorTranslateY);
|
||||
return;
|
||||
}
|
||||
|
||||
++idx;
|
||||
}
|
||||
|
||||
onDragMove?.(idx);
|
||||
});
|
||||
|
||||
let ended = false;
|
||||
const dragEnd = () => {
|
||||
if (ended) return;
|
||||
|
||||
ended = true;
|
||||
cardElements.forEach(child => child.remove());
|
||||
maskElement.remove();
|
||||
|
||||
disposeMove();
|
||||
onDragEnd?.(idx);
|
||||
};
|
||||
|
||||
once(domHost as Document, 'mouseup', dragEnd);
|
||||
}
|
||||
|
||||
function createMaskElement(doc: Document) {
|
||||
const mask = doc.createElement('div');
|
||||
|
||||
mask.style.height = '100vh';
|
||||
mask.style.width = '100vw';
|
||||
mask.style.position = 'fixed';
|
||||
mask.style.left = '0';
|
||||
mask.style.top = '0';
|
||||
mask.style.zIndex = 'calc(var(--affine-z-index-popover, 0) + 3)';
|
||||
mask.style.cursor = 'grabbing';
|
||||
|
||||
return mask;
|
||||
}
|
||||
20
blocksuite/affine/fragments/frame-panel/tsconfig.json
Normal file
20
blocksuite/affine/fragments/frame-panel/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../blocks/frame" },
|
||||
{ "path": "../../blocks/surface" },
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user