mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
refactor(editor): extract slider component (#12210)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new slider component for line width selection, providing a more interactive and streamlined UI. - Added support for using the slider component across relevant panels. - **Improvements** - Simplified the line width selection panel for easier use and improved maintainability. - Enhanced event handling to prevent dropdowns from closing when interacting with the panel. - **Bug Fixes** - Improved event propagation control within the line styles panel. - **Chores** - Updated package exports to include the new slider component. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -72,7 +72,8 @@
|
|||||||
"./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts",
|
"./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts",
|
||||||
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts",
|
"./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts",
|
||||||
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts",
|
"./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts",
|
||||||
"./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts"
|
"./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts",
|
||||||
|
"./slider": "./src/slider/index.ts"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LineWidth, StrokeStyle } from '@blocksuite/affine-model';
|
import { LineWidth, StrokeStyle } from '@blocksuite/affine-model';
|
||||||
|
import { WithDisposable } from '@blocksuite/global/lit';
|
||||||
import { BanIcon, DashLineIcon, StraightLineIcon } from '@blocksuite/icons/lit';
|
import { BanIcon, DashLineIcon, StraightLineIcon } from '@blocksuite/icons/lit';
|
||||||
import { html, LitElement } from 'lit';
|
import { css, html, LitElement } from 'lit';
|
||||||
import { property } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
@@ -33,7 +34,13 @@ const LINE_STYLE_LIST = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export class EdgelessLineStylesPanel extends LitElement {
|
export class EdgelessLineStylesPanel extends WithDisposable(LitElement) {
|
||||||
|
static override styles = css`
|
||||||
|
edgeless-line-width-panel {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
select(detail: LineDetailType) {
|
select(detail: LineDetailType) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('select', {
|
new CustomEvent('select', {
|
||||||
@@ -47,10 +54,9 @@ export class EdgelessLineStylesPanel extends LitElement {
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const { lineSize, lineStyle, lineStyles } = this;
|
const { lineSize, lineStyle, lineStyles } = this;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<edgeless-line-width-panel
|
<edgeless-line-width-panel
|
||||||
?disabled="${lineStyle === StrokeStyle.None}"
|
.disabled=${lineStyle === StrokeStyle.None}
|
||||||
.selectedSize=${lineSize}
|
.selectedSize=${lineSize}
|
||||||
@select=${(e: CustomEvent<LineWidth>) => {
|
@select=${(e: CustomEvent<LineWidth>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -1,130 +1,12 @@
|
|||||||
import { BRUSH_LINE_WIDTHS, LineWidth } from '@blocksuite/affine-model';
|
import { BRUSH_LINE_WIDTHS, LineWidth } from '@blocksuite/affine-model';
|
||||||
import { on, once } from '@blocksuite/affine-shared/utils';
|
|
||||||
import { WithDisposable } from '@blocksuite/global/lit';
|
import { WithDisposable } from '@blocksuite/global/lit';
|
||||||
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
|
import { html, LitElement } from 'lit';
|
||||||
import { property, query } from 'lit/decorators.js';
|
import { property } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
|
||||||
import clamp from 'lodash-es/clamp';
|
|
||||||
|
|
||||||
interface Config {
|
import type { SliderSelectEvent } from '../slider';
|
||||||
width: number;
|
|
||||||
itemSize: number;
|
|
||||||
itemIconSize: number;
|
|
||||||
dragHandleSize: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EdgelessLineWidthPanel extends WithDisposable(LitElement) {
|
export class EdgelessLineWidthPanel extends WithDisposable(LitElement) {
|
||||||
static override styles = css`
|
|
||||||
:host {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
align-self: stretch;
|
|
||||||
|
|
||||||
--width: 140px;
|
|
||||||
--item-size: 16px;
|
|
||||||
--item-icon-size: 8px;
|
|
||||||
--drag-handle-size: 14px;
|
|
||||||
--cursor: 0;
|
|
||||||
--count: 6;
|
|
||||||
/* (16 - 14) / 2 + (cursor / (count - 1)) * (140 - 16) */
|
|
||||||
--drag-handle-center-x: calc(
|
|
||||||
(var(--item-size) - var(--drag-handle-size)) / 2 +
|
|
||||||
(var(--cursor) / (var(--count) - 1)) *
|
|
||||||
(var(--width) - var(--item-size))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([disabled]) {
|
|
||||||
opacity: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-width-panel {
|
|
||||||
width: var(--width);
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: relative;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-width-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: var(--item-size);
|
|
||||||
height: var(--item-size);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-width-icon {
|
|
||||||
width: var(--item-icon-size);
|
|
||||||
height: var(--item-icon-size);
|
|
||||||
background-color: var(--affine-border-color);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-width-button[data-selected] .line-width-icon {
|
|
||||||
background-color: var(--affine-icon-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-handle {
|
|
||||||
position: absolute;
|
|
||||||
width: var(--drag-handle-size);
|
|
||||||
height: var(--drag-handle-size);
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--affine-icon-color);
|
|
||||||
z-index: 3;
|
|
||||||
transform: translateX(var(--drag-handle-center-x));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-line,
|
|
||||||
.line-width-overlay {
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
left: calc(var(--item-size) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-line {
|
|
||||||
width: calc(100% - var(--item-size));
|
|
||||||
background-color: var(--affine-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-width-overlay {
|
|
||||||
background-color: var(--affine-icon-color);
|
|
||||||
z-index: 1;
|
|
||||||
width: var(--drag-handle-center-x);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
private readonly _getDragHandlePosition = (e: PointerEvent) => {
|
|
||||||
return clamp(e.offsetX, 0, this.config.width);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _onPointerDown = (e: PointerEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this._onPointerMove(e);
|
|
||||||
|
|
||||||
const dispose = on(this, 'pointermove', this._onPointerMove);
|
|
||||||
this._disposables.add(once(this, 'pointerup', dispose));
|
|
||||||
this._disposables.add(once(this, 'pointerout', dispose));
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _onPointerMove = (e: PointerEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const x = this._getDragHandlePosition(e);
|
|
||||||
|
|
||||||
this._updateLineWidthPanelByDragHandlePosition(x);
|
|
||||||
};
|
|
||||||
|
|
||||||
private _onSelect(lineWidth: number) {
|
private _onSelect(lineWidth: number) {
|
||||||
// If the selected size is the same as the previous one, do nothing.
|
|
||||||
if (lineWidth === this.selectedSize) return;
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('select', {
|
new CustomEvent('select', {
|
||||||
detail: lineWidth,
|
detail: lineWidth,
|
||||||
@@ -133,98 +15,22 @@ export class EdgelessLineWidthPanel extends WithDisposable(LitElement) {
|
|||||||
cancelable: true,
|
cancelable: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.selectedSize = lineWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateLineWidthPanel(selectedSize: number) {
|
|
||||||
if (!this._lineWidthOverlay) return;
|
|
||||||
const index = this.lineWidths.findIndex(w => w === selectedSize);
|
|
||||||
if (index === -1) return;
|
|
||||||
|
|
||||||
this.style.setProperty('--cursor', `${index}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateLineWidthPanelByDragHandlePosition(x: number) {
|
|
||||||
// Calculate the selected size based on the drag handle position.
|
|
||||||
// Need to select the nearest size.
|
|
||||||
|
|
||||||
const {
|
|
||||||
config: { width, itemSize, count },
|
|
||||||
lineWidths,
|
|
||||||
} = this;
|
|
||||||
const targetWidth = width - itemSize;
|
|
||||||
const halfItemSize = itemSize / 2;
|
|
||||||
const offsetX = halfItemSize + (width - itemSize * count) / (count - 1) / 2;
|
|
||||||
const selectedSize = lineWidths.findLast((_, n) => {
|
|
||||||
const cx = halfItemSize + (n / (count - 1)) * targetWidth;
|
|
||||||
return x >= cx - offsetX && x < cx + offsetX;
|
|
||||||
});
|
|
||||||
if (!selectedSize) return;
|
|
||||||
|
|
||||||
this._updateLineWidthPanel(selectedSize);
|
|
||||||
this._onSelect(selectedSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
const {
|
|
||||||
style,
|
|
||||||
config: { width, itemSize, itemIconSize, dragHandleSize, count },
|
|
||||||
} = this;
|
|
||||||
style.setProperty('--width', `${width}px`);
|
|
||||||
style.setProperty('--item-size', `${itemSize}px`);
|
|
||||||
style.setProperty('--item-icon-size', `${itemIconSize}px`);
|
|
||||||
style.setProperty('--drag-handle-size', `${dragHandleSize}px`);
|
|
||||||
style.setProperty('--count', `${count}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
override firstUpdated() {
|
|
||||||
this._updateLineWidthPanel(this.selectedSize);
|
|
||||||
this._disposables.addFromEvent(this, 'pointerdown', this._onPointerDown);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`<div class="line-width-panel">
|
return html`<affine-slider
|
||||||
${repeat(
|
?disabled=${this.disabled}
|
||||||
this.lineWidths,
|
.range=${{ points: this.lineWidths }}
|
||||||
w => w,
|
.value=${this.selectedSize}
|
||||||
(w, n) =>
|
.tooltip=${this.hasTooltip ? 'Thickness' : undefined}
|
||||||
html`<div
|
@select=${(e: SliderSelectEvent) => {
|
||||||
class="line-width-button"
|
e.stopPropagation();
|
||||||
aria-label=${w}
|
this._onSelect(e.detail.value);
|
||||||
data-index=${n}
|
}}
|
||||||
?data-selected=${w <= this.selectedSize}
|
></affine-slider>`;
|
||||||
>
|
|
||||||
<div class="line-width-icon"></div>
|
|
||||||
</div>`
|
|
||||||
)}
|
|
||||||
<div class="drag-handle"></div>
|
|
||||||
<div class="bottom-line"></div>
|
|
||||||
<div class="line-width-overlay"></div>
|
|
||||||
${this.hasTooltip
|
|
||||||
? html`<affine-tooltip .offset=${8}>Thickness</affine-tooltip>`
|
|
||||||
: nothing}
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override willUpdate(changedProperties: PropertyValues<this>) {
|
@property({ attribute: false })
|
||||||
if (changedProperties.has('selectedSize')) {
|
|
||||||
this._updateLineWidthPanel(this.selectedSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@query('.line-width-overlay')
|
|
||||||
private accessor _lineWidthOverlay!: HTMLElement;
|
|
||||||
|
|
||||||
accessor config: Config = {
|
|
||||||
width: 140,
|
|
||||||
itemSize: 16,
|
|
||||||
itemIconSize: 8,
|
|
||||||
dragHandleSize: 14,
|
|
||||||
count: BRUSH_LINE_WIDTHS.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
@property({ attribute: false, type: Boolean })
|
|
||||||
accessor disabled = false;
|
accessor disabled = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
|
|||||||
6
blocksuite/affine/components/src/slider/index.ts
Normal file
6
blocksuite/affine/components/src/slider/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { Slider } from './slider';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
export function effects() {
|
||||||
|
customElements.define('affine-slider', Slider);
|
||||||
|
}
|
||||||
159
blocksuite/affine/components/src/slider/slider.ts
Normal file
159
blocksuite/affine/components/src/slider/slider.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { on, once } from '@blocksuite/affine-shared/utils';
|
||||||
|
import { clamp } from '@blocksuite/global/gfx';
|
||||||
|
import { WithDisposable } from '@blocksuite/global/lit';
|
||||||
|
import { PropTypes, requiredProperties } from '@blocksuite/std';
|
||||||
|
import { html, LitElement, nothing, type PropertyValues } from 'lit';
|
||||||
|
import { property } from 'lit/decorators.js';
|
||||||
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
|
import { styles } from './styles';
|
||||||
|
import type { SliderRange, SliderSelectEvent, SliderStyle } from './types';
|
||||||
|
import { isDiscreteRange } from './utils';
|
||||||
|
|
||||||
|
const defaultSliderStyle: SliderStyle = {
|
||||||
|
width: '100%',
|
||||||
|
itemSize: 16,
|
||||||
|
itemIconSize: 8,
|
||||||
|
dragHandleSize: 14,
|
||||||
|
};
|
||||||
|
|
||||||
|
@requiredProperties({
|
||||||
|
range: PropTypes.of(isDiscreteRange),
|
||||||
|
})
|
||||||
|
export class Slider extends WithDisposable(LitElement) {
|
||||||
|
static override styles = styles;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor value: number = 0;
|
||||||
|
|
||||||
|
@property({ attribute: true, type: Boolean })
|
||||||
|
accessor disabled = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor range!: SliderRange;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor sliderStyle: Partial<SliderStyle> | undefined = defaultSliderStyle;
|
||||||
|
|
||||||
|
private get _sliderStyle(): SliderStyle {
|
||||||
|
return {
|
||||||
|
...defaultSliderStyle,
|
||||||
|
...this.sliderStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onSelect(value: number) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('select', {
|
||||||
|
detail: { value },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}) satisfies SliderSelectEvent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateLineWidthPanelByDragHandlePosition(x: number) {
|
||||||
|
// Calculate the selected size based on the drag handle position.
|
||||||
|
// Need to select the nearest size.
|
||||||
|
|
||||||
|
const {
|
||||||
|
_sliderStyle: { itemSize },
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const width = this.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
const { points } = this.range;
|
||||||
|
const count = points.length;
|
||||||
|
|
||||||
|
const targetWidth = width - itemSize;
|
||||||
|
const halfItemSize = itemSize / 2;
|
||||||
|
const offsetX = halfItemSize + (width - itemSize * count) / (count - 1) / 2;
|
||||||
|
const selectedSize = points.findLast((_, n) => {
|
||||||
|
const cx = halfItemSize + (n / (count - 1)) * targetWidth;
|
||||||
|
return x >= cx - offsetX && x < cx + offsetX;
|
||||||
|
});
|
||||||
|
if (!selectedSize) return;
|
||||||
|
|
||||||
|
this._onSelect(selectedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _getDragHandlePosition = (e: PointerEvent) => {
|
||||||
|
const width = this.getBoundingClientRect().width;
|
||||||
|
return clamp(e.offsetX, 0, width);
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _onPointerDown = (e: PointerEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this._onPointerMove(e);
|
||||||
|
|
||||||
|
const dispose = on(this, 'pointermove', this._onPointerMove);
|
||||||
|
this._disposables.add(once(this, 'pointerup', dispose));
|
||||||
|
this._disposables.add(once(this, 'pointerout', dispose));
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _onPointerMove = (e: PointerEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const x = this._getDragHandlePosition(e);
|
||||||
|
|
||||||
|
this._updateLineWidthPanelByDragHandlePosition(x);
|
||||||
|
};
|
||||||
|
|
||||||
|
override connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
this._disposables.addFromEvent(this, 'pointerdown', this._onPointerDown);
|
||||||
|
this._disposables.addFromEvent(this, 'click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
|
const { style } = this;
|
||||||
|
if (changedProperties.has('sliderStyle')) {
|
||||||
|
const {
|
||||||
|
_sliderStyle: { width, itemSize, itemIconSize, dragHandleSize },
|
||||||
|
} = this;
|
||||||
|
style.setProperty('--width', width);
|
||||||
|
style.setProperty('--item-size', `${itemSize}px`);
|
||||||
|
style.setProperty('--item-icon-size', `${itemIconSize}px`);
|
||||||
|
style.setProperty('--drag-handle-size', `${dragHandleSize}px`);
|
||||||
|
}
|
||||||
|
if (changedProperties.has('range')) {
|
||||||
|
style.setProperty('--count', `${this.range.points.length}`);
|
||||||
|
}
|
||||||
|
if (changedProperties.has('value')) {
|
||||||
|
const index = this.range.points.findIndex(p => p === this.value);
|
||||||
|
style.setProperty('--cursor', `${index}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`<div class="slider-container">
|
||||||
|
${repeat(
|
||||||
|
this.range.points,
|
||||||
|
w => w,
|
||||||
|
(w, n) =>
|
||||||
|
html`<div
|
||||||
|
class="point-button"
|
||||||
|
aria-label=${w}
|
||||||
|
data-index=${n}
|
||||||
|
?data-selected=${w <= this.value}
|
||||||
|
>
|
||||||
|
<div class="point-circle"></div>
|
||||||
|
</div>`
|
||||||
|
)}
|
||||||
|
<div class="drag-handle"></div>
|
||||||
|
<div class="bottom-line"></div>
|
||||||
|
<div class="slider-selected-overlay"></div>
|
||||||
|
${this.tooltip
|
||||||
|
? html`<affine-tooltip .offset=${8}>${this.tooltip}</affine-tooltip>`
|
||||||
|
: nothing}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
74
blocksuite/affine/components/src/slider/styles.ts
Normal file
74
blocksuite/affine/components/src/slider/styles.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
|
import { css } from 'lit';
|
||||||
|
|
||||||
|
export const styles = css`
|
||||||
|
:host([disabled]) {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
--drag-handle-center-x: calc(
|
||||||
|
(var(--item-size) - var(--drag-handle-size)) / 2 +
|
||||||
|
(var(--cursor) / (var(--count) - 1)) *
|
||||||
|
calc(var(--width) - var(--item-size))
|
||||||
|
);
|
||||||
|
|
||||||
|
width: var(--width);
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--item-size);
|
||||||
|
height: var(--item-size);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-circle {
|
||||||
|
width: var(--item-icon-size);
|
||||||
|
height: var(--item-icon-size);
|
||||||
|
background-color: ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-button[data-selected] .point-circle {
|
||||||
|
background-color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: var(--drag-handle-size);
|
||||||
|
height: var(--drag-handle-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
|
z-index: 3;
|
||||||
|
left: var(--drag-handle-center-x);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-line,
|
||||||
|
.slider-selected-overlay {
|
||||||
|
position: absolute;
|
||||||
|
height: 1px;
|
||||||
|
left: calc(var(--item-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-line {
|
||||||
|
width: calc(100% - var(--item-size));
|
||||||
|
background-color: ${unsafeCSSVarV2('layer/insideBorder/border')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-selected-overlay {
|
||||||
|
background-color: ${unsafeCSSVarV2('icon/primary')};
|
||||||
|
z-index: 1;
|
||||||
|
width: var(--drag-handle-center-x);
|
||||||
|
}
|
||||||
|
`;
|
||||||
22
blocksuite/affine/components/src/slider/types.ts
Normal file
22
blocksuite/affine/components/src/slider/types.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export type SliderRange = {
|
||||||
|
/**
|
||||||
|
* a series of points in slider
|
||||||
|
*/
|
||||||
|
points: number[];
|
||||||
|
/**
|
||||||
|
* whether the points are uniformly distributed
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
uniform?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SliderStyle = {
|
||||||
|
width: string;
|
||||||
|
itemSize: number;
|
||||||
|
itemIconSize: number;
|
||||||
|
dragHandleSize: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SliderSelectEvent = CustomEvent<{
|
||||||
|
value: number;
|
||||||
|
}>;
|
||||||
10
blocksuite/affine/components/src/slider/utils.ts
Normal file
10
blocksuite/affine/components/src/slider/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { SliderRange } from './types';
|
||||||
|
|
||||||
|
export function isDiscreteRange(range: unknown): range is SliderRange {
|
||||||
|
return (
|
||||||
|
typeof range === 'object' &&
|
||||||
|
range !== null &&
|
||||||
|
'points' in range &&
|
||||||
|
Array.isArray(range.points)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import { effects as componentOpenDocDropdownMenuEffects } from '@blocksuite/affi
|
|||||||
import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal';
|
import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal';
|
||||||
import { effects as componentResourceEffects } from '@blocksuite/affine-components/resource';
|
import { effects as componentResourceEffects } from '@blocksuite/affine-components/resource';
|
||||||
import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu';
|
import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu';
|
||||||
|
import { effects as componentSliderEffects } from '@blocksuite/affine-components/slider';
|
||||||
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
|
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
|
||||||
import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button';
|
import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button';
|
||||||
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
|
import { ToggleSwitch } from '@blocksuite/affine-components/toggle-switch';
|
||||||
@@ -53,6 +54,7 @@ export function effects() {
|
|||||||
componentViewDropdownMenuEffects();
|
componentViewDropdownMenuEffects();
|
||||||
componentTooltipContentWithShortcutEffects();
|
componentTooltipContentWithShortcutEffects();
|
||||||
componentSizeDropdownMenuEffects();
|
componentSizeDropdownMenuEffects();
|
||||||
|
componentSliderEffects();
|
||||||
componentEdgelessLineWidthEffects();
|
componentEdgelessLineWidthEffects();
|
||||||
componentEdgelessLineStylesEffects();
|
componentEdgelessLineStylesEffects();
|
||||||
componentEdgelessShapeColorPickerEffects();
|
componentEdgelessShapeColorPickerEffects();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const PropTypes = {
|
|||||||
if (typeof value !== 'object' || value === null) return false;
|
if (typeof value !== 'object' || value === null) return false;
|
||||||
return Object.values(value).every(validator);
|
return Object.values(value).every(validator);
|
||||||
},
|
},
|
||||||
|
of: (validator: ValidatorFunction) => (value: unknown) => validator(value),
|
||||||
};
|
};
|
||||||
|
|
||||||
function validatePropTypes<T extends InstanceType<Constructor>>(
|
function validatePropTypes<T extends InstanceType<Constructor>>(
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ test('should add highlighter', async ({ page }) => {
|
|||||||
await expect(toolbar).toBeVisible();
|
await expect(toolbar).toBeVisible();
|
||||||
|
|
||||||
const lineWidthButton = toolbar
|
const lineWidthButton = toolbar
|
||||||
.locator('.line-width-button[data-selected]')
|
.locator('affine-slider')
|
||||||
|
.locator('.point-button[data-selected]')
|
||||||
.last();
|
.last();
|
||||||
const defaultLineWidth = await lineWidthButton.getAttribute('aria-label');
|
const defaultLineWidth = await lineWidthButton.getAttribute('aria-label');
|
||||||
|
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ test.describe('edgeless note element toolbar', () => {
|
|||||||
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
||||||
await toolbar.locator('.mode-solid').click();
|
await toolbar.locator('.mode-solid').click();
|
||||||
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
await toolbar.getByRole('button', { name: 'Border style' }).click();
|
||||||
await toolbar.locator('edgeless-line-width-panel').getByLabel('8').click();
|
await toolbar.locator('affine-slider').getByLabel('8').click();
|
||||||
|
|
||||||
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
|
expect(await getNoteEdgelessProps(page, noteId)).toEqual({
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -157,10 +157,6 @@ test('change connector line width', async ({ page }) => {
|
|||||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
||||||
await changeConnectorStrokeWidth(page, 5);
|
await changeConnectorStrokeWidth(page, 5);
|
||||||
|
|
||||||
await waitNextFrame(page);
|
|
||||||
|
|
||||||
await triggerComponentToolbarAction(page, 'changeConnectorStrokeStyles');
|
|
||||||
|
|
||||||
const pickedColor = await pickColorAtPoints(page, [
|
const pickedColor = await pickColorAtPoints(page, [
|
||||||
[start.x + 5, start.y],
|
[start.x + 5, start.y],
|
||||||
[start.x + 10, start.y],
|
[start.x + 10, start.y],
|
||||||
|
|||||||
@@ -670,7 +670,7 @@ export async function selectBrushSize(page: Page, size: string) {
|
|||||||
twelve: 6,
|
twelve: 6,
|
||||||
};
|
};
|
||||||
const sizeButton = page.locator(
|
const sizeButton = page.locator(
|
||||||
`edgeless-pen-menu .line-width-panel .line-width-button:nth-child(${sizeIndexMap[size]})`
|
`edgeless-pen-menu edgeless-line-width-panel .point-button:nth-child(${sizeIndexMap[size]})`
|
||||||
);
|
);
|
||||||
await sizeButton.click();
|
await sizeButton.click();
|
||||||
}
|
}
|
||||||
@@ -794,7 +794,7 @@ export async function updateExistedBrushElementSize(
|
|||||||
) {
|
) {
|
||||||
// get the nth brush size button
|
// get the nth brush size button
|
||||||
const btn = page.locator(
|
const btn = page.locator(
|
||||||
`.line-width-panel > div:nth-child(${nthSizeButton})`
|
`edgeless-line-width-panel .point-button:nth-child(${nthSizeButton})`
|
||||||
);
|
);
|
||||||
|
|
||||||
await btn.click();
|
await btn.click();
|
||||||
@@ -1192,9 +1192,7 @@ export async function triggerComponentToolbarAction(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'changeConnectorStrokeStyles': {
|
case 'changeConnectorStrokeStyles': {
|
||||||
const button = locatorComponentToolbar(page).getByRole('button', {
|
const button = locatorComponentToolbar(page).getByLabel('Stroke style');
|
||||||
name: 'Stroke style',
|
|
||||||
});
|
|
||||||
await button.click();
|
await button.click();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1438,8 +1436,7 @@ export async function resizeConnectorByStartCapitalHandler(
|
|||||||
export function getEdgelessLineWidthPanel(page: Page) {
|
export function getEdgelessLineWidthPanel(page: Page) {
|
||||||
return page
|
return page
|
||||||
.locator('affine-toolbar-widget editor-toolbar')
|
.locator('affine-toolbar-widget editor-toolbar')
|
||||||
.locator('edgeless-line-width-panel')
|
.locator('edgeless-line-width-panel');
|
||||||
.locator('.line-width-panel');
|
|
||||||
}
|
}
|
||||||
export async function changeShapeStrokeWidth(page: Page) {
|
export async function changeShapeStrokeWidth(page: Page) {
|
||||||
const lineWidthPanel = getEdgelessLineWidthPanel(page);
|
const lineWidthPanel = getEdgelessLineWidthPanel(page);
|
||||||
@@ -1501,7 +1498,7 @@ export function locatorConnectorStrokeWidthButton(
|
|||||||
) {
|
) {
|
||||||
return locatorComponentToolbar(page)
|
return locatorComponentToolbar(page)
|
||||||
.locator('edgeless-line-width-panel')
|
.locator('edgeless-line-width-panel')
|
||||||
.locator(`.line-width-button:nth-child(${buttonPosition})`);
|
.locator(`.point-button:nth-child(${buttonPosition})`);
|
||||||
}
|
}
|
||||||
export async function changeConnectorStrokeWidth(
|
export async function changeConnectorStrokeWidth(
|
||||||
page: Page,
|
page: Page,
|
||||||
|
|||||||
Reference in New Issue
Block a user