refactor(editor): extract common components (#9282)

This commit is contained in:
Saul-Mirone
2024-12-24 08:41:22 +00:00
parent 3cf4bcf651
commit 190e7e6f30
7 changed files with 14 additions and 11 deletions

View File

@@ -1,242 +0,0 @@
import { baseTheme } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import {
css,
html,
LitElement,
nothing,
type TemplateResult,
unsafeCSS,
} from 'lit';
import { property, query } from 'lit/decorators.js';
/**
* Default size is 32px, you can override it by setting `size` property.
* For example, `<icon-button size="32px"></icon-button>`.
*
* You can also set `width` or `height` property to override the size.
*
* Set `text` property to show a text label.
*
* @example
* ```ts
* html`<icon-button @click=${this.onUnlink}>
* ${UnlinkIcon}
* </icon-button>`
*
* html`<icon-button size="32px" text="HTML" @click=${this._importHtml}>
* ${ExportToHTMLIcon}
* </icon-button>`
* ```
*/
export class IconButton extends LitElement {
static override styles = css`
:host {
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
border: none;
width: var(--button-width);
height: var(--button-height);
border-radius: 4px;
background: transparent;
cursor: pointer;
user-select: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: var(--affine-text-primary-color);
pointer-events: auto;
padding: 4px;
}
// This media query can detect if the device has a hover capability
@media (hover: hover) {
:host(:hover) {
background: var(--affine-hover-color);
}
}
:host(:active) {
background: transparent;
}
:host([disabled]),
:host(:disabled) {
background: transparent;
color: var(--affine-text-disable-color);
cursor: not-allowed;
}
/* You can add a 'hover' attribute to the button to show the hover style */
:host([hover='true']) {
background: var(--affine-hover-color);
}
:host([hover='false']) {
background: transparent;
}
:host(:active[active]) {
background: transparent;
}
/* not supported "until-found" yet */
:host([hidden]) {
display: none;
}
:host > .text-container {
display: flex;
flex-direction: column;
overflow: hidden;
}
:host .text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: var(--affine-font-sm);
line-height: var(--affine-line-height);
}
:host .sub-text {
font-size: var(--affine-font-xs);
color: var(
--light-textColor-textSecondaryColor,
var(--textColor-textSecondaryColor, #8e8d91)
);
line-height: var(--affine-line-height);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-top: -2px;
}
::slotted(svg) {
flex-shrink: 0;
color: var(--svg-icon-color);
}
::slotted([slot='suffix']) {
margin-left: auto;
}
`;
constructor() {
super();
// Allow activate button by pressing Enter key
this.addEventListener('keypress', event => {
if (this.disabled) {
return;
}
if (event.key === 'Enter' && !event.isComposing) {
this.click();
}
});
// Prevent click event when disabled
this.addEventListener(
'click',
event => {
if (this.disabled === true) {
event.preventDefault();
event.stopPropagation();
}
},
{ capture: true }
);
}
override connectedCallback() {
super.connectedCallback();
this.tabIndex = 0;
this.role = 'button';
const DEFAULT_SIZE = '28px';
if (this.size && (this.width || this.height)) {
return;
}
let width = this.width ?? DEFAULT_SIZE;
let height = this.height ?? DEFAULT_SIZE;
if (this.size) {
width = this.size;
height = this.size;
}
this.style.setProperty(
'--button-width',
typeof width === 'string' ? width : `${width}px`
);
this.style.setProperty(
'--button-height',
typeof height === 'string' ? height : `${height}px`
);
}
override render() {
if (this.hidden) return nothing;
if (this.disabled) {
const disabledColor = cssVarV2('icon/disable');
this.style.setProperty('--svg-icon-color', disabledColor);
this.dataset.testDisabled = 'true';
} else {
this.dataset.testDisabled = 'false';
const iconColor = this.active
? cssVarV2('icon/activated')
: cssVarV2('icon/primary');
this.style.setProperty('--svg-icon-color', iconColor);
}
const text = this.text
? // wrap a span around the text so we can ellipsis it automatically
html`<div class="text">${this.text}</div>`
: nothing;
const subText = this.subText
? html`<div class="sub-text">${this.subText}</div>`
: nothing;
const textContainer =
this.text || this.subText
? html`<div class="text-container">${text}${subText}</div>`
: nothing;
return html`<slot></slot>
${textContainer}
<slot name="suffix"></slot>`;
}
@property({ attribute: true, type: Boolean })
accessor active: boolean = false;
// Do not add `{ attribute: false }` option here, otherwise the `disabled` styles will not work
@property({ attribute: true, type: Boolean })
accessor disabled: boolean | undefined = undefined;
@property()
accessor height: string | number | null = null;
@property({ attribute: true, type: String })
accessor hover: 'true' | 'false' | undefined = undefined;
@property()
accessor size: string | number | null = null;
@property()
accessor subText: string | TemplateResult<1> | null = null;
@property()
accessor text: string | TemplateResult<1> | null = null;
@query('.text-container .text')
accessor textElement: HTMLDivElement | null = null;
@property()
accessor width: string | number | null = null;
}
declare global {
interface HTMLElementTagNameMap {
'icon-button': IconButton;
}
}

View File

@@ -1,184 +0,0 @@
import { getFigmaSquircleSvgPath } from '@blocksuite/global/utils';
import { css, html, LitElement, svg, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
/**
* ### A component to use figma 'smoothing radius'
*
* ```html
* <smooth-corner
* .borderRadius=${10}
* .smooth=${0.5}
* .borderWidth=${2}
* .bgColor=${'white'}
* style="filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.1));"
* >
* <h1>Smooth Corner</h1>
* </smooth-corner>
* ```
*
* **Just wrap your content with it.**
* - There is a ResizeObserver inside to observe the size of the content.
* - In order to use both border and shadow, we use svg to draw.
* - So we need to use `stroke` and `drop-shadow` to replace `border` and `box-shadow`.
*
* #### required properties
* - `borderRadius`: Equal to the border-radius
* - `smooth`: From 0 to 1, refer to the figma smoothing radius
*
* #### customizable style properties
* Provides some commonly used styles, dealing with their mapping with SVG attributes, such as:
* - `borderWidth` (stroke-width)
* - `borderColor` (stroke)
* - `bgColor` (fill)
* - `bgOpacity` (fill-opacity)
*
* #### More customization
* Use css to customize this component, such as drop-shadow:
* ```css
* smooth-corner {
* filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.1));
* }
* ```
*/
export class SmoothCorner extends LitElement {
static override styles = css`
:host {
position: relative;
}
.smooth-corner-bg,
.smooth-corner-border {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.smooth-corner-border {
z-index: 2;
}
.smooth-corner-content {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
}
`;
private readonly _resizeObserver: ResizeObserver | null = null;
get _path() {
return getFigmaSquircleSvgPath({
width: this.width,
height: this.height,
cornerRadius: this.borderRadius, // defaults to 0
cornerSmoothing: this.smooth, // cornerSmoothing goes from 0 to 1
});
}
constructor() {
super();
this._resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
this.width = entry.contentRect.width;
this.height = entry.contentRect.height;
}
});
}
private _getSvg(className: string, path: TemplateResult) {
return svg`<svg
class="${className}"
width=${this.width + this.borderWidth}
height=${this.height + this.borderWidth}
viewBox="0 0 ${this.width + this.borderWidth} ${
this.height + this.borderWidth
}"
xmlns="http://www.w3.org/2000/svg"
>
${path}
</svg>`;
}
override connectedCallback(): void {
super.connectedCallback();
this._resizeObserver?.observe(this);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this._resizeObserver?.unobserve(this);
}
override render() {
return html`${this._getSvg(
'smooth-corner-bg',
svg`<path
d="${this._path}"
fill="${this.bgColor}"
fill-opacity="${this.bgOpacity}"
transform="translate(${this.borderWidth / 2} ${this.borderWidth / 2})"
>`
)}
${this._getSvg(
'smooth-corner-border',
svg`<path
fill="none"
d="${this._path}"
stroke="${this.borderColor}"
stroke-width="${this.borderWidth}"
transform="translate(${this.borderWidth / 2} ${this.borderWidth / 2})"
>`
)}
<div class="smooth-corner-content">
<slot></slot>
</div>`;
}
/**
* Background color of the element
*/
@property({ type: String })
accessor bgColor: string = 'white';
/**
* Background opacity of the element
*/
@property({ type: Number })
accessor bgOpacity: number = 1;
/**
* Border color of the element
*/
@property({ type: String })
accessor borderColor: string = 'black';
/**
* Equal to the border-radius
*/
@property({ type: Number })
accessor borderRadius = 0;
/**
* Border width of the element in px
*/
@property({ type: Number })
accessor borderWidth: number = 2;
@state()
accessor height: number = 0;
/**
* From 0 to 1
*/
@property({ type: Number })
accessor smooth: number = 0;
@state()
accessor width: number = 0;
}
declare global {
interface HTMLElementTagNameMap {
'smooth-corner': SmoothCorner;
}
}

