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,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);
|
||||
}
|
||||
Reference in new issue
Block a user