mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
refactor(editor): extract common components (#9282)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { BLOCK_ID_ATTR } from '@blocksuite/block-std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class Loader extends LitElement {
|
||||
static override styles = css`
|
||||
.load-container {
|
||||
margin: 10px auto;
|
||||
width: var(--loader-width);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-container .load {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--affine-text-primary-color);
|
||||
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
animation: bouncedelay 1.4s infinite ease-in-out;
|
||||
/* Prevent first note from flickering when animation starts */
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
.load-container .load1 {
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
.load-container .load2 {
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes bouncedelay {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
-webkit-transform: scale(0.625);
|
||||
}
|
||||
40% {
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bouncedelay {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: scale(0);
|
||||
-webkit-transform: scale(0.625);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
-webkit-transform: scale(1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hostModel) {
|
||||
this.setAttribute(BLOCK_ID_ATTR, this.hostModel.id);
|
||||
this.dataset.serviceLoading = 'true';
|
||||
}
|
||||
|
||||
const width = this.width;
|
||||
this.style.setProperty(
|
||||
'--loader-width',
|
||||
typeof width === 'string' ? width : `${width}px`
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="load-container">
|
||||
<div class="load load1"></div>
|
||||
<div class="load load2"></div>
|
||||
<div class="load"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor hostModel: BlockModel | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor radius: string | number = '8px';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor width: string | number = '150px';
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'loader-element': Loader;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user