mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
refactor(editor): remove edit view of database block properties (#10748)
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
|
||||
@@ -4,37 +4,20 @@ import { DatabaseBlockComponent } from './database-block';
|
||||
import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block';
|
||||
import { BlockRenderer } from './detail-panel/block-renderer';
|
||||
import { NoteRenderer } from './detail-panel/note-renderer';
|
||||
import { LinkCell, LinkCellEditing } from './properties/link/cell-renderer';
|
||||
import { LinkNode } from './properties/link/components/link-node';
|
||||
import {
|
||||
RichTextCell,
|
||||
RichTextCellEditing,
|
||||
} from './properties/rich-text/cell-renderer';
|
||||
import { LinkCell } from './properties/link/cell-renderer';
|
||||
import { RichTextCell } from './properties/rich-text/cell-renderer';
|
||||
import { IconCell } from './properties/title/icon';
|
||||
import {
|
||||
HeaderAreaTextCell,
|
||||
HeaderAreaTextCellEditing,
|
||||
} from './properties/title/text';
|
||||
import { HeaderAreaTextCell } from './properties/title/text';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-database-title', DatabaseTitle);
|
||||
customElements.define('data-view-header-area-icon', IconCell);
|
||||
customElements.define('affine-database-link-cell', LinkCell);
|
||||
customElements.define('affine-database-link-cell-editing', LinkCellEditing);
|
||||
customElements.define('data-view-header-area-text', HeaderAreaTextCell);
|
||||
customElements.define(
|
||||
'data-view-header-area-text-editing',
|
||||
HeaderAreaTextCellEditing
|
||||
);
|
||||
customElements.define('affine-database-rich-text-cell', RichTextCell);
|
||||
customElements.define(
|
||||
'affine-database-rich-text-cell-editing',
|
||||
RichTextCellEditing
|
||||
);
|
||||
customElements.define('center-peek', CenterPeek);
|
||||
customElements.define('database-datasource-note-renderer', NoteRenderer);
|
||||
customElements.define('database-datasource-block-renderer', BlockRenderer);
|
||||
customElements.define('affine-database-link-node', LinkNode);
|
||||
customElements.define('affine-database', DatabaseBlockComponent);
|
||||
|
||||
customElements.define(
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const linkCellStyle = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const linkContainerStyle = style({
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
overflow: 'hidden',
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
wordBreak: 'break-all',
|
||||
});
|
||||
export const linkIconContainerStyle = style({
|
||||
position: 'absolute',
|
||||
right: '8px',
|
||||
top: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
visibility: 'hidden',
|
||||
backgroundColor: cssVarV2.layer.background.primary,
|
||||
boxShadow: 'var(--affine-button-shadow)',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
zIndex: 1,
|
||||
});
|
||||
export const linkIconStyle = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
color: cssVarV2.icon.primary,
|
||||
fontSize: '14px',
|
||||
padding: '2px',
|
||||
':hover': {
|
||||
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||
},
|
||||
});
|
||||
|
||||
export const showLinkIconStyle = style({
|
||||
selectors: {
|
||||
[`${linkCellStyle}:hover &`]: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const linkedDocStyle = style({
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'var(--affine-divider-color)',
|
||||
transition: 'text-decoration-color 0.2s ease-out',
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
textDecorationColor: 'var(--affine-icon-color)',
|
||||
},
|
||||
});
|
||||
|
||||
export const linkEditingStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
wordBreak: 'break-all',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
export const inlineLinkNodeStyle = style({
|
||||
wordBreak: 'break-all',
|
||||
color: 'var(--affine-link-color)',
|
||||
fill: 'var(--affine-link-color)',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'normal',
|
||||
textDecoration: 'none',
|
||||
});
|
||||
|
||||
export const normalTextStyle = style({
|
||||
wordBreak: 'break-all',
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine-rich-text';
|
||||
import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import {
|
||||
isValidUrl,
|
||||
normalizeUrl,
|
||||
@@ -12,106 +11,69 @@ import {
|
||||
createIcon,
|
||||
} from '@blocksuite/data-view';
|
||||
import { EditIcon } from '@blocksuite/icons/lit';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, nothing, unsafeCSS } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, nothing, type PropertyValues } from 'lit';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
|
||||
import { HostContextKey } from '../../context/host-context.js';
|
||||
import {
|
||||
inlineLinkNodeStyle,
|
||||
linkCellStyle,
|
||||
linkContainerStyle,
|
||||
linkedDocStyle,
|
||||
linkEditingStyle,
|
||||
linkIconContainerStyle,
|
||||
linkIconStyle,
|
||||
normalTextStyle,
|
||||
showLinkIconStyle,
|
||||
} from './cell-renderer.css.js';
|
||||
import { linkPropertyModelConfig } from './define.js';
|
||||
|
||||
export class LinkCell extends BaseCellRenderer<string> {
|
||||
static override styles = css`
|
||||
affine-database-link-cell {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
affine-database-link-cell:hover .affine-database-link-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.affine-database-link {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
affine-database-link-node {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.affine-database-link-icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
background: ${unsafeCSSVarV2('button/iconButtonSolid')};
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
box-shadow: var(--affine-button-shadow);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.affine-database-link-icon:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.data-view-link-column-linked-doc {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--affine-divider-color);
|
||||
transition: text-decoration-color 0.2s ease-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-view-link-column-linked-doc:hover {
|
||||
text-decoration-color: var(--affine-icon-color);
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _onClick = (event: Event) => {
|
||||
event.stopPropagation();
|
||||
const value = this.value ?? '';
|
||||
|
||||
if (!value || !isValidUrl(value)) {
|
||||
this.selectCurrentCell(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValidUrl(value)) {
|
||||
const target = event.target as HTMLElement;
|
||||
const link = target.querySelector<HTMLAnchorElement>('.link-node');
|
||||
if (link) {
|
||||
event.preventDefault();
|
||||
link.click();
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
protected override firstUpdated(_changedProperties: PropertyValues) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this.classList.add(linkCellStyle);
|
||||
}
|
||||
|
||||
private readonly _onEdit = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
this.selectCurrentCell(true);
|
||||
this.selectCurrentCell(true);
|
||||
};
|
||||
|
||||
private preValue?: string;
|
||||
private readonly _focusEnd = () => {
|
||||
const ele = this._container.value;
|
||||
if (!ele) {
|
||||
return;
|
||||
}
|
||||
const end = ele?.value.length;
|
||||
ele?.focus();
|
||||
ele?.setSelectionRange(end, end);
|
||||
};
|
||||
|
||||
private readonly _onKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
this.selectCurrentCell(false);
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _setValue = (
|
||||
value: string = this._container.value?.value ?? ''
|
||||
) => {
|
||||
let url = value;
|
||||
if (isValidUrl(value)) {
|
||||
url = normalizeUrl(value);
|
||||
}
|
||||
|
||||
this.valueSetNextTick(url);
|
||||
if (this._container.value) {
|
||||
this._container.value.value = url;
|
||||
}
|
||||
};
|
||||
|
||||
openDoc = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!this.docId) {
|
||||
if (!this.docId$.value) {
|
||||
return;
|
||||
}
|
||||
const std = this.std;
|
||||
@@ -120,7 +82,7 @@ export class LinkCell extends BaseCellRenderer<string> {
|
||||
}
|
||||
|
||||
std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.emit({
|
||||
pageId: this.docId,
|
||||
pageId: this.docId$.value,
|
||||
host: std.host,
|
||||
});
|
||||
};
|
||||
@@ -130,128 +92,95 @@ export class LinkCell extends BaseCellRenderer<string> {
|
||||
return host?.std;
|
||||
}
|
||||
|
||||
override render() {
|
||||
const linkText = this.value ?? '';
|
||||
const docName =
|
||||
this.docId && this.std?.workspace.getDoc(this.docId)?.meta?.title;
|
||||
return html`
|
||||
<div class="affine-database-link" @click="${this._onClick}">
|
||||
${docName
|
||||
? html`<span
|
||||
class="data-view-link-column-linked-doc"
|
||||
@click="${this.openDoc}"
|
||||
>${docName}</span
|
||||
>`
|
||||
: html` <affine-database-link-node
|
||||
.link="${linkText}"
|
||||
></affine-database-link-node>`}
|
||||
</div>
|
||||
${docName || linkText
|
||||
? html` <div class="affine-database-link-icon" @click="${this._onEdit}">
|
||||
${EditIcon()}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
override updated() {
|
||||
if (this.value !== this.preValue) {
|
||||
const std = this.std;
|
||||
this.preValue = this.value;
|
||||
if (!this.value || !isValidUrl(this.value)) {
|
||||
this.docId = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.docId =
|
||||
std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(this.value)?.docId ??
|
||||
undefined;
|
||||
docId$ = computed(() => {
|
||||
if (!this.value || !isValidUrl(this.value)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.parseDocUrl(this.value)?.docId;
|
||||
});
|
||||
|
||||
@state()
|
||||
accessor docId: string | undefined = undefined;
|
||||
}
|
||||
private readonly _container = createRef<HTMLInputElement>();
|
||||
|
||||
export class LinkCellEditing extends BaseCellRenderer<string> {
|
||||
static override styles = css`
|
||||
affine-database-link-cell-editing {
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.affine-database-link-editing {
|
||||
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);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.affine-database-link-editing:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _focusEnd = () => {
|
||||
const end = this._container.value.length;
|
||||
this._container.focus();
|
||||
this._container.setSelectionRange(end, end);
|
||||
};
|
||||
|
||||
private readonly _onKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
this._setValue();
|
||||
setTimeout(() => {
|
||||
this.selectCurrentCell(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _setValue = (value: string = this._container.value) => {
|
||||
let url = value;
|
||||
if (isValidUrl(value)) {
|
||||
url = normalizeUrl(value);
|
||||
}
|
||||
|
||||
this.onChange(url);
|
||||
this._container.value = url;
|
||||
};
|
||||
|
||||
override firstUpdated() {
|
||||
override afterEnterEditingMode() {
|
||||
this._focusEnd();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
override beforeExitEditingMode() {
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const linkText = this.value ?? '';
|
||||
|
||||
return html`<input
|
||||
class="affine-database-link-editing link"
|
||||
.value="${linkText}"
|
||||
@keydown="${this._onKeydown}"
|
||||
@pointerdown="${stopPropagation}"
|
||||
/>`;
|
||||
parseDocUrl(url: string) {
|
||||
return this.std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(url);
|
||||
}
|
||||
|
||||
@query('.affine-database-link-editing')
|
||||
private accessor _container!: HTMLInputElement;
|
||||
docName$ = computed(() => {
|
||||
const title =
|
||||
this.docId$.value &&
|
||||
this.std?.workspace.getDoc(this.docId$.value)?.meta?.title;
|
||||
if (title == null) {
|
||||
return;
|
||||
}
|
||||
return title || 'Untitled';
|
||||
});
|
||||
|
||||
renderLink() {
|
||||
const linkText = this.value ?? '';
|
||||
const docName = this.docName$.value;
|
||||
const isDoc = !!docName;
|
||||
const isLink = !!linkText;
|
||||
const hasLink = isDoc || isLink;
|
||||
return html`
|
||||
<div>
|
||||
<div class="${linkContainerStyle}">
|
||||
${isDoc
|
||||
? html`<span class="${linkedDocStyle}" @click="${this.openDoc}"
|
||||
>${docName}</span
|
||||
>`
|
||||
: isValidUrl(linkText)
|
||||
? html`<a
|
||||
data-testid="property-link-a"
|
||||
class="${inlineLinkNodeStyle}"
|
||||
href="${linkText}"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>${linkText}</a
|
||||
>`
|
||||
: html`<span class="${normalTextStyle}">${linkText}</span>`}
|
||||
</div>
|
||||
${hasLink
|
||||
? html` <div class="${linkIconContainerStyle} ${showLinkIconStyle}">
|
||||
<div
|
||||
class="${linkIconStyle}"
|
||||
data-testid="edit-link-button"
|
||||
@click="${this._onEdit}"
|
||||
>
|
||||
${EditIcon()}
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.isEditing$.value) {
|
||||
const linkText = this.value ?? '';
|
||||
return html`<input
|
||||
class="${linkEditingStyle} link"
|
||||
${ref(this._container)}
|
||||
.value="${linkText}"
|
||||
@keydown="${this._onKeydown}"
|
||||
@pointerdown="${stopPropagation}"
|
||||
/>`;
|
||||
} else {
|
||||
return this.renderLink();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const linkColumnConfig = linkPropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('LinkIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(LinkCell),
|
||||
edit: createFromBaseCellRenderer(LinkCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { isValidUrl } from '@blocksuite/affine-shared/utils';
|
||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { css, html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class LinkNode extends ShadowlessElement {
|
||||
static override styles = css`
|
||||
.link-node {
|
||||
word-break: break-all;
|
||||
color: var(--affine-link-color);
|
||||
fill: var(--affine-link-color);
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
protected override render() {
|
||||
if (!isValidUrl(this.link)) {
|
||||
return html`<span class="normal-text">${this.link}</span>`;
|
||||
}
|
||||
|
||||
return html`<a
|
||||
class="link-node"
|
||||
href=${this.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
><span class="link-node-text">${this.link}</span></a
|
||||
>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor link!: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-database-link-node': LinkNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const richTextCellStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const richTextContainerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
wordBreak: 'break-all',
|
||||
});
|
||||
@@ -21,13 +21,16 @@ import { IS_MAC } from '@blocksuite/global/env';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import type { BlockSnapshot } from '@blocksuite/store';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { css } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { ref } from 'lit/directives/ref.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { HostContextKey } from '../../context/host-context.js';
|
||||
import type { DatabaseBlockComponent } from '../../database-block.js';
|
||||
import {
|
||||
richTextCellStyle,
|
||||
richTextContainerStyle,
|
||||
} from './cell-renderer.css.js';
|
||||
import { richTextPropertyModelConfig } from './define.js';
|
||||
|
||||
function toggleStyle(
|
||||
@@ -78,61 +81,10 @@ function toggleStyle(
|
||||
inlineEditor.syncInlineRange();
|
||||
}
|
||||
|
||||
abstract class BaseRichTextCell extends BaseCellRenderer<Text> {
|
||||
static override styles = css`
|
||||
affine-database-rich-text-cell,
|
||||
affine-database-rich-text-cell-editing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.affine-database-rich-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.data-view-header-area-icon {
|
||||
height: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
background-color: var(--affine-background-secondary-color);
|
||||
}
|
||||
|
||||
.data-view-header-area-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--affine-icon-color);
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get inlineEditor() {
|
||||
return this.richText?.inlineEditor;
|
||||
}
|
||||
export class RichTextCell extends BaseCellRenderer<Text> {
|
||||
inlineEditor$ = computed(() => {
|
||||
return this.richText$.value?.inlineEditor;
|
||||
});
|
||||
|
||||
get inlineManager() {
|
||||
return this.view
|
||||
@@ -146,57 +98,11 @@ abstract class BaseRichTextCell extends BaseCellRenderer<Text> {
|
||||
return databaseBlock?.topContenteditableElement;
|
||||
}
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager?.getRenderer();
|
||||
}
|
||||
|
||||
get attributesSchema() {
|
||||
return this.inlineManager?.getSchema();
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.view.contextGet(HostContextKey);
|
||||
}
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
|
||||
@query('.affine-database-rich-text')
|
||||
accessor _richTextElement!: HTMLElement;
|
||||
}
|
||||
|
||||
export class RichTextCell extends BaseRichTextCell {
|
||||
static override styles = css`
|
||||
affine-database-rich-text-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.affine-database-rich-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
private readonly richText$ = signal<RichText>();
|
||||
|
||||
private changeUserSelectAccordToReadOnly() {
|
||||
if (this && this instanceof HTMLElement) {
|
||||
@@ -204,61 +110,6 @@ export class RichTextCell extends BaseRichTextCell {
|
||||
}
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.changeUserSelectAccordToReadOnly();
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (!this.value || !(this.value instanceof Text)) {
|
||||
return html`<div class="affine-database-rich-text"></div>`;
|
||||
}
|
||||
return keyed(
|
||||
this.value,
|
||||
html`<rich-text
|
||||
.yText=${this.value}
|
||||
.attributesSchema=${this.attributesSchema}
|
||||
.attributeRenderer=${this.attributeRenderer}
|
||||
.embedChecker=${this.inlineManager?.embedChecker}
|
||||
.markdownMatches=${this.inlineManager?.markdownMatches}
|
||||
.readonly=${true}
|
||||
class="affine-database-rich-text inline-editor"
|
||||
></rich-text>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class RichTextCellEditing extends BaseRichTextCell {
|
||||
static override styles = css`
|
||||
affine-database-rich-text-cell-editing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 1px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.affine-database-rich-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-database-rich-text v-line > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Escape') {
|
||||
if (event.key === 'Tab') {
|
||||
@@ -280,7 +131,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
return;
|
||||
}
|
||||
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const inlineEditor = this.inlineEditor$.value;
|
||||
if (!inlineEditor) return;
|
||||
|
||||
switch (event.key) {
|
||||
@@ -331,17 +182,17 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
|
||||
private readonly _initYText = (text?: string) => {
|
||||
const yText = new Text(text);
|
||||
this.onChange(yText);
|
||||
this.valueSetImmediate(yText);
|
||||
};
|
||||
|
||||
private readonly _onSoftEnter = () => {
|
||||
if (this.value && this.inlineEditor) {
|
||||
const inlineRange = this.inlineEditor.getInlineRange();
|
||||
if (this.value && this.inlineEditor$.value) {
|
||||
const inlineRange = this.inlineEditor$.value.getInlineRange();
|
||||
if (!inlineRange) return;
|
||||
|
||||
const text = new Text(this.inlineEditor.yText);
|
||||
const text = new Text(this.inlineEditor$.value.yText);
|
||||
text.replace(inlineRange.index, inlineRange.length, '\n');
|
||||
this.inlineEditor.setInlineRange({
|
||||
this.inlineEditor$.value.setInlineRange({
|
||||
index: inlineRange.index + 1,
|
||||
length: 0,
|
||||
});
|
||||
@@ -349,7 +200,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
};
|
||||
|
||||
private readonly _onCopy = (e: ClipboardEvent) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const inlineEditor = this.inlineEditor$.value;
|
||||
if (!inlineEditor) return;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
@@ -366,7 +217,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
};
|
||||
|
||||
private readonly _onCut = (e: ClipboardEvent) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const inlineEditor = this.inlineEditor$.value;
|
||||
if (!inlineEditor) return;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
@@ -388,7 +239,9 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
};
|
||||
|
||||
private readonly _onPaste = (e: ClipboardEvent) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const inlineEditor = this.inlineEditor$.value;
|
||||
if (!inlineEditor) return;
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
@@ -419,8 +272,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
?.getData('text/plain')
|
||||
?.replace(/\r?\n|\r/g, '\n');
|
||||
if (!text) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isValidUrl(text)) {
|
||||
const std = this.std;
|
||||
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
|
||||
@@ -459,6 +311,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(text);
|
||||
inlineEditor.insertText(inlineRange, text);
|
||||
inlineEditor.setInlineRange({
|
||||
index: inlineRange.index + text.length,
|
||||
@@ -469,67 +322,78 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.value || typeof this.value === 'string') {
|
||||
this._initYText(this.value);
|
||||
}
|
||||
this.classList.add(richTextCellStyle);
|
||||
|
||||
this.changeUserSelectAccordToReadOnly();
|
||||
|
||||
const selectAll = (e: KeyboardEvent) => {
|
||||
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.inlineEditor?.selectAll();
|
||||
this.inlineEditor$.value?.selectAll();
|
||||
}
|
||||
};
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const editor = this.inlineEditor$.value;
|
||||
if (editor) {
|
||||
const disposable = editor.slots.keydown.on(this._handleKeyDown);
|
||||
return () => disposable.dispose();
|
||||
}
|
||||
return;
|
||||
})
|
||||
);
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const richText = this.richText$.value;
|
||||
if (richText) {
|
||||
richText.addEventListener('copy', this._onCopy, true);
|
||||
richText.addEventListener('cut', this._onCut, true);
|
||||
richText.addEventListener('paste', this._onPaste, true);
|
||||
return () => {
|
||||
richText.removeEventListener('copy', this._onCopy);
|
||||
richText.removeEventListener('cut', this._onCut);
|
||||
richText.removeEventListener('paste', this._onPaste);
|
||||
};
|
||||
}
|
||||
return;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.richText?.updateComplete
|
||||
.then(() => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (!inlineEditor) return;
|
||||
override beforeEnterEditMode() {
|
||||
if (!this.value || typeof this.value === 'string') {
|
||||
this._initYText(this.value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
this.disposables.add(
|
||||
inlineEditor.slots.keydown.on(this._handleKeyDown)
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(
|
||||
this._richTextElement!,
|
||||
'copy',
|
||||
this._onCopy
|
||||
);
|
||||
this.disposables.addFromEvent(
|
||||
this._richTextElement!,
|
||||
'cut',
|
||||
this._onCut
|
||||
);
|
||||
this.disposables.addFromEvent(
|
||||
this._richTextElement!,
|
||||
'paste',
|
||||
this._onPaste
|
||||
);
|
||||
|
||||
inlineEditor.focusEnd();
|
||||
})
|
||||
.catch(console.error);
|
||||
override afterEnterEditingMode() {
|
||||
this.inlineEditor$.value?.focusEnd();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<rich-text
|
||||
if (!this.value || !(this.value instanceof Text)) {
|
||||
return html` <div class="${richTextContainerStyle}"></div>`;
|
||||
}
|
||||
return html` <rich-text
|
||||
${ref(this.richText$)}
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText=${this.value}
|
||||
.inlineEventSource=${this.topContenteditableElement}
|
||||
.attributesSchema=${this.attributesSchema}
|
||||
.attributeRenderer=${this.attributeRenderer}
|
||||
.embedChecker=${this.inlineManager?.embedChecker}
|
||||
.markdownMatches=${this.inlineManager?.markdownMatches}
|
||||
.verticalScrollContainerGetter=${() =>
|
||||
.yText="${this.value}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.attributesSchema="${this.inlineManager?.getSchema()}"
|
||||
.attributeRenderer="${this.inlineManager?.getRenderer()}"
|
||||
.embedChecker="${this.inlineManager?.embedChecker}"
|
||||
.markdownMatches="${this.inlineManager?.markdownMatches}"
|
||||
.readonly="${!this.isEditing$.value || this.readonly}"
|
||||
.verticalScrollContainerGetter="${() =>
|
||||
this.topContenteditableElement?.host
|
||||
? getViewportElement(this.topContenteditableElement.host)
|
||||
: null}
|
||||
class="affine-database-rich-text inline-editor"
|
||||
: null}"
|
||||
class="${richTextContainerStyle} inline-editor"
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
@@ -538,7 +402,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
}
|
||||
|
||||
insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const inlineEditor = this.inlineEditor$.value;
|
||||
const range = inlineEditor?.getInlineRange();
|
||||
if (!range || !delta.insert) {
|
||||
return;
|
||||
@@ -553,7 +417,7 @@ export class RichTextCellEditing extends BaseRichTextCell {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-database-rich-text-cell-editing': RichTextCellEditing;
|
||||
'affine-database-rich-text-cell': RichTextCell;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,6 +427,5 @@ export const richTextColumnConfig =
|
||||
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(RichTextCell),
|
||||
edit: createFromBaseCellRenderer(RichTextCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const titleCellStyle = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
export const titleRichTextStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
outline: 'none',
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
});
|
||||
|
||||
export const headerAreaIconStyle = style({
|
||||
height: 'max-content',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginRight: '8px',
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '2px',
|
||||
color: cssVarV2.icon.primary,
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { TableSingleView } from '@blocksuite/data-view/view-presets';
|
||||
|
||||
import { titlePropertyModelConfig } from './define.js';
|
||||
import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js';
|
||||
import { HeaderAreaTextCell } from './text.js';
|
||||
|
||||
export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('TitleIcon'),
|
||||
@@ -19,12 +19,5 @@ export const titleColumnConfig = titlePropertyModelConfig.createPropertyMeta({
|
||||
showIcon: props.cell.view instanceof TableSingleView,
|
||||
})
|
||||
),
|
||||
edit: uniMap(
|
||||
createFromBaseCellRenderer(HeaderAreaTextCellEditing),
|
||||
(props: CellRenderProps) => ({
|
||||
...props,
|
||||
showIcon: props.cell.view instanceof TableSingleView,
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RootBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DefaultInlineManagerExtension,
|
||||
type RichText,
|
||||
@@ -16,106 +15,31 @@ import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { LinkedPageIcon } from '@blocksuite/icons/lit';
|
||||
import type { DeltaInsert } from '@blocksuite/inline';
|
||||
import type { BlockSnapshot, Text } from '@blocksuite/store';
|
||||
import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { css, type TemplateResult } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { HostContextKey } from '../../context/host-context.js';
|
||||
import type { DatabaseBlockComponent } from '../../database-block.js';
|
||||
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
|
||||
import {
|
||||
headerAreaIconStyle,
|
||||
titleCellStyle,
|
||||
titleRichTextStyle,
|
||||
} from './cell-renderer.css.js';
|
||||
|
||||
const styles = css`
|
||||
data-view-header-area-text {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
data-view-header-area-text rich-text {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
data-view-header-area-text-editing {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.data-view-header-area-rich-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
outline: none;
|
||||
word-break: break-all;
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
}
|
||||
|
||||
.data-view-header-area-rich-text v-line {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-view-header-area-rich-text v-line > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.data-view-header-area-icon {
|
||||
height: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
margin-top: 2px;
|
||||
background-color: var(--affine-background-secondary-color);
|
||||
}
|
||||
|
||||
.data-view-header-area-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: var(--affine-icon-color);
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
`;
|
||||
|
||||
abstract class BaseTextCell extends BaseCellRenderer<Text> {
|
||||
static override styles = styles;
|
||||
|
||||
export class HeaderAreaTextCell extends BaseCellRenderer<Text> {
|
||||
activity = true;
|
||||
|
||||
docId$ = signal<string>();
|
||||
|
||||
isLinkedDoc$ = computed(() => false);
|
||||
|
||||
linkedDocTitle$ = computed(() => {
|
||||
if (!this.docId$.value) {
|
||||
return this.value;
|
||||
}
|
||||
const doc = this.host?.std.workspace.getDoc(this.docId$.value);
|
||||
const root = doc?.root as RootBlockModel;
|
||||
return root.title;
|
||||
});
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager?.getRenderer();
|
||||
}
|
||||
|
||||
get attributesSchema() {
|
||||
return this.inlineManager?.getSchema();
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this.view.contextGet(HostContextKey);
|
||||
}
|
||||
|
||||
get inlineEditor() {
|
||||
return this.richText.inlineEditor;
|
||||
return this.richText.value?.inlineEditor;
|
||||
}
|
||||
|
||||
get inlineManager() {
|
||||
@@ -128,80 +52,10 @@ abstract class BaseTextCell extends BaseCellRenderer<Text> {
|
||||
return databaseBlock?.topContenteditableElement;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const yText = this.value?.yText;
|
||||
if (yText) {
|
||||
const cb = () => {
|
||||
const id = getSingleDocIdFromText(this.value);
|
||||
this.docId$.value = id;
|
||||
};
|
||||
cb();
|
||||
if (this.activity) {
|
||||
yText.observe(cb);
|
||||
this.disposables.add(() => {
|
||||
yText.unobserve(cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
get std() {
|
||||
return this.view.contextGet(HostContextKey)?.std;
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
return html`${this.renderIcon()}${this.renderBlockText()}`;
|
||||
}
|
||||
|
||||
abstract renderBlockText(): TemplateResult;
|
||||
|
||||
renderIcon() {
|
||||
if (this.docId$.value) {
|
||||
return html` <div class="data-view-header-area-icon">
|
||||
${LinkedPageIcon()}
|
||||
</div>`;
|
||||
}
|
||||
if (!this.showIcon) {
|
||||
return;
|
||||
}
|
||||
const iconColumn = this.view.mainProperties$.value.iconColumn;
|
||||
if (!iconColumn) return;
|
||||
|
||||
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
|
||||
if (!icon) return;
|
||||
|
||||
return html` <div class="data-view-header-area-icon">${icon}</div>`;
|
||||
}
|
||||
|
||||
abstract renderLinkedDoc(): TemplateResult;
|
||||
|
||||
@query('rich-text')
|
||||
accessor richText!: RichText;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showIcon = false;
|
||||
}
|
||||
|
||||
export class HeaderAreaTextCell extends BaseTextCell {
|
||||
override renderBlockText() {
|
||||
return html` <rich-text
|
||||
.yText="${this.value}"
|
||||
.attributesSchema="${this.attributesSchema}"
|
||||
.attributeRenderer="${this.attributeRenderer}"
|
||||
.embedChecker="${this.inlineManager?.embedChecker}"
|
||||
.markdownMatches="${this.inlineManager?.markdownMatches}"
|
||||
.readonly="${true}"
|
||||
class="data-view-header-area-rich-text"
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
override renderLinkedDoc(): TemplateResult {
|
||||
return html` <rich-text
|
||||
.yText="${this.linkedDocTitle$.value}"
|
||||
.readonly="${true}"
|
||||
class="data-view-header-area-rich-text"
|
||||
></rich-text>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
private readonly _onCopy = (e: ClipboardEvent) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
if (!inlineEditor) return;
|
||||
@@ -318,8 +172,6 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
}
|
||||
};
|
||||
|
||||
override activity = false;
|
||||
|
||||
insertDelta = (delta: DeltaInsert) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
const range = inlineEditor?.getInlineRange();
|
||||
@@ -333,12 +185,25 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
});
|
||||
};
|
||||
|
||||
private get std() {
|
||||
return this.host?.std;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.classList.add(titleCellStyle);
|
||||
|
||||
const yText = this.value?.yText;
|
||||
if (yText) {
|
||||
const cb = () => {
|
||||
const id = getSingleDocIdFromText(this.value);
|
||||
this.docId$.value = id;
|
||||
};
|
||||
cb();
|
||||
if (this.activity) {
|
||||
yText.observe(cb);
|
||||
this.disposables.add(() => {
|
||||
yText.unobserve(cb);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const selectAll = (e: KeyboardEvent) => {
|
||||
if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) {
|
||||
e.stopPropagation();
|
||||
@@ -346,82 +211,86 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
|
||||
this.inlineEditor?.selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('keydown', selectAll);
|
||||
this.disposables.add(() => {
|
||||
this.removeEventListener('keydown', selectAll);
|
||||
});
|
||||
this.disposables.addFromEvent(this, 'keydown', selectAll);
|
||||
}
|
||||
|
||||
override firstUpdated(props: Map<string, unknown>) {
|
||||
super.firstUpdated(props);
|
||||
if (!this.isLinkedDoc$.value) {
|
||||
this.disposables.addFromEvent(this.richText, 'copy', this._onCopy);
|
||||
this.disposables.addFromEvent(this.richText, 'cut', this._onCut);
|
||||
this.disposables.addFromEvent(this.richText, 'paste', this._onPaste);
|
||||
}
|
||||
this.richText.updateComplete
|
||||
this.richText.value?.updateComplete
|
||||
.then(() => {
|
||||
this.inlineEditor?.focusEnd();
|
||||
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const inlineRange = this.inlineEditor?.inlineRange$.value;
|
||||
if (inlineRange) {
|
||||
if (!this.isEditing) {
|
||||
this.selectCurrentCell(true);
|
||||
}
|
||||
} else {
|
||||
if (this.isEditing) {
|
||||
this.selectCurrentCell(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
this.disposables.addFromEvent(
|
||||
this.richText.value,
|
||||
'copy',
|
||||
this._onCopy
|
||||
);
|
||||
this.disposables.addFromEvent(this.richText.value, 'cut', this._onCut);
|
||||
this.disposables.addFromEvent(
|
||||
this.richText.value,
|
||||
'paste',
|
||||
this._onPaste
|
||||
);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
override renderBlockText() {
|
||||
override afterEnterEditingMode() {
|
||||
this.inlineEditor?.focusEnd();
|
||||
}
|
||||
|
||||
protected override render(): unknown {
|
||||
return html`${this.renderIcon()}${this.renderBlockText()}`;
|
||||
}
|
||||
|
||||
renderBlockText() {
|
||||
return html` <rich-text
|
||||
${ref(this.richText)}
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText="${this.value}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.attributesSchema="${this.attributesSchema}"
|
||||
.attributeRenderer="${this.attributeRenderer}"
|
||||
.attributesSchema="${this.inlineManager?.getSchema()}"
|
||||
.attributeRenderer="${this.inlineManager?.getRenderer()}"
|
||||
.embedChecker="${this.inlineManager?.embedChecker}"
|
||||
.markdownMatches="${this.inlineManager?.markdownMatches}"
|
||||
.readonly="${this.readonly}"
|
||||
.readonly="${!this.isEditing$.value}"
|
||||
.enableClipboard="${false}"
|
||||
.verticalScrollContainerGetter="${() =>
|
||||
this.topContenteditableElement?.host
|
||||
? getViewportElement(this.topContenteditableElement.host)
|
||||
: null}"
|
||||
data-parent-flavour="affine:database"
|
||||
class="data-view-header-area-rich-text"
|
||||
class="${titleRichTextStyle}"
|
||||
></rich-text>`;
|
||||
}
|
||||
|
||||
override renderLinkedDoc(): TemplateResult {
|
||||
return html` <rich-text
|
||||
data-disable-ask-ai
|
||||
data-not-block-text
|
||||
.yText="${this.linkedDocTitle$.value}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.readonly="${this.readonly}"
|
||||
.enableClipboard="${true}"
|
||||
.verticalScrollContainerGetter="${() =>
|
||||
this.topContenteditableElement?.host
|
||||
? getViewportElement(this.topContenteditableElement.host)
|
||||
: null}"
|
||||
class="data-view-header-area-rich-text"
|
||||
></rich-text>`;
|
||||
renderIcon() {
|
||||
if (!this.showIcon) {
|
||||
return;
|
||||
}
|
||||
if (this.docId$.value) {
|
||||
return html` <div class="${headerAreaIconStyle}">
|
||||
${LinkedPageIcon({})}
|
||||
</div>`;
|
||||
}
|
||||
const iconColumn = this.view.mainProperties$.value.iconColumn;
|
||||
if (!iconColumn) return;
|
||||
|
||||
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
|
||||
if (!icon) return;
|
||||
|
||||
return html` <div class="${headerAreaIconStyle}">${icon}</div>`;
|
||||
}
|
||||
|
||||
private readonly richText = createRef<RichText>();
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor showIcon = false;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'data-view-header-area-text': HeaderAreaTextCell;
|
||||
'data-view-header-area-text-editing': HeaderAreaTextCellEditing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vanilla-extract/css": "^1.17.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -14,31 +14,13 @@ import { GroupSetting } from './core/group-by/setting.js';
|
||||
import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js';
|
||||
import { AnyRender } from './core/utils/uni-component/render-template.js';
|
||||
import { CheckboxCell } from './property-presets/checkbox/cell-renderer.js';
|
||||
import {
|
||||
DateCell,
|
||||
DateCellEditing,
|
||||
} from './property-presets/date/cell-renderer.js';
|
||||
import { DateCell } from './property-presets/date/cell-renderer.js';
|
||||
import { TextCell as ImageTextCell } from './property-presets/image/cell-renderer.js';
|
||||
import {
|
||||
MultiSelectCell,
|
||||
MultiSelectCellEditing,
|
||||
} from './property-presets/multi-select/cell-renderer.js';
|
||||
import {
|
||||
NumberCell,
|
||||
NumberCellEditing,
|
||||
} from './property-presets/number/cell-renderer.js';
|
||||
import {
|
||||
ProgressCell,
|
||||
ProgressCellEditing,
|
||||
} from './property-presets/progress/cell-renderer.js';
|
||||
import {
|
||||
SelectCell,
|
||||
SelectCellEditing,
|
||||
} from './property-presets/select/cell-renderer.js';
|
||||
import {
|
||||
TextCell,
|
||||
TextCellEditing,
|
||||
} from './property-presets/text/cell-renderer.js';
|
||||
import { MultiSelectCell } from './property-presets/multi-select/cell-renderer.js';
|
||||
import { NumberCell } from './property-presets/number/cell-renderer.js';
|
||||
import { ProgressCell } from './property-presets/progress/cell-renderer.js';
|
||||
import { SelectCell } from './property-presets/select/cell-renderer.js';
|
||||
import { TextCell } from './property-presets/text/cell-renderer.js';
|
||||
import { DataViewKanban, DataViewTable } from './view-presets/index.js';
|
||||
import { MobileKanbanCard } from './view-presets/kanban/mobile/card.js';
|
||||
import { MobileKanbanCell } from './view-presets/kanban/mobile/cell.js';
|
||||
@@ -83,16 +65,8 @@ import { DataViewHeaderViews } from './widget-presets/views-bar/views-view.js';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-database-progress-cell', ProgressCell);
|
||||
customElements.define(
|
||||
'affine-database-progress-cell-editing',
|
||||
ProgressCellEditing
|
||||
);
|
||||
customElements.define('data-view-header-tools', DataViewHeaderTools);
|
||||
customElements.define('affine-database-number-cell', NumberCell);
|
||||
customElements.define(
|
||||
'affine-database-number-cell-editing',
|
||||
NumberCellEditing
|
||||
);
|
||||
customElements.define(
|
||||
'affine-database-cell-container',
|
||||
DatabaseCellContainer
|
||||
@@ -102,24 +76,14 @@ export function effects() {
|
||||
customElements.define('any-render', AnyRender);
|
||||
customElements.define('affine-database-image-cell', ImageTextCell);
|
||||
customElements.define('affine-database-date-cell', DateCell);
|
||||
customElements.define('affine-database-date-cell-editing', DateCellEditing);
|
||||
customElements.define(
|
||||
'data-view-properties-setting',
|
||||
DataViewPropertiesSettingView
|
||||
);
|
||||
customElements.define('affine-database-checkbox-cell', CheckboxCell);
|
||||
customElements.define('affine-database-text-cell', TextCell);
|
||||
customElements.define('affine-database-text-cell-editing', TextCellEditing);
|
||||
customElements.define('affine-database-select-cell', SelectCell);
|
||||
customElements.define(
|
||||
'affine-database-select-cell-editing',
|
||||
SelectCellEditing
|
||||
);
|
||||
customElements.define('affine-database-multi-select-cell', MultiSelectCell);
|
||||
customElements.define(
|
||||
'affine-database-multi-select-cell-editing',
|
||||
MultiSelectCellEditing
|
||||
);
|
||||
customElements.define('affine-data-view-record-field', RecordField);
|
||||
customElements.define('data-view-drag-to-fill', DragToFillElement);
|
||||
customElements.define('affine-data-view-table-group', TableGroup);
|
||||
|
||||
@@ -75,7 +75,7 @@ export class CheckboxCell extends BaseCellRenderer<boolean> {
|
||||
override beforeEnterEditMode() {
|
||||
const checked = !this.value;
|
||||
|
||||
this.onChange(checked);
|
||||
this.valueSetImmediate(checked);
|
||||
if (checked) {
|
||||
playCheckAnimation(this._checkbox, { left: 2 }).catch(console.error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const dateCellStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
height: 'var(--data-view-cell-text-line-height)',
|
||||
});
|
||||
|
||||
export const dateValueContainerStyle = style({
|
||||
padding: '12px',
|
||||
backgroundColor: 'var(--layer-background-primary)',
|
||||
borderRadius: '12px',
|
||||
color: 'var(--text-secondary)',
|
||||
fontSize: '17px',
|
||||
lineHeight: '22px',
|
||||
height: '46px',
|
||||
});
|
||||
|
||||
export const datePickerContainerStyle = style({
|
||||
padding: '12px',
|
||||
backgroundColor: 'var(--layer-background-primary)',
|
||||
borderRadius: '12px',
|
||||
});
|
||||
@@ -4,66 +4,23 @@ import {
|
||||
} from '@blocksuite/affine-components/context-menu';
|
||||
import { DatePicker } from '@blocksuite/affine-components/date-picker';
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { IS_MOBILE } from '@blocksuite/global/env';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { format } from 'date-fns/format';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import {
|
||||
dateCellStyle,
|
||||
datePickerContainerStyle,
|
||||
dateValueContainerStyle,
|
||||
} from './cell-renderer.css.js';
|
||||
import { datePropertyModelConfig } from './define.js';
|
||||
|
||||
export class DateCell extends BaseCellRenderer<number> {
|
||||
static override styles = css`
|
||||
affine-database-date-cell {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.affine-database-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
height: var(--data-view-cell-text-line-height);
|
||||
}
|
||||
|
||||
input.affine-database-date[type='date']::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const value = this.value ? format(this.value, 'yyyy/MM/dd') : '';
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return html` <div class="affine-database-date date">${value}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class DateCellEditing extends BaseCellRenderer<number> {
|
||||
static override styles = css`
|
||||
affine-database-date-cell-editing {
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.affine-database-date:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private _prevPortalAbortController: AbortController | null = null;
|
||||
|
||||
private readonly openDatePicker = () => {
|
||||
@@ -96,18 +53,8 @@ export class DateCellEditing extends BaseCellRenderer<number> {
|
||||
},
|
||||
items: [
|
||||
() =>
|
||||
html`<div
|
||||
style="
|
||||
padding: 12px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
border-radius: 12px;
|
||||
color: ${unsafeCSSVarV2('text/secondary')};
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
height: 46px;
|
||||
"
|
||||
>
|
||||
${this.dateString}
|
||||
html` <div class="${dateValueContainerStyle}">
|
||||
${this.formattedTempValue$.value}
|
||||
</div>`,
|
||||
() => {
|
||||
const datePicker = new DatePicker();
|
||||
@@ -123,11 +70,7 @@ height: 46px;
|
||||
abortController.abort();
|
||||
};
|
||||
requestAnimationFrame(() => datePicker.focusDateCell());
|
||||
return html`<div
|
||||
style="padding: 12px;background-color: ${unsafeCSSVarV2(
|
||||
'layer/background/primary'
|
||||
)};border-radius: 12px"
|
||||
>
|
||||
return html` <div class="${datePickerContainerStyle}">
|
||||
${datePicker}
|
||||
</div>`;
|
||||
},
|
||||
@@ -179,36 +122,39 @@ height: 46px;
|
||||
return;
|
||||
}
|
||||
|
||||
this.onChange(tempValue?.getTime());
|
||||
const time = tempValue?.getTime();
|
||||
this.valueSetNextTick(time);
|
||||
this.tempValue$.value = undefined;
|
||||
};
|
||||
|
||||
tempValue$ = signal<Date>();
|
||||
tempValue$ = signal<Date | undefined>();
|
||||
|
||||
get dateString() {
|
||||
const value = this.tempValue;
|
||||
format(value?: Date) {
|
||||
return value ? format(value, 'yyyy/MM/dd') : '';
|
||||
}
|
||||
|
||||
get tempValue() {
|
||||
return this.tempValue$.value;
|
||||
}
|
||||
formattedTempValue$ = computed(() => {
|
||||
return this.format(this.tempValue$.value);
|
||||
});
|
||||
formattedValue$ = computed(() => {
|
||||
return (
|
||||
this.formattedTempValue$.value ||
|
||||
this.format(this.value ? new Date(this.value) : undefined)
|
||||
);
|
||||
});
|
||||
|
||||
override firstUpdated() {
|
||||
override afterEnterEditingMode() {
|
||||
this.openDatePicker();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
override beforeExitEditingMode() {
|
||||
this.updateValue();
|
||||
this._prevPortalAbortController?.abort();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` <div
|
||||
class="affine-database-date date"
|
||||
@click="${this.openDatePicker}"
|
||||
>
|
||||
${this.dateString}
|
||||
return html` <div class="${dateCellStyle} date">
|
||||
${this.formattedValue$.value}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -217,6 +163,5 @@ export const datePropertyConfig = datePropertyModelConfig.createPropertyMeta({
|
||||
icon: createIcon('DateTimeIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(DateCell),
|
||||
edit: createFromBaseCellRenderer(DateCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const multiSelectStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
});
|
||||
@@ -1,53 +1,33 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
|
||||
import type { SelectTag } from '../../core/index.js';
|
||||
import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { stopPropagation } from '../../core/utils/event.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import type { SelectPropertyData } from '../select/define.js';
|
||||
import { multiSelectStyle } from './cell-renderer.css.js';
|
||||
import { multiSelectPropertyModelConfig } from './define.js';
|
||||
|
||||
export class MultiSelectCell extends BaseCellRenderer<
|
||||
string[],
|
||||
SelectPropertyData
|
||||
> {
|
||||
override render() {
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${Array.isArray(this.value) ? this.value : []}"
|
||||
.options="${this.property.data$.value.options}"
|
||||
></affine-multi-tag-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class MultiSelectCellEditing extends BaseCellRenderer<
|
||||
string[],
|
||||
SelectPropertyData
|
||||
> {
|
||||
closePopup?: () => void;
|
||||
private readonly popTagSelect = () => {
|
||||
const value = signal(this._value);
|
||||
this._disposables.add({
|
||||
dispose: popTagSelect(
|
||||
popupTargetFromElement(
|
||||
this.querySelector('affine-multi-tag-view') ?? this
|
||||
),
|
||||
{
|
||||
name: this.cell.property.name$.value,
|
||||
options: this.options$,
|
||||
onOptionsChange: this._onOptionsChange,
|
||||
value: value,
|
||||
onChange: v => {
|
||||
this._onChange(v);
|
||||
value.value = v;
|
||||
},
|
||||
onComplete: this._editComplete,
|
||||
minWidth: 400,
|
||||
}
|
||||
),
|
||||
this.closePopup = popTagSelect(popupTargetFromElement(this), {
|
||||
name: this.cell.property.name$.value,
|
||||
options: this.options$,
|
||||
onOptionsChange: this._onOptionsChange,
|
||||
value: this._value$,
|
||||
onChange: v => {
|
||||
this.valueSetImmediate(v);
|
||||
},
|
||||
onComplete: this._editComplete,
|
||||
minWidth: 400,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,10 +35,6 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
|
||||
this.selectCurrentCell(false);
|
||||
};
|
||||
|
||||
_onChange = (ids: string[]) => {
|
||||
this.onChange(ids);
|
||||
};
|
||||
|
||||
_onOptionsChange = (options: SelectTag[]) => {
|
||||
this.property.dataUpdate(data => {
|
||||
return {
|
||||
@@ -71,21 +47,32 @@ export class MultiSelectCellEditing extends BaseCellRenderer<
|
||||
options$ = computed(() => {
|
||||
return this.property.data$.value.options;
|
||||
});
|
||||
|
||||
get _value() {
|
||||
_value$ = computed(() => {
|
||||
return this.value ?? [];
|
||||
});
|
||||
|
||||
override afterEnterEditingMode() {
|
||||
if (!this.closePopup) {
|
||||
this.popTagSelect();
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.popTagSelect();
|
||||
override beforeExitEditingMode() {
|
||||
this.closePopup?.();
|
||||
this.closePopup = undefined;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
<div
|
||||
class="${multiSelectStyle}"
|
||||
@pointerdown="${this.isEditing$.value ? stopPropagation : undefined}"
|
||||
>
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value$.value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -95,6 +82,5 @@ export const multiSelectPropertyConfig =
|
||||
icon: createIcon('MultiSelectIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(MultiSelectCell),
|
||||
edit: createFromBaseCellRenderer(MultiSelectCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const numberStyle = style({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-all',
|
||||
});
|
||||
|
||||
export const numberInputStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
textAlign: 'right',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import { IS_MAC } from '@blocksuite/global/env';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { stopPropagation } from '../../core/utils/event.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import { numberInputStyle, numberStyle } from './cell-renderer.css.js';
|
||||
import { numberPropertyModelConfig } from './define.js';
|
||||
import type { NumberPropertyDataType } from './types.js';
|
||||
import {
|
||||
@@ -19,93 +19,23 @@ export class NumberCell extends BaseCellRenderer<
|
||||
number,
|
||||
NumberPropertyDataType
|
||||
> {
|
||||
static override styles = css`
|
||||
affine-database-number-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
|
||||
.affine-database-number {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
word-break: break-all;
|
||||
}
|
||||
`;
|
||||
|
||||
private _getFormattedString() {
|
||||
private _getFormattedString(value: number | undefined = this.value) {
|
||||
const enableNewFormatting =
|
||||
this.view.featureFlags$.value.enable_number_formatting;
|
||||
const decimals = this.property.data$.value.decimal ?? 0;
|
||||
const formatMode = (this.property.data$.value.format ??
|
||||
'number') as NumberFormat;
|
||||
|
||||
return this.value != undefined
|
||||
return value != undefined
|
||||
? enableNewFormatting
|
||||
? formatNumber(this.value, formatMode, decimals)
|
||||
: this.value.toString()
|
||||
? formatNumber(value, formatMode, decimals)
|
||||
: value.toString()
|
||||
: '';
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html` <div class="affine-database-number number">
|
||||
${this._getFormattedString()}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NumberCellEditing extends BaseCellRenderer<
|
||||
number,
|
||||
NumberPropertyDataType
|
||||
> {
|
||||
static override styles = css`
|
||||
affine-database-number-cell-editing {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.affine-database-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: var(--data-view-cell-text-size);
|
||||
line-height: var(--data-view-cell-text-line-height);
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.affine-database-number:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
private readonly _getFormattedString = (value: number) => {
|
||||
const enableNewFormatting =
|
||||
this.view.featureFlags$.value.enable_number_formatting;
|
||||
const decimals = this.property.data$.value.decimal ?? 0;
|
||||
const formatMode = (this.property.data$.value.format ??
|
||||
'number') as NumberFormat;
|
||||
return enableNewFormatting
|
||||
? formatNumber(value, formatMode, decimals)
|
||||
: value.toString();
|
||||
};
|
||||
|
||||
private readonly _keydown = (e: KeyboardEvent) => {
|
||||
const ctrlKey = IS_MAC ? e.metaKey : e.ctrlKey;
|
||||
|
||||
@@ -121,9 +51,9 @@ export class NumberCellEditing extends BaseCellRenderer<
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _setValue = (str: string = this._inputEle.value) => {
|
||||
private readonly _setValue = (str: string = this._inputEle?.value) => {
|
||||
if (!str) {
|
||||
this.onChange(undefined);
|
||||
this.valueSetNextTick(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -131,17 +61,23 @@ export class NumberCellEditing extends BaseCellRenderer<
|
||||
this.view.featureFlags$.value.enable_number_formatting;
|
||||
const value = enableNewFormatting ? parseNumber(str) : parseFloat(str);
|
||||
if (isNaN(value)) {
|
||||
this._inputEle.value = this.value
|
||||
? this._getFormattedString(this.value)
|
||||
: '';
|
||||
if (this._inputEle) {
|
||||
this._inputEle.value = this.value
|
||||
? this._getFormattedString(this.value)
|
||||
: '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputEle.value = this._getFormattedString(value);
|
||||
this.onChange(value);
|
||||
if (this._inputEle) {
|
||||
this._inputEle.value = this._getFormattedString(value);
|
||||
}
|
||||
this.valueSetNextTick(value);
|
||||
};
|
||||
|
||||
focusEnd = () => {
|
||||
if (!this._inputEle) return;
|
||||
|
||||
const end = this._inputEle.value.length;
|
||||
this._inputEle.focus();
|
||||
this._inputEle.setSelectionRange(end, end);
|
||||
@@ -152,38 +88,39 @@ export class NumberCellEditing extends BaseCellRenderer<
|
||||
}
|
||||
|
||||
_focus() {
|
||||
if (!this.isEditing) {
|
||||
if (!this.isEditing$.value) {
|
||||
this.selectCurrentCell(true);
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
requestAnimationFrame(() => {
|
||||
this.focusEnd();
|
||||
});
|
||||
override afterEnterEditingMode() {
|
||||
this.focusEnd();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
override beforeExitEditingMode() {
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
override render() {
|
||||
const formatted = this.value ? this._getFormattedString(this.value) : '';
|
||||
if (this.isEditing$.value) {
|
||||
const formatted = this.value ? this._getFormattedString(this.value) : '';
|
||||
|
||||
return html`<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
.value="${formatted}"
|
||||
@keydown="${this._keydown}"
|
||||
@blur="${this._blur}"
|
||||
@focus="${this._focus}"
|
||||
class="affine-database-number number"
|
||||
@pointerdown="${stopPropagation}"
|
||||
/>`;
|
||||
return html`<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
.value="${formatted}"
|
||||
@keydown="${this._keydown}"
|
||||
@blur="${this._blur}"
|
||||
@focus="${this._focus}"
|
||||
class="${numberInputStyle} number"
|
||||
@pointerdown="${stopPropagation}"
|
||||
/>`;
|
||||
} else {
|
||||
return html` <div class="${numberStyle} number">
|
||||
${this._getFormattedString()}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
}
|
||||
|
||||
export const numberPropertyConfig =
|
||||
@@ -191,6 +128,5 @@ export const numberPropertyConfig =
|
||||
icon: createIcon('NumberIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(NumberCell),
|
||||
edit: createFromBaseCellRenderer(NumberCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const progressCellStyle = style({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: '0 4px',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
export const progressContainerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: 'var(--data-view-cell-text-line-height)',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const progressBarStyle = style({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const progressBgStyle = style({
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
height: '10px',
|
||||
borderRadius: '22px',
|
||||
});
|
||||
|
||||
export const progressFgStyle = style({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const progressDragHandleStyle = style({
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
transform: 'translate(0px, -1px)',
|
||||
width: '6px',
|
||||
height: '12px',
|
||||
borderRadius: '2px',
|
||||
opacity: '1',
|
||||
cursor: 'ew-resize',
|
||||
background: 'var(--affine-primary-color)',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
});
|
||||
|
||||
export const progressNumberStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '18px',
|
||||
width: '25px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: '14px',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css, html } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
@@ -6,69 +6,17 @@ import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { startDrag } from '../../core/utils/drag.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import {
|
||||
progressBarStyle,
|
||||
progressBgStyle,
|
||||
progressCellStyle,
|
||||
progressContainerStyle,
|
||||
progressDragHandleStyle,
|
||||
progressFgStyle,
|
||||
progressNumberStyle,
|
||||
} from './cell-renderer.css.js';
|
||||
import { progressPropertyModelConfig } from './define.js';
|
||||
|
||||
const styles = css`
|
||||
affine-database-progress-cell-editing {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
affine-database-progress-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.affine-database-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--data-view-cell-text-line-height);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.affine-database-progress-bar {
|
||||
position: relative;
|
||||
width: 104px;
|
||||
}
|
||||
|
||||
.affine-database-progress-bg {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.affine-database-progress-fg {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-database-progress-drag-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(0px, -1px);
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
opacity: 1;
|
||||
cursor: ew-resize;
|
||||
background: var(--affine-primary-color);
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.progress-number {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
width: 25px;
|
||||
color: var(--affine-text-secondary-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
const progressColors = {
|
||||
empty: 'var(--affine-black-10)',
|
||||
processing: 'var(--affine-processing-color)',
|
||||
@@ -76,38 +24,9 @@ const progressColors = {
|
||||
};
|
||||
|
||||
export class ProgressCell extends BaseCellRenderer<number> {
|
||||
static override styles = styles;
|
||||
|
||||
protected override render() {
|
||||
const progress = this.value ?? 0;
|
||||
let backgroundColor = progressColors.processing;
|
||||
if (progress === 100) {
|
||||
backgroundColor = progressColors.success;
|
||||
}
|
||||
const fgStyles = styleMap({
|
||||
width: `${progress}%`,
|
||||
backgroundColor,
|
||||
});
|
||||
const bgStyles = styleMap({
|
||||
backgroundColor:
|
||||
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
|
||||
});
|
||||
|
||||
return html` <div class="affine-database-progress">
|
||||
<div class="affine-database-progress-bar">
|
||||
<div class="affine-database-progress-bg" style=${bgStyles}>
|
||||
<div class="affine-database-progress-fg" style=${fgStyles}></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-number progress">${progress}</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProgressCellEditing extends BaseCellRenderer<number> {
|
||||
static override styles = styles;
|
||||
|
||||
startDrag = (event: MouseEvent) => {
|
||||
if (!this.isEditing$.value) return;
|
||||
|
||||
const bgRect = this._progressBg.getBoundingClientRect();
|
||||
const min = bgRect.left;
|
||||
const max = bgRect.right;
|
||||
@@ -135,7 +54,9 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
|
||||
};
|
||||
|
||||
get _value() {
|
||||
return this.tempValue ?? this.value ?? 0;
|
||||
return this.isEditing$.value
|
||||
? (this.tempValue ?? this.value ?? 0)
|
||||
: (this.value ?? 0);
|
||||
}
|
||||
|
||||
_onChange(value?: number) {
|
||||
@@ -146,13 +67,17 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
|
||||
const disposables = this._disposables;
|
||||
|
||||
disposables.addFromEvent(this._progressBg, 'pointerdown', this.startDrag);
|
||||
|
||||
disposables.addFromEvent(window, 'keydown', evt => {
|
||||
if (evt.key === 'ArrowDown') {
|
||||
if (!this.isEditing$.value) {
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'ArrowDown' || evt.key === 'ArrowLeft') {
|
||||
evt.preventDefault();
|
||||
this._onChange(Math.max(0, this._value - 1));
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'ArrowUp') {
|
||||
if (evt.key === 'ArrowUp' || evt.key === 'ArrowRight') {
|
||||
evt.preventDefault();
|
||||
this._onChange(Math.min(100, this._value + 1));
|
||||
return;
|
||||
@@ -160,20 +85,25 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
|
||||
});
|
||||
}
|
||||
|
||||
preventDefault(e: ClipboardEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
override onCopy(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
this.preventDefault(_e);
|
||||
}
|
||||
|
||||
override onCut(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
this.preventDefault(_e);
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
this.onChange(this._value);
|
||||
override beforeExitEditingMode() {
|
||||
const value = this._value;
|
||||
this.valueSetNextTick(value);
|
||||
}
|
||||
|
||||
override onPaste(_e: ClipboardEvent) {
|
||||
_e.preventDefault();
|
||||
this.preventDefault(_e);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
@@ -190,25 +120,37 @@ export class ProgressCellEditing extends BaseCellRenderer<number> {
|
||||
backgroundColor:
|
||||
progress === 0 ? progressColors.empty : 'var(--affine-hover-color)',
|
||||
});
|
||||
const handleStyles = styleMap({
|
||||
left: `calc(${progress}% - 3px)`,
|
||||
});
|
||||
|
||||
return html` <div class="affine-database-progress">
|
||||
<div class="affine-database-progress-bar">
|
||||
<div class="affine-database-progress-bg" style=${bgStyles}>
|
||||
<div class="affine-database-progress-fg" style=${fgStyles}></div>
|
||||
<div
|
||||
class="affine-database-progress-drag-handle"
|
||||
style=${handleStyles}
|
||||
></div>
|
||||
return html`
|
||||
<div class="${progressCellStyle}">
|
||||
<div class="${progressContainerStyle}">
|
||||
<div class="${progressBarStyle}">
|
||||
<div
|
||||
class="${progressBgStyle}"
|
||||
data-testid="progress-background"
|
||||
style=${bgStyles}
|
||||
>
|
||||
<div class="${progressFgStyle}" style=${fgStyles}></div>
|
||||
${this.isEditing$.value
|
||||
? html` <div
|
||||
class="${progressDragHandleStyle}"
|
||||
data-testid="progress-drag-handle"
|
||||
style=${styleMap({
|
||||
left: `calc(${progress}% - 3px)`,
|
||||
})}
|
||||
></div>`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
<span class="${progressNumberStyle}" data-testid="progress"
|
||||
>${progress}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-number progress">${progress}</div>
|
||||
</div>`;
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.affine-database-progress-bg')
|
||||
@query(`.${progressBgStyle}`)
|
||||
private accessor _progressBg!: HTMLElement;
|
||||
|
||||
@state()
|
||||
@@ -220,6 +162,5 @@ export const progressPropertyConfig =
|
||||
icon: createIcon('ProgressIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(ProgressCell),
|
||||
edit: createFromBaseCellRenderer(ProgressCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const selectStyle = style({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--data-view-cell-text-size)',
|
||||
lineHeight: 'var(--data-view-cell-text-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-all',
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { popupTargetFromElement } from '@blocksuite/affine-components/context-menu';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import { popTagSelect } from '../../core/component/tags/multi-tag-select.js';
|
||||
@@ -7,48 +7,26 @@ import type { SelectTag } from '../../core/index.js';
|
||||
import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import { selectStyle } from './cell-renderer.css.js';
|
||||
import {
|
||||
type SelectPropertyData,
|
||||
selectPropertyModelConfig,
|
||||
} from './define.js';
|
||||
|
||||
export class SelectCell extends BaseCellRenderer<string[], SelectPropertyData> {
|
||||
override render() {
|
||||
const value = this.value ? [this.value] : [];
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${value}"
|
||||
.options="${this.property.data$.value.options}"
|
||||
></affine-multi-tag-view>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectCellEditing extends BaseCellRenderer<
|
||||
string,
|
||||
SelectPropertyData
|
||||
> {
|
||||
export class SelectCell extends BaseCellRenderer<string, SelectPropertyData> {
|
||||
closePopup?: () => void;
|
||||
private readonly popTagSelect = () => {
|
||||
const value = signal(this._value);
|
||||
this._disposables.add({
|
||||
dispose: popTagSelect(
|
||||
popupTargetFromElement(
|
||||
this.querySelector('affine-multi-tag-view') ?? this
|
||||
),
|
||||
{
|
||||
name: this.cell.property.name$.value,
|
||||
mode: 'single',
|
||||
options: this.options$,
|
||||
onOptionsChange: this._onOptionsChange,
|
||||
value: signal(this._value),
|
||||
onChange: v => {
|
||||
this._onChange(v);
|
||||
value.value = v;
|
||||
},
|
||||
onComplete: this._editComplete,
|
||||
minWidth: 400,
|
||||
}
|
||||
),
|
||||
this.closePopup = popTagSelect(popupTargetFromElement(this), {
|
||||
name: this.cell.property.name$.value,
|
||||
mode: 'single',
|
||||
options: this.options$,
|
||||
onOptionsChange: this._onOptionsChange,
|
||||
value: this._value$,
|
||||
onChange: v => {
|
||||
this.valueSetImmediate(v[0]);
|
||||
},
|
||||
onComplete: this._editComplete,
|
||||
minWidth: 400,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -56,10 +34,6 @@ export class SelectCellEditing extends BaseCellRenderer<
|
||||
this.selectCurrentCell(false);
|
||||
};
|
||||
|
||||
_onChange = ([id]: string[]) => {
|
||||
this.onChange(id);
|
||||
};
|
||||
|
||||
_onOptionsChange = (options: SelectTag[]) => {
|
||||
this.property.dataUpdate(data => {
|
||||
return {
|
||||
@@ -73,21 +47,30 @@ export class SelectCellEditing extends BaseCellRenderer<
|
||||
return this.property.data$.value.options;
|
||||
});
|
||||
|
||||
get _value() {
|
||||
_value$ = computed(() => {
|
||||
const value = this.value;
|
||||
return value ? [value] : [];
|
||||
});
|
||||
|
||||
override afterEnterEditingMode() {
|
||||
if (!this.closePopup) {
|
||||
this.popTagSelect();
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.popTagSelect();
|
||||
override beforeExitEditingMode() {
|
||||
this.closePopup?.();
|
||||
this.closePopup = undefined;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
<div class="${selectStyle}">
|
||||
<affine-multi-tag-view
|
||||
.value="${this._value$.value}"
|
||||
.options="${this.options$.value}"
|
||||
></affine-multi-tag-view>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -97,6 +80,5 @@ export const selectPropertyConfig =
|
||||
icon: createIcon('SingleSelectIcon'),
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(SelectCell),
|
||||
edit: createFromBaseCellRenderer(SelectCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const textStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
|
||||
export const textInputStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
padding: '0',
|
||||
border: 'none',
|
||||
fontFamily: baseTheme.fontSansFamily,
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
lineHeight: 'var(--affine-line-height)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '400',
|
||||
backgroundColor: 'transparent',
|
||||
cursor: 'text',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
});
|
||||
@@ -1,74 +1,15 @@
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, unsafeCSS } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
|
||||
import { BaseCellRenderer } from '../../core/property/index.js';
|
||||
import { createFromBaseCellRenderer } from '../../core/property/renderer.js';
|
||||
import { createIcon } from '../../core/utils/uni-icon.js';
|
||||
import { textInputStyle, textStyle } from './cell-renderer.css.js';
|
||||
import { textPropertyModelConfig } from './define.js';
|
||||
|
||||
export class TextCell extends BaseCellRenderer<string> {
|
||||
static override styles = css`
|
||||
affine-database-text-cell {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.affine-database-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: var(--affine-font-base);
|
||||
line-height: var(--affine-line-height);
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html` <div class="affine-database-text">${this.value ?? ''}</div>`;
|
||||
}
|
||||
}
|
||||
export class TextCellEditing extends BaseCellRenderer<string> {
|
||||
static override styles = css`
|
||||
affine-database-text-cell-editing {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.affine-database-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
font-size: var(--affine-font-base);
|
||||
line-height: var(--affine-line-height);
|
||||
color: var(--affine-text-primary-color);
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.affine-database-text:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
|
||||
private readonly _keydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.isComposing) {
|
||||
@@ -79,35 +20,40 @@ export class TextCellEditing extends BaseCellRenderer<string> {
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _setValue = (str: string = this._inputEle.value) => {
|
||||
this._inputEle.value = `${this.value ?? ''}`;
|
||||
this.onChange(str);
|
||||
private readonly _setValue = (str: string = this._inputEle?.value) => {
|
||||
if (this._inputEle) {
|
||||
this._inputEle.value = `${this.value ?? ''}`;
|
||||
}
|
||||
this.valueSetNextTick(str);
|
||||
};
|
||||
|
||||
focusEnd = () => {
|
||||
if (!this._inputEle) return;
|
||||
|
||||
const end = this._inputEle.value.length;
|
||||
this._inputEle.focus();
|
||||
this._inputEle.setSelectionRange(end, end);
|
||||
};
|
||||
|
||||
override firstUpdated() {
|
||||
override afterEnterEditingMode() {
|
||||
this.focusEnd();
|
||||
}
|
||||
|
||||
override onExitEditMode() {
|
||||
override beforeExitEditingMode() {
|
||||
this._setValue();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<input
|
||||
.value="${this.value ?? ''}"
|
||||
@keydown="${this._keydown}"
|
||||
class="affine-database-text"
|
||||
/>`;
|
||||
if (this.isEditing$.value) {
|
||||
return html`<input
|
||||
.value="${this.value ?? ''}"
|
||||
@keydown="${this._keydown}"
|
||||
class="${textInputStyle}"
|
||||
/>`;
|
||||
} else {
|
||||
return html`<div class="${textStyle}">${this.value ?? ''}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@query('input')
|
||||
private accessor _inputEle!: HTMLInputElement;
|
||||
}
|
||||
|
||||
export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
|
||||
@@ -115,6 +61,5 @@ export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({
|
||||
|
||||
cellRenderer: {
|
||||
view: createFromBaseCellRenderer(TextCell),
|
||||
edit: createFromBaseCellRenderer(TextCellEditing),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { html } from 'lit/static-html.js';
|
||||
|
||||
import type {
|
||||
@@ -52,7 +52,7 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
|
||||
private readonly _cell = signal<DataViewCellLifeCycle>();
|
||||
|
||||
isEditing$ = computed(() => {
|
||||
isSelectionEditing$ = computed(() => {
|
||||
const selection = this.kanban?.props.selection$.value;
|
||||
if (selection?.selectionType !== 'cell') {
|
||||
return false;
|
||||
@@ -108,19 +108,21 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
if (this.column.readonly$.value) return;
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const isEditing = this.isEditing$.value;
|
||||
const isEditing = this.isSelectionEditing$.value;
|
||||
if (isEditing) {
|
||||
this.isEditing = true;
|
||||
this._cell.value?.onEnterEditMode();
|
||||
this.isEditing$.value = true;
|
||||
requestAnimationFrame(() => {
|
||||
this._cell.value?.afterEnterEditingMode();
|
||||
});
|
||||
} else {
|
||||
this._cell.value?.onExitEditMode();
|
||||
this.isEditing = false;
|
||||
this._cell.value?.beforeExitEditingMode();
|
||||
this.isEditing$.value = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
this._disposables.addFromEvent(this, 'click', e => {
|
||||
e.stopPropagation();
|
||||
if (!this.isEditing) {
|
||||
if (!this.isEditing$.value) {
|
||||
this.selectCurrentCell(!this.column.readonly$.value);
|
||||
}
|
||||
});
|
||||
@@ -129,16 +131,16 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
override render() {
|
||||
const props: CellRenderProps = {
|
||||
cell: this.column.cellGet(this.cardId),
|
||||
isEditing: this.isEditing,
|
||||
isEditing$: this.isEditing$,
|
||||
selectCurrentCell: this.selectCurrentCell,
|
||||
};
|
||||
const renderer = this.column.renderer$.value;
|
||||
if (!renderer) return;
|
||||
const { view, edit } = renderer;
|
||||
this.view.lockRows(this.isEditing);
|
||||
this.dataset['editing'] = `${this.isEditing}`;
|
||||
const { view } = renderer;
|
||||
this.view.lockRows(this.isEditing$.value);
|
||||
this.dataset['editing'] = `${this.isEditing$.value}`;
|
||||
return html` ${this.renderIcon()}
|
||||
${renderUniLit(this.isEditing && edit ? edit : view, props, {
|
||||
${renderUniLit(view, props, {
|
||||
ref: this._cell,
|
||||
class: 'mobile-kanban-cell',
|
||||
style: { display: 'block', flex: '1', overflow: 'hidden' },
|
||||
@@ -167,8 +169,7 @@ export class MobileKanbanCell extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor groupKey!: string;
|
||||
|
||||
@state()
|
||||
accessor isEditing = false;
|
||||
isEditing$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor view!: KanbanSingleView;
|
||||
|
||||
@@ -109,7 +109,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
if (!selectionElement) return;
|
||||
if (e.shiftKey) return;
|
||||
|
||||
if (!this.editing) {
|
||||
if (!this.isEditing$.value) {
|
||||
this.selectCurrentCell(!this.column.readonly$.value);
|
||||
}
|
||||
});
|
||||
@@ -130,22 +130,22 @@ export class KanbanCell extends SignalWatcher(
|
||||
override render() {
|
||||
const props: CellRenderProps = {
|
||||
cell: this.column.cellGet(this.cardId),
|
||||
isEditing: this.editing,
|
||||
isEditing$: this.isEditing$,
|
||||
selectCurrentCell: this.selectCurrentCell,
|
||||
};
|
||||
const renderer = this.column.renderer$.value;
|
||||
if (!renderer) return;
|
||||
const { view, edit } = renderer;
|
||||
this.view.lockRows(this.editing);
|
||||
this.dataset['editing'] = `${this.editing}`;
|
||||
const { view } = renderer;
|
||||
this.view.lockRows(this.isEditing$.value);
|
||||
this.dataset['editing'] = `${this.isEditing$.value}`;
|
||||
this.style.border = this.isFocus
|
||||
? '1px solid var(--affine-primary-color)'
|
||||
: '';
|
||||
this.style.boxShadow = this.editing
|
||||
this.style.boxShadow = this.isEditing$.value
|
||||
? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)'
|
||||
: '';
|
||||
return html` ${this.renderIcon()}
|
||||
${renderUniLit(this.editing && edit ? edit : view, props, {
|
||||
${renderUniLit(view, props, {
|
||||
ref: this._cell,
|
||||
class: 'kanban-cell',
|
||||
style: { display: 'block', flex: '1', overflow: 'hidden' },
|
||||
@@ -168,8 +168,7 @@ export class KanbanCell extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor contentOnly = false;
|
||||
|
||||
@state()
|
||||
accessor editing = false;
|
||||
isEditing$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor groupKey!: string;
|
||||
|
||||
@@ -153,7 +153,7 @@ export class KanbanDragController implements ReactiveController {
|
||||
const target = event.target;
|
||||
if (target instanceof Element) {
|
||||
const cell = target.closest('affine-data-view-kanban-cell');
|
||||
if (cell?.editing) {
|
||||
if (cell?.isEditing$.value) {
|
||||
return;
|
||||
}
|
||||
cell?.selectCurrentCell(false);
|
||||
|
||||
@@ -96,13 +96,11 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
const cell = container?.cell;
|
||||
|
||||
if (selection.isEditing) {
|
||||
requestAnimationFrame(() => {
|
||||
cell?.onExitEditMode();
|
||||
});
|
||||
cell?.beforeExitEditingMode();
|
||||
if (cell?.blurCell()) {
|
||||
container.blur();
|
||||
}
|
||||
container.editing = false;
|
||||
container.isEditing$.value = false;
|
||||
} else {
|
||||
container.blur();
|
||||
}
|
||||
@@ -142,11 +140,13 @@ export class KanbanSelectionController implements ReactiveController {
|
||||
container.isFocus = true;
|
||||
const cell = container?.cell;
|
||||
if (selection.isEditing) {
|
||||
cell?.onEnterEditMode();
|
||||
if (cell?.focusCell()) {
|
||||
container.focus();
|
||||
}
|
||||
container.editing = true;
|
||||
container.isEditing$.value = true;
|
||||
requestAnimationFrame(() => {
|
||||
cell?.afterEnterEditingMode();
|
||||
});
|
||||
} else {
|
||||
container.focus();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { computed, effect, signal } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
type CellRenderProps,
|
||||
@@ -47,7 +47,7 @@ export class MobileTableCell extends SignalWatcher(
|
||||
return this.column.cellGet(this.rowId);
|
||||
});
|
||||
|
||||
isEditing$ = computed(() => {
|
||||
isSelectionEditing$ = computed(() => {
|
||||
const selection = this.table?.props.selection$.value;
|
||||
if (selection?.selectionType !== 'area') {
|
||||
return false;
|
||||
@@ -97,10 +97,6 @@ export class MobileTableCell extends SignalWatcher(
|
||||
return this.closest('mobile-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.column.readonly$.value;
|
||||
}
|
||||
|
||||
private get table() {
|
||||
return this.closest('mobile-data-view-table');
|
||||
}
|
||||
@@ -110,18 +106,21 @@ export class MobileTableCell extends SignalWatcher(
|
||||
if (this.column.readonly$.value) return;
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const isEditing = this.isEditing$.value;
|
||||
const isEditing = this.isSelectionEditing$.value;
|
||||
if (isEditing) {
|
||||
this.isEditing = true;
|
||||
this._cell.value?.onEnterEditMode();
|
||||
this.isEditing$.value = true;
|
||||
const cell = this._cell.value;
|
||||
requestAnimationFrame(() => {
|
||||
cell?.afterEnterEditingMode();
|
||||
});
|
||||
} else {
|
||||
this._cell.value?.onExitEditMode();
|
||||
this.isEditing = false;
|
||||
this._cell.value?.beforeExitEditingMode();
|
||||
this.isEditing$.value = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
this.disposables.addFromEvent(this, 'click', () => {
|
||||
if (!this.isEditing) {
|
||||
if (!this.isEditing$.value) {
|
||||
this.selectCurrentCell(!this.column.readonly$.value);
|
||||
}
|
||||
});
|
||||
@@ -132,17 +131,16 @@ export class MobileTableCell extends SignalWatcher(
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const { edit, view } = renderer;
|
||||
const uni = !this.readonly && this.isEditing && edit != null ? edit : view;
|
||||
this.view.lockRows(this.isEditing);
|
||||
this.dataset['editing'] = `${this.isEditing}`;
|
||||
const { view } = renderer;
|
||||
this.view.lockRows(this.isEditing$.value);
|
||||
this.dataset['editing'] = `${this.isEditing$.value}`;
|
||||
const props: CellRenderProps = {
|
||||
cell: this.cell$.value,
|
||||
isEditing: this.isEditing,
|
||||
isEditing$: this.isEditing$,
|
||||
selectCurrentCell: this.selectCurrentCell,
|
||||
};
|
||||
|
||||
return renderUniLit(uni, props, {
|
||||
return renderUniLit(view, props, {
|
||||
ref: this._cell,
|
||||
style: {
|
||||
display: 'contents',
|
||||
@@ -156,8 +154,7 @@ export class MobileTableCell extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor columnIndex!: number;
|
||||
|
||||
@state()
|
||||
accessor isEditing = false;
|
||||
isEditing$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ShadowlessElement } from '@blocksuite/block-std';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { css } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { renderUniLit } from '../../../core/index.js';
|
||||
import type {
|
||||
@@ -88,10 +88,6 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
return this.closest<TableGroup>('affine-data-view-table-group')?.group?.key;
|
||||
}
|
||||
|
||||
private get readonly() {
|
||||
return this.column.readonly$.value;
|
||||
}
|
||||
|
||||
private get selectionView() {
|
||||
return this.closest('affine-database-table')?.selectionController;
|
||||
}
|
||||
@@ -104,7 +100,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._disposables.addFromEvent(this, 'click', () => {
|
||||
if (!this.isEditing) {
|
||||
if (!this.isEditing$.value) {
|
||||
this.selectCurrentCell(!this.column.readonly$.value);
|
||||
}
|
||||
});
|
||||
@@ -128,17 +124,16 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
if (!renderer) {
|
||||
return;
|
||||
}
|
||||
const { edit, view } = renderer;
|
||||
const uni = !this.readonly && this.isEditing && edit != null ? edit : view;
|
||||
this.view.lockRows(this.isEditing);
|
||||
this.dataset['editing'] = `${this.isEditing}`;
|
||||
const { view } = renderer;
|
||||
this.view.lockRows(this.isEditing$.value);
|
||||
this.dataset['editing'] = `${this.isEditing$.value}`;
|
||||
const props: CellRenderProps = {
|
||||
cell: this.cell$.value,
|
||||
isEditing: this.isEditing,
|
||||
isEditing$: this.isEditing$,
|
||||
selectCurrentCell: this.selectCurrentCell,
|
||||
};
|
||||
|
||||
return renderUniLit(uni, props, {
|
||||
return renderUniLit(view, props, {
|
||||
ref: this._cell,
|
||||
style: {
|
||||
display: 'contents',
|
||||
@@ -152,8 +147,7 @@ export class DatabaseCellContainer extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor columnIndex!: number;
|
||||
|
||||
@state()
|
||||
accessor isEditing = false;
|
||||
isEditing$ = signal(false);
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor rowIndex!: number;
|
||||
|
||||
@@ -192,11 +192,9 @@ export class TableSelectionController implements ReactiveController {
|
||||
if (container) {
|
||||
const cell = container.cell;
|
||||
if (old.isEditing) {
|
||||
requestAnimationFrame(() => {
|
||||
cell?.onExitEditMode();
|
||||
});
|
||||
cell?.beforeExitEditingMode();
|
||||
cell?.blurCell();
|
||||
container.isEditing = false;
|
||||
container.isEditing$.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,9 +209,11 @@ export class TableSelectionController implements ReactiveController {
|
||||
if (container) {
|
||||
const cell = container.cell;
|
||||
if (newSelection.isEditing) {
|
||||
cell?.onEnterEditMode();
|
||||
container.isEditing = true;
|
||||
cell?.focusCell();
|
||||
container.isEditing$.value = true;
|
||||
requestAnimationFrame(() => {
|
||||
cell?.afterEnterEditingMode();
|
||||
cell?.focusCell();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type VLine,
|
||||
} from '@blocksuite/inline';
|
||||
import { Text } from '@blocksuite/store';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { css, html, type TemplateResult } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
@@ -57,7 +57,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
#verticalScrollContainer: HTMLElement | null = null;
|
||||
|
||||
private _inlineEditor: AffineInlineEditor | null = null;
|
||||
private readonly _inlineEditor$ = signal<AffineInlineEditor | null>(null);
|
||||
|
||||
private readonly _onCopy = (e: ClipboardEvent) => {
|
||||
const inlineEditor = this.inlineEditor;
|
||||
@@ -144,7 +144,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
|
||||
// It will listen ctrl+z/ctrl+shift+z and call undoManager.undo/redo, keydown event will not
|
||||
get inlineEditor() {
|
||||
return this._inlineEditor;
|
||||
return this._inlineEditor$.value;
|
||||
}
|
||||
|
||||
get inlineEditorContainer() {
|
||||
@@ -152,7 +152,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
private _init() {
|
||||
if (this._inlineEditor) {
|
||||
if (this.inlineEditor) {
|
||||
console.error('Inline editor already exists.');
|
||||
return;
|
||||
}
|
||||
@@ -162,22 +162,25 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
}
|
||||
|
||||
// init inline editor
|
||||
this._inlineEditor = new InlineEditor<AffineTextAttributes>(this._yText, {
|
||||
isEmbed: delta => this.embedChecker(delta),
|
||||
hooks: {
|
||||
beforeinput: onVBeforeinput,
|
||||
compositionEnd: onVCompositionEnd,
|
||||
},
|
||||
inlineRangeProvider: this.inlineRangeProvider,
|
||||
vLineRenderer: this.vLineRenderer,
|
||||
});
|
||||
this._inlineEditor$.value = new InlineEditor<AffineTextAttributes>(
|
||||
this._yText,
|
||||
{
|
||||
isEmbed: delta => this.embedChecker(delta),
|
||||
hooks: {
|
||||
beforeinput: onVBeforeinput,
|
||||
compositionEnd: onVCompositionEnd,
|
||||
},
|
||||
inlineRangeProvider: this.inlineRangeProvider,
|
||||
vLineRenderer: this.vLineRenderer,
|
||||
}
|
||||
);
|
||||
const inlineEditor = this._inlineEditor$.value;
|
||||
if (this.attributesSchema) {
|
||||
this._inlineEditor.setAttributeSchema(this.attributesSchema);
|
||||
inlineEditor.setAttributeSchema(this.attributesSchema);
|
||||
}
|
||||
if (this.attributeRenderer) {
|
||||
this._inlineEditor.setAttributeRenderer(this.attributeRenderer);
|
||||
inlineEditor.setAttributeRenderer(this.attributeRenderer);
|
||||
}
|
||||
const inlineEditor = this._inlineEditor;
|
||||
|
||||
const markdownMatches = this.markdownMatches;
|
||||
if (markdownMatches) {
|
||||
@@ -291,7 +294,7 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
if (this.inlineEditor?.mounted) {
|
||||
this.inlineEditor.unmount();
|
||||
}
|
||||
this._inlineEditor = null;
|
||||
this._inlineEditor$.value = null;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
@@ -384,8 +387,8 @@ export class RichText extends WithDisposable(ShadowlessElement) {
|
||||
this._init();
|
||||
return;
|
||||
}
|
||||
if (this._inlineEditor && changedProperties.has('readonly')) {
|
||||
this._inlineEditor.setReadonly(this.readonly);
|
||||
if (this.inlineEditor && changedProperties.has('readonly')) {
|
||||
this.inlineEditor.setReadonly(this.readonly);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user