mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
feat(editor): gfx link extension (#12046)
Closes: BS-3368 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new link tool extension, enabling enhanced link-related functionality within the edgeless workspace. - Added a new view extension for link tools, improving integration and usability in edgeless mode. - **Chores** - Added a new package for link tool functionality with appropriate dependencies and exports. - Registered new custom elements for edgeless toolbars and link tools to support modular UI components. - Updated project configurations and workspace dependencies to include the new link tool module. - **Refactor** - Removed unused quick tool exports and toolbar component registrations to streamline the edgeless extension codebase. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import {
|
||||
type EdgelessToolbarSlots,
|
||||
edgelessToolbarSlotsContext,
|
||||
} from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
export class EdgelessSlideMenu extends WithDisposable(LitElement) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
max-width: 100%;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.slide-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.menu-container {
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
height: calc(var(--menu-height) + 1px);
|
||||
box-sizing: border-box;
|
||||
padding-left: 10px;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.menu-container-scrollable {
|
||||
overflow-x: auto;
|
||||
overscroll-behavior: none;
|
||||
scrollbar-width: none;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.slide-menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
transition: left 0.5s ease-in-out;
|
||||
width: fit-content;
|
||||
}
|
||||
.next-slide-button,
|
||||
.previous-slide-button {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
box-shadow: var(--affine-shadow-2);
|
||||
color: var(--affine-icon-color);
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
opacity 0.5s ease-in-out;
|
||||
z-index: 12;
|
||||
}
|
||||
.next-slide-button {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%) scale(0.5);
|
||||
}
|
||||
.next-slide-button:hover {
|
||||
cursor: pointer;
|
||||
transform: translate(50%, -50%) scale(1);
|
||||
}
|
||||
.previous-slide-button {
|
||||
opacity: 0;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
}
|
||||
.previous-slide-button:hover {
|
||||
cursor: pointer;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
.previous-slide-button svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleSlideButtonClick(direction: 'left' | 'right') {
|
||||
const totalWidth = this._slideMenuContent.clientWidth;
|
||||
const currentScrollLeft = this._menuContainer.scrollLeft;
|
||||
const menuWidth = this._menuContainer.clientWidth;
|
||||
const newLeft =
|
||||
currentScrollLeft + (direction === 'left' ? -menuWidth : menuWidth);
|
||||
this._menuContainer.scrollTo({
|
||||
left: Math.max(0, Math.min(newLeft, totalWidth)),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
private _handleWheel(event: WheelEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private _toggleSlideButton() {
|
||||
const scrollLeft = this._menuContainer.scrollLeft;
|
||||
const menuWidth = this._menuContainer.clientWidth;
|
||||
|
||||
const leftMin = 0;
|
||||
const leftMax = this._slideMenuContent.clientWidth - menuWidth;
|
||||
this.showPrevious = scrollLeft > leftMin;
|
||||
this.showNext = scrollLeft < leftMax;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
setTimeout(this._toggleSlideButton.bind(this), 0);
|
||||
this._disposables.addFromEvent(this._menuContainer, 'scrollend', () => {
|
||||
this._toggleSlideButton();
|
||||
});
|
||||
this._disposables.add(
|
||||
this.toolbarSlots.resize.subscribe(() => this._toggleSlideButton())
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const iconSize = { width: '32px', height: '32px' };
|
||||
|
||||
return html`
|
||||
<div class="slide-menu-wrapper">
|
||||
<div
|
||||
class="previous-slide-button"
|
||||
@click=${() => this._handleSlideButtonClick('left')}
|
||||
style=${styleMap({ opacity: this.showPrevious ? '1' : '0' })}
|
||||
>
|
||||
${ArrowRightSmallIcon(iconSize)}
|
||||
</div>
|
||||
<div
|
||||
class="menu-container"
|
||||
style=${styleMap({ '--menu-height': this.height })}
|
||||
>
|
||||
<slot name="prefix"></slot>
|
||||
<div class="menu-container-scrollable">
|
||||
<div class="slide-menu-content" @wheel=${this._handleWheel}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style=${styleMap({ opacity: this.showNext ? '1' : '0' })}
|
||||
class="next-slide-button"
|
||||
@click=${() => this._handleSlideButtonClick('right')}
|
||||
>
|
||||
${ArrowRightSmallIcon(iconSize)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.menu-container-scrollable')
|
||||
private accessor _menuContainer!: HTMLDivElement;
|
||||
|
||||
@query('.slide-menu-content')
|
||||
private accessor _slideMenuContent!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor height = '40px';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showNext = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showPrevious = false;
|
||||
|
||||
@consume({ context: edgelessToolbarSlotsContext })
|
||||
accessor toolbarSlots!: EdgelessToolbarSlots;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ArrowUpSmallIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
export class ToolbarArrowUpIcon extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
.arrow-up-icon {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<span class="arrow-up-icon"> ${ArrowUpSmallIcon()} </span>`;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { menu } from '@blocksuite/affine-components/context-menu';
|
||||
import { LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { DenseMenuBuilder } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
|
||||
export const buildLinkDenseMenu: DenseMenuBuilder = edgeless =>
|
||||
menu.action({
|
||||
name: 'Link',
|
||||
prefix: LinkIcon,
|
||||
select: () => {
|
||||
const [_, { insertedLinkType }] = edgeless.std.command.exec(
|
||||
insertLinkByQuickSearchCommand
|
||||
);
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
type: flavour.split(':')[1],
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmark';
|
||||
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
|
||||
import { toggleEmbedCardCreateModal } from '@blocksuite/affine-components/embed-card-modal';
|
||||
import { LinkIcon } from '@blocksuite/affine-components/icons';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import { QuickToolMixin } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
|
||||
export class EdgelessLinkToolButton extends QuickToolMixin(LitElement) {
|
||||
static override styles = css`
|
||||
.link-icon,
|
||||
.link-icon > svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
override type = 'default' as const;
|
||||
|
||||
private _onClick() {
|
||||
const [success, { insertedLinkType }] = this.edgeless.std.command.exec(
|
||||
insertLinkByQuickSearchCommand
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
// fallback to create a bookmark block with input modal
|
||||
toggleEmbedCardCreateModal(
|
||||
this.edgeless.host,
|
||||
'Links',
|
||||
'The added link will be displayed as a card view.',
|
||||
{
|
||||
mode: 'edgeless',
|
||||
onSave: url => {
|
||||
insertEmbedCard(this.edgeless.std, {
|
||||
flavour: 'affine:bookmark',
|
||||
targetStyle: 'vertical',
|
||||
props: { url },
|
||||
});
|
||||
},
|
||||
}
|
||||
).catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('CanvasElementAdded', {
|
||||
control: 'toolbar:general',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: flavour.split(':')[1],
|
||||
});
|
||||
|
||||
this.edgeless.std
|
||||
.getOptional(TelemetryProvider)
|
||||
?.track('LinkedDocCreated', {
|
||||
control: 'links',
|
||||
page: 'whiteboard editor',
|
||||
module: 'edgeless toolbar',
|
||||
segment: 'whiteboard',
|
||||
type: flavour.split(':')[1],
|
||||
other: 'existing doc',
|
||||
});
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<edgeless-tool-icon-button
|
||||
.iconContainerPadding="${6}"
|
||||
.tooltip="${html`<affine-tooltip-content-with-shortcut
|
||||
data-tip="${'Link'}"
|
||||
data-shortcut="${'@'}"
|
||||
></affine-tooltip-content-with-shortcut>`}"
|
||||
.tooltipOffset=${17}
|
||||
class="edgeless-link-tool-button"
|
||||
@click=${this._onClick}
|
||||
>
|
||||
<span class="link-icon">${LinkIcon}</span>
|
||||
</edgeless-tool-icon-button>`;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { QuickToolExtension } from '@blocksuite/affine-widget-edgeless-toolbar';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { buildLinkDenseMenu } from './link/link-dense-menu.js';
|
||||
|
||||
const linkQuickTool = QuickToolExtension('link', ({ block, gfx }) => {
|
||||
return {
|
||||
content: html`<edgeless-link-tool-button
|
||||
.edgeless=${block}
|
||||
></edgeless-link-tool-button>`,
|
||||
menu: buildLinkDenseMenu(block, gfx),
|
||||
};
|
||||
});
|
||||
|
||||
export const quickTools = [linkQuickTool];
|
||||
@@ -13,7 +13,6 @@ import { EdgelessClipboardController } from './clipboard/clipboard.js';
|
||||
import { NOTE_SLICER_WIDGET } from './components/note-slicer/index.js';
|
||||
import { EDGELESS_DRAGGING_AREA_WIDGET } from './components/rects/edgeless-dragging-area-rect.js';
|
||||
import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selected-rect.js';
|
||||
import { quickTools } from './components/toolbar/tools.js';
|
||||
import { EdgelessRootService } from './edgeless-root-service.js';
|
||||
|
||||
export const edgelessDraggingAreaWidget = WidgetViewExtension(
|
||||
@@ -45,7 +44,6 @@ const EdgelessCommonExtension: ExtensionType[] = [
|
||||
CommonSpecs,
|
||||
EdgelessRootService,
|
||||
ViewportElementExtension('.affine-edgeless-viewport'),
|
||||
...quickTools,
|
||||
].flat();
|
||||
|
||||
export const EdgelessRootBlockSpec: ExtensionType[] = [
|
||||
|
||||
@@ -12,9 +12,6 @@ import {
|
||||
EDGELESS_SELECTED_RECT_WIDGET,
|
||||
EdgelessSelectedRectWidget,
|
||||
} from './edgeless/components/rects/edgeless-selected-rect.js';
|
||||
import { EdgelessSlideMenu } from './edgeless/components/toolbar/common/slide-menu.js';
|
||||
import { ToolbarArrowUpIcon } from './edgeless/components/toolbar/common/toolbar-arrow-up-icon.js';
|
||||
import { EdgelessLinkToolButton } from './edgeless/components/toolbar/link/link-tool-button.js';
|
||||
import {
|
||||
EdgelessRootBlockComponent,
|
||||
EdgelessRootPreviewBlockComponent,
|
||||
@@ -25,7 +22,6 @@ import {
|
||||
export function effects() {
|
||||
// Register components by category
|
||||
registerRootComponents();
|
||||
registerEdgelessToolbarComponents();
|
||||
registerMiscComponents();
|
||||
}
|
||||
|
||||
@@ -39,17 +35,6 @@ function registerRootComponents() {
|
||||
);
|
||||
}
|
||||
|
||||
function registerEdgelessToolbarComponents() {
|
||||
// Tool buttons
|
||||
customElements.define('edgeless-link-tool-button', EdgelessLinkToolButton);
|
||||
|
||||
// Menus
|
||||
customElements.define('edgeless-slide-menu', EdgelessSlideMenu);
|
||||
|
||||
// Toolbar components
|
||||
customElements.define('toolbar-arrow-up-icon', ToolbarArrowUpIcon);
|
||||
}
|
||||
|
||||
function registerMiscComponents() {
|
||||
// Auto-complete components
|
||||
customElements.define(
|
||||
@@ -81,9 +66,6 @@ declare global {
|
||||
'note-slicer': NoteSlicer;
|
||||
'edgeless-dragging-area-rect': EdgelessDraggingAreaRectWidget;
|
||||
'edgeless-selected-rect': EdgelessSelectedRectWidget;
|
||||
'edgeless-slide-menu': EdgelessSlideMenu;
|
||||
'toolbar-arrow-up-icon': ToolbarArrowUpIcon;
|
||||
'edgeless-link-tool-button': EdgelessLinkToolButton;
|
||||
'affine-page-root': PageRootBlockComponent;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user