mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 06:47:02 +08:00
feat(editor): gfx template package (#11480)
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"@blocksuite/affine-gfx-mindmap": "workspace:*",
|
||||
"@blocksuite/affine-gfx-note": "workspace:*",
|
||||
"@blocksuite/affine-gfx-shape": "workspace:*",
|
||||
"@blocksuite/affine-gfx-template": "workspace:*",
|
||||
"@blocksuite/affine-gfx-text": "workspace:*",
|
||||
"@blocksuite/affine-inline-latex": "workspace:*",
|
||||
"@blocksuite/affine-inline-link": "workspace:*",
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import type {
|
||||
Template,
|
||||
TemplateCategory,
|
||||
TemplateManager,
|
||||
} from './template-type.js';
|
||||
|
||||
export const templates: TemplateCategory[] = [];
|
||||
|
||||
function lcs(text1: string, text2: string) {
|
||||
const dp: number[][] = Array.from(
|
||||
{
|
||||
length: text1.length + 1,
|
||||
},
|
||||
() => Array.from({ length: text2.length + 1 }, () => 0)
|
||||
);
|
||||
|
||||
for (let i = 1; i <= text1.length; i++) {
|
||||
for (let j = 1; j <= text2.length; j++) {
|
||||
if (text1[i - 1] === text2[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||||
} else {
|
||||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[text1.length][text2.length];
|
||||
}
|
||||
const extendTemplate: TemplateManager[] = [];
|
||||
|
||||
const flat = <T>(arr: T[][]) =>
|
||||
arr.reduce((pre, current) => {
|
||||
if (current) {
|
||||
return pre.concat(current);
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
export const builtInTemplates = {
|
||||
list: async (category: string): Promise<Template[]> => {
|
||||
const extendTemplates = flat(
|
||||
await Promise.all(extendTemplate.map(manager => manager.list(category)))
|
||||
);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-empty-collection
|
||||
const cate = templates.find(cate => cate.name === category);
|
||||
if (!cate) return extendTemplates;
|
||||
|
||||
const result: Template[] =
|
||||
cate.templates instanceof Function
|
||||
? await cate.templates()
|
||||
: await Promise.all(Object.values(cate.templates));
|
||||
|
||||
return result.concat(extendTemplates);
|
||||
},
|
||||
|
||||
categories: async (): Promise<string[]> => {
|
||||
const extendCates = flat(
|
||||
await Promise.all(extendTemplate.map(manager => manager.categories()))
|
||||
);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-empty-collection
|
||||
return templates.map(cate => cate.name).concat(extendCates);
|
||||
},
|
||||
|
||||
search: async (keyword: string, cateName?: string): Promise<Template[]> => {
|
||||
const candidates: Template[] = flat(
|
||||
await Promise.all(
|
||||
extendTemplate.map(manager => manager.search(keyword, cateName))
|
||||
)
|
||||
);
|
||||
|
||||
keyword = keyword.trim().toLocaleLowerCase();
|
||||
|
||||
await Promise.all(
|
||||
// eslint-disable-next-line sonarjs/no-empty-collection
|
||||
templates.map(async categroy => {
|
||||
if (cateName && cateName !== categroy.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (categroy.templates instanceof Function) {
|
||||
return categroy.templates();
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(categroy.templates).map(async ([name, template]) => {
|
||||
if (
|
||||
lcs(keyword, (name as string).toLocaleLowerCase()) ===
|
||||
keyword.length
|
||||
) {
|
||||
candidates.push(template);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return candidates;
|
||||
},
|
||||
|
||||
extend(manager: TemplateManager) {
|
||||
if (extendTemplate.includes(manager)) return;
|
||||
|
||||
extendTemplate.push(manager);
|
||||
},
|
||||
} satisfies TemplateManager;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,174 +0,0 @@
|
||||
import {
|
||||
on,
|
||||
once,
|
||||
requestConnectedFrame,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
/**
|
||||
* A scrollbar that is only visible when the user is interacting with it.
|
||||
* Append this element to the a container that has a scrollable element. Which means
|
||||
* the scrollable element should lay on the same level as the overlay-scrollbar.
|
||||
*
|
||||
* And the scrollable element should have a `data-scrollable` attribute.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* <div class="container">
|
||||
* <div class="scrollable-element-with-fixed-height" data-scrollable>
|
||||
* <!--.... very long content ....-->
|
||||
* </div>
|
||||
* <overlay-scrollbar></overlay-scrollbar>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* Note:
|
||||
* - It only works with vertical scrollbars.
|
||||
*/
|
||||
export class OverlayScrollbar extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 10px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.overlay-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.44);
|
||||
border-radius: 3px;
|
||||
width: 6px;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _disposable = new DisposableGroup();
|
||||
|
||||
private _handleVisible = false;
|
||||
|
||||
private _scrollable: HTMLElement | null = null;
|
||||
|
||||
private _dragHandle(event: PointerEvent) {
|
||||
let startY = event.clientY;
|
||||
|
||||
this._handleVisible = true;
|
||||
|
||||
const dispose = on(document, 'pointermove', evt => {
|
||||
this._scroll(evt.clientY - startY);
|
||||
startY = evt.clientY;
|
||||
});
|
||||
|
||||
once(document, 'pointerup', e => {
|
||||
this._handleVisible = false;
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
setTimeout(() => {
|
||||
this._toggleScrollbarVisible(false);
|
||||
}, 800);
|
||||
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private _initWheelHandler() {
|
||||
const container = this.parentElement as HTMLElement;
|
||||
|
||||
container.style.contain = 'layout';
|
||||
container.style.overflow = 'hidden';
|
||||
|
||||
let hideScrollbarTimeId: null | ReturnType<typeof setTimeout> = null;
|
||||
const delayHideScrollbar = () => {
|
||||
if (hideScrollbarTimeId) clearTimeout(hideScrollbarTimeId);
|
||||
hideScrollbarTimeId = setTimeout(() => {
|
||||
this._toggleScrollbarVisible(false);
|
||||
hideScrollbarTimeId = null;
|
||||
}, 800);
|
||||
};
|
||||
|
||||
let scrollable: HTMLElement | null = null;
|
||||
this._disposable.addFromEvent(container, 'wheel', event => {
|
||||
scrollable = scrollable?.isConnected
|
||||
? scrollable
|
||||
: (container.querySelector('[data-scrollable]') as HTMLElement);
|
||||
|
||||
this._scrollable = scrollable;
|
||||
|
||||
if (!scrollable) return;
|
||||
|
||||
// firefox may report a wheel event with deltaMode of value other than 0
|
||||
// we just simply multiply it by 16 which is common default line height to get the correct value
|
||||
const scrollDistance =
|
||||
event.deltaMode === 0 ? event.deltaY : event.deltaY * 16;
|
||||
|
||||
this._scroll(scrollDistance ?? 0);
|
||||
|
||||
delayHideScrollbar();
|
||||
});
|
||||
}
|
||||
|
||||
private _scroll(scrollDistance: number) {
|
||||
const scrollable = this._scrollable!;
|
||||
|
||||
if (!scrollable) return;
|
||||
|
||||
scrollable.scrollBy({
|
||||
left: 0,
|
||||
top: scrollDistance,
|
||||
behavior: 'instant',
|
||||
});
|
||||
|
||||
requestConnectedFrame(() => {
|
||||
this._updateScrollbarRect(scrollable);
|
||||
this._toggleScrollbarVisible(true);
|
||||
}, this);
|
||||
}
|
||||
|
||||
private _toggleScrollbarVisible(visible: boolean) {
|
||||
const vis = visible || this._handleVisible ? '1' : '0';
|
||||
|
||||
if (this.style.opacity !== vis) {
|
||||
this.style.opacity = vis;
|
||||
}
|
||||
}
|
||||
|
||||
private _updateScrollbarRect(rect: {
|
||||
scrollTop?: number;
|
||||
clientHeight?: number;
|
||||
scrollHeight?: number;
|
||||
}) {
|
||||
if (rect.scrollHeight !== undefined && rect.clientHeight !== undefined) {
|
||||
this._handle.style.height = `${(rect.clientHeight / rect.scrollHeight) * 100}%`;
|
||||
}
|
||||
|
||||
if (rect.scrollTop !== undefined && rect.scrollHeight !== undefined) {
|
||||
this._handle.style.top = `${(rect.scrollTop / rect.scrollHeight) * 100}%`;
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._disposable.dispose();
|
||||
}
|
||||
|
||||
override firstUpdated(): void {
|
||||
this._initWheelHandler();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div
|
||||
class="overlay-handle"
|
||||
@pointerdown=${this._dragHandle}
|
||||
></div>`;
|
||||
}
|
||||
|
||||
@query('.overlay-handle')
|
||||
private accessor _handle!: HTMLDivElement;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
export class AffineTemplateLoading extends LitElement {
|
||||
static override styles = css`
|
||||
@keyframes affine-template-block-rotate {
|
||||
from {
|
||||
rotate: 0deg;
|
||||
}
|
||||
to {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
.affine-template-block-container {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.affine-template-block-loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
background: conic-gradient(
|
||||
rgba(30, 150, 235, 1) 90deg,
|
||||
rgba(0, 0, 0, 0.1) 90deg 360deg
|
||||
);
|
||||
border-radius: 50%;
|
||||
animation: affine-template-block-rotate 1s infinite ease-in;
|
||||
}
|
||||
|
||||
.affine-template-block-loading::before {
|
||||
content: '';
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<div class="affine-template-block-container">
|
||||
<div class="affine-template-block-loading"></div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
import {
|
||||
darkToolbarStyles,
|
||||
lightToolbarStyles,
|
||||
} from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
EditPropsStore,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
requestConnectedFrame,
|
||||
stopPropagation,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { EdgelessDraggableElementController } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import type { Bound } from '@blocksuite/global/gfx';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import type { BlockComponent } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
|
||||
import { EdgelessRootService } from '../../../edgeless-root-service.js';
|
||||
import { createTemplateJob } from '../../../services/template.js';
|
||||
import { builtInTemplates } from './builtin-templates.js';
|
||||
import { defaultPreview, Triangle } from './cards.js';
|
||||
import type { Template } from './template-type.js';
|
||||
import { cloneDeep } from './utils.js';
|
||||
|
||||
export class EdgelessTemplatePanel extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: absolute;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.edgeless-templates-panel {
|
||||
width: 467px;
|
||||
height: 568px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: 0px 10px 80px 0px rgba(0, 0, 0, 0.2);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
${unsafeCSS(lightToolbarStyles('.edgeless-templates-panel'))}
|
||||
${unsafeCSS(darkToolbarStyles('.edgeless-templates-panel'))}
|
||||
|
||||
.search-bar {
|
||||
padding: 21px 24px;
|
||||
font-size: 18px;
|
||||
color: var(--affine-secondary);
|
||||
border-bottom: 1px solid var(--affine-divider-color);
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: 0;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: 20px;
|
||||
background-color: inherit;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--affine-text-secondary-color);
|
||||
}
|
||||
|
||||
.template-categories {
|
||||
display: flex;
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
overflow-x: scroll;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.category-entry {
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
width: fit-content;
|
||||
padding: 4px 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-entry.selected,
|
||||
.category-entry:hover {
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-tertiary-color);
|
||||
}
|
||||
|
||||
.template-viewport {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.template-scrollcontent {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-content: flex-start;
|
||||
gap: 10px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
position: relative;
|
||||
width: 135px;
|
||||
height: 80px;
|
||||
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.02);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-item > svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 135px;
|
||||
height: 80px;
|
||||
color: var(--affine-background-primary-color);
|
||||
}
|
||||
|
||||
/* .template-item:hover::before {
|
||||
content: attr(data-hover-text);
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 110px;
|
||||
border-radius: 8px;
|
||||
padding: 4px 22px;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
|
||||
background-color: var(--affine-primary-color);
|
||||
color: var(--affine-white);
|
||||
} */
|
||||
|
||||
.template-item:hover::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--affine-black-10);
|
||||
border-radius: 4px;
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.template-item.loading::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.template-item.loading > affine-template-loading {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.template-item img.template-preview {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
transform: translateY(20px);
|
||||
color: var(--affine-background-overlay-panel-color);
|
||||
}
|
||||
`;
|
||||
|
||||
static templates = builtInTemplates;
|
||||
|
||||
private _fetchJob: null | { cancel: () => void } = null;
|
||||
|
||||
draggableController!: EdgelessDraggableElementController<Template>;
|
||||
|
||||
private _closePanel() {
|
||||
if (this.isDragging) return;
|
||||
this.dispatchEvent(new CustomEvent('closepanel'));
|
||||
}
|
||||
|
||||
private _fetch(fn: (state: { canceled: boolean }) => Promise<unknown>) {
|
||||
if (this._fetchJob) {
|
||||
this._fetchJob.cancel();
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
|
||||
const state = { canceled: false };
|
||||
const job = {
|
||||
cancel: () => {
|
||||
state.canceled = true;
|
||||
},
|
||||
};
|
||||
|
||||
this._fetchJob = job;
|
||||
|
||||
fn(state)
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (!state.canceled && job === this._fetchJob) {
|
||||
this._loading = false;
|
||||
this._fetchJob = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getLocalSelectedCategory() {
|
||||
return this.edgeless.std.get(EditPropsStore).getStorage('templateCache');
|
||||
}
|
||||
|
||||
private async _initCategory() {
|
||||
try {
|
||||
this._categories = await EdgelessTemplatePanel.templates.categories();
|
||||
this._currentCategory =
|
||||
this._getLocalSelectedCategory() ?? this._categories[0];
|
||||
this._updateTemplates();
|
||||
} catch (e) {
|
||||
console.error('Failed to load categories', e);
|
||||
}
|
||||
}
|
||||
|
||||
private _initDragController() {
|
||||
if (this.draggableController) return;
|
||||
this.draggableController = new EdgelessDraggableElementController(this, {
|
||||
edgeless: this.edgeless,
|
||||
clickToDrag: true,
|
||||
standardWidth: 560,
|
||||
onOverlayCreated: overlay => {
|
||||
this.isDragging = true;
|
||||
overlay.mask.style.color = 'transparent';
|
||||
},
|
||||
onDrop: (el, bound) => {
|
||||
this._insertTemplate(el.data, bound)
|
||||
.finally(() => {
|
||||
this.isDragging = false;
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
onCanceled: () => {
|
||||
this.isDragging = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get service() {
|
||||
return this.edgeless.std.get(EdgelessRootService);
|
||||
}
|
||||
|
||||
get gfx() {
|
||||
return this.edgeless.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
private async _insertTemplate(template: Template, bound: Bound) {
|
||||
this._loadingTemplate = template;
|
||||
|
||||
template = cloneDeep(template);
|
||||
|
||||
const center = {
|
||||
x: bound.x + bound.w / 2,
|
||||
y: bound.y + bound.h / 2,
|
||||
};
|
||||
const templateJob = createTemplateJob(
|
||||
this.edgeless.std,
|
||||
template.type,
|
||||
center
|
||||
);
|
||||
|
||||
try {
|
||||
const { assets } = template;
|
||||
|
||||
if (assets) {
|
||||
await Promise.all(
|
||||
Object.entries(assets).map(([key, value]) =>
|
||||
fetch(value)
|
||||
.then(res => res.blob())
|
||||
.then(blob => templateJob.job.assets.set(key, blob))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const insertedBound = await templateJob.insertTemplate(template.content);
|
||||
|
||||
if (insertedBound && template.type === 'template') {
|
||||
const padding = 20 / this.gfx.viewport.zoom;
|
||||
this.gfx.viewport.setViewportByBound(
|
||||
insertedBound,
|
||||
[padding, padding, padding, padding],
|
||||
true
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this._loadingTemplate = null;
|
||||
this.gfx.tool.setTool('default');
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSearchKeyword(inputEvt: InputEvent) {
|
||||
this._searchKeyword = (inputEvt.target as HTMLInputElement).value;
|
||||
this._updateTemplates();
|
||||
}
|
||||
|
||||
private _updateTemplates() {
|
||||
this._fetch(async state => {
|
||||
try {
|
||||
const templates = this._searchKeyword
|
||||
? await EdgelessTemplatePanel.templates.search(this._searchKeyword)
|
||||
: await EdgelessTemplatePanel.templates.list(this._currentCategory);
|
||||
|
||||
if (state.canceled) return;
|
||||
|
||||
this._templates = templates;
|
||||
} catch (e) {
|
||||
if (state.canceled) return;
|
||||
|
||||
console.error('Failed to load templates', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._initDragController();
|
||||
|
||||
this.addEventListener('keydown', stopPropagation, false);
|
||||
this._disposables.add(() => {
|
||||
if (this._currentCategory) {
|
||||
this.edgeless.std
|
||||
.get(EditPropsStore)
|
||||
.setStorage('templateCache', this._currentCategory);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
requestConnectedFrame(() => {
|
||||
this._disposables.addFromEvent(document, 'click', evt => {
|
||||
if (this.contains(evt.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._closePanel();
|
||||
});
|
||||
}, this);
|
||||
this._disposables.addFromEvent(this, 'click', stopPropagation);
|
||||
this._disposables.addFromEvent(this, 'wheel', stopPropagation);
|
||||
|
||||
this._initCategory().catch(() => {});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { _categories, _currentCategory, _templates } = this;
|
||||
const { draggingElement } = this.draggableController?.states || {};
|
||||
const appTheme = this.edgeless.std.get(ThemeProvider).app$.value;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="edgeless-templates-panel"
|
||||
data-app-theme=${appTheme}
|
||||
style=${styleMap({
|
||||
opacity: this.isDragging ? '0' : '1',
|
||||
transition: 'opacity 0.2s',
|
||||
})}
|
||||
>
|
||||
<div class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="Search file or anything..."
|
||||
@input=${this._updateSearchKeyword}
|
||||
@cut=${stopPropagation}
|
||||
@copy=${stopPropagation}
|
||||
@paste=${stopPropagation}
|
||||
/>
|
||||
</div>
|
||||
<div class="template-categories">
|
||||
${repeat(
|
||||
_categories,
|
||||
cate => cate,
|
||||
cate => {
|
||||
return html`<div
|
||||
class="category-entry ${_currentCategory === cate
|
||||
? 'selected'
|
||||
: ''}"
|
||||
@click=${() => {
|
||||
this._currentCategory = cate;
|
||||
this._updateTemplates();
|
||||
}}
|
||||
>
|
||||
${cate}
|
||||
</div>`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div class="template-viewport">
|
||||
<div class="template-scrollcontent" data-scrollable>
|
||||
<div class="template-list">
|
||||
${this._loading
|
||||
? html`<affine-template-loading
|
||||
style=${styleMap({
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
})}
|
||||
></affine-template-loading>`
|
||||
: repeat(
|
||||
_templates,
|
||||
template => template.name,
|
||||
template => {
|
||||
const preview = template.preview
|
||||
? template.preview.startsWith('<svg')
|
||||
? html`${unsafeSVG(template.preview)}`
|
||||
: html`<img
|
||||
src="${template.preview}"
|
||||
class="template-preview"
|
||||
loading="lazy"
|
||||
/>`
|
||||
: defaultPreview;
|
||||
|
||||
const isBeingDragged =
|
||||
draggingElement &&
|
||||
draggingElement.data.name === template.name;
|
||||
return html`
|
||||
<div
|
||||
class=${`template-item ${
|
||||
template === this._loadingTemplate ? 'loading' : ''
|
||||
}`}
|
||||
style=${styleMap({
|
||||
opacity: isBeingDragged ? '0' : '1',
|
||||
})}
|
||||
data-hover-text="Add"
|
||||
@mousedown=${(e: MouseEvent) =>
|
||||
this.draggableController.onMouseDown(e, {
|
||||
data: template,
|
||||
preview,
|
||||
})}
|
||||
@touchstart=${(e: TouchEvent) => {
|
||||
this.draggableController.onTouchStart(e, {
|
||||
data: template,
|
||||
preview,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${preview}
|
||||
${template === this._loadingTemplate
|
||||
? html`<affine-template-loading></affine-template-loading>`
|
||||
: nothing}
|
||||
${template.name
|
||||
? html`<affine-tooltip
|
||||
.offset=${12}
|
||||
.autoHide=${true}
|
||||
tip-position="top"
|
||||
>
|
||||
${template.name}
|
||||
</affine-tooltip>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<overlay-scrollbar></overlay-scrollbar>
|
||||
</div>
|
||||
<div class="arrow">${Triangle}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _categories: string[] = [];
|
||||
|
||||
@state()
|
||||
private accessor _currentCategory = '';
|
||||
|
||||
@state()
|
||||
private accessor _loading = false;
|
||||
|
||||
@state()
|
||||
private accessor _loadingTemplate: Template | null = null;
|
||||
|
||||
@state()
|
||||
private accessor _searchKeyword = '';
|
||||
|
||||
@state()
|
||||
private accessor _templates: Template[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor edgeless!: BlockComponent;
|
||||
|
||||
@state()
|
||||
accessor isDragging = false;
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/affine-components/icons';
|
||||
import { once } from '@blocksuite/affine-shared/utils';
|
||||
import { EdgelessToolbarToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import type { GfxToolsFullOptionValue } from '@blocksuite/std/gfx';
|
||||
import {
|
||||
arrow,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
offset,
|
||||
shift,
|
||||
} from '@floating-ui/dom';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
|
||||
import { TemplateCard1, TemplateCard2, TemplateCard3 } from './cards.js';
|
||||
import type { EdgelessTemplatePanel } from './template-panel.js';
|
||||
|
||||
export class EdgelessTemplateButton extends EdgelessToolbarToolMixin(
|
||||
LitElement
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
edgeless-template-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.template-cards {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.template-card,
|
||||
.arrow-icon {
|
||||
--x: 0;
|
||||
--y: 0;
|
||||
--r: 0;
|
||||
--s: 1;
|
||||
position: absolute;
|
||||
transform: translate(var(--x), var(--y)) rotate(var(--r)) scale(var(--s));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
--y: 17px;
|
||||
background: var(--affine-black-10);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.arrow-icon > svg {
|
||||
color: var(--affine-icon-color);
|
||||
fill: currentColor;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.template-card.card1 {
|
||||
transform-origin: 100% 50%;
|
||||
--x: 15px;
|
||||
--y: 8px;
|
||||
}
|
||||
.template-card.card2 {
|
||||
transform-origin: 0% 50%;
|
||||
--x: -17px;
|
||||
}
|
||||
.template-card.card3 {
|
||||
--y: 27px;
|
||||
}
|
||||
|
||||
/* hover */
|
||||
.template-cards:not(.expanded):hover .card1 {
|
||||
--r: 8.69deg;
|
||||
}
|
||||
.template-cards:not(.expanded):hover .card2 {
|
||||
--r: -10.93deg;
|
||||
}
|
||||
.template-cards:not(.expanded):hover .card3 {
|
||||
--y: 22px;
|
||||
--r: 5.19deg;
|
||||
}
|
||||
|
||||
/* expanded */
|
||||
.template-cards.expanded .card1 {
|
||||
--x: 17px;
|
||||
--y: -5px;
|
||||
--r: 8.69deg;
|
||||
--s: 0.64;
|
||||
}
|
||||
.template-cards.expanded .card2 {
|
||||
--x: -19px;
|
||||
--y: -6px;
|
||||
--r: -10.93deg;
|
||||
--s: 0.64;
|
||||
}
|
||||
.template-cards.expanded .card3 {
|
||||
--y: -10px;
|
||||
--s: 0.599;
|
||||
--r: 5.19deg;
|
||||
}
|
||||
`;
|
||||
|
||||
private _cleanup: (() => void) | null = null;
|
||||
|
||||
private _prevTool: GfxToolsFullOptionValue | null = null;
|
||||
|
||||
override enableActiveBackground = true;
|
||||
|
||||
override type: GfxToolsFullOptionValue['type'] = 'template';
|
||||
|
||||
get cards() {
|
||||
const { theme } = this;
|
||||
return [TemplateCard1[theme], TemplateCard2[theme], TemplateCard3[theme]];
|
||||
}
|
||||
|
||||
private _closePanel() {
|
||||
if (this._openedPanel) {
|
||||
this._openedPanel.remove();
|
||||
this._openedPanel = null;
|
||||
this._cleanup?.();
|
||||
this._cleanup = null;
|
||||
this.requestUpdate();
|
||||
|
||||
if (this._prevTool && this._prevTool.type !== 'template') {
|
||||
this.setEdgelessTool(this._prevTool);
|
||||
this._prevTool = null;
|
||||
} else {
|
||||
this.setEdgelessTool('default');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _togglePanel() {
|
||||
if (this._openedPanel) {
|
||||
this._closePanel();
|
||||
if (this._prevTool) {
|
||||
this.setEdgelessTool(this._prevTool);
|
||||
this._prevTool = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._prevTool = this.edgelessTool ? { ...this.edgelessTool } : null;
|
||||
|
||||
this.setEdgelessTool('template');
|
||||
|
||||
const panel = document.createElement('edgeless-templates-panel');
|
||||
panel.edgeless = this.edgeless;
|
||||
|
||||
this._cleanup = once(panel, 'closepanel', () => {
|
||||
this._closePanel();
|
||||
});
|
||||
this._openedPanel = panel;
|
||||
|
||||
this.renderRoot.append(panel);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const arrowEl = panel.renderRoot.querySelector('.arrow') as HTMLElement;
|
||||
|
||||
autoUpdate(this, panel, () => {
|
||||
computePosition(this, panel, {
|
||||
placement: 'top',
|
||||
middleware: [offset(20), arrow({ element: arrowEl }), shift()],
|
||||
})
|
||||
.then(({ x, y, middlewareData }) => {
|
||||
panel.style.left = `${x}px`;
|
||||
panel.style.top = `${y}px`;
|
||||
|
||||
arrowEl.style.left = `${
|
||||
(middlewareData.arrow?.x ?? 0) - (middlewareData.shift?.x ?? 0)
|
||||
}px`;
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn("Can't compute position", e);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { cards, _openedPanel } = this;
|
||||
const expanded = _openedPanel !== null;
|
||||
|
||||
return html`<edgeless-toolbar-button @click=${this._togglePanel}>
|
||||
<div class="template-cards ${expanded ? 'expanded' : ''}">
|
||||
<div class="arrow-icon">${ArrowDownSmallIcon}</div>
|
||||
${repeat(
|
||||
cards,
|
||||
(card, n) => html`
|
||||
<div
|
||||
class=${classMap({
|
||||
'template-card': true,
|
||||
[`card${n + 1}`]: true,
|
||||
})}
|
||||
>
|
||||
${card}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</edgeless-toolbar-button>`;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _openedPanel: EdgelessTemplatePanel | null = null;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
export type Template = {
|
||||
/**
|
||||
* name of the sticker
|
||||
*
|
||||
* if not provided, it cannot be searched
|
||||
*/
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* template content
|
||||
*/
|
||||
content: unknown;
|
||||
|
||||
/**
|
||||
* external assets
|
||||
*/
|
||||
assets?: Record<string, string>;
|
||||
|
||||
preview?: string;
|
||||
|
||||
/**
|
||||
* type of template
|
||||
* `template`: normal template, looks like an article
|
||||
* `sticker`: sticker template, only contains one image block under surface block
|
||||
*/
|
||||
type: 'template' | 'sticker';
|
||||
};
|
||||
|
||||
export type TemplateCategory = {
|
||||
name: string;
|
||||
templates: Template[] | (() => Promise<Template[]>);
|
||||
};
|
||||
|
||||
export interface TemplateManager {
|
||||
list(category: string): Promise<Template[]> | Template[];
|
||||
|
||||
categories(): Promise<string[]> | string[];
|
||||
|
||||
search(keyword: string, category?: string): Promise<Template[]> | Template[];
|
||||
|
||||
extend?(manager: TemplateManager): void;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { on } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
export function onClickOutside(target: HTMLElement, fn: () => void) {
|
||||
return on(document, 'click', (evt: MouseEvent) => {
|
||||
if (target.contains(evt.target as Node)) return;
|
||||
|
||||
fn();
|
||||
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
export function cloneDeep<T>(obj: T): T {
|
||||
const seen = new WeakMap();
|
||||
|
||||
const clone = (val: unknown) => {
|
||||
if (typeof val !== 'object' || val === null) return val;
|
||||
if (seen.has(val)) return seen.get(val);
|
||||
|
||||
const copy = Array.isArray(val) ? [] : {};
|
||||
|
||||
seen.set(val, copy);
|
||||
|
||||
Object.keys(val).forEach(key => {
|
||||
// @ts-expect-error deep clone
|
||||
copy[key] = clone(val[key]);
|
||||
});
|
||||
|
||||
return copy;
|
||||
};
|
||||
|
||||
return clone(obj);
|
||||
}
|
||||
@@ -4,10 +4,8 @@ import { connectorQuickTool } from '@blocksuite/affine-gfx-connector';
|
||||
import { mindMapSeniorTool } from '@blocksuite/affine-gfx-mindmap';
|
||||
import { noteSeniorTool } from '@blocksuite/affine-gfx-note';
|
||||
import { shapeSeniorTool } from '@blocksuite/affine-gfx-shape';
|
||||
import {
|
||||
QuickToolExtension,
|
||||
SeniorToolExtension,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { templateSeniorTool } from '@blocksuite/affine-gfx-template';
|
||||
import { QuickToolExtension } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { buildLinkDenseMenu } from './link/link-dense-menu.js';
|
||||
@@ -30,14 +28,6 @@ const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
|
||||
};
|
||||
});
|
||||
|
||||
const templateSeniorTool = SeniorToolExtension('template', ({ block }) => {
|
||||
return {
|
||||
name: 'Template',
|
||||
content: html`<edgeless-template-button .edgeless=${block}>
|
||||
</edgeless-template-button>`,
|
||||
};
|
||||
});
|
||||
|
||||
export const quickTools = [
|
||||
defaultQuickTool,
|
||||
frameQuickTool,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type SurfaceBlockModel,
|
||||
type SurfaceContext,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { TemplateJob } from '@blocksuite/affine-gfx-template';
|
||||
import {
|
||||
type ConnectorElementModel,
|
||||
RootBlockSchema,
|
||||
@@ -31,7 +32,6 @@ import { effect } from '@preact/signals-core';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
import { RootService } from '../root-service.js';
|
||||
import { TemplateJob } from './services/template.js';
|
||||
import { getCursorMode } from './utils/query.js';
|
||||
|
||||
export class EdgelessRootService extends RootService implements SurfaceContext {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
export * from './clipboard/clipboard';
|
||||
export * from './clipboard/command';
|
||||
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
|
||||
export * from './components/toolbar/template/template-type.js';
|
||||
export * from './edgeless-root-block.js';
|
||||
export { EdgelessRootPreviewBlockComponent } from './edgeless-root-preview-block.js';
|
||||
export { EdgelessRootService } from './edgeless-root-service.js';
|
||||
export * from './gfx-tool';
|
||||
export * from './services/template.js';
|
||||
export * from './utils/clipboard-utils.js';
|
||||
export { sortEdgelessElements } from './utils/clone-utils.js';
|
||||
export { isCanvasElement } from './utils/query.js';
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
import { generateElementId, sortIndex } from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import type { BlockSnapshot, SnapshotNode } from '@blocksuite/store';
|
||||
|
||||
import type { SlotBlockPayload, TemplateJob } from './template.js';
|
||||
|
||||
export const replaceIdMiddleware = (job: TemplateJob) => {
|
||||
const regeneratedIdMap = new Map<string, string>();
|
||||
|
||||
job.slots.beforeInsert.subscribe(payload => {
|
||||
switch (payload.type) {
|
||||
case 'block':
|
||||
regenerateBlockId(payload.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const regenerateBlockId = (data: SlotBlockPayload['data']) => {
|
||||
const { blockJson } = data;
|
||||
const newId = regeneratedIdMap.has(blockJson.id)
|
||||
? regeneratedIdMap.get(blockJson.id)!
|
||||
: job.model.doc.workspace.idGenerator();
|
||||
|
||||
if (!regeneratedIdMap.has(blockJson.id)) {
|
||||
regeneratedIdMap.set(blockJson.id, newId);
|
||||
}
|
||||
|
||||
blockJson.id = newId;
|
||||
|
||||
data.parent = data.parent
|
||||
? (regeneratedIdMap.get(data.parent) ?? data.parent)
|
||||
: undefined;
|
||||
|
||||
if (blockJson.flavour === 'affine:surface-ref') {
|
||||
assertType<
|
||||
SnapshotNode<{
|
||||
reference: string;
|
||||
}>
|
||||
>(blockJson);
|
||||
|
||||
blockJson.props['reference'] =
|
||||
regeneratedIdMap.get(blockJson.props['reference']) ?? '';
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
const elements: Record<string, Record<string, unknown>> = {};
|
||||
const defered: string[] = [];
|
||||
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([id, val]) => {
|
||||
const newId = generateElementId();
|
||||
|
||||
regeneratedIdMap.set(id, newId);
|
||||
val.id = newId;
|
||||
elements[newId] = val;
|
||||
|
||||
if (['connector', 'group'].includes(val['type'] as string)) {
|
||||
defered.push(newId);
|
||||
}
|
||||
});
|
||||
|
||||
blockJson.children.forEach(block => {
|
||||
regeneratedIdMap.set(block.id, job.model.doc.workspace.idGenerator());
|
||||
});
|
||||
|
||||
defered.forEach(id => {
|
||||
const element = elements[id]!;
|
||||
|
||||
switch (element['type'] as string) {
|
||||
case 'group':
|
||||
{
|
||||
const children = element['children'] as {
|
||||
json: Record<string, boolean>;
|
||||
};
|
||||
const newChildrenJson: Record<string, boolean> = {};
|
||||
|
||||
Object.entries(children.json).forEach(([key, val]) => {
|
||||
newChildrenJson[regeneratedIdMap.get(key) ?? key] = val;
|
||||
});
|
||||
|
||||
children.json = newChildrenJson;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'connector':
|
||||
{
|
||||
const target = element['target'] as { id?: string };
|
||||
|
||||
if (target.id) {
|
||||
element['target'] = {
|
||||
...target,
|
||||
id: regeneratedIdMap.get(target.id),
|
||||
};
|
||||
}
|
||||
|
||||
const source = element['source'] as { id?: string };
|
||||
|
||||
if (source.id) {
|
||||
element['source'] = {
|
||||
...source,
|
||||
id: regeneratedIdMap.get(source.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
blockJson.props.elements = elements;
|
||||
}
|
||||
|
||||
// remap childElementIds of frame
|
||||
if (blockJson.flavour === 'affine:frame') {
|
||||
assertType<Record<string, boolean>>(blockJson.props.childElementIds);
|
||||
const newChildElementIds: Record<string, boolean> = {};
|
||||
Object.entries(blockJson.props.childElementIds).forEach(([key, val]) => {
|
||||
newChildElementIds[regeneratedIdMap.get(key) ?? key] = val;
|
||||
});
|
||||
blockJson.props.childElementIds = newChildElementIds;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const createInsertPlaceMiddleware = (targetPlace: Bound) => {
|
||||
return (job: TemplateJob) => {
|
||||
if (job.type !== 'template') return;
|
||||
|
||||
let templateBound: Bound | null = null;
|
||||
let offset: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'template') {
|
||||
templateBound = blockData.bound;
|
||||
|
||||
if (templateBound) {
|
||||
offset = {
|
||||
x: targetPlace.x - templateBound.x,
|
||||
y: targetPlace.y - templateBound.y,
|
||||
};
|
||||
|
||||
templateBound.x = targetPlace.x;
|
||||
templateBound.y = targetPlace.y;
|
||||
}
|
||||
} else {
|
||||
if (templateBound && offset) changePosition(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const ignoreType = new Set(['group', 'connector']);
|
||||
const changePosition = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.props.xywh) {
|
||||
const bound = Bound.deserialize(blockJson.props['xywh'] as string);
|
||||
|
||||
blockJson.props['xywh'] = new Bound(
|
||||
bound.x + offset.x,
|
||||
bound.y + offset.y,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, val]) => {
|
||||
const type = val['type'] as string;
|
||||
|
||||
if (ignoreType.has(type) && val['xywh']) {
|
||||
delete val['xywh'];
|
||||
}
|
||||
|
||||
if (val['xywh']) {
|
||||
const bound = Bound.deserialize(val['xywh'] as string);
|
||||
|
||||
val['xywh'] = new Bound(
|
||||
bound.x + offset.x,
|
||||
bound.y + offset.y,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
}
|
||||
|
||||
if (type === 'connector') {
|
||||
(['target', 'source'] as const).forEach(prop => {
|
||||
const propVal = val[prop];
|
||||
assertType<ConnectorElementModel['target']>(propVal);
|
||||
|
||||
if (propVal['id'] || !propVal['position']) return;
|
||||
const pos = propVal['position'];
|
||||
|
||||
propVal['position'] = [pos[0] + offset.x, pos[1] + offset.y];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createStickerMiddleware = (
|
||||
center: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
getIndex: () => string
|
||||
) => {
|
||||
return (job: TemplateJob) => {
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'block') {
|
||||
changeInserPosition(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const changeInserPosition = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.flavour === 'affine:image' && blockJson.props.xywh) {
|
||||
const bound = Bound.deserialize(blockJson.props['xywh'] as string);
|
||||
|
||||
blockJson.props['xywh'] = new Bound(
|
||||
center.x - bound.w / 2,
|
||||
center.y - bound.h / 2,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
|
||||
blockJson.props.index = getIndex();
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createRegenerateIndexMiddleware = (
|
||||
generateIndex: () => string
|
||||
) => {
|
||||
return (job: TemplateJob) => {
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'template') {
|
||||
generateIndexMap();
|
||||
}
|
||||
|
||||
if (blockData.type === 'block') {
|
||||
resetIndex(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const indexMap = new Map<string, string>();
|
||||
|
||||
const generateIndexMap = () => {
|
||||
const indexList: {
|
||||
id: string;
|
||||
index: string;
|
||||
flavour: string;
|
||||
element?: boolean;
|
||||
}[] = [];
|
||||
const frameList: {
|
||||
id: string;
|
||||
index: string;
|
||||
}[] = [];
|
||||
const groupIndexMap = new Map<
|
||||
string,
|
||||
{
|
||||
index: string;
|
||||
id: string;
|
||||
}
|
||||
>();
|
||||
|
||||
job.walk(block => {
|
||||
if (block.props.index) {
|
||||
if (block.flavour === 'affine:frame') {
|
||||
frameList.push({
|
||||
id: block.id,
|
||||
index: block.props.index as string,
|
||||
});
|
||||
} else {
|
||||
indexList.push({
|
||||
id: block.id,
|
||||
index: block.props.index as string,
|
||||
flavour: block.flavour,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (block.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
block.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, element]) => {
|
||||
indexList.push({
|
||||
index: element['index'] as string,
|
||||
flavour: element['type'] as string,
|
||||
id: element['id'] as string,
|
||||
element: true,
|
||||
});
|
||||
|
||||
if (element['type'] === 'group') {
|
||||
const children = element['children'] as {
|
||||
json: Record<string, boolean>;
|
||||
};
|
||||
const groupIndex = {
|
||||
index: element['index'] as string,
|
||||
id: element['id'] as string,
|
||||
};
|
||||
|
||||
Object.keys(children.json).forEach(key => {
|
||||
groupIndexMap.set(key, groupIndex);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
indexList.sort((a, b) => sortIndex(a, b, groupIndexMap));
|
||||
frameList.sort((a, b) => sortIndex(a, b, groupIndexMap));
|
||||
|
||||
frameList.forEach(index => {
|
||||
indexMap.set(index.id, generateIndex());
|
||||
});
|
||||
|
||||
indexList.forEach(index => {
|
||||
indexMap.set(index.id, generateIndex());
|
||||
});
|
||||
};
|
||||
const resetIndex = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.props.index) {
|
||||
blockJson.props.index =
|
||||
indexMap.get(blockJson.id) ?? blockJson.props.index;
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, element]) => {
|
||||
if (element['index']) {
|
||||
element['index'] = indexMap.get(element['id'] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -1,432 +0,0 @@
|
||||
import {
|
||||
getSurfaceBlock,
|
||||
type SurfaceBlockModel,
|
||||
type SurfaceBlockTransformer,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||
import { Bound, getCommonBound } from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import {
|
||||
type BlockModel,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
DocSnapshotSchema,
|
||||
type SnapshotNode,
|
||||
type Transformer,
|
||||
} from '@blocksuite/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import {
|
||||
createInsertPlaceMiddleware,
|
||||
createRegenerateIndexMiddleware,
|
||||
createStickerMiddleware,
|
||||
replaceIdMiddleware,
|
||||
} from './template-middlewares';
|
||||
/**
|
||||
* Those block contains other block's id
|
||||
* should defer the loading
|
||||
*/
|
||||
const DEFERED_BLOCK = [
|
||||
'affine:surface',
|
||||
'affine:surface-ref',
|
||||
'affine:frame',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Those block should not be inserted directly
|
||||
* it should be merged with current existing block
|
||||
*/
|
||||
const MERGE_BLOCK = ['affine:surface', 'affine:page'] as const;
|
||||
|
||||
type MergeBlockFlavour = (typeof MERGE_BLOCK)[number];
|
||||
|
||||
/**
|
||||
* Template type will affect the inserting behaviour
|
||||
*/
|
||||
const TEMPLATE_TYPES = ['template', 'sticker'] as const;
|
||||
|
||||
type TemplateType = (typeof TEMPLATE_TYPES)[number];
|
||||
|
||||
export type SlotBlockPayload = {
|
||||
type: 'block';
|
||||
data: {
|
||||
blockJson: BlockSnapshot;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SlotPayload =
|
||||
| SlotBlockPayload
|
||||
| {
|
||||
type: 'template';
|
||||
template: DocSnapshot;
|
||||
bound: Bound | null;
|
||||
};
|
||||
|
||||
export type TemplateJobConfig = {
|
||||
model: SurfaceBlockModel;
|
||||
type: string;
|
||||
middlewares: ((job: TemplateJob) => void)[];
|
||||
};
|
||||
|
||||
export class TemplateJob {
|
||||
static middlewares: ((job: TemplateJob) => void)[] = [];
|
||||
|
||||
private _template: DocSnapshot | null = null;
|
||||
|
||||
job: Transformer;
|
||||
|
||||
model: SurfaceBlockModel;
|
||||
|
||||
slots = {
|
||||
beforeInsert: new Subject<
|
||||
| SlotBlockPayload
|
||||
| {
|
||||
type: 'template';
|
||||
template: DocSnapshot;
|
||||
bound: Bound | null;
|
||||
}
|
||||
>(),
|
||||
};
|
||||
|
||||
type: TemplateType;
|
||||
|
||||
constructor({ model, type, middlewares }: TemplateJobConfig) {
|
||||
this.job = model.doc.getTransformer();
|
||||
this.model = model;
|
||||
this.type = TEMPLATE_TYPES.includes(type as TemplateType)
|
||||
? (type as TemplateType)
|
||||
: 'template';
|
||||
|
||||
middlewares.forEach(middleware => middleware(this));
|
||||
TemplateJob.middlewares.forEach(middleware => middleware(this));
|
||||
}
|
||||
|
||||
static create(options: {
|
||||
model: SurfaceBlockModel;
|
||||
type: string;
|
||||
middlewares: ((job: TemplateJob) => void)[];
|
||||
}) {
|
||||
return new TemplateJob(options);
|
||||
}
|
||||
|
||||
private _getMergeBlockId(modelData: BlockSnapshot) {
|
||||
switch (modelData.flavour as MergeBlockFlavour) {
|
||||
case 'affine:page':
|
||||
return this.model.doc.root!.id;
|
||||
case 'affine:surface':
|
||||
return this.model.id;
|
||||
}
|
||||
}
|
||||
|
||||
private _getTemplateBound() {
|
||||
const bounds: Bound[] = [];
|
||||
|
||||
this.walk(block => {
|
||||
if (block.props.xywh) {
|
||||
bounds.push(Bound.deserialize(block.props['xywh'] as string));
|
||||
}
|
||||
|
||||
if (block.flavour === 'affine:surface') {
|
||||
const ignoreType = new Set(['connector', 'group']);
|
||||
|
||||
Object.entries(
|
||||
block.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, val]) => {
|
||||
const type = val['type'] as string;
|
||||
|
||||
if (val['xywh'] && !ignoreType.has(type)) {
|
||||
bounds.push(Bound.deserialize(val['xywh'] as string));
|
||||
}
|
||||
|
||||
if (type === 'connector') {
|
||||
(['target', 'source'] as const).forEach(prop => {
|
||||
const propVal = val[prop];
|
||||
assertType<ConnectorElementModel['source']>(propVal);
|
||||
|
||||
if (propVal['id'] || !propVal['position']) return;
|
||||
|
||||
const pos = propVal['position'];
|
||||
|
||||
if (pos) {
|
||||
bounds.push(new Bound(pos[0], pos[1], 0, 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return getCommonBound(bounds);
|
||||
}
|
||||
|
||||
private _insertToDoc(
|
||||
modelDataList: {
|
||||
flavour: string;
|
||||
json: BlockSnapshot;
|
||||
modelData: SnapshotNode<object> | null;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[]
|
||||
) {
|
||||
const doc = this.model.doc;
|
||||
const mergeIdMapping = new Map<string, string>();
|
||||
const deferInserting: typeof modelDataList = [];
|
||||
|
||||
const insert = (
|
||||
data: (typeof modelDataList)[number],
|
||||
defered: boolean = true
|
||||
) => {
|
||||
const { flavour, json, modelData, parent, index } = data;
|
||||
const isMergeBlock = MERGE_BLOCK.includes(flavour as MergeBlockFlavour);
|
||||
|
||||
if (isMergeBlock) {
|
||||
mergeIdMapping.set(json.id, this._getMergeBlockId(json));
|
||||
}
|
||||
|
||||
if (
|
||||
defered &&
|
||||
DEFERED_BLOCK.includes(flavour as (typeof DEFERED_BLOCK)[number])
|
||||
) {
|
||||
deferInserting.push(data);
|
||||
return;
|
||||
} else {
|
||||
if (isMergeBlock) {
|
||||
this._mergeProps(
|
||||
json,
|
||||
this.model.doc.getModelById(
|
||||
this._getMergeBlockId(json)
|
||||
) as BlockModel
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modelData) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.addBlock(
|
||||
modelData.flavour,
|
||||
{
|
||||
...modelData.props,
|
||||
id: modelData.id,
|
||||
},
|
||||
parent ? (mergeIdMapping.get(parent) ?? parent) : undefined,
|
||||
index
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
modelDataList.forEach(data => insert(data));
|
||||
deferInserting.forEach(data => insert(data, false));
|
||||
}
|
||||
|
||||
private async _jsonToModelData(json: BlockSnapshot) {
|
||||
const job = this.job;
|
||||
const defered: {
|
||||
snapshot: BlockSnapshot;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[] = [];
|
||||
const modelDataList: {
|
||||
flavour: string;
|
||||
json: BlockSnapshot;
|
||||
modelData: SnapshotNode<object> | null;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[] = [];
|
||||
const toModel = async (
|
||||
snapshot: BlockSnapshot,
|
||||
parent?: string,
|
||||
index?: number,
|
||||
defer: boolean = true
|
||||
) => {
|
||||
if (
|
||||
defer &&
|
||||
DEFERED_BLOCK.includes(
|
||||
snapshot.flavour as (typeof DEFERED_BLOCK)[number]
|
||||
)
|
||||
) {
|
||||
defered.push({
|
||||
snapshot,
|
||||
parent,
|
||||
index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const slotData = {
|
||||
blockJson: snapshot,
|
||||
parent,
|
||||
index,
|
||||
};
|
||||
|
||||
this.slots.beforeInsert.next({ type: 'block', data: slotData });
|
||||
|
||||
/**
|
||||
* merge block should not be converted to model data
|
||||
*/
|
||||
const modelData = MERGE_BLOCK.includes(
|
||||
snapshot.flavour as MergeBlockFlavour
|
||||
)
|
||||
? null
|
||||
: ((await job.snapshotToModelData(snapshot)) ?? null);
|
||||
|
||||
modelDataList.push({
|
||||
flavour: snapshot.flavour,
|
||||
json: snapshot,
|
||||
modelData,
|
||||
parent,
|
||||
index,
|
||||
});
|
||||
|
||||
if (snapshot.children) {
|
||||
let index = 0;
|
||||
for (const child of snapshot.children) {
|
||||
await toModel(child, snapshot.id, index);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await toModel(json);
|
||||
|
||||
for (const json of defered) {
|
||||
await toModel(json.snapshot, json.parent, json.index, false);
|
||||
}
|
||||
|
||||
return modelDataList;
|
||||
}
|
||||
|
||||
private _mergeProps(from: BlockSnapshot, to: BlockModel) {
|
||||
switch (from.flavour as MergeBlockFlavour) {
|
||||
case 'affine:page':
|
||||
break;
|
||||
case 'affine:surface':
|
||||
this._mergeSurfaceElements(
|
||||
from.props.elements as Record<string, Record<string, unknown>>,
|
||||
(to as SurfaceBlockModel).elements.getValue()!
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _mergeSurfaceElements(
|
||||
from: Record<string, Record<string, unknown>>,
|
||||
to: Y.Map<Y.Map<unknown>>
|
||||
) {
|
||||
const schema = this.model.doc.schema.get('affine:surface');
|
||||
const surfaceTransformer = schema?.transformer?.(
|
||||
new Map()
|
||||
) as SurfaceBlockTransformer;
|
||||
|
||||
this.model.doc.transact(() => {
|
||||
const defered: [string, Record<string, unknown>][] = [];
|
||||
|
||||
Object.entries(from).forEach(([id, val]) => {
|
||||
if (['connector', 'group'].includes(val.type as string)) {
|
||||
defered.push([id, val]);
|
||||
} else {
|
||||
to.set(id, surfaceTransformer.elementFromJSON(val));
|
||||
}
|
||||
});
|
||||
|
||||
defered.forEach(([key, val]) => {
|
||||
to.set(key, surfaceTransformer.elementFromJSON(val));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async insertTemplate(template: unknown) {
|
||||
DocSnapshotSchema.parse(template);
|
||||
|
||||
assertType<DocSnapshot>(template);
|
||||
|
||||
this._template = template;
|
||||
|
||||
const templateBound = this._getTemplateBound();
|
||||
|
||||
this.slots.beforeInsert.next({
|
||||
type: 'template',
|
||||
template: template,
|
||||
bound: templateBound,
|
||||
});
|
||||
|
||||
const modelDataList = await this._jsonToModelData(template.blocks);
|
||||
|
||||
this._insertToDoc(modelDataList);
|
||||
|
||||
return templateBound;
|
||||
}
|
||||
|
||||
walk(callback: (block: BlockSnapshot, template: DocSnapshot) => void) {
|
||||
if (!this._template) {
|
||||
throw new Error('Template not loaded, please call insertTemplate first');
|
||||
}
|
||||
|
||||
const iterate = (block: BlockSnapshot, template: DocSnapshot) => {
|
||||
callback(block, template);
|
||||
|
||||
if (block.children) {
|
||||
block.children.forEach(child => iterate(child, template));
|
||||
}
|
||||
};
|
||||
|
||||
iterate(this._template.blocks, this._template);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTemplateJob(
|
||||
std: BlockStdScope,
|
||||
type: 'template' | 'sticker',
|
||||
center?: { x: number; y: number }
|
||||
) {
|
||||
const surface = getSurfaceBlock(std.store);
|
||||
if (!surface) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.NoSurfaceModelError,
|
||||
'This doc is missing surface block in edgeless.'
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const middlewares: ((job: TemplateJob) => void)[] = [];
|
||||
const { layer, viewport } = gfx;
|
||||
const blocks = layer.blocks;
|
||||
const elements = layer.canvasElements;
|
||||
|
||||
if (type === 'template') {
|
||||
const bounds = [...blocks, ...elements].map(i => Bound.deserialize(i.xywh));
|
||||
const currentContentBound = getCommonBound(bounds);
|
||||
|
||||
if (currentContentBound) {
|
||||
currentContentBound.x += currentContentBound.w + 20 / viewport.zoom;
|
||||
middlewares.push(createInsertPlaceMiddleware(currentContentBound));
|
||||
}
|
||||
|
||||
const idxGenerator = layer.createIndexGenerator();
|
||||
|
||||
middlewares.push(createRegenerateIndexMiddleware(() => idxGenerator()));
|
||||
}
|
||||
|
||||
if (type === 'sticker') {
|
||||
middlewares.push(
|
||||
createStickerMiddleware(center || viewport.center, () =>
|
||||
layer.generateIndex()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
middlewares.push(replaceIdMiddleware);
|
||||
|
||||
return TemplateJob.create({
|
||||
model: surface,
|
||||
type,
|
||||
middlewares,
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { effects as gfxGroupEffects } from '@blocksuite/affine-gfx-group/effects
|
||||
import { effects as gfxMindmapEffects } from '@blocksuite/affine-gfx-mindmap/effects';
|
||||
import { effects as gfxNoteEffects } from '@blocksuite/affine-gfx-note/effects';
|
||||
import { effects as gfxShapeEffects } from '@blocksuite/affine-gfx-shape/effects';
|
||||
import { effects as gfxTemplateEffects } from '@blocksuite/affine-gfx-template/effects';
|
||||
import { effects as gfxCanvasTextEffects } from '@blocksuite/affine-gfx-text/effects';
|
||||
import { effects as widgetEdgelessToolbarEffects } from '@blocksuite/affine-widget-edgeless-toolbar/effects';
|
||||
|
||||
@@ -26,10 +27,6 @@ import { ToolbarArrowUpIcon } from './edgeless/components/toolbar/common/toolbar
|
||||
import { EdgelessDefaultToolButton } from './edgeless/components/toolbar/default/default-tool-button.js';
|
||||
import { EdgelessLassoToolButton } from './edgeless/components/toolbar/lasso/lasso-tool-button.js';
|
||||
import { EdgelessLinkToolButton } from './edgeless/components/toolbar/link/link-tool-button.js';
|
||||
import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js';
|
||||
import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js';
|
||||
import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js';
|
||||
import { EdgelessTemplateButton } from './edgeless/components/toolbar/template/template-tool-button.js';
|
||||
import {
|
||||
AffineModalWidget,
|
||||
EdgelessRootBlockComponent,
|
||||
@@ -97,6 +94,7 @@ function registerGfxEffects() {
|
||||
gfxMindmapEffects();
|
||||
gfxGroupEffects();
|
||||
gfxBrushEffects();
|
||||
gfxTemplateEffects();
|
||||
}
|
||||
|
||||
function registerWidgets() {
|
||||
@@ -124,7 +122,6 @@ function registerEdgelessToolbarComponents() {
|
||||
);
|
||||
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
|
||||
customElements.define('edgeless-lasso-tool-button', EdgelessLassoToolButton);
|
||||
customElements.define('edgeless-template-button', EdgelessTemplateButton);
|
||||
|
||||
// Menus
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
@@ -139,12 +136,10 @@ function registerMiscComponents() {
|
||||
|
||||
// Loading and preview components
|
||||
customElements.define('loader-element', Loader);
|
||||
customElements.define('affine-template-loading', AffineTemplateLoading);
|
||||
|
||||
// Toolbar and UI components
|
||||
customElements.define('edgeless-zoom-toolbar', EdgelessZoomToolbar);
|
||||
customElements.define('zoom-bar-toggle-button', ZoomBarToggleButton);
|
||||
customElements.define('overlay-scrollbar', OverlayScrollbar);
|
||||
|
||||
// Auto-complete components
|
||||
customElements.define(
|
||||
@@ -155,7 +150,6 @@ function registerMiscComponents() {
|
||||
|
||||
// Note and template components
|
||||
customElements.define(NOTE_SLICER_WIDGET, NoteSlicer);
|
||||
customElements.define('edgeless-templates-panel', EdgelessTemplatePanel);
|
||||
|
||||
// Navigation components
|
||||
customElements.define(
|
||||
@@ -189,9 +183,6 @@ declare global {
|
||||
'edgeless-default-tool-button': EdgelessDefaultToolButton;
|
||||
'edgeless-lasso-tool-button': EdgelessLassoToolButton;
|
||||
'edgeless-link-tool-button': EdgelessLinkToolButton;
|
||||
'overlay-scrollbar': OverlayScrollbar;
|
||||
'affine-template-loading': AffineTemplateLoading;
|
||||
'edgeless-templates-panel': EdgelessTemplatePanel;
|
||||
'affine-page-root': PageRootBlockComponent;
|
||||
'zoom-bar-toggle-button': ZoomBarToggleButton;
|
||||
'edgeless-zoom-toolbar': EdgelessZoomToolbar;
|
||||
|
||||
@@ -4,8 +4,6 @@ export * from './common-specs/index.js';
|
||||
export * from './edgeless/edgeless-builtin-spec.js';
|
||||
export * from './edgeless/edgeless-root-spec.js';
|
||||
export * from './edgeless/index.js';
|
||||
export { TemplateJob } from './edgeless/services/template.js';
|
||||
export * as TemplateMiddlewares from './edgeless/services/template-middlewares.js';
|
||||
export * from './page/page-root-block.js';
|
||||
export { PageRootService } from './page/page-root-service.js';
|
||||
export * from './page/page-root-spec.js';
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
{ "path": "../../gfx/mindmap" },
|
||||
{ "path": "../../gfx/note" },
|
||||
{ "path": "../../gfx/shape" },
|
||||
{ "path": "../../gfx/template" },
|
||||
{ "path": "../../gfx/text" },
|
||||
{ "path": "../../inlines/latex" },
|
||||
{ "path": "../../inlines/link" },
|
||||
|
||||
Reference in New Issue
Block a user