chore: merge blocksuite source code (#9213)
This commit is contained in:
1 parent
2c9ef916f4
commit
30200ff86d
2031 files changed
+238888
-229
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { CheckBoxCkeckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit';
|
||||
import { css, 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 { checkboxPropertyModelConfig } from './define.js';
|
||||
|
||||
const playCheckAnimation = async (
|
||||
refElement: Element,
|
||||
{ left = 0, size = 20 }: { left?: number; size?: number } = {}
|
||||
) => {
|
||||
const sparkingEl = document.createElement('div');
|
||||
sparkingEl.classList.add('affine-check-animation');
|
||||
if (size < 20) {
|
||||
console.warn('If the size is less than 20, the animation may be abnormal.');
|
||||
}
|
||||
sparkingEl.style.cssText = `
|
||||
position: absolute;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
sparkingEl.style.left = `${left}px`;
|
||||
refElement.append(sparkingEl);
|
||||
|
||||
await sparkingEl.animate(
|
||||
[
|
||||
{
|
||||
boxShadow:
|
||||
'0 -18px 0 -8px #1e96eb, 16px -8px 0 -8px #1e96eb, 16px 8px 0 -8px #1e96eb, 0 18px 0 -8px #1e96eb, -16px 8px 0 -8px #1e96eb, -16px -8px 0 -8px #1e96eb',
|
||||
},
|
||||
],
|
||||
{ duration: 240, easing: 'ease', fill: 'forwards' }
|
||||
).finished;
|
||||
await sparkingEl.animate(
|
||||
[
|
||||
{
|
||||
boxShadow:
|
||||
'0 -36px 0 -10px transparent, 32px -16px 0 -10px transparent, 32px 16px 0 -10px transparent, 0 36px 0 -10px transparent, -32px 16px 0 -10px transparent, -32px -16px 0 -10px transparent',
|
||||
},
|
||||
],
|
||||
{ duration: 360, easing: 'ease', fill: 'forwards' }
|
||||
).finished;
|
||||
|
||||
sparkingEl.remove();
|
||||
};
|
||||
|
||||
export class CheckboxCell extends BaseCellRenderer<boolean> {
|
||||
static override styles = css`
|
||||
affine-database-checkbox-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.affine-database-checkbox-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-database-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--data-view-cell-text-line-height);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
font-size: 24px;
|
||||
color: ${unsafeCSSVarV2('database/textSecondary')};
|
||||
margin-left: -1px;
|
||||
}
|
||||
`;
|
||||
|
||||
override beforeEnterEditMode() {
|
||||
const checked = !this.value;
|
||||
|
||||
this.onChange(checked);
|
||||
if (checked) {
|
||||
playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
override onCopy(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
override onCut(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
override onPaste(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const checked = this.value ?? false;
|
||||
const icon = checked
|
||||
? CheckBoxCkeckSolidIcon({ style: `color:#1E96EB` })
|
||||
: CheckBoxUnIcon();
|
||||
return html` <div class="affine-database-checkbox-container">
|
||||
<div
|
||||
class="affine-database-checkbox checkbox ${checked ? 'checked' : ''}"
|
||||
>
|
||||
${icon}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@query('.affine-database-checkbox')
|
||||
private accessor _checkbox!: HTMLDivElement;
|
||||
}
|
||||
|
||||
export const checkboxPropertyConfig =
|
||||
checkboxPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('CheckBoxCheckLinearIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(CheckboxCell),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { t } from '../../core/logical/type-presets.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const checkboxPropertyType = propertyType('checkbox');
|
||||
|
||||
export const checkboxPropertyModelConfig =
|
||||
checkboxPropertyType.modelConfig<boolean>({
|
||||
name: 'Checkbox',
|
||||
type: () => t.boolean.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => (value ? 'True' : 'False'),
|
||||
cellFromString: ({ value }) => {
|
||||
return {
|
||||
value: value !== 'False',
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) =>
|
||||
typeof value !== 'boolean' ? undefined : value,
|
||||
isEmpty: () => false,
|
||||
minWidth: 34,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { clamp } from '@blocksuite/affine-shared/utils';
|
||||
|
||||
import { createPropertyConvert } from '../core/index.js';
|
||||
import { multiSelectPropertyModelConfig } from './multi-select/define.js';
|
||||
import { numberPropertyModelConfig } from './number/define.js';
|
||||
import { progressPropertyModelConfig } from './progress/define.js';
|
||||
import { selectPropertyModelConfig } from './select/define.js';
|
||||
|
||||
export const presetPropertyConverts = [
|
||||
createPropertyConvert(
|
||||
multiSelectPropertyModelConfig,
|
||||
selectPropertyModelConfig,
|
||||
(property, cells) => ({
|
||||
property,
|
||||
cells: cells.map(v => v?.[0]),
|
||||
})
|
||||
),
|
||||
createPropertyConvert(
|
||||
numberPropertyModelConfig,
|
||||
progressPropertyModelConfig,
|
||||
(_property, cells) => ({
|
||||
property: {},
|
||||
cells: cells.map(v => clamp(v ?? 0, 0, 100)),
|
||||
})
|
||||
),
|
||||
createPropertyConvert(
|
||||
progressPropertyModelConfig,
|
||||
numberPropertyModelConfig,
|
||||
(_property, cells) => ({
|
||||
property: {
|
||||
decimal: 0,
|
||||
format: 'number' as const,
|
||||
},
|
||||
cells: cells.map(v => v),
|
||||
})
|
||||
),
|
||||
createPropertyConvert(
|
||||
selectPropertyModelConfig,
|
||||
multiSelectPropertyModelConfig,
|
||||
(property, cells) => ({
|
||||
property,
|
||||
cells: cells.map(v => (v ? [v] : undefined)),
|
||||
})
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,222 @@
|
||||
import {
|
||||
popMenu,
|
||||
popupTargetFromElement,
|
||||
} 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 { format } from 'date-fns/format';
|
||||
import { css, html, unsafeCSS } 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 { 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 openDatePicker = () => {
|
||||
if (
|
||||
this._prevPortalAbortController &&
|
||||
!this._prevPortalAbortController.signal.aborted
|
||||
)
|
||||
return;
|
||||
|
||||
this.tempValue$.value = this.value ? new Date(this.value) : undefined;
|
||||
|
||||
this._prevPortalAbortController?.abort();
|
||||
const abortController = new AbortController();
|
||||
abortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
this.selectCurrentCell(false);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this._prevPortalAbortController = abortController;
|
||||
if (IS_MOBILE) {
|
||||
popMenu(popupTargetFromElement(this), {
|
||||
options: {
|
||||
title: {
|
||||
text: this.property.name$.value,
|
||||
},
|
||||
onClose: () => {
|
||||
abortController.abort();
|
||||
},
|
||||
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}
|
||||
</div>`,
|
||||
() => {
|
||||
const datePicker = new DatePicker();
|
||||
datePicker.padding = 0;
|
||||
datePicker.value = this.tempValue$.value?.getTime() ?? Date.now();
|
||||
datePicker.onChange = (date: Date) => {
|
||||
this.tempValue$.value = date;
|
||||
};
|
||||
datePicker.onClear = () => {
|
||||
this.tempValue$.value = undefined;
|
||||
};
|
||||
datePicker.onEscape = () => {
|
||||
abortController.abort();
|
||||
};
|
||||
requestAnimationFrame(() => datePicker.focusDateCell());
|
||||
return html`<div
|
||||
style="padding: 12px;background-color: ${unsafeCSSVarV2(
|
||||
'layer/background/primary'
|
||||
)};border-radius: 12px"
|
||||
>
|
||||
${datePicker}
|
||||
</div>`;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const root = createLitPortal({
|
||||
abortController,
|
||||
closeOnClickAway: true,
|
||||
computePosition: {
|
||||
referenceElement: this,
|
||||
placement: 'bottom',
|
||||
middleware: [offset(10), flip()],
|
||||
},
|
||||
template: () => {
|
||||
const datePicker = new DatePicker();
|
||||
datePicker.value = this.tempValue$.value?.getTime() ?? Date.now();
|
||||
datePicker.popup = true;
|
||||
datePicker.onClear = () => {
|
||||
this.tempValue$.value = undefined;
|
||||
};
|
||||
datePicker.onChange = (date: Date) => {
|
||||
this.tempValue$.value = date;
|
||||
};
|
||||
datePicker.onEscape = () => {
|
||||
abortController.abort();
|
||||
};
|
||||
requestAnimationFrame(() => datePicker.focusDateCell());
|
||||
return datePicker;
|
||||
},
|
||||
});
|
||||
// TODO: use z-index from variable,
|
||||
// for now the slide-layout-modal's z-index is `1001`
|
||||
// the z-index of popover should be higher than it
|
||||
// root.style.zIndex = 'var(--affine-z-index-popover)';
|
||||
root.style.zIndex = '1002';
|
||||
}
|
||||
};
|
||||
|
||||
private updateValue = () => {
|
||||
const tempValue = this.tempValue$.value;
|
||||
const currentValue = this.value;
|
||||
|
||||
if (
|
||||
(!tempValue && !currentValue) ||
|
||||
(tempValue && currentValue && tempValue.getTime() === currentValue)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onChange(tempValue?.getTime());
|
||||
this.tempValue$.value = undefined;
|
||||
};
|
||||
|
||||
tempValue$ = signal<Date>();
|
||||
|
||||
get dateString() {
|
||||
const value = this.tempValue;
|
||||
return value ? format(value, 'yyyy/MM/dd') : '';
|
||||
}
|
||||
|
||||
get tempValue() {
|
||||
return this.tempValue$.value;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.openDatePicker();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
this.updateValue();
|
||||
this._prevPortalAbortController?.abort();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` <div
|
||||
class="affine-database-date date"
|
||||
@click="${this.openDatePicker}"
|
||||
>
|
||||
${this.dateString}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const datePropertyConfig = datePropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('DateTimeIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(DateCell),
|
||||
edit: createFromBaseCellRenderer(DateCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { t } from '../../core/logical/type-presets.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const datePropertyType = propertyType('date');
|
||||
export const datePropertyModelConfig = datePropertyType.modelConfig<number>({
|
||||
name: 'Date',
|
||||
type: () => t.date.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => value?.toString() ?? '',
|
||||
cellFromString: ({ value }) => {
|
||||
const isDateFormat = !isNaN(Date.parse(value));
|
||||
|
||||
return {
|
||||
value: isDateFormat ? +new Date(value) : null,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => (typeof value !== 'number' ? undefined : value),
|
||||
isEmpty: ({ value }) => value == null,
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { css, 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 { imagePropertyModelConfig } from './define.js';
|
||||
|
||||
export class TextCell extends BaseCellRenderer<string> {
|
||||
static override styles = css`
|
||||
affine-database-image-cell {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
affine-database-image-cell img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<img src=${this.value ?? ''}></img>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const imagePropertyConfig = imagePropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('ImageIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(TextCell),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { t } from '../../core/logical/type-presets.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const imagePropertyType = propertyType('image');
|
||||
|
||||
export const imagePropertyModelConfig = imagePropertyType.modelConfig<string>({
|
||||
name: 'image',
|
||||
type: () => t.image.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => value ?? '',
|
||||
cellFromString: ({ value }) => {
|
||||
return {
|
||||
value: value,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value),
|
||||
isEmpty: ({ value }) => value == null,
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { checkboxPropertyConfig } from './checkbox/cell-renderer.js';
|
||||
import { datePropertyConfig } from './date/cell-renderer.js';
|
||||
import { imagePropertyConfig } from './image/cell-renderer.js';
|
||||
import { multiSelectPropertyConfig } from './multi-select/cell-renderer.js';
|
||||
import { numberPropertyConfig } from './number/cell-renderer.js';
|
||||
import { progressPropertyConfig } from './progress/cell-renderer.js';
|
||||
import { selectPropertyConfig } from './select/cell-renderer.js';
|
||||
import { textPropertyConfig } from './text/cell-renderer.js';
|
||||
|
||||
export * from './converts.js';
|
||||
export * from './number/types.js';
|
||||
export * from './select/define.js';
|
||||
export const propertyPresets = {
|
||||
checkboxPropertyConfig,
|
||||
datePropertyConfig,
|
||||
imagePropertyConfig,
|
||||
multiSelectPropertyConfig,
|
||||
numberPropertyConfig,
|
||||
progressPropertyConfig,
|
||||
selectPropertyConfig,
|
||||
textPropertyConfig,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { computed, signal } 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 { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import type { SelectPropertyData } from '../select/define.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
|
||||
> {
|
||||
private 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,
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
_editComplete = () => {
|
||||
this.selectCurrentCell(false);
|
||||
};
|
||||
|
||||
_onChange = (ids: string[]) => {
|
||||
this.onChange(ids);
|
||||
};
|
||||
|
||||
_onOptionsChange = (options: SelectTag[]) => {
|
||||
this.property.dataUpdate(data => {
|
||||
return {
|
||||
...data,
|
||||
options,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
options$ = computed(() => {
|
||||
return this.property.data$.value.options;
|
||||
});
|
||||
|
||||
get _value() {
|
||||
return this.value ?? [];
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.popTagSelect();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const multiSelectPropertyConfig =
|
||||
multiSelectPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('MultiSelectIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(MultiSelectCell),
|
||||
edit: createFromBaseCellRenderer(MultiSelectCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
import { getTagColor } from '../../core/component/tags/colors.js';
|
||||
import { type SelectTag, t } from '../../core/index.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
import type { SelectPropertyData } from '../select/define.js';
|
||||
|
||||
export const multiSelectPropertyType = propertyType('multi-select');
|
||||
export const multiSelectPropertyModelConfig =
|
||||
multiSelectPropertyType.modelConfig<string[], SelectPropertyData>({
|
||||
name: 'Multi-select',
|
||||
type: ({ data }) => t.array.instance(t.tag.instance(data.options)),
|
||||
defaultData: () => ({
|
||||
options: [],
|
||||
}),
|
||||
addGroup: ({ text, oldData }) => {
|
||||
return {
|
||||
options: [
|
||||
...(oldData.options ?? []),
|
||||
{
|
||||
id: nanoid(),
|
||||
value: text,
|
||||
color: getTagColor(),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
formatValue: ({ value }) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter(v => v != null);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
cellToString: ({ value, data }) =>
|
||||
value?.map(id => data.options.find(v => v.id === id)?.value).join(','),
|
||||
cellFromString: ({ value: oldValue, data }) => {
|
||||
const optionMap = Object.fromEntries(data.options.map(v => [v.value, v]));
|
||||
const optionNames = oldValue
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
.filter(v => v);
|
||||
|
||||
const value: string[] = [];
|
||||
optionNames.forEach(name => {
|
||||
if (!optionMap[name]) {
|
||||
const newOption: SelectTag = {
|
||||
id: nanoid(),
|
||||
value: name,
|
||||
color: getTagColor(),
|
||||
};
|
||||
data.options.push(newOption);
|
||||
value.push(newOption.id);
|
||||
} else {
|
||||
value.push(optionMap[name].id);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
data: data,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) =>
|
||||
Array.isArray(value) && value.every(v => typeof v === 'string')
|
||||
? value
|
||||
: undefined,
|
||||
isEmpty: ({ value }) => value == null || value.length === 0,
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, unsafeCSS } 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 { numberPropertyModelConfig } from './define.js';
|
||||
import type { NumberPropertyDataType } from './types.js';
|
||||
import {
|
||||
formatNumber,
|
||||
type NumberFormat,
|
||||
parseNumber,
|
||||
} from './utils/formatter.js';
|
||||
|
||||
export class NumberCell extends BaseCellRenderer<
|
||||
number,
|
||||
NumberPropertyDataType
|
||||
> {
|
||||
static override styles = css`
|
||||
affine-database-number-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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() {
|
||||
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
|
||||
? enableNewFormatting
|
||||
? formatNumber(this.value, formatMode, decimals)
|
||||
: this.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 _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 _keydown = (e: KeyboardEvent) => {
|
||||
const ctrlKey = IS_MAC ? e.metaKey : e.ctrlKey;
|
||||
|
||||
if (e.key.toLowerCase() === 'z' && ctrlKey) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
requestAnimationFrame(() => {
|
||||
this.selectCurrentCell(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _setValue = (str: string = this._inputEle.value) => {
|
||||
if (!str) {
|
||||
this.onChange(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const enableNewFormatting =
|
||||
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)
|
||||
: '';
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputEle.value = this._getFormattedString(value);
|
||||
this.onChange(value);
|
||||
};
|
||||
|
||||
focusEnd = () => {
|
||||
const end = this._inputEle.value.length;
|
||||
this._inputEle.focus();
|
||||
this._inputEle.setSelectionRange(end, end);
|
||||
};
|
||||
|
||||
_blur() {
|
||||
this.selectCurrentCell(false);
|
||||
}
|
||||
|
||||
_focus() {
|
||||
if (!this.isEditing) {
|
||||
this.selectCurrentCell(true);
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
requestAnimationFrame(() => {
|
||||
this.focusEnd();
|
||||
});
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
override render() {
|
||||
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}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
}
|
||||
|
||||
export const numberPropertyConfig =
|
||||
numberPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('NumberIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(NumberCell),
|
||||
edit: createFromBaseCellRenderer(NumberCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { t } from '../../core/logical/type-presets.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
import type { NumberPropertyDataType } from './types.js';
|
||||
|
||||
export const numberPropertyType = propertyType('number');
|
||||
|
||||
export const numberPropertyModelConfig = numberPropertyType.modelConfig<
|
||||
number,
|
||||
NumberPropertyDataType
|
||||
>({
|
||||
name: 'Number',
|
||||
type: () => t.number.instance(),
|
||||
defaultData: () => ({ decimal: 0, format: 'number' }),
|
||||
cellToString: ({ value }) => value?.toString() ?? '',
|
||||
cellFromString: ({ value }) => {
|
||||
const num = value ? Number(value) : NaN;
|
||||
return {
|
||||
value: isNaN(num) ? null : num,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => (typeof value !== 'number' ? undefined : value),
|
||||
isEmpty: ({ value }) => value == null,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './types.js';
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { NumberFormat } from './utils/formatter.js';
|
||||
|
||||
export type NumberPropertyDataType = {
|
||||
decimal?: number;
|
||||
format?: NumberFormat;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NumberFormat } from './formatter.js';
|
||||
|
||||
export type NumberCellFormat = {
|
||||
type: NumberFormat;
|
||||
label: string;
|
||||
symbol: string; // New property for symbol
|
||||
};
|
||||
|
||||
export const numberFormats: NumberCellFormat[] = [
|
||||
{ type: 'number', label: 'Number', symbol: '#' },
|
||||
{ type: 'numberWithCommas', label: 'Number With Commas', symbol: '#' },
|
||||
{ type: 'percent', label: 'Percent', symbol: '%' },
|
||||
{ type: 'currencyYen', label: 'Japanese Yen', symbol: '¥' },
|
||||
{ type: 'currencyCNY', label: 'Chinese Yuan', symbol: '¥' },
|
||||
{ type: 'currencyINR', label: 'Indian Rupee', symbol: '₹' },
|
||||
{ type: 'currencyUSD', label: 'US Dollar', symbol: '$' },
|
||||
{ type: 'currencyEUR', label: 'Euro', symbol: '€' },
|
||||
{ type: 'currencyGBP', label: 'British Pound', symbol: '£' },
|
||||
];
|
||||
@@ -0,0 +1,101 @@
|
||||
export type NumberFormat =
|
||||
| 'number'
|
||||
| 'numberWithCommas'
|
||||
| 'percent'
|
||||
| 'currencyYen'
|
||||
| 'currencyINR'
|
||||
| 'currencyCNY'
|
||||
| 'currencyUSD'
|
||||
| 'currencyEUR'
|
||||
| 'currencyGBP';
|
||||
|
||||
const currency = (currency: string): Intl.NumberFormatOptions => ({
|
||||
style: 'currency',
|
||||
currency,
|
||||
currencyDisplay: 'symbol',
|
||||
});
|
||||
|
||||
const numberFormatDefaultConfig: Record<
|
||||
NumberFormat,
|
||||
Intl.NumberFormatOptions
|
||||
> = {
|
||||
number: { style: 'decimal', useGrouping: false },
|
||||
numberWithCommas: { style: 'decimal', useGrouping: true },
|
||||
percent: { style: 'percent', useGrouping: false },
|
||||
currencyINR: currency('INR'),
|
||||
currencyYen: currency('JPY'),
|
||||
currencyCNY: currency('CNY'),
|
||||
currencyUSD: currency('USD'),
|
||||
currencyEUR: currency('EUR'),
|
||||
currencyGBP: currency('GBP'),
|
||||
};
|
||||
|
||||
export function formatNumber(
|
||||
value: number,
|
||||
format: NumberFormat,
|
||||
decimals?: number
|
||||
) {
|
||||
const formatterOptions = { ...numberFormatDefaultConfig[format] };
|
||||
if (decimals !== undefined) {
|
||||
// for feature flag should default to 0 after release
|
||||
Object.assign(formatterOptions, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
});
|
||||
}
|
||||
const formatter = new Intl.NumberFormat(navigator.language, formatterOptions);
|
||||
return formatter.format(value);
|
||||
}
|
||||
|
||||
export function getLocaleDecimalSeparator(locale?: string) {
|
||||
return (1.1).toLocaleString(locale ?? navigator.language).slice(1, 2);
|
||||
}
|
||||
|
||||
// Since we Intl does not provide a parse function we just made it ourself
|
||||
export function parseNumber(value: string, decimalSeparator?: string): number {
|
||||
decimalSeparator = decimalSeparator ?? getLocaleDecimalSeparator();
|
||||
|
||||
// Normalize decimal separator to a period for consistency
|
||||
const normalizedValue = value.replace(
|
||||
new RegExp(`\\${decimalSeparator}`, 'g'),
|
||||
'.'
|
||||
);
|
||||
|
||||
// Remove any leading and trailing non-numeric characters except valid signs, decimal points, and exponents
|
||||
let sanitizedValue = normalizedValue.replace(/^[^\d-+eE.]+|[^\d]+$/g, '');
|
||||
|
||||
// Remove non-numeric characters except decimal points, exponents, and valid signs
|
||||
sanitizedValue = sanitizedValue.replace(/[^0-9.eE+-]/g, '');
|
||||
|
||||
// Handle multiple signs: Keep only the first sign
|
||||
sanitizedValue = sanitizedValue.replace(/([-+]){2,}/g, '$1');
|
||||
|
||||
// Handle misplaced signs: Keep only the leading sign and sign after 'e' or 'E'
|
||||
sanitizedValue = sanitizedValue.replace(
|
||||
/^([-+]?)[^eE]*([eE][-+]?\d+)?$/,
|
||||
(_, p1, p2) =>
|
||||
p1 +
|
||||
sanitizedValue.replace(/[eE].*/, '').replace(/[^\d.]/g, '') +
|
||||
(p2 || '')
|
||||
);
|
||||
|
||||
// Handle multiple decimal points: Keep only the first one in the main part
|
||||
sanitizedValue = sanitizedValue.replace(/(\..*)\./g, '$1');
|
||||
|
||||
// If there is an 'e' or 'E', handle the scientific notation
|
||||
if (/[eE]/.test(sanitizedValue)) {
|
||||
const [base, exp] = sanitizedValue.split(/[eE]/);
|
||||
if (
|
||||
!base ||
|
||||
!exp ||
|
||||
exp.includes('.') ||
|
||||
exp.includes('e') ||
|
||||
exp.includes('E')
|
||||
) {
|
||||
return NaN; // Invalid scientific notation
|
||||
}
|
||||
return parseFloat(sanitizedValue);
|
||||
}
|
||||
|
||||
return parseFloat(sanitizedValue);
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import { css, html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
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 { 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)',
|
||||
success: 'var(--affine-success-color)',
|
||||
};
|
||||
|
||||
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) => {
|
||||
const bgRect = this._progressBg.getBoundingClientRect();
|
||||
const min = bgRect.left;
|
||||
const max = bgRect.right;
|
||||
const setValue = (x: number) => {
|
||||
this.tempValue = Math.round(
|
||||
((Math.min(max, Math.max(min, x)) - min) / (max - min)) * 100
|
||||
);
|
||||
};
|
||||
startDrag(event, {
|
||||
onDrag: ({ x }) => {
|
||||
setValue(x);
|
||||
return;
|
||||
},
|
||||
onMove: ({ x }) => {
|
||||
setValue(x);
|
||||
return;
|
||||
},
|
||||
onDrop: () => {
|
||||
//
|
||||
},
|
||||
onClear: () => {
|
||||
//
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
get _value() {
|
||||
return this.tempValue ?? this.value ?? 0;
|
||||
}
|
||||
|
||||
_onChange(value?: number) {
|
||||
this.tempValue = value;
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const disposables = this._disposables;
|
||||
|
||||
disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag);
|
||||
disposables.addFromEvent(window, 'keydown', evt => {
|
||||
if (evt.key === 'ArrowDown') {
|
||||
evt.preventDefault();
|
||||
this._onChange(Math.max(0, this._value - 1));
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'ArrowUp') {
|
||||
evt.preventDefault();
|
||||
this._onChange(Math.min(100, this._value + 1));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
override onCopy(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
override onCut(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
this.onChange(this._value);
|
||||
}
|
||||
|
||||
override onPaste(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const progress = this._value;
|
||||
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)',
|
||||
});
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-number progress">${progress}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@query('.affine-database-progress-bg')
|
||||
private accessor _progressBg!: HTMLElement;
|
||||
|
||||
@state()
|
||||
private accessor tempValue: number | undefined = undefined;
|
||||
}
|
||||
|
||||
export const progressPropertyConfig =
|
||||
progressPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('ProgressIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(ProgressCell),
|
||||
edit: createFromBaseCellRenderer(ProgressCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { t } from '../../core/logical/type-presets.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const progressPropertyType = propertyType('progress');
|
||||
|
||||
export const progressPropertyModelConfig =
|
||||
progressPropertyType.modelConfig<number>({
|
||||
name: 'Progress',
|
||||
type: () => t.number.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => value?.toString() ?? '',
|
||||
cellFromString: ({ value }) => {
|
||||
const num = value ? Number(value) : NaN;
|
||||
return {
|
||||
value: isNaN(num) ? null : num,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => {
|
||||
if (typeof value !== 'number') return undefined;
|
||||
return value;
|
||||
},
|
||||
isEmpty: () => false,
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { checkboxPropertyModelConfig } from './checkbox/define.js';
|
||||
import { datePropertyModelConfig } from './date/define.js';
|
||||
import { imagePropertyModelConfig } from './image/define.js';
|
||||
import { multiSelectPropertyModelConfig } from './multi-select/define.js';
|
||||
import { numberPropertyModelConfig } from './number/define.js';
|
||||
import { progressPropertyModelConfig } from './progress/define.js';
|
||||
import { selectPropertyModelConfig } from './select/define.js';
|
||||
import { textPropertyModelConfig } from './text/define.js';
|
||||
|
||||
export const propertyModelPresets = {
|
||||
checkboxPropertyModelConfig,
|
||||
datePropertyModelConfig,
|
||||
imagePropertyModelConfig,
|
||||
multiSelectPropertyModelConfig,
|
||||
numberPropertyModelConfig,
|
||||
progressPropertyModelConfig,
|
||||
selectPropertyModelConfig,
|
||||
textPropertyModelConfig,
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { computed, signal } 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 { createIcon } from '../../core/utils/uni-icon.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
|
||||
> {
|
||||
private 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,
|
||||
}
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
_editComplete = () => {
|
||||
this.selectCurrentCell(false);
|
||||
};
|
||||
|
||||
_onChange = ([id]: string[]) => {
|
||||
this.onChange(id);
|
||||
};
|
||||
|
||||
_onOptionsChange = (options: SelectTag[]) => {
|
||||
this.property.dataUpdate(data => {
|
||||
return {
|
||||
...data,
|
||||
options,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
options$ = computed(() => {
|
||||
return this.property.data$.value.options;
|
||||
});
|
||||
|
||||
get _value() {
|
||||
const value = this.value;
|
||||
return value ? [value] : [];
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.popTagSelect();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export const selectPropertyConfig =
|
||||
selectPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('SingleSelectIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(SelectCell),
|
||||
edit: createFromBaseCellRenderer(SelectCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
import { getTagColor } from '../../core/component/tags/colors.js';
|
||||
import { type SelectTag, t } from '../../core/index.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const selectPropertyType = propertyType('select');
|
||||
|
||||
export type SelectPropertyData = {
|
||||
options: SelectTag[];
|
||||
};
|
||||
export const selectPropertyModelConfig = selectPropertyType.modelConfig<
|
||||
string,
|
||||
SelectPropertyData
|
||||
>({
|
||||
name: 'Select',
|
||||
type: ({ data }) => t.tag.instance(data.options),
|
||||
defaultData: () => ({
|
||||
options: [],
|
||||
}),
|
||||
addGroup: ({ text, oldData }) => {
|
||||
return {
|
||||
options: [
|
||||
...(oldData.options ?? []),
|
||||
{ id: nanoid(), value: text, color: getTagColor() },
|
||||
],
|
||||
};
|
||||
},
|
||||
cellToString: ({ value, data }) =>
|
||||
data.options.find(v => v.id === value)?.value ?? '',
|
||||
cellFromString: ({ value: oldValue, data }) => {
|
||||
if (!oldValue) {
|
||||
return { value: null, data: data };
|
||||
}
|
||||
const optionMap = Object.fromEntries(data.options.map(v => [v.value, v]));
|
||||
const name = oldValue
|
||||
.split(',')
|
||||
.map(v => v.trim())
|
||||
.filter(v => v)[0];
|
||||
|
||||
let value: string | undefined;
|
||||
const option = optionMap[name];
|
||||
if (!option) {
|
||||
const newOption: SelectTag = {
|
||||
id: nanoid(),
|
||||
value: name,
|
||||
color: getTagColor(),
|
||||
};
|
||||
data.options.push(newOption);
|
||||
value = newOption.id;
|
||||
} else {
|
||||
value = option.id;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
data: data,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value),
|
||||
isEmpty: ({ value }) => value == null,
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, unsafeCSS } 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 { 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;
|
||||
}
|
||||
`;
|
||||
|
||||
private _keydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
this._setValue();
|
||||
setTimeout(() => {
|
||||
this.selectCurrentCell(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _setValue = (str: string = this._inputEle.value) => {
|
||||
this._inputEle.value = `${this.value ?? ''}`;
|
||||
this.onChange(str);
|
||||
};
|
||||
|
||||
focusEnd = () => {
|
||||
const end = this._inputEle.value.length;
|
||||
this._inputEle.focus();
|
||||
this._inputEle.setSelectionRange(end, end);
|
||||
};
|
||||
|
||||
override firstUpdated() {
|
||||
this.focusEnd();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<input
|
||||
.value="${this.value ?? ''}"
|
||||
@keydown="${this._keydown}"
|
||||
class="affine-database-text"
|
||||
/>`;
|
||||
}
|
||||
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
}
|
||||
|
||||
export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('TextIcon'),
|
||||
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(TextCell),
|
||||
edit: createFromBaseCellRenderer(TextCellEditing),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { t } from '../../core/index.js';
|
||||
import { propertyType } from '../../core/property/property-config.js';
|
||||
|
||||
export const textPropertyType = propertyType('text');
|
||||
|
||||
export const textPropertyModelConfig = textPropertyType.modelConfig<string>({
|
||||
name: 'Plain-Text',
|
||||
type: () => t.string.instance(),
|
||||
defaultData: () => ({}),
|
||||
cellToString: ({ value }) => value ?? '',
|
||||
cellFromString: ({ value }) => {
|
||||
return {
|
||||
value: value,
|
||||
};
|
||||
},
|
||||
cellToJson: ({ value }) => value ?? null,
|
||||
cellFromJson: ({ value }) => (typeof value !== 'string' ? undefined : value),
|
||||
isEmpty: ({ value }) => value == null || value.length === 0,
|
||||
});
|
||||
Reference in new issue
Block a user