View File

@@ -1,89 +0,0 @@
import { css, html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
const styles = css`
:host {
display: flex;
}
.switch {
height: 0;
width: 0;
visibility: hidden;
margin: 0;
}
label {
cursor: pointer;
text-indent: -9999px;
width: 38px;
height: 20px;
background: var(--affine-icon-color);
border: 1px solid var(--affine-black-10);
display: block;
border-radius: 20px;
position: relative;
}
label:after {
content: '';
position: absolute;
top: 1px;
left: 1px;
width: 16px;
height: 16px;
background: var(--affine-white);
border: 1px solid var(--affine-black-10);
border-radius: 16px;
transition: 0.1s;
}
label.on {
background: var(--affine-primary-color);
}
label.on:after {
left: calc(100% - 1px);
transform: translateX(-100%);
}
label:active:after {
width: 24px;
}
`;
export class ToggleSwitch extends LitElement {
static override styles = styles;
private _toggleSwitch() {
this.on = !this.on;
if (this.onChange) {
this.onChange(this.on);
}
}
override render() {
return html`
<label class=${this.on ? 'on' : ''}>
<input
type="checkbox"
class="switch"
?checked=${this.on}
@change=${this._toggleSwitch}
/>
</label>
`;
}
@property({ attribute: false })
accessor on = false;
@property({ attribute: false })
accessor onChange: ((on: boolean) => void) | undefined = undefined;
}
declare global {
interface HTMLElementTagNameMap {
'toggle-switch': ToggleSwitch;
}
}

View File

@@ -9,9 +9,12 @@ import { effects as componentContextMenuEffects } from '@blocksuite/affine-compo
import { effects as componentDatePickerEffects } from '@blocksuite/affine-components/date-picker';
import { effects as componentDragIndicatorEffects } from '@blocksuite/affine-components/drag-indicator';
import { FilterableListComponent } from '@blocksuite/affine-components/filterable-list';
import { IconButton } from '@blocksuite/affine-components/icon-button';
import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal';
import { effects as componentRichTextEffects } from '@blocksuite/affine-components/rich-text';
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button';
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
import { effects as componentToolbarEffects } from '@blocksuite/affine-components/toolbar';
import { effects as widgetScrollAnchoringEffects } from '@blocksuite/affine-widget-scroll-anchoring/effects';
import type { BlockComponent } from '@blocksuite/block-std';
@@ -22,16 +25,12 @@ import type { BlockModel } from '@blocksuite/store';
import { AIItem } from './_common/components/ai-item/ai-item.js';
import { AISubItemList } from './_common/components/ai-item/ai-sub-item-list.js';
import { IconButton } from './_common/components/button.js';
import { EmbedCardMoreMenu } from './_common/components/embed-card/embed-card-more-menu-popper.js';
import { EmbedCardStyleMenu } from './_common/components/embed-card/embed-card-style-popper.js';
import { EmbedCardEditCaptionEditModal } from './_common/components/embed-card/modal/embed-card-caption-edit-modal.js';
import { EmbedCardCreateModal } from './_common/components/embed-card/modal/embed-card-create-modal.js';
import { EmbedCardEditModal } from './_common/components/embed-card/modal/embed-card-edit-modal.js';
import { AIItemList } from './_common/components/index.js';
import { Loader } from './_common/components/loader.js';
import { SmoothCorner } from './_common/components/smooth-corner.js';
import { ToggleSwitch } from './_common/components/toggle-switch.js';
import { registerSpecs } from './_specs/register-specs.js';
import { AttachmentEdgelessBlockComponent } from './attachment-block/attachment-edgeless-block.js';
import {
@@ -246,6 +245,7 @@ import { AFFINE_IMAGE_TOOLBAR_WIDGET } from './root-block/widgets/image-toolbar/
import { AFFINE_INNER_MODAL_WIDGET } from './root-block/widgets/inner-modal/inner-modal.js';
import { effects as widgetMobileToolbarEffects } from './root-block/widgets/keyboard-toolbar/effects.js';
import { effects as widgetLinkedDocEffects } from './root-block/widgets/linked-doc/effects.js';
import { Loader } from './root-block/widgets/linked-doc/import-doc/loader';
import { AffineCustomModal } from './root-block/widgets/modal/custom-modal.js';
import { AFFINE_MODAL_WIDGET } from './root-block/widgets/modal/modal.js';
import { AFFINE_PAGE_DRAGGING_AREA_WIDGET } from './root-block/widgets/page-dragging-area/page-dragging-area.js';

View File

@@ -1,3 +1,4 @@
import type { IconButton } from '@blocksuite/affine-components/icon-button';
import { MoreHorizontalIcon } from '@blocksuite/affine-components/icons';
import {
getCurrentNativeRange,
@@ -13,7 +14,6 @@ import { html, LitElement, nothing } from 'lit';
import { property, query, queryAll, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { IconButton } from '../../../_common/components/button.js';
import {
cleanSpecifiedTail,
createKeydownObserver,