mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-19 11:06:25 +08:00
refactor(editor): remove edit view of database block properties (#10748)
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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
|
||||
>`;
|
||||
})}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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;
|
||||
// }
|
||||
`;
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user