refactor(editor): remove edit view of database block properties (#10748)

This commit is contained in:
zzj3720
2025-03-10 16:24:44 +00:00
parent 4a45cc9ba4
commit db707dff7f
49 changed files with 1387 additions and 1782 deletions

View File

@@ -20,8 +20,9 @@ import { flip, offset, shift } from '@floating-ui/dom';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { cssVarV2 } from '@toeverything/theme/v2';
import { nothing } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html } from 'lit/static-html.js';
@@ -37,7 +38,22 @@ import {
import { verticalListSortingStrategy } from '../../utils/wc-dnd/sort/strategies/index.js';
import { arrayMove } from '../../utils/wc-dnd/utils/array-move.js';
import { getTagColor, selectOptionColors } from './colors.js';
import { styles } from './styles.js';
import {
selectedStyle,
selectOptionContentStyle,
selectOptionDragHandlerStyle,
selectOptionIconStyle,
selectOptionNewIconStyle,
selectOptionsContainerStyle,
selectOptionsTipsStyle,
selectOptionStyle,
tagContainerStyle,
tagDeleteIconStyle,
tagSelectContainerStyle,
tagSelectInputContainerStyle,
tagSelectInputStyle,
tagTextStyle,
} from './styles.css.js';
type RenderOption = {
value: string;
@@ -70,23 +86,23 @@ class TagManager {
);
};
color = signal(getTagColor());
color$ = signal(getTagColor());
createOption = () => {
const value = this.text.value.trim();
const value = this.text$.value.trim();
if (value === '') return;
const id = nanoid();
this.ops.onOptionsChange([
{
id: id,
value: value,
color: this.color.value,
color: this.color$.value,
},
...this.ops.options.value,
]);
this.selectTag(id);
this.text.value = '';
this.color.value = getTagColor();
this.text$.value = '';
this.color$.value = getTagColor();
if (this.isSingleMode) {
this.ops.onComplete?.();
}
@@ -101,12 +117,12 @@ class TagManager {
filteredOptions$ = computed(() => {
let matched = false;
const options: RenderOption[] = [];
for (const option of this.options.value) {
for (const option of this.options$.value) {
if (
!this.text.value ||
!this.text$.value ||
option.value
.toLocaleLowerCase()
.includes(this.text.value.toLocaleLowerCase())
.includes(this.text$.value.toLocaleLowerCase())
) {
options.push({
...option,
@@ -114,15 +130,15 @@ class TagManager {
select: () => this.selectTag(option.id),
});
}
if (option.value === this.text.value) {
if (option.value === this.text$.value) {
matched = true;
}
}
if (this.text.value && !matched) {
if (this.text$.value && !matched) {
options.push({
id: 'create',
color: this.color.value,
value: this.text.value,
color: this.color$.value,
value: this.text$.value,
isCreate: true,
select: this.createOption,
});
@@ -136,37 +152,37 @@ class TagManager {
);
});
text = signal('');
text$ = signal('');
get isSingleMode() {
return this.ops.mode === 'single';
}
get options() {
get options$() {
return this.ops.options;
}
get value() {
get value$() {
return this.ops.value;
}
constructor(private readonly ops: TagManagerOptions) {}
deleteTag(id: string) {
this.ops.onChange(this.value.value.filter(item => item !== id));
this.ops.onChange(this.value$.value.filter(item => item !== id));
}
isSelected(id: string) {
return this.value.value.includes(id);
return this.value$.value.includes(id);
}
selectTag(id: string) {
if (this.isSelected(id)) {
return;
}
const newValue = this.isSingleMode ? [id] : [...this.value.value, id];
const newValue = this.isSingleMode ? [id] : [...this.value$.value, id];
this.ops.onChange(newValue);
this.text.value = '';
this.text$.value = '';
if (this.isSingleMode) {
requestAnimationFrame(() => {
this.ops.onComplete?.();
@@ -178,8 +194,6 @@ class TagManager {
export class MultiTagSelect extends SignalWatcher(
WithDisposable(ShadowlessElement)
) {
static override styles = styles;
private readonly _clickItemOption = (e: MouseEvent, id: string) => {
e.stopPropagation();
const option = this.options.value.find(v => v.id === id);
@@ -236,7 +250,7 @@ export class MultiTagSelect extends SignalWatcher(
};
private readonly _onInput = (event: KeyboardEvent) => {
this.tagManager.text.value = (event.target as HTMLInputElement).value;
this.tagManager.text$.value = (event.target as HTMLInputElement).value;
};
private readonly _onInputKeydown = (event: KeyboardEvent) => {
@@ -251,10 +265,10 @@ export class MultiTagSelect extends SignalWatcher(
this.selectedTag$.value?.select();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
this.setSelectedOption(this.selectedIndex - 1);
this.setSelectedOption(this.selectedIndex$.value - 1);
} else if (event.key === 'ArrowDown') {
event.preventDefault();
this.setSelectedOption(this.selectedIndex + 1);
this.setSelectedOption(this.selectedIndex$.value + 1);
} else if (event.key === 'Escape') {
this.onComplete();
}
@@ -263,7 +277,7 @@ export class MultiTagSelect extends SignalWatcher(
private readonly tagManager = new TagManager(this);
private readonly selectedTag$ = computed(() => {
return this.tagManager.filteredOptions$.value[this.selectedIndex];
return this.tagManager.filteredOptions$.value[this.selectedIndex$.value];
});
sortContext = createSortContext({
@@ -301,12 +315,12 @@ export class MultiTagSelect extends SignalWatcher(
});
private get text() {
return this.tagManager.text;
return this.tagManager.text$;
}
private renderInput() {
return html`
<div class="tag-select-input-container">
<div class="${tagSelectInputContainerStyle}">
${this.value.value.map(id => {
const option = this.tagManager.optionsMap$.value.get(id);
if (!option) {
@@ -317,7 +331,8 @@ export class MultiTagSelect extends SignalWatcher(
);
})}
<input
class="tag-select-input"
class="${tagSelectInputStyle}"
${ref(this._selectInput)}
placeholder="Type here..."
.value="${this.text.value}"
@input="${this._onInput}"
@@ -332,10 +347,10 @@ export class MultiTagSelect extends SignalWatcher(
const style = styleMap({
backgroundColor: color,
});
return html` <div class="tag-container" style=${style}>
<div class="tag-text">${name}</div>
return html` <div class="${tagContainerStyle}" style=${style}>
<div data-testid="tag-name" class="${tagTextStyle}">${name}</div>
${onDelete
? html` <div class="tag-delete-icon" @click="${onDelete}">
? html` <div class="${tagDeleteIconStyle}" @click="${onDelete}">
${CloseIcon()}
</div>`
: nothing}
@@ -349,19 +364,19 @@ export class MultiTagSelect extends SignalWatcher(
'layer/insideBorder/border'
)};margin: 4px 0;"
></div>
<div class="select-options-tips">Select tag or create one</div>
<div class="select-options-container">
<div class="${selectOptionsTipsStyle}">Select tag or create one</div>
<div data-testid="tag-option-list" class="${selectOptionsContainerStyle}">
${repeat(
this.tagManager.filteredOptions$.value,
select => select.id,
(select, index) => {
const isSelected = index === this.selectedIndex;
const isSelected = index === this.selectedIndex$.value;
const mouseenter = () => {
this.setSelectedOption(index);
};
const classes = classMap({
'select-option': true,
selected: isSelected,
[selectOptionStyle]: true,
[selectedStyle]: isSelected,
});
const clickOption = (e: MouseEvent) => {
e.stopPropagation();
@@ -374,21 +389,24 @@ export class MultiTagSelect extends SignalWatcher(
@mouseenter="${mouseenter}"
@click="${select.select}"
>
<div class="select-option-content">
<div class="${selectOptionContentStyle}">
${select.isCreate
? html` <div class="select-option-new-icon">Create</div>`
? html` <div class="${selectOptionNewIconStyle}">
Create
</div>`
: html`
<div
${dragHandler(select.id)}
class="select-option-drag-handler"
class="${selectOptionDragHandlerStyle}"
></div>
`}
${this.renderTag(select.value, select.color)}
</div>
${!select.isCreate
? html` <div
class="select-option-icon"
class="${selectOptionIconStyle}"
@click="${clickOption}"
data-testid="option-more"
>
${MoreHorizontalIcon()}
</div>`
@@ -402,7 +420,7 @@ export class MultiTagSelect extends SignalWatcher(
}
private setSelectedOption(index: number) {
this.selectedIndex = rangeWrap(
this.selectedIndex$.value = rangeWrap(
index,
0,
this.tagManager.filteredOptions$.value.length
@@ -410,28 +428,29 @@ export class MultiTagSelect extends SignalWatcher(
}
protected override firstUpdated() {
const disposables = this.disposables;
this.classList.add(tagSelectContainerStyle);
requestAnimationFrame(() => {
this._selectInput.focus();
this._selectInput.value?.focus();
});
this._disposables.addFromEvent(this, 'click', () => {
this._selectInput.focus();
disposables.addFromEvent(this, 'click', () => {
this._selectInput.value?.focus();
});
this._disposables.addFromEvent(this._selectInput, 'copy', e => {
disposables.addFromEvent(this._selectInput.value, 'copy', e => {
e.stopPropagation();
});
this._disposables.addFromEvent(this._selectInput, 'cut', e => {
disposables.addFromEvent(this._selectInput.value, 'cut', e => {
e.stopPropagation();
});
}
override render() {
this.setSelectedOption(this.selectedIndex);
this.setSelectedOption(this.selectedIndex$.value);
return html` ${this.renderInput()} ${this.renderTags()} `;
}
@query('.tag-select-input')
private accessor _selectInput!: HTMLInputElement;
private readonly _selectInput = createRef<HTMLInputElement>();
@property()
accessor mode: 'multi' | 'single' = 'multi';
@@ -448,8 +467,7 @@ export class MultiTagSelect extends SignalWatcher(
@property({ attribute: false })
accessor options!: ReadonlySignal<SelectTag[]>;
@state()
private accessor selectedIndex = 0;
private readonly selectedIndex$ = signal(0);
@property({ attribute: false })
accessor value!: ReadonlySignal<string[]>;
@@ -464,7 +482,7 @@ declare global {
const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
const tagManager = new TagManager(ops);
const onInput = (e: InputEvent) => {
tagManager.text.value = (e.target as HTMLInputElement).value;
tagManager.text$.value = (e.target as HTMLInputElement).value;
};
return popMenu(target, {
options: {
@@ -491,12 +509,12 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
backgroundColor: option.color,
width: 'max-content',
});
return html` <div class="tag-container" style=${style}>
<div class="tag-text">${option.value}</div>
return html` <div class="${tagContainerStyle}" style=${style}>
<div class="${tagTextStyle}">${option.value}</div>
</div>`;
})}
<input
.value="${tagManager.text.value}"
.value="${tagManager.text$.value}"
@input="${onInput}"
placeholder="Type here..."
type="text"
@@ -522,8 +540,8 @@ const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => {
${option.isCreate
? html` <div style="margin-right: 8px;">Create</div>`
: ''}
<div class="tag-container" style=${style}>
<div class="tag-text">${option.value}</div>
<div class="${tagContainerStyle}" style=${style}>
<div class="${tagTextStyle}">${option.value}</div>
</div>
</div>
`;

View File

@@ -60,7 +60,10 @@ export class MultiTagView extends WithDisposable(ShadowlessElement) {
const style = styleMap({
backgroundColor: getColorByColor(option.color),
});
return html`<span class="select-selected" style=${style}
return html`<span
data-testid="tag-selected"
class="select-selected"
style=${style}
>${option.value}</span
>`;
})}

View File

@@ -0,0 +1,147 @@
import { baseTheme } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const tagSelectContainerStyle = style({
position: 'absolute',
zIndex: 2,
color: cssVarV2('text/primary'),
border: `0.5px solid ${cssVarV2('layer/insideBorder/blackBorder')}`,
borderRadius: '8px',
background: cssVarV2('layer/background/primary'),
boxShadow: 'var(--affine-shadow-1)',
fontFamily: 'var(--affine-font-family)',
maxWidth: '400px',
padding: '8px',
display: 'flex',
flexDirection: 'column',
gap: '4px',
'@media': {
print: {
display: 'none',
},
},
});
export const tagSelectInputContainerStyle = style({
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: '6px',
padding: '4px',
});
export const tagSelectInputStyle = style({
flex: '1 1 0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
color: cssVarV2('text/primary'),
backgroundColor: 'transparent',
lineHeight: '22px',
fontSize: '14px',
outline: 'none',
'::placeholder': {
color: 'var(--affine-placeholder-color)',
},
});
export const selectOptionsTipsStyle = style({
padding: '4px',
color: cssVarV2('text/secondary'),
fontSize: '14px',
fontWeight: 500,
lineHeight: '22px',
userSelect: 'none',
});
export const selectOptionsContainerStyle = style({
maxHeight: '400px',
overflowY: 'auto',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
gap: '4px',
});
export const selectOptionStyle = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '4px 4px 4px 0',
borderRadius: '4px',
cursor: 'pointer',
});
export const selectedStyle = style({
background: cssVarV2('layer/background/hoverOverlay'),
});
export const tagContainerStyle = style({
display: 'flex',
alignItems: 'center',
padding: '0 8px',
gap: '4px',
borderRadius: '4px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
border: `1px solid ${cssVarV2('database/border')}`,
userSelect: 'none',
});
export const tagTextStyle = style({
fontSize: '14px',
lineHeight: '22px',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
export const tagDeleteIconStyle = style({
display: 'flex',
alignItems: 'center',
color: cssVarV2('chip/label/text'),
});
export const selectOptionContentStyle = style({
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
});
export const selectOptionIconStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '20px',
borderRadius: '4px',
cursor: 'pointer',
visibility: 'hidden',
color: cssVarV2('icon/primary'),
marginLeft: '4px',
':hover': {
background: cssVarV2('layer/background/hoverOverlay'),
},
selectors: {
[`${selectedStyle} &`]: {
visibility: 'visible',
},
},
});
export const selectOptionDragHandlerStyle = style({
width: '4px',
height: '12px',
borderRadius: '1px',
backgroundColor: cssVarV2('button/grabber/default'),
marginRight: '4px',
cursor: '-webkit-grab',
flexShrink: 0,
});
export const selectOptionNewIconStyle = style({
fontSize: '14px',
lineHeight: '22px',
color: cssVarV2('text/primary'),
marginRight: '8px',
marginLeft: '4px',
});

View File

@@ -1,251 +0,0 @@
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit';
export const styles = css`
affine-multi-tag-select {
position: absolute;
z-index: 2;
color: ${unsafeCSSVarV2('text/primary')};
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/blackBorder')};
border-radius: 8px;
background: ${unsafeCSSVarV2('layer/background/primary')};
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
font-family: var(--affine-font-family);
max-width: 400px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
@media print {
affine-multi-tag-select {
display: none;
}
}
.tag-select-input-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
padding: 4px;
}
.tag-select-input {
flex: 1 1 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: ${unsafeCSSVarV2('text/primary')};
background-color: transparent;
line-height: 22px;
font-size: 14px;
outline: none;
}
.tag-select-input::placeholder {
color: var(--affine-placeholder-color);
}
.select-options-tips {
padding: 4px;
color: ${unsafeCSSVarV2('text/secondary')};
font-size: 14px;
font-weight: 500;
line-height: 22px;
user-select: none;
}
.select-options-container {
max-height: 400px;
overflow-y: auto;
user-select: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.select-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 4px 4px 0;
border-radius: 4px;
cursor: pointer;
}
.tag-container {
display: flex;
align-items: center;
padding: 0 8px;
gap: 4px;
border-radius: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
border: 1px solid ${unsafeCSSVarV2('database/border')};
user-select: none;
}
.tag-text {
font-size: 14px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-delete-icon {
display: flex;
align-items: center;
color: ${unsafeCSSVarV2('chip/label/text')};
}
.select-option.selected {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.select-option-content {
display: flex;
align-items: center;
overflow: hidden;
}
.select-option-icon {
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
border-radius: 4px;
cursor: pointer;
visibility: hidden;
color: ${unsafeCSSVarV2('icon/primary')};
margin-left: 4px;
}
.select-option.selected .select-option-icon {
visibility: visible;
}
.select-option-icon:hover {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.select-option-drag-handler {
width: 4px;
height: 12px;
border-radius: 1px;
background-color: ${unsafeCSSVarV2('button/grabber/default')};
margin-right: 4px;
cursor: -webkit-grab;
flex-shrink: 0;
}
.select-option-new-icon {
font-size: 14px;
line-height: 22px;
color: ${unsafeCSSVarV2('text/primary')};
margin-right: 8px;
margin-left: 4px;
}
// .select-selected-text {
// width: calc(100% - 16px);
// white-space: nowrap;
// text-overflow: ellipsis;
// overflow: hidden;
// }
//
// .select-selected > .close-icon {
// display: flex;
// align-items: center;
// }
//
// .select-selected > .close-icon:hover {
// cursor: pointer;
// }
//
// .select-selected > .close-icon > svg {
// fill: var(--affine-black-90);
// }
//
// .select-option-new {
// display: flex;
// flex-direction: row;
// align-items: center;
// height: 36px;
// padding: 4px;
// gap: 5px;
// border-radius: 4px;
// background: var(--affine-selected-color);
// }
//
// .select-option-new-text {
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// height: 28px;
// padding: 2px 10px;
// border-radius: 4px;
// background: var(--affine-tag-red);
// }
//
// .select-option-new-icon {
// display: flex;
// align-items: center;
// gap: 6px;
// height: 28px;
// color: var(--affine-text-primary-color);
// margin-right: 8px;
// }
//
// .select-option-new-icon svg {
// width: 16px;
// height: 16px;
// }
//
// .select-option {
// position: relative;
// display: flex;
// justify-content: space-between;
// align-items: center;
// padding: 4px;
// border-radius: 4px;
// margin-bottom: 4px;
// cursor: pointer;
// }
//
// .select-option.selected {
// background: var(--affine-hover-color);
// }
//
// .select-option-text-container {
// width: 100%;
// overflow: hidden;
// display: flex;
// }
//
// .select-option-group-name {
// font-size: 9px;
// padding: 0 2px;
// border-radius: 2px;
// }
//
// .select-option-name {
// padding: 4px 8px;
// border-radius: 4px;
// white-space: nowrap;
// text-overflow: ellipsis;
// overflow: hidden;
// }
//
//
// .select-option-icon:hover {
// background: var(--affine-hover-color);
// }
//
// .select-option-icon svg {
// width: 16px;
// height: 16px;
// pointer-events: none;
// }
`;

View File

@@ -237,18 +237,18 @@ export class RecordField extends SignalWatcher(
const props: CellRenderProps = {
cell: this.cell$.value,
isEditing: this.editing,
isEditing$: this.isEditing$,
selectCurrentCell: this.changeEditing,
};
const renderer = this.column.renderer$.value;
if (!renderer) {
return;
}
const { view, edit } = renderer;
const { view } = renderer;
const contentClass = classMap({
'field-content': true,
empty: !this.editing && this.cell$.value.isEmpty$.value,
'is-editing': this.editing,
empty: !this.isEditing$.value && this.cell$.value.isEmpty$.value,
'is-editing': this.isEditing$.value,
'is-focus': this.isFocus,
});
return html`
@@ -261,7 +261,7 @@ export class RecordField extends SignalWatcher(
</div>
</div>
<div @click="${this._click}" class="${contentClass}">
${renderUniLit(this.editing && edit ? edit : view, props, {
${renderUniLit(view, props, {
ref: this._cell,
class: 'kanban-cell',
})}
@@ -269,8 +269,7 @@ export class RecordField extends SignalWatcher(
`;
}
@state()
accessor editing = false;
isEditing$ = signal(false);
@state()
accessor isFocus = false;

View File

@@ -61,13 +61,11 @@ export class DetailSelection {
const cell = container.cell;
if (selection.isEditing) {
requestAnimationFrame(() => {
cell?.onExitEditMode();
});
cell?.beforeExitEditingMode();
if (cell?.blurCell()) {
container.blur();
}
container.editing = false;
container.isEditing$.value = false;
} else {
container.blur();
}
@@ -85,11 +83,13 @@ export class DetailSelection {
container.isFocus = true;
const cell = container.cell;
if (selection.isEditing) {
cell?.onEnterEditMode();
if (cell?.focusCell()) {
container.focus();
}
container.editing = true;
container.isEditing$.value = true;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else {
container.focus();
}

View File

@@ -1,6 +1,7 @@
import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed } from '@preact/signals-core';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import type { PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import type { Cell } from '../view-manager/cell.js';
@@ -56,29 +57,37 @@ export abstract class BaseCellRenderer<
return true;
}
type: string | undefined;
protected override shouldUpdate(_changedProperties: PropertyValues): boolean {
return this.cell.property.type$.value === this.type;
}
override connectedCallback() {
super.connectedCallback();
this.type = this.cell.property.type$.value;
this.dataset.testid = this.type;
this.style.width = '100%';
this._disposables.addFromEvent(this, 'click', e => {
if (this.isEditing) {
if (this.isEditing$.value) {
e.stopPropagation();
}
});
this._disposables.addFromEvent(this, 'copy', e => {
if (!this.isEditing) return;
if (!this.isEditing$.value) return;
e.stopPropagation();
this.onCopy(e);
});
this._disposables.addFromEvent(this, 'cut', e => {
if (!this.isEditing) return;
if (!this.isEditing$.value) return;
e.stopPropagation();
this.onCut(e);
});
this._disposables.addFromEvent(this, 'paste', e => {
if (!this.isEditing) return;
if (!this.isEditing$.value) return;
e.stopPropagation();
this.onPaste(e);
});
@@ -92,26 +101,32 @@ export abstract class BaseCellRenderer<
this.requestUpdate();
}
onChange(value: Value | undefined): void {
valueSetImmediate(value: Value | undefined): void {
this.cell.valueSet(value);
}
valueSetNextTick(value: Value | undefined) {
requestAnimationFrame(() => {
this.cell.valueSet(value);
});
}
onCopy(_e: ClipboardEvent) {}
onCut(_e: ClipboardEvent) {}
onEnterEditMode(): void {
afterEnterEditingMode(): void {
// do nothing
}
onExitEditMode() {
beforeExitEditingMode() {
// do nothing
}
onPaste(_e: ClipboardEvent) {}
@property({ attribute: false })
accessor isEditing!: boolean;
accessor isEditing$!: ReadonlySignal<boolean>;
@property({ attribute: false })
accessor selectCurrentCell!: (editing: boolean) => void;

View File

@@ -1,4 +1,5 @@
import type { UniComponent } from '@blocksuite/affine-shared/types';
import type { ReadonlySignal } from '@preact/signals-core';
import type { Cell } from '../view-manager/cell.js';
@@ -7,16 +8,15 @@ export interface CellRenderProps<
Value = unknown,
> {
cell: Cell<Value, Data>;
isEditing: boolean;
isEditing$: ReadonlySignal<boolean>;
selectCurrentCell: (editing: boolean) => void;
}
export interface DataViewCellLifeCycle {
beforeEnterEditMode(): boolean;
beforeExitEditingMode(): void;
onEnterEditMode(): void;
onExitEditMode(): void;
afterEnterEditingMode(): void;
focusCell(): boolean;
@@ -35,5 +35,4 @@ export type CellRenderer<
Value = unknown,
> = {
view: DataViewCellComponent<Data, Value>;
edit?: DataViewCellComponent<Data, Value>;
};

View File

@@ -14,31 +14,13 @@ import { GroupSetting } from './core/group-by/setting.js';
import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js';
import { AnyRender } from './core/utils/uni-component/render-template.js';
import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js';
import {
DateCell,
DateCellEditing,
} from './property-presets/date/cell-renderer.js';
import { DateCell } from './property-presets/date/cell-renderer.js';
import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js';
import {
MultiSelectCell,
MultiSelectCellEditing,
} from './property-presets/multi-select/cell-renderer.js';
import {
NumberCell,
NumberCellEditing,
} from './property-presets/number/cell-renderer.js';
import {
ProgressCell,
ProgressCellEditing,
} from './property-presets/progress/cell-renderer.js';
import {
SelectCell,
SelectCellEditing,
} from './property-presets/select/cell-renderer.js';
import {
TextCell,
TextCellEditing,
} from './property-presets/text/cell-renderer.js';
import { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js';
import { NumberCell } from './property-presets/number/cell-renderer.js';
import { ProgressCell } from './property-presets/progress/cell-renderer.js';
import { SelectCell } from './property-presets/select/cell-renderer.js';
import { TextCell } from './property-presets/text/cell-renderer.js';
import { DataViewKanban, DataViewTable } from './view-presets/index.js';
import { MobileKanbanCard } from './view-presets/kanban/mobile/card.js';
import { MobileKanbanCell } from './view-presets/kanban/mobile/cell.js';
@@ -83,16 +65,8 @@ import { DataViewHeaderViews } from './widget-presets/views-bar/views-view.js';
export function effects() {
customElements.define('affine-database-progress-cell', ProgressCell);
customElements.define(
'affine-database-progress-cell-editing',
ProgressCellEditing
);
customElements.define('data-view-header-tools', DataViewHeaderTools);
customElements.define('affine-database-number-cell', NumberCell);
customElements.define(
'affine-database-number-cell-editing',
NumberCellEditing
);
customElements.define(
'affine-database-cell-container',
DatabaseCellContainer
@@ -102,24 +76,14 @@ export function effects() {
customElements.define('any-render', AnyRender);
customElements.define('affine-database-image-cell', ImageTextCell);
customElements.define('affine-database-date-cell', DateCell);
customElements.define('affine-database-date-cell-editing', DateCellEditing);
customElements.define(
'data-view-properties-setting',
DataViewPropertiesSettingView
);
customElements.define('affine-database-checkbox-cell', CheckboxCell);
customElements.define('affine-database-text-cell', TextCell);
customElements.define('affine-database-text-cell-editing', TextCellEditing);
customElements.define('affine-database-select-cell', SelectCell);
customElements.define(
'affine-database-select-cell-editing',
SelectCellEditing
);
customElements.define('affine-database-multi-select-cell', MultiSelectCell);
customElements.define(
'affine-database-multi-select-cell-editing',
MultiSelectCellEditing
);
customElements.define('affine-data-view-record-field', RecordField);
customElements.define('data-view-drag-to-fill', DragToFillElement);
customElements.define('affine-data-view-table-group', TableGroup);

View File

@@ -75,7 +75,7 @@ export class CheckboxCell extends BaseCellRenderer<boolean> {
override beforeEnterEditMode() {
const checked = !this.value;
this.onChange(checked);
this.valueSetImmediate(checked);
if (checked) {
playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error);
}

View File

@@ -0,0 +1,33 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const dateCellStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
height: 'var(--data-view-cell-text-line-height)',
});
export const dateValueContainerStyle = style({
padding: '12px',
backgroundColor: 'var(--layer-background-primary)',
borderRadius: '12px',
color: 'var(--text-secondary)',
fontSize: '17px',
lineHeight: '22px',
height: '46px',
});
export const datePickerContainerStyle = style({
padding: '12px',
backgroundColor: 'var(--layer-background-primary)',
borderRadius: '12px',
});

View File

@@ -4,66 +4,23 @@ import {
} from '@blocksuite/affine-components/context-menu';
import { DatePicker } from '@blocksuite/affine-components/date-picker';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { IS_MOBILE } from '@blocksuite/global/env';
import { flip, offset } from '@floating-ui/dom';
import { signal } from '@preact/signals-core';
import { baseTheme } from '@toeverything/theme';
import { computed, signal } from '@preact/signals-core';
import { format } from 'date-fns/format';
import { css, html, unsafeCSS } from 'lit';
import { html } from 'lit';
import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import {
dateCellStyle,
datePickerContainerStyle,
dateValueContainerStyle,
} from './cell-renderer.css.js';
import { datePropertyModelConfig } from './define.js';
export class DateCell extends BaseCellRenderer<number> {
static override styles = css`
affine-database-date-cell {
width: 100%;
}
.affine-database-date {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
height: var(--data-view-cell-text-line-height);
}
input.affine-database-date[type='date']::-webkit-calendar-picker-indicator {
display: none;
}
`;
override render() {
const value = this.value ? format(this.value, 'yyyy/MM/dd') : '';
if (!value) {
return '';
}
return html` <div class="affine-database-date date">${value}</div>`;
}
}
export class DateCellEditing extends BaseCellRenderer<number> {
static override styles = css`
affine-database-date-cell-editing {
width: 100%;
cursor: text;
}
.affine-database-date:focus {
outline: none;
}
`;
private _prevPortalAbortController: AbortController | null = null;
private readonly openDatePicker = () => {
@@ -96,18 +53,8 @@ export class DateCellEditing extends BaseCellRenderer<number> {
},
items: [
() =>
html`<div
style="
padding: 12px;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
border-radius: 12px;
color: ${unsafeCSSVarV2('text/secondary')};
font-size: 17px;
line-height: 22px;
height: 46px;
"
>
${this.dateString}
html` <div class="${dateValueContainerStyle}">
${this.formattedTempValue$.value}
</div>`,
() => {
const datePicker = new DatePicker();
@@ -123,11 +70,7 @@ height: 46px;
abortController.abort();
};
requestAnimationFrame(() => datePicker.focusDateCell());
return html`<div
style="padding: 12px;background-color: ${unsafeCSSVarV2(
'layer/background/primary'
)};border-radius: 12px"
>
return html` <div class="${datePickerContainerStyle}">
${datePicker}
</div>`;
},
@@ -179,36 +122,39 @@ height: 46px;
return;
}
this.onChange(tempValue?.getTime());
const time = tempValue?.getTime();
this.valueSetNextTick(time);
this.tempValue$.value = undefined;
};
tempValue$ = signal<Date>();
tempValue$ = signal<Date | undefined>();
get dateString() {
const value = this.tempValue;
format(value?: Date) {
return value ? format(value, 'yyyy/MM/dd') : '';
}
get tempValue() {
return this.tempValue$.value;
}
formattedTempValue$ = computed(() => {
return this.format(this.tempValue$.value);
});
formattedValue$ = computed(() => {
return (
this.formattedTempValue$.value ||
this.format(this.value ? new Date(this.value) : undefined)
);
});
override firstUpdated() {
override afterEnterEditingMode() {
this.openDatePicker();
}
override onExitEditMode() {
override beforeExitEditingMode() {
this.updateValue();
this._prevPortalAbortController?.abort();
}
override render() {
return html` <div
class="affine-database-date date"
@click="${this.openDatePicker}"
>
${this.dateString}
return html` <div class="${dateCellStyle} date">
${this.formattedValue$.value}
</div>`;
}
}
@@ -217,6 +163,5 @@ export const datePropertyConfig = datePropertyModelConfig.createPropertyMeta({
icon: createIcon('DateTimeIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(DateCell),
edit: createFromBaseCellRenderer(DateCellEditing),
},
});

View File

@@ -0,0 +1,17 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const multiSelectStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
});

View File

@@ -1,53 +1,33 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { computed, signal } from '@preact/signals-core';
import { computed } from '@preact/signals-core';
import { html } from 'lit/static-html.js';
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
import type { SelectTag } from '../../core/index.js';
import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { stopPropagation } from '../../core/utils/event.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import type { SelectPropertyData } from '../select/define.js';
import { multiSelectStyle } from './cell-renderer.css.js';
import { multiSelectPropertyModelConfig } from './define.js';
export class MultiSelectCell extends BaseCellRenderer<
string[],
SelectPropertyData
> {
override render() {
return html`
<affine-multi-tag-view
.value="${Array.isArray(this.value) ? this.value : []}"
.options="${this.property.data$.value.options}"
></affine-multi-tag-view>
`;
}
}
export class MultiSelectCellEditing extends BaseCellRenderer<
string[],
SelectPropertyData
> {
closePopup?: () => void;
private readonly popTagSelect = () => {
const value = signal(this._value);
this._disposables.add({
dispose: popTagSelect(
popupTargetFromElement(
this.querySelector('affine-multi-tag-view') ?? this
),
{
name: this.cell.property.name$.value,
options: this.options$,
onOptionsChange: this._onOptionsChange,
value: value,
onChange: v => {
this._onChange(v);
value.value = v;
},
onComplete: this._editComplete,
minWidth: 400,
}
),
this.closePopup = popTagSelect(popupTargetFromElement(this), {
name: this.cell.property.name$.value,
options: this.options$,
onOptionsChange: this._onOptionsChange,
value: this._value$,
onChange: v => {
this.valueSetImmediate(v);
},
onComplete: this._editComplete,
minWidth: 400,
});
};
@@ -55,10 +35,6 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
this.selectCurrentCell(false);
};
_onChange = (ids: string[]) => {
this.onChange(ids);
};
_onOptionsChange = (options: SelectTag[]) => {
this.property.dataUpdate(data => {
return {
@@ -71,21 +47,32 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
options$ = computed(() => {
return this.property.data$.value.options;
});
get _value() {
_value$ = computed(() => {
return this.value ?? [];
});
override afterEnterEditingMode() {
if (!this.closePopup) {
this.popTagSelect();
}
}
override firstUpdated() {
this.popTagSelect();
override beforeExitEditingMode() {
this.closePopup?.();
this.closePopup = undefined;
}
override render() {
return html`
<affine-multi-tag-view
.value="${this._value}"
.options="${this.options$.value}"
></affine-multi-tag-view>
<div
class="${multiSelectStyle}"
@pointerdown="${this.isEditing$.value ? stopPropagation : undefined}"
>
<affine-multi-tag-view
.value="${this._value$.value}"
.options="${this.options$.value}"
></affine-multi-tag-view>
</div>
`;
}
}
@@ -95,6 +82,5 @@ export const multiSelectPropertyConfig =
icon: createIcon('MultiSelectIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(MultiSelectCell),
edit: createFromBaseCellRenderer(MultiSelectCellEditing),
},
});

View File

@@ -0,0 +1,37 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const numberStyle = style({
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
wordBreak: 'break-all',
});
export const numberInputStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
textAlign: 'right',
':focus': {
outline: 'none',
},
});

View File

@@ -1,12 +1,12 @@
import { IS_MAC } from '@blocksuite/global/env';
import { baseTheme } from '@toeverything/theme';
import { css, html, unsafeCSS } from 'lit';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { stopPropagation } from '../../core/utils/event.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { numberInputStyle, numberStyle } from './cell-renderer.css.js';
import { numberPropertyModelConfig } from './define.js';
import type { NumberPropertyDataType } from './types.js';
import {
@@ -19,93 +19,23 @@ export class NumberCell extends BaseCellRenderer<
number,
NumberPropertyDataType
> {
static override styles = css`
affine-database-number-cell {
display: block;
width: 100%;
}
@query('input')
private accessor _inputEle!: HTMLInputElement;
.affine-database-number {
overflow: hidden;
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
word-break: break-all;
}
`;
private _getFormattedString() {
private _getFormattedString(value: number | undefined = this.value) {
const enableNewFormatting =
this.view.featureFlags$.value.enable_number_formatting;
const decimals = this.property.data$.value.decimal ?? 0;
const formatMode = (this.property.data$.value.format ??
'number') as NumberFormat;
return this.value != undefined
return value != undefined
? enableNewFormatting
? formatNumber(this.value, formatMode, decimals)
: this.value.toString()
? formatNumber(value, formatMode, decimals)
: value.toString()
: '';
}
override render() {
return html` <div class="affine-database-number number">
${this._getFormattedString()}
</div>`;
}
}
export class NumberCellEditing extends BaseCellRenderer<
number,
NumberPropertyDataType
> {
static override styles = css`
affine-database-number-cell-editing {
display: block;
width: 100%;
cursor: text;
}
.affine-database-number {
display: flex;
align-items: center;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--data-view-cell-text-size);
line-height: var(--data-view-cell-text-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
text-align: right;
}
.affine-database-number:focus {
outline: none;
}
`;
private readonly _getFormattedString = (value: number) => {
const enableNewFormatting =
this.view.featureFlags$.value.enable_number_formatting;
const decimals = this.property.data$.value.decimal ?? 0;
const formatMode = (this.property.data$.value.format ??
'number') as NumberFormat;
return enableNewFormatting
? formatNumber(value, formatMode, decimals)
: value.toString();
};
private readonly _keydown = (e: KeyboardEvent) => {
const ctrlKey = IS_MAC ? e.metaKey : e.ctrlKey;
@@ -121,9 +51,9 @@ export class NumberCellEditing extends BaseCellRenderer<
}
};
private readonly _setValue = (str: string = this._inputEle.value) => {
private readonly _setValue = (str: string = this._inputEle?.value) => {
if (!str) {
this.onChange(undefined);
this.valueSetNextTick(undefined);
return;
}
@@ -131,17 +61,23 @@ export class NumberCellEditing extends BaseCellRenderer<
this.view.featureFlags$.value.enable_number_formatting;
const value = enableNewFormatting ? parseNumber(str) : parseFloat(str);
if (isNaN(value)) {
this._inputEle.value = this.value
? this._getFormattedString(this.value)
: '';
if (this._inputEle) {
this._inputEle.value = this.value
? this._getFormattedString(this.value)
: '';
}
return;
}
this._inputEle.value = this._getFormattedString(value);
this.onChange(value);
if (this._inputEle) {
this._inputEle.value = this._getFormattedString(value);
}
this.valueSetNextTick(value);
};
focusEnd = () => {
if (!this._inputEle) return;
const end = this._inputEle.value.length;
this._inputEle.focus();
this._inputEle.setSelectionRange(end, end);
@@ -152,38 +88,39 @@ export class NumberCellEditing extends BaseCellRenderer<
}
_focus() {
if (!this.isEditing) {
if (!this.isEditing$.value) {
this.selectCurrentCell(true);
}
}
override firstUpdated() {
requestAnimationFrame(() => {
this.focusEnd();
});
override afterEnterEditingMode() {
this.focusEnd();
}
override onExitEditMode() {
override beforeExitEditingMode() {
this._setValue();
}
override render() {
const formatted = this.value ? this._getFormattedString(this.value) : '';
if (this.isEditing$.value) {
const formatted = this.value ? this._getFormattedString(this.value) : '';
return html`<input
type="text"
autocomplete="off"
.value="${formatted}"
@keydown="${this._keydown}"
@blur="${this._blur}"
@focus="${this._focus}"
class="affine-database-number number"
@pointerdown="${stopPropagation}"
/>`;
return html`<input
type="text"
autocomplete="off"
.value="${formatted}"
@keydown="${this._keydown}"
@blur="${this._blur}"
@focus="${this._focus}"
class="${numberInputStyle} number"
@pointerdown="${stopPropagation}"
/>`;
} else {
return html` <div class="${numberStyle} number">
${this._getFormattedString()}
</div>`;
}
}
@query('input')
private accessor _inputEle!: HTMLInputElement;
}
export const numberPropertyConfig =
@@ -191,6 +128,5 @@ export const numberPropertyConfig =
icon: createIcon('NumberIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(NumberCell),
edit: createFromBaseCellRenderer(NumberCellEditing),
},
});

View File

@@ -0,0 +1,57 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const progressCellStyle = style({
display: 'block',
width: '100%',
padding: '0 4px',
userSelect: 'none',
});
export const progressContainerStyle = style({
display: 'flex',
alignItems: 'center',
height: 'var(--data-view-cell-text-line-height)',
gap: '4px',
});
export const progressBarStyle = style({
position: 'relative',
width: '100%',
});
export const progressBgStyle = style({
overflow: 'hidden',
width: '100%',
height: '10px',
borderRadius: '22px',
});
export const progressFgStyle = style({
height: '100%',
});
export const progressDragHandleStyle = style({
position: 'absolute',
top: '0',
left: '0',
transform: 'translate(0px, -1px)',
width: '6px',
height: '12px',
borderRadius: '2px',
opacity: '1',
cursor: 'ew-resize',
background: 'var(--affine-primary-color)',
transition: 'opacity 0.2s ease-in-out',
});
export const progressNumberStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '18px',
width: '25px',
color: 'var(--affine-text-secondary-color)',
fontSize: '14px',
fontFamily: baseTheme.fontSansFamily,
});

View File

@@ -1,4 +1,4 @@
import { css, html } from 'lit';
import { html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
@@ -6,69 +6,17 @@ import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { startDrag } from '../../core/utils/drag.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import {
progressBarStyle,
progressBgStyle,
progressCellStyle,
progressContainerStyle,
progressDragHandleStyle,
progressFgStyle,
progressNumberStyle,
} from './cell-renderer.css.js';
import { progressPropertyModelConfig } from './define.js';
const styles = css`
affine-database-progress-cell-editing {
display: block;
width: 100%;
padding: 0 4px;
}
affine-database-progress-cell {
display: block;
width: 100%;
padding: 0 4px;
}
.affine-database-progress {
display: flex;
align-items: center;
height: var(--data-view-cell-text-line-height);
gap: 4px;
}
.affine-database-progress-bar {
position: relative;
width: 104px;
}
.affine-database-progress-bg {
overflow: hidden;
width: 100%;
height: 10px;
border-radius: 22px;
}
.affine-database-progress-fg {
height: 100%;
}
.affine-database-progress-drag-handle {
position: absolute;
top: 0;
left: 0;
transform: translate(0px, -1px);
width: 6px;
height: 12px;
border-radius: 2px;
opacity: 1;
cursor: ew-resize;
background: var(--affine-primary-color);
transition: opacity 0.2s ease-in-out;
}
.progress-number {
display: flex;
justify-content: center;
align-items: center;
height: 18px;
width: 25px;
color: var(--affine-text-secondary-color);
font-size: 14px;
}
`;
const progressColors = {
empty: 'var(--affine-black-10)',
processing: 'var(--affine-processing-color)',
@@ -76,38 +24,9 @@ const progressColors = {
};
export class ProgressCell extends BaseCellRenderer<number> {
static override styles = styles;
protected override render() {
const progress = this.value ?? 0;
let backgroundColor = progressColors.processing;
if (progress === 100) {
backgroundColor = progressColors.success;
}
const fgStyles = styleMap({
width: `${progress}%`,
backgroundColor,
});
const bgStyles = styleMap({
backgroundColor:
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
});
return html` <div class="affine-database-progress">
<div class="affine-database-progress-bar">
<div class="affine-database-progress-bg" style=${bgStyles}>
<div class="affine-database-progress-fg" style=${fgStyles}></div>
</div>
</div>
<div class="progress-number progress">${progress}</div>
</div>`;
}
}
export class ProgressCellEditing extends BaseCellRenderer<number> {
static override styles = styles;
startDrag = (event: MouseEvent) => {
if (!this.isEditing$.value) return;
const bgRect = this._progressBg.getBoundingClientRect();
const min = bgRect.left;
const max = bgRect.right;
@@ -135,7 +54,9 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
};
get _value() {
return this.tempValue ?? this.value ?? 0;
return this.isEditing$.value
? (this.tempValue ?? this.value ?? 0)
: (this.value ?? 0);
}
_onChange(value?: number) {
@@ -146,13 +67,17 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
const disposables = this._disposables;
disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag);
disposables.addFromEvent(window, 'keydown', evt => {
if (evt.key === 'ArrowDown') {
if (!this.isEditing$.value) {
return;
}
if (evt.key === 'ArrowDown' || evt.key === 'ArrowLeft') {
evt.preventDefault();
this._onChange(Math.max(0, this._value - 1));
return;
}
if (evt.key === 'ArrowUp') {
if (evt.key === 'ArrowUp' || evt.key === 'ArrowRight') {
evt.preventDefault();
this._onChange(Math.min(100, this._value + 1));
return;
@@ -160,20 +85,25 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
});
}
preventDefault(e: ClipboardEvent) {
e.stopPropagation();
}
override onCopy(_e: ClipboardEvent) {
_e.preventDefault();
this.preventDefault(_e);
}
override onCut(_e: ClipboardEvent) {
_e.preventDefault();
this.preventDefault(_e);
}
override onExitEditMode() {
this.onChange(this._value);
override beforeExitEditingMode() {
const value = this._value;
this.valueSetNextTick(value);
}
override onPaste(_e: ClipboardEvent) {
_e.preventDefault();
this.preventDefault(_e);
}
protected override render() {
@@ -190,25 +120,37 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
backgroundColor:
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
});
const handleStyles = styleMap({
left: `calc(${progress}% - 3px)`,
});
return html` <div class="affine-database-progress">
<div class="affine-database-progress-bar">
<div class="affine-database-progress-bg" style=${bgStyles}>
<div class="affine-database-progress-fg" style=${fgStyles}></div>
<div
class="affine-database-progress-drag-handle"
style=${handleStyles}
></div>
return html`
<div class="${progressCellStyle}">
<div class="${progressContainerStyle}">
<div class="${progressBarStyle}">
<div
class="${progressBgStyle}"
data-testid="progress-background"
style=${bgStyles}
>
<div class="${progressFgStyle}" style=${fgStyles}></div>
${this.isEditing$.value
? html` <div
class="${progressDragHandleStyle}"
data-testid="progress-drag-handle"
style=${styleMap({
left: `calc(${progress}% - 3px)`,
})}
></div>`
: ''}
</div>
</div>
<span class="${progressNumberStyle}" data-testid="progress"
>${progress}</span
>
</div>
</div>
<div class="progress-number progress">${progress}</div>
</div>`;
`;
}
@query('.affine-database-progress-bg')
@query(`.${progressBgStyle}`)
private accessor _progressBg!: HTMLElement;
@state()
@@ -220,6 +162,5 @@ export const progressPropertyConfig =
icon: createIcon('ProgressIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(ProgressCell),
edit: createFromBaseCellRenderer(ProgressCellEditing),
},
});

View File

@@ -0,0 +1,18 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const selectStyle = style({
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
wordBreak: 'break-all',
});

View File

@@ -1,5 +1,5 @@
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
import { computed, signal } from '@preact/signals-core';
import { computed } from '@preact/signals-core';
import { html } from 'lit/static-html.js';
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
@@ -7,48 +7,26 @@ import type { SelectTag } from '../../core/index.js';
import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { selectStyle } from './cell-renderer.css.js';
import {
type SelectPropertyData,
selectPropertyModelConfig,
} from './define.js';
export class SelectCell extends BaseCellRenderer<string[], SelectPropertyData> {
override render() {
const value = this.value ? [this.value] : [];
return html`
<affine-multi-tag-view
.value="${value}"
.options="${this.property.data$.value.options}"
></affine-multi-tag-view>
`;
}
}
export class SelectCellEditing extends BaseCellRenderer<
string,
SelectPropertyData
> {
export class SelectCell extends BaseCellRenderer<string, SelectPropertyData> {
closePopup?: () => void;
private readonly popTagSelect = () => {
const value = signal(this._value);
this._disposables.add({
dispose: popTagSelect(
popupTargetFromElement(
this.querySelector('affine-multi-tag-view') ?? this
),
{
name: this.cell.property.name$.value,
mode: 'single',
options: this.options$,
onOptionsChange: this._onOptionsChange,
value: signal(this._value),
onChange: v => {
this._onChange(v);
value.value = v;
},
onComplete: this._editComplete,
minWidth: 400,
}
),
this.closePopup = popTagSelect(popupTargetFromElement(this), {
name: this.cell.property.name$.value,
mode: 'single',
options: this.options$,
onOptionsChange: this._onOptionsChange,
value: this._value$,
onChange: v => {
this.valueSetImmediate(v[0]);
},
onComplete: this._editComplete,
minWidth: 400,
});
};
@@ -56,10 +34,6 @@ export class SelectCellEditing extends BaseCellRenderer<
this.selectCurrentCell(false);
};
_onChange = ([id]: string[]) => {
this.onChange(id);
};
_onOptionsChange = (options: SelectTag[]) => {
this.property.dataUpdate(data => {
return {
@@ -73,21 +47,30 @@ export class SelectCellEditing extends BaseCellRenderer<
return this.property.data$.value.options;
});
get _value() {
_value$ = computed(() => {
const value = this.value;
return value ? [value] : [];
});
override afterEnterEditingMode() {
if (!this.closePopup) {
this.popTagSelect();
}
}
override firstUpdated() {
this.popTagSelect();
override beforeExitEditingMode() {
this.closePopup?.();
this.closePopup = undefined;
}
override render() {
return html`
<affine-multi-tag-view
.value="${this._value}"
.options="${this.options$.value}"
></affine-multi-tag-view>
<div class="${selectStyle}">
<affine-multi-tag-view
.value="${this._value$.value}"
.options="${this.options$.value}"
></affine-multi-tag-view>
</div>
`;
}
}
@@ -97,6 +80,5 @@ export const selectPropertyConfig =
icon: createIcon('SingleSelectIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(SelectCell),
edit: createFromBaseCellRenderer(SelectCellEditing),
},
});

View File

@@ -0,0 +1,42 @@
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const textStyle = style({
display: 'flex',
alignItems: 'center',
height: '100%',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const textInputStyle = style({
display: 'flex',
alignItems: 'center',
height: '100%',
width: '100%',
padding: '0',
border: 'none',
fontFamily: baseTheme.fontSansFamily,
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
color: 'var(--affine-text-primary-color)',
fontWeight: '400',
backgroundColor: 'transparent',
cursor: 'text',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
':focus': {
outline: 'none',
},
});

View File

@@ -1,74 +1,15 @@
import { baseTheme } from '@toeverything/theme';
import { css, html, unsafeCSS } from 'lit';
import { html } from 'lit';
import { query } from 'lit/decorators.js';
import { BaseCellRenderer } from '../../core/property/index.js';
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
import { createIcon } from '../../core/utils/uni-icon.js';
import { textInputStyle, textStyle } from './cell-renderer.css.js';
import { textPropertyModelConfig } from './define.js';
export class TextCell extends BaseCellRenderer<string> {
static override styles = css`
affine-database-text-cell {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.affine-database-text {
display: flex;
align-items: center;
height: 100%;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
}
`;
override render() {
return html` <div class="affine-database-text">${this.value ?? ''}</div>`;
}
}
export class TextCellEditing extends BaseCellRenderer<string> {
static override styles = css`
affine-database-text-cell-editing {
display: block;
width: 100%;
height: 100%;
cursor: text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.affine-database-text {
display: flex;
align-items: center;
height: 100%;
width: 100%;
padding: 0;
border: none;
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-primary-color);
font-weight: 400;
background-color: transparent;
}
.affine-database-text:focus {
outline: none;
}
`;
@query('input')
private accessor _inputEle!: HTMLInputElement;
private readonly _keydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !e.isComposing) {
@@ -79,35 +20,40 @@ export class TextCellEditing extends BaseCellRenderer<string> {
}
};
private readonly _setValue = (str: string = this._inputEle.value) => {
this._inputEle.value = `${this.value ?? ''}`;
this.onChange(str);
private readonly _setValue = (str: string = this._inputEle?.value) => {
if (this._inputEle) {
this._inputEle.value = `${this.value ?? ''}`;
}
this.valueSetNextTick(str);
};
focusEnd = () => {
if (!this._inputEle) return;
const end = this._inputEle.value.length;
this._inputEle.focus();
this._inputEle.setSelectionRange(end, end);
};
override firstUpdated() {
override afterEnterEditingMode() {
this.focusEnd();
}
override onExitEditMode() {
override beforeExitEditingMode() {
this._setValue();
}
override render() {
return html`<input
.value="${this.value ?? ''}"
@keydown="${this._keydown}"
class="affine-database-text"
/>`;
if (this.isEditing$.value) {
return html`<input
.value="${this.value ?? ''}"
@keydown="${this._keydown}"
class="${textInputStyle}"
/>`;
} else {
return html`<div class="${textStyle}">${this.value ?? ''}</div>`;
}
}
@query('input')
private accessor _inputEle!: HTMLInputElement;
}
export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
@@ -115,6 +61,5 @@ export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
cellRenderer: {
view: createFromBaseCellRenderer(TextCell),
edit: createFromBaseCellRenderer(TextCellEditing),
},
});

View File

@@ -5,7 +5,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { html } from 'lit/static-html.js';
import type {
@@ -52,7 +52,7 @@ export class MobileKanbanCell extends SignalWatcher(
private readonly _cell = signal<DataViewCellLifeCycle>();
isEditing$ = computed(() => {
isSelectionEditing$ = computed(() => {
const selection = this.kanban?.props.selection$.value;
if (selection?.selectionType !== 'cell') {
return false;
@@ -108,19 +108,21 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.column.readonly$.value) return;
this.disposables.add(
effect(() => {
const isEditing = this.isEditing$.value;
const isEditing = this.isSelectionEditing$.value;
if (isEditing) {
this.isEditing = true;
this._cell.value?.onEnterEditMode();
this.isEditing$.value = true;
requestAnimationFrame(() => {
this._cell.value?.afterEnterEditingMode();
});
} else {
this._cell.value?.onExitEditMode();
this.isEditing = false;
this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false;
}
})
);
this._disposables.addFromEvent(this, 'click', e => {
e.stopPropagation();
if (!this.isEditing) {
if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value);
}
});
@@ -129,16 +131,16 @@ export class MobileKanbanCell extends SignalWatcher(
override render() {
const props: CellRenderProps = {
cell: this.column.cellGet(this.cardId),
isEditing: this.isEditing,
isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell,
};
const renderer = this.column.renderer$.value;
if (!renderer) return;
const { view, edit } = renderer;
this.view.lockRows(this.isEditing);
this.dataset['editing'] = `${this.isEditing}`;
const { view } = renderer;
this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`;
return html` ${this.renderIcon()}
${renderUniLit(this.isEditing && edit ? edit : view, props, {
${renderUniLit(view, props, {
ref: this._cell,
class: 'mobile-kanban-cell',
style: { display: 'block', flex: '1', overflow: 'hidden' },
@@ -167,8 +169,7 @@ export class MobileKanbanCell extends SignalWatcher(
@property({ attribute: false })
accessor groupKey!: string;
@state()
accessor isEditing = false;
isEditing$ = signal(false);
@property({ attribute: false })
accessor view!: KanbanSingleView;

View File

@@ -109,7 +109,7 @@ export class KanbanCell extends SignalWatcher(
if (!selectionElement) return;
if (e.shiftKey) return;
if (!this.editing) {
if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value);
}
});
@@ -130,22 +130,22 @@ export class KanbanCell extends SignalWatcher(
override render() {
const props: CellRenderProps = {
cell: this.column.cellGet(this.cardId),
isEditing: this.editing,
isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell,
};
const renderer = this.column.renderer$.value;
if (!renderer) return;
const { view, edit } = renderer;
this.view.lockRows(this.editing);
this.dataset['editing'] = `${this.editing}`;
const { view } = renderer;
this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`;
this.style.border = this.isFocus
? '1px solid var(--affine-primary-color)'
: '';
this.style.boxShadow = this.editing
this.style.boxShadow = this.isEditing$.value
? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)'
: '';
return html` ${this.renderIcon()}
${renderUniLit(this.editing && edit ? edit : view, props, {
${renderUniLit(view, props, {
ref: this._cell,
class: 'kanban-cell',
style: { display: 'block', flex: '1', overflow: 'hidden' },
@@ -168,8 +168,7 @@ export class KanbanCell extends SignalWatcher(
@property({ attribute: false })
accessor contentOnly = false;
@state()
accessor editing = false;
isEditing$ = signal(false);
@property({ attribute: false })
accessor groupKey!: string;

View File

@@ -153,7 +153,7 @@ export class KanbanDragController implements ReactiveController {
const target = event.target;
if (target instanceof Element) {
const cell = target.closest('affine-data-view-kanban-cell');
if (cell?.editing) {
if (cell?.isEditing$.value) {
return;
}
cell?.selectCurrentCell(false);

View File

@@ -96,13 +96,11 @@ export class KanbanSelectionController implements ReactiveController {
const cell = container?.cell;
if (selection.isEditing) {
requestAnimationFrame(() => {
cell?.onExitEditMode();
});
cell?.beforeExitEditingMode();
if (cell?.blurCell()) {
container.blur();
}
container.editing = false;
container.isEditing$.value = false;
} else {
container.blur();
}
@@ -142,11 +140,13 @@ export class KanbanSelectionController implements ReactiveController {
container.isFocus = true;
const cell = container?.cell;
if (selection.isEditing) {
cell?.onEnterEditMode();
if (cell?.focusCell()) {
container.focus();
}
container.editing = true;
container.isEditing$.value = true;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else {
container.focus();
}

View File

@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, effect, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import {
type CellRenderProps,
@@ -47,7 +47,7 @@ export class MobileTableCell extends SignalWatcher(
return this.column.cellGet(this.rowId);
});
isEditing$ = computed(() => {
isSelectionEditing$ = computed(() => {
const selection = this.table?.props.selection$.value;
if (selection?.selectionType !== 'area') {
return false;
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
return this.closest('mobile-table-group')?.group?.key;
}
private get readonly() {
return this.column.readonly$.value;
}
private get table() {
return this.closest('mobile-data-view-table');
}
@@ -110,18 +106,21 @@ export class MobileTableCell extends SignalWatcher(
if (this.column.readonly$.value) return;
this.disposables.add(
effect(() => {
const isEditing = this.isEditing$.value;
const isEditing = this.isSelectionEditing$.value;
if (isEditing) {
this.isEditing = true;
this._cell.value?.onEnterEditMode();
this.isEditing$.value = true;
const cell = this._cell.value;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else {
this._cell.value?.onExitEditMode();
this.isEditing = false;
this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false;
}
})
);
this.disposables.addFromEvent(this, 'click', () => {
if (!this.isEditing) {
if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value);
}
});
@@ -132,17 +131,16 @@ export class MobileTableCell extends SignalWatcher(
if (!renderer) {
return;
}
const { edit, view } = renderer;
const uni = !this.readonly && this.isEditing && edit != null ? edit : view;
this.view.lockRows(this.isEditing);
this.dataset['editing'] = `${this.isEditing}`;
const { view } = renderer;
this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`;
const props: CellRenderProps = {
cell: this.cell$.value,
isEditing: this.isEditing,
isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell,
};
return renderUniLit(uni, props, {
return renderUniLit(view, props, {
ref: this._cell,
style: {
display: 'contents',
@@ -156,8 +154,7 @@ export class MobileTableCell extends SignalWatcher(
@property({ attribute: false })
accessor columnIndex!: number;
@state()
accessor isEditing = false;
isEditing$ = signal(false);
@property({ attribute: false })
accessor rowIndex!: number;

View File

@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { computed, signal } from '@preact/signals-core';
import { css } from 'lit';
import { property, state } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { renderUniLit } from '../../../core/index.js';
import type {
@@ -88,10 +88,6 @@ export class DatabaseCellContainer extends SignalWatcher(
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
}
private get readonly() {
return this.column.readonly$.value;
}
private get selectionView() {
return this.closest('affine-database-table')?.selectionController;
}
@@ -104,7 +100,7 @@ export class DatabaseCellContainer extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
this._disposables.addFromEvent(this, 'click', () => {
if (!this.isEditing) {
if (!this.isEditing$.value) {
this.selectCurrentCell(!this.column.readonly$.value);
}
});
@@ -128,17 +124,16 @@ export class DatabaseCellContainer extends SignalWatcher(
if (!renderer) {
return;
}
const { edit, view } = renderer;
const uni = !this.readonly && this.isEditing && edit != null ? edit : view;
this.view.lockRows(this.isEditing);
this.dataset['editing'] = `${this.isEditing}`;
const { view } = renderer;
this.view.lockRows(this.isEditing$.value);
this.dataset['editing'] = `${this.isEditing$.value}`;
const props: CellRenderProps = {
cell: this.cell$.value,
isEditing: this.isEditing,
isEditing$: this.isEditing$,
selectCurrentCell: this.selectCurrentCell,
};
return renderUniLit(uni, props, {
return renderUniLit(view, props, {
ref: this._cell,
style: {
display: 'contents',
@@ -152,8 +147,7 @@ export class DatabaseCellContainer extends SignalWatcher(
@property({ attribute: false })
accessor columnIndex!: number;
@state()
accessor isEditing = false;
isEditing$ = signal(false);
@property({ attribute: false })
accessor rowIndex!: number;

View File

@@ -192,11 +192,9 @@ export class TableSelectionController implements ReactiveController {
if (container) {
const cell = container.cell;
if (old.isEditing) {
requestAnimationFrame(() => {
cell?.onExitEditMode();
});
cell?.beforeExitEditingMode();
cell?.blurCell();
container.isEditing = false;
container.isEditing$.value = false;
}
}
}
@@ -211,9 +209,11 @@ export class TableSelectionController implements ReactiveController {
if (container) {
const cell = container.cell;
if (newSelection.isEditing) {
cell?.onEnterEditMode();
container.isEditing = true;
cell?.focusCell();
container.isEditing$.value = true;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
cell?.focusCell();
});
}
}
}