chore(editor): reorg packages (#10702)

This commit is contained in:
Saul-Mirone
2025-03-08 03:57:04 +00:00
parent 334912e85b
commit 8aedef0a36
961 changed files with 837 additions and 927 deletions

View File

@@ -0,0 +1,13 @@
import type { ExtensionType } from '@blocksuite/store';
import { EmbedLoomBlockHtmlAdapterExtension } from './html.js';
import { EmbedLoomMarkdownAdapterExtension } from './markdown.js';
import { EmbedLoomBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { EmbedLoomBlockPlainTextAdapterExtension } from './plain-text.js';
export const EmbedLoomBlockAdapterExtensions: ExtensionType[] = [
EmbedLoomBlockHtmlAdapterExtension,
EmbedLoomMarkdownAdapterExtension,
EmbedLoomBlockPlainTextAdapterExtension,
EmbedLoomBlockNotionHtmlAdapterExtension,
];

View File

@@ -0,0 +1,11 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { BlockHtmlAdapterExtension } from '@blocksuite/affine-shared/adapters';
import { createEmbedBlockHtmlAdapterMatcher } from '../../common/adapters/html.js';
export const embedLoomBlockHtmlAdapterMatcher =
createEmbedBlockHtmlAdapterMatcher(EmbedLoomBlockSchema.model.flavour);
export const EmbedLoomBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
embedLoomBlockHtmlAdapterMatcher
);

View File

@@ -0,0 +1,4 @@
export * from './html.js';
export * from './markdown.js';
export * from './notion-html.js';
export * from './plain-text.js';

View File

@@ -0,0 +1,11 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
import { createEmbedBlockMarkdownAdapterMatcher } from '../../common/adapters/markdown.js';
export const embedLoomBlockMarkdownAdapterMatcher =
createEmbedBlockMarkdownAdapterMatcher(EmbedLoomBlockSchema.model.flavour);
export const EmbedLoomMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
embedLoomBlockMarkdownAdapterMatcher
);

View File

@@ -0,0 +1,14 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { BlockNotionHtmlAdapterExtension } from '@blocksuite/affine-shared/adapters';
import { createEmbedBlockNotionHtmlAdapterMatcher } from '../../common/adapters/notion-html.js';
import { loomUrlRegex } from '../embed-loom-model.js';
export const embedLoomBlockNotionHtmlAdapterMatcher =
createEmbedBlockNotionHtmlAdapterMatcher(
EmbedLoomBlockSchema.model.flavour,
loomUrlRegex
);
export const EmbedLoomBlockNotionHtmlAdapterExtension =
BlockNotionHtmlAdapterExtension(embedLoomBlockNotionHtmlAdapterMatcher);

View File

@@ -0,0 +1,10 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { BlockPlainTextAdapterExtension } from '@blocksuite/affine-shared/adapters';
import { createEmbedBlockPlainTextAdapterMatcher } from '../../common/adapters/plain-text.js';
export const embedLoomBlockPlainTextAdapterMatcher =
createEmbedBlockPlainTextAdapterMatcher(EmbedLoomBlockSchema.model.flavour);
export const EmbedLoomBlockPlainTextAdapterExtension =
BlockPlainTextAdapterExtension(embedLoomBlockPlainTextAdapterMatcher);

View File

@@ -0,0 +1,32 @@
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
import type { SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { LoomLogoDuotoneIcon } from '@blocksuite/icons/lit';
export const embedLoomSlashMenuConfig: SlashMenuConfig = {
items: [
{
name: 'Loom',
icon: LoomLogoDuotoneIcon(),
group: '4_Content & Media@8',
when: ({ model }) =>
model.doc.schema.flavourSchemaMap.has('affine:embed-loom'),
action: ({ std, model }) => {
(async () => {
const { host } = std;
const parentModel = host.doc.getParent(model);
if (!parentModel) {
return;
}
const index = parentModel.children.indexOf(model) + 1;
await toggleEmbedCardCreateModal(
host,
'Loom',
'The added Loom video link will be displayed as an embed view.',
{ mode: 'page', parentModel, index }
);
if (model.text?.length === 0) std.store.deleteBlock(model);
})().catch(console.error);
},
},
],
};

View File

@@ -0,0 +1,6 @@
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
import { EmbedLoomBlockComponent } from './embed-loom-block.js';
export class EmbedEdgelessLoomBlockComponent extends toEdgelessEmbedBlock(
EmbedLoomBlockComponent
) {}

View File

@@ -0,0 +1,196 @@
import { OpenIcon } from '@blocksuite/affine-components/icons';
import type { EmbedLoomModel, EmbedLoomStyles } from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { BlockSelection } from '@blocksuite/block-std';
import { html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { EmbedBlockComponent } from '../common/embed-block-element.js';
import { getEmbedCardIcons } from '../common/utils.js';
import { loomUrlRegex } from './embed-loom-model.js';
import type { EmbedLoomBlockService } from './embed-loom-service.js';
import { LoomIcon, styles } from './styles.js';
import { refreshEmbedLoomUrlData } from './utils.js';
export class EmbedLoomBlockComponent extends EmbedBlockComponent<
EmbedLoomModel,
EmbedLoomBlockService
> {
static override styles = styles;
override _cardStyle: (typeof EmbedLoomStyles)[number] = 'video';
protected _isDragging = false;
protected _isResizing = false;
open = () => {
let link = this.model.url;
if (!link.match(/^[a-zA-Z]+:\/\//)) {
link = 'https://' + link;
}
window.open(link, '_blank');
};
refreshData = () => {
refreshEmbedLoomUrlData(this, this.fetchAbortController.signal).catch(
console.error
);
};
private _handleDoubleClick(event: MouseEvent) {
event.stopPropagation();
this.open();
}
private _selectBlock() {
const selectionManager = this.host.selection;
const blockSelection = selectionManager.create(BlockSelection, {
blockId: this.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
}
protected _handleClick(event: MouseEvent) {
event.stopPropagation();
this._selectBlock();
}
override connectedCallback() {
super.connectedCallback();
this._cardStyle = this.model.style;
if (!this.model.videoId) {
this.doc.withoutTransact(() => {
const url = this.model.url;
const urlMatch = url.match(loomUrlRegex);
if (urlMatch) {
const [, videoId] = urlMatch;
this.doc.updateBlock(this.model, {
videoId,
});
}
});
}
if (!this.model.description && !this.model.title) {
this.doc.withoutTransact(() => {
this.refreshData();
});
}
this.disposables.add(
this.model.propsUpdated.on(({ key }) => {
this.requestUpdate();
if (key === 'url') {
this.refreshData();
}
})
);
// this is required to prevent iframe from capturing pointer events
this.disposables.add(
this.selected$.subscribe(selected => {
this._showOverlay = this._isResizing || this._isDragging || !selected;
})
);
// this is required to prevent iframe from capturing pointer events
this.handleEvent('dragStart', () => {
this._isDragging = true;
this._showOverlay =
this._isResizing || this._isDragging || !this.selected$.peek();
});
this.handleEvent('dragEnd', () => {
this._isDragging = false;
this._showOverlay =
this._isResizing || this._isDragging || !this.selected$.peek();
});
}
override renderBlock() {
const { image, title = 'Loom', description, videoId } = this.model;
const loading = this.loading;
const theme = this.std.get(ThemeProvider).theme;
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIcon = loading ? LoadingIcon : LoomIcon;
const titleText = loading ? 'Loading...' : title;
const descriptionText = loading ? '' : description;
const bannerImage =
!loading && image
? html`<object type="image/webp" data=${image} draggable="false">
${EmbedCardBannerIcon}
</object>`
: EmbedCardBannerIcon;
return this.renderEmbed(
() => html`
<div
class=${classMap({
'affine-embed-loom-block': true,
loading,
selected: this.selected$.value,
})}
style=${styleMap({
transform: `scale(${this._scale})`,
transformOrigin: '0 0',
})}
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>
<div class="affine-embed-loom-video">
${videoId
? html`
<div class="affine-embed-loom-video-iframe-container">
<iframe
src=${`https://www.loom.com/embed/${videoId}?hide_title=true`}
frameborder="0"
allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
loading="lazy"
></iframe>
<div
class=${classMap({
'affine-embed-loom-video-iframe-overlay': true,
hide: !this._showOverlay,
})}
></div>
</div>
`
: bannerImage}
</div>
<div class="affine-embed-loom-content">
<div class="affine-embed-loom-content-header">
<div class="affine-embed-loom-content-title-icon">
${titleIcon}
</div>
<div class="affine-embed-loom-content-title-text">
${titleText}
</div>
</div>
<div class="affine-embed-loom-content-description">
${descriptionText}
</div>
<div class="affine-embed-loom-content-url" @click=${this.open}>
<span>loom.com</span>
<div class="affine-embed-loom-content-url-icon">${OpenIcon}</div>
</div>
</div>
</div>
`
);
}
@state()
protected accessor _showOverlay = true;
@property({ attribute: false })
accessor loading = false;
}

View File

@@ -0,0 +1,2 @@
export const loomUrlRegex: RegExp =
/(?:https?:\/\/)??(?:www\.)?loom\.com\/share\/([a-zA-Z0-9]+)/;

View File

@@ -0,0 +1,25 @@
import {
EmbedLoomBlockSchema,
type EmbedLoomModel,
EmbedLoomStyles,
} from '@blocksuite/affine-model';
import { EmbedOptionConfig } from '@blocksuite/affine-shared/services';
import { BlockService } from '@blocksuite/block-std';
import { loomUrlRegex } from './embed-loom-model.js';
import { queryEmbedLoomData } from './utils.js';
export class EmbedLoomBlockService extends BlockService {
static override readonly flavour = EmbedLoomBlockSchema.model.flavour;
queryUrlData = (embedLoomModel: EmbedLoomModel, signal?: AbortSignal) => {
return queryEmbedLoomData(embedLoomModel, signal);
};
}
export const EmbedLoomBlockOptionConfig = EmbedOptionConfig({
flavour: EmbedLoomBlockSchema.model.flavour,
urlRegex: loomUrlRegex,
styles: EmbedLoomStyles,
viewType: 'embed',
});

View File

@@ -0,0 +1,38 @@
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
import { ToolbarModuleExtension } from '@blocksuite/affine-shared/services';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import {
BlockServiceIdentifier,
BlockViewExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { createBuiltinToolbarConfigForExternal } from '../configs/toolbar';
import { EmbedLoomBlockAdapterExtensions } from './adapters/extension';
import { embedLoomSlashMenuConfig } from './configs/slash-menu';
import { EmbedLoomBlockComponent } from './embed-loom-block';
import {
EmbedLoomBlockOptionConfig,
EmbedLoomBlockService,
} from './embed-loom-service';
const flavour = EmbedLoomBlockSchema.model.flavour;
export const EmbedLoomBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
EmbedLoomBlockService,
BlockViewExtension(flavour, model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-embed-edgeless-loom-block`
: literal`affine-embed-loom-block`;
}),
EmbedLoomBlockAdapterExtensions,
EmbedLoomBlockOptionConfig,
ToolbarModuleExtension({
id: BlockServiceIdentifier(flavour),
config: createBuiltinToolbarConfigForExternal(EmbedLoomBlockComponent),
}),
SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig),
].flat();

View File

@@ -0,0 +1,6 @@
export * from './adapters/index.js';
export * from './embed-loom-block.js';
export * from './embed-loom-model.js';
export * from './embed-loom-service.js';
export * from './embed-loom-spec.js';
export { LoomIcon } from './styles.js';

View File

@@ -0,0 +1,217 @@
import { css, html } from 'lit';
export const styles = css`
.affine-embed-loom-block {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 20px;
padding: 12px;
width: 100%;
height: 100%;
border-radius: 8px;
border: 1px solid var(--affine-background-tertiary-color);
opacity: var(--add, 1);
background: var(--affine-background-primary-color);
user-select: none;
}
.affine-embed-loom-video {
flex-grow: 1;
width: 100%;
opacity: var(--add, 1);
}
.affine-embed-loom-video img,
.affine-embed-loom-video object,
.affine-embed-loom-video svg {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px 4px var(--1, 0px) var(--1, 0px);
}
.affine-embed-loom-video-iframe-container {
position: relative;
height: 100%;
}
.affine-embed-loom-video-iframe-container > iframe {
width: 100%;
height: 100%;
border-radius: 4px 4px var(--1, 0px) var(--1, 0px);
}
.affine-embed-loom-video-iframe-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.affine-embed-loom-video-iframe-overlay.hide {
display: none;
}
.affine-embed-loom-content {
display: flex;
flex-direction: column;
width: 100%;
height: fit-content;
border-radius: var(--1, 0px);
opacity: var(--add, 1);
}
.affine-embed-loom-content-header {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
align-self: stretch;
padding: var(--1, 0px);
border-radius: var(--1, 0px);
opacity: var(--add, 1);
}
.affine-embed-loom-content-title-icon {
display: flex;
width: 20px;
height: 20px;
justify-content: center;
align-items: center;
}
.affine-embed-loom-content-title-icon img,
.affine-embed-loom-content-title-icon object,
.affine-embed-loom-content-title-icon svg {
width: 20px;
height: 20px;
fill: var(--affine-background-primary-color);
}
.affine-embed-loom-content-title-text {
flex: 1 0 0;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 600;
line-height: 22px;
}
.affine-embed-loom-content-description {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
flex: 1 0 0;
align-self: stretch;
word-break: break-word;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.affine-embed-loom-content-url {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
width: max-content;
max-width: 100%;
cursor: pointer;
}
.affine-embed-loom-content-url > span {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-text-secondary-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.affine-embed-loom-content-url:hover > span {
color: var(--affine-link-color);
}
.affine-embed-loom-content-url:hover .open-icon {
fill: var(--affine-link-color);
}
.affine-embed-loom-content-url-icon {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
}
.affine-embed-loom-content-url-icon .open-icon {
height: 12px;
width: 12px;
fill: var(--affine-text-secondary-color);
}
.affine-embed-loom-block.loading {
.affine-embed-loom-content-title-text {
color: var(--affine-placeholder-color);
}
}
.affine-embed-loom-block.selected {
.affine-embed-loom-content-url > span {
color: var(--affine-link-color);
}
.affine-embed-loom-content-url .open-icon {
fill: var(--affine-link-color);
}
}
`;
export const LoomIcon = html`<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_1780_25276)">
<path
d="M18.3333 9.07327H13.4597L17.6805 6.63642L16.7536 5.03052L12.5328 7.46736L14.9691 3.24695L13.3632 2.3195L10.9269 6.5399V1.66669H9.073V6.54037L6.63577 2.3195L5.03036 3.24648L7.46713 7.4669L3.24638 5.03052L2.31942 6.63596L6.54017 9.07281H1.66663V10.9268H6.53971L2.31942 13.3636L3.24638 14.9695L7.46667 12.5331L5.0299 16.7535L6.63577 17.6805L9.07254 13.4597V18.3334H10.9265V13.4601L13.3628 17.6805L14.9686 16.7535L12.5319 12.5327L16.7526 14.9695L17.6796 13.3636L13.4593 10.9272H18.3323V9.07327H18.3333ZM9.99996 12.5215C8.60206 12.5215 7.469 11.3884 7.469 9.99047C7.469 8.59253 8.60206 7.45943 9.99996 7.45943C11.3979 7.45943 12.5309 8.59253 12.5309 9.99047C12.5309 11.3884 11.3979 12.5215 9.99996 12.5215Z"
fill="#625DF5"
/>
</g>
<defs>
<clipPath id="clip0_1780_25276">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>`;

View File

@@ -0,0 +1,75 @@
import type {
EmbedLoomBlockUrlData,
EmbedLoomModel,
} from '@blocksuite/affine-model';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import type { EmbedLoomBlockComponent } from './embed-loom-block.js';
const LoomOEmbedEndpoint = 'https://www.loom.com/v1/oembed';
export async function queryEmbedLoomData(
embedLoomModel: EmbedLoomModel,
signal?: AbortSignal
): Promise<Partial<EmbedLoomBlockUrlData>> {
const url = embedLoomModel.url;
const loomEmbedData: Partial<EmbedLoomBlockUrlData> =
await queryLoomOEmbedData(url, signal);
return loomEmbedData;
}
export async function queryLoomOEmbedData(
url: string,
signal?: AbortSignal
): Promise<Partial<EmbedLoomBlockUrlData>> {
let loomOEmbedData: Partial<EmbedLoomBlockUrlData> = {};
const oEmbedUrl = `${LoomOEmbedEndpoint}?url=${url}`;
const oEmbedResponse = await fetch(oEmbedUrl, { signal }).catch(() => null);
if (oEmbedResponse && oEmbedResponse.ok) {
const oEmbedJson = await oEmbedResponse.json();
const { title, description, thumbnail_url: image } = oEmbedJson;
loomOEmbedData = {
title,
description,
image,
};
}
return loomOEmbedData;
}
export async function refreshEmbedLoomUrlData(
embedLoomElement: EmbedLoomBlockComponent,
signal?: AbortSignal
): Promise<void> {
let title = null,
description = null,
image = null;
try {
embedLoomElement.loading = true;
const queryUrlData = embedLoomElement.service?.queryUrlData;
if (!queryUrlData) return;
const loomUrlData = await queryUrlData(embedLoomElement.model);
({ title = null, description = null, image = null } = loomUrlData);
if (signal?.aborted) return;
embedLoomElement.doc.updateBlock(embedLoomElement.model, {
title,
description,
image,
});
} catch (error) {
if (signal?.aborted || isAbortError(error)) return;
} finally {
embedLoomElement.loading = false;
}
}