+ return html`
+
Use headings to create a table of contents.
`;
@@ -330,77 +291,6 @@ export class OutlinePanelBody extends SignalWatcher(
});
}
- private _PanelList(withEdgelessOnlyNotes: boolean) {
- const selectedNotesSet = new Set(this._selectedNotes$.value);
- const theme = this.editor.std.get(ThemeProvider).theme;
-
- return html`
- ${this.insertIndex !== undefined
- ? html`
`
- : nothing}
- ${this._pageVisibleNotes.length
- ? repeat(
- this._pageVisibleNotes,
- item => item.note.id,
- (item, idx) => html`
-
{
- this._scrollToBlock(e.detail.blockId).catch(console.error);
- }}
- @displaymodechange=${this._handleDisplayModeChange}
- >
- `
- )
- : html`${nothing}`}
- ${withEdgelessOnlyNotes
- ? html`
Hidden Contents
- ${repeat(
- this._edgelessOnlyNotes,
- note => note.note.id,
- (item, idx) =>
- html`
`
- )} `
- : nothing}
-
`;
- }
-
private _renderDocTitle() {
if (!this.doc.root) return nothing;
@@ -431,6 +321,61 @@ export class OutlinePanelBody extends SignalWatcher(
>`;
}
+ private _renderNoteCards(items: OutlineNoteItem[]) {
+ return repeat(
+ items,
+ item => item.note.id,
+ (item, idx) =>
+ html`
{
+ this._scrollToBlock(e.detail.blockId).catch(console.error);
+ }}
+ >`
+ );
+ }
+
+ private _renderPageVisibleCardList() {
+ return html`
+ ${when(
+ this.insertIndex !== undefined,
+ () =>
+ html`
`
+ )}
+ ${this._renderNoteCards(this._pageVisibleNoteItems$.value)}
+
`;
+ }
+
+ private _renderEdgelessOnlyCardList() {
+ const items = this._edgelessOnlyNoteItems$.value;
+ return html`
+ ${when(
+ items.length > 0,
+ () =>
+ html`
Hidden Contents
`
+ )}
+ ${this._renderNoteCards(items)}
+
`;
+ }
+
private async _scrollToBlock(blockId: string) {
this._lockActiveHeadingId = true;
this._activeHeadingId$.value = blockId;
@@ -471,69 +416,41 @@ export class OutlinePanelBody extends SignalWatcher(
this._docDisposables = new DisposableGroup();
this._docDisposables.add(
effect(() => {
- this._updateNotes();
this._updateNoticeVisibility();
})
);
+
this._docDisposables.add(
- this.doc.slots.blockUpdated.on(payload => {
- if (
- payload.type === 'update' &&
- payload.flavour === 'affine:note' &&
- payload.props.key === 'displayMode'
- ) {
- this._updateNotes();
- }
+ effect(() => {
+ this._updateNotes();
})
);
}
- /**
- * There are two cases that we should render note list:
- * 1. There are headings in the notes
- * 2. No headings, but there are blocks in the notes and note sorting option is enabled
- */
- private _shouldRenderNoteList(noteItems: OutlineNoteItem[]) {
- if (!noteItems.length) return false;
+ private _updateNotes() {
+ if (this._dragging$.value) return;
- let hasHeadings = false;
- let hasChildrenBlocks = false;
-
- for (const noteItem of noteItems) {
- for (const block of noteItem.note.children) {
- hasChildrenBlocks = true;
+ const isRenderableNote = (item: OutlineNoteItem) => {
+ let hasHeadings = false;
+ for (const block of item.note.children) {
if (isHeadingBlock(block)) {
hasHeadings = true;
break;
}
}
- if (hasHeadings) {
- break;
- }
- }
+ return hasHeadings || this.enableNotesSorting;
+ };
- return hasHeadings || (this.enableNotesSorting && hasChildrenBlocks);
- }
-
- private _updateNotes() {
- const rootModel = this.doc.root;
-
- if (this._dragging) return;
-
- if (!rootModel) {
- this._pageVisibleNotes = [];
- return;
- }
-
- this._pageVisibleNotes = getNotesFromDoc(this.doc, [
+ this._pageVisibleNoteItems$.value = getNotesFromDoc(this.doc, [
NoteDisplayMode.DocAndEdgeless,
NoteDisplayMode.DocOnly,
- ]);
- this._edgelessOnlyNotes = getNotesFromDoc(this.doc, [
+ ]).filter(isRenderableNote);
+
+ this._edgelessOnlyNoteItems$.value = getNotesFromDoc(this.doc, [
NoteDisplayMode.EdgelessOnly,
- ]);
+ ]).filter(isRenderableNote);
}
private _updateNoticeVisibility() {
@@ -544,9 +461,8 @@ export class OutlinePanelBody extends SignalWatcher(
return;
}
- const shouldShowNotice = this._pageVisibleNotes.some(
- note => note.note.displayMode === NoteDisplayMode.DocOnly
- );
+ const shouldShowNotice =
+ getNotesFromDoc(this.doc, [NoteDisplayMode.DocOnly]).length > 0;
if (shouldShowNotice && !this.noticeVisible) {
this.setNoticeVisibility(true);
@@ -580,6 +496,8 @@ export class OutlinePanelBody extends SignalWatcher(
override connectedCallback(): void {
super.connectedCallback();
+ this.classList.add(styles.outlinePanelBody);
+
this.disposables.add(
observeActiveHeadingDuringScroll(
() => this.editor,
@@ -603,54 +521,33 @@ export class OutlinePanelBody extends SignalWatcher(
}
override render() {
- const shouldRenderPageVisibleNotes = this._shouldRenderNoteList(
- this._pageVisibleNotes
- );
- const shouldRenderEdgelessOnlyNotes =
- this.renderEdgelessOnlyNotes &&
- this._shouldRenderNoteList(this._edgelessOnlyNotes);
-
- const shouldRenderEmptyPanel =
- !shouldRenderPageVisibleNotes && !shouldRenderEdgelessOnlyNotes;
-
return html`
-
- ${this._renderDocTitle()}
- ${shouldRenderEmptyPanel
- ? this._EmptyPanel()
- : this._PanelList(shouldRenderEdgelessOnlyNotes)}
-
+ ${this._renderDocTitle()}
+ ${when(
+ this._shouldRenderEmptyPanel,
+ () => this._EmptyPanel(),
+ () => html`
+ ${this._renderPageVisibleCardList()}
+ ${this._renderEdgelessOnlyCardList()}
+ `
+ )}
`;
}
override willUpdate(_changedProperties: PropertyValues) {
- if (_changedProperties.has('doc') || _changedProperties.has('edgeless')) {
+ if (_changedProperties.has('editor')) {
this._setDocDisposables();
}
- if (_changedProperties.has('edgeless')) {
- this._clearHighlightMask();
+ if (_changedProperties.has('enableNotesSorting')) {
+ this._updateNoticeVisibility();
}
}
- @state()
- private accessor _dragging = false;
-
- @state()
- private accessor _edgelessOnlyNotes: OutlineNoteItem[] = [];
-
- @state()
- private accessor _pageVisibleNotes: OutlineNoteItem[] = [];
-
- @property({ attribute: false })
- accessor doc!: Store;
-
- @property({ attribute: false })
- accessor domHost!: Document | HTMLElement;
-
- @property({ attribute: false })
- accessor edgeless!: EdgelessRootBlockComponent | null;
+ @query('.page-visible-card-list')
+ private accessor _pageVisibleList!: HTMLElement;
+ @consume({ context: editorContext })
@property({ attribute: false })
accessor editor!: AffineEditorContainer;
@@ -666,12 +563,6 @@ export class OutlinePanelBody extends SignalWatcher(
@property({ attribute: false })
accessor noticeVisible!: boolean;
- @query('.outline-panel-body-container')
- accessor OutlinePanelContainer!: HTMLElement;
-
- @query('.panel-list')
- accessor panelListElement!: HTMLElement;
-
@property({ attribute: false })
accessor renderEdgelessOnlyNotes: boolean = true;
diff --git a/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts b/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts
new file mode 100644
index 0000000000..38e4919dff
--- /dev/null
+++ b/blocksuite/presets/src/fragments/outline/card/outline-card.css.ts
@@ -0,0 +1,162 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const outlineCard = style({
+ position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ boxSizing: 'border-box',
+
+ selectors: {
+ '&[data-status="placeholder"]': {
+ pointerEvents: 'none',
+ opacity: 0.5,
+ },
+ '&[data-sortable="true"]': {
+ padding: '2px 0px',
+ },
+ },
+});
+
+export const cardPreview = style({
+ position: 'relative',
+ width: '100%',
+ borderRadius: '4px',
+ cursor: 'default',
+ userSelect: 'none',
+ ':hover': {
+ background: cssVarV2('layer/background/hoverOverlay'),
+ },
+ selectors: {
+ [`${outlineCard}[data-status="selected"] &`]: {
+ background: cssVarV2('layer/background/hoverOverlay'),
+ },
+ [`${outlineCard}[data-status="placeholder"] &`]: {
+ background: cssVarV2('layer/background/hoverOverlay'),
+ opacity: 0.9,
+ },
+ },
+});
+
+export const cardHeader = style({
+ padding: '0 8px',
+ width: '100%',
+ minHeight: '28px',
+ display: 'none',
+ alignItems: 'center',
+ gap: '8px',
+ boxSizing: 'border-box',
+ selectors: {
+ [`${outlineCard}[data-sortable="true"] &`]: {
+ display: 'flex',
+ },
+ [`${outlineCard}[data-invisible="false"] &:hover`]: {
+ cursor: 'grab',
+ },
+ },
+});
+
+const invisibleCard = style({
+ selectors: {
+ [`${outlineCard}[data-invisible="true"] &`]: {
+ color: cssVarV2('text/disable'),
+ pointerEvents: 'none',
+ },
+ },
+});
+
+export const headerIcon = style([
+ {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ invisibleCard,
+]);
+
+export const headerNumber = style([
+ {
+ textAlign: 'center',
+ fontSize: cssVar('fontSm'),
+ color: cssVar('brandColor'),
+ fontWeight: 500,
+ lineHeight: '14px',
+ },
+ invisibleCard,
+]);
+
+export const divider = style({
+ height: '1px',
+ flex: 1,
+ borderTop: `1px dashed ${cssVar('borderColor')}`,
+ transform: 'translateY(50%)',
+});
+
+export const displayModeButtonGroup = style({
+ display: 'none',
+ position: 'absolute',
+ right: '8px',
+ top: '-6px',
+ paddingTop: '8px',
+ paddingBottom: '8px',
+ alignItems: 'center',
+ gap: '4px',
+ fontSize: '12px',
+ fontWeight: 500,
+ lineHeight: '20px',
+
+ selectors: {
+ [`${cardPreview}:hover &`]: {
+ display: 'flex',
+ },
+ },
+});
+
+export const displayModeButton = style({
+ display: 'flex',
+ borderRadius: '4px',
+ backgroundColor: cssVar('hoverColor'),
+ alignItems: 'center',
+});
+
+export const currentModeLabel = style({
+ display: 'flex',
+ padding: '2px 0px 2px 4px',
+ alignItems: 'center',
+});
+
+export const cardContent = style([
+ {
+ fontFamily: cssVar('fontSansFamily'),
+ userSelect: 'none',
+ color: cssVarV2('text/primary'),
+
+ ':hover': {
+ cursor: 'pointer',
+ },
+ },
+ invisibleCard,
+]);
+
+export const modeChangePanel = style({
+ position: 'absolute',
+ display: 'none',
+ background: cssVarV2('layer/background/overlayPanel'),
+ borderRadius: '8px',
+ boxShadow: cssVar('shadow2'),
+ boxSizing: 'border-box',
+ padding: '8px',
+ fontSize: cssVar('fontSm'),
+ color: cssVarV2('text/primary'),
+ lineHeight: '22px',
+ fontWeight: 400,
+ fontFamily: cssVar('fontSansFamily'),
+
+ selectors: {
+ '&[data-show]': {
+ display: 'flex',
+ },
+ },
+});
diff --git a/blocksuite/presets/src/fragments/outline/card/outline-card.ts b/blocksuite/presets/src/fragments/outline/card/outline-card.ts
index 7ebd2dd10e..811f4ff4aa 100644
--- a/blocksuite/presets/src/fragments/outline/card/outline-card.ts
+++ b/blocksuite/presets/src/fragments/outline/card/outline-card.ts
@@ -1,5 +1,5 @@
+import { ShadowlessElement } from '@blocksuite/block-std';
import {
- type ColorScheme,
createButtonPopper,
type NoteBlockModel,
NoteDisplayMode,
@@ -7,189 +7,19 @@ import {
once,
} from '@blocksuite/blocks';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
-import type { BlockModel, Store } from '@blocksuite/store';
-import { baseTheme } from '@toeverything/theme';
-import { css, html, LitElement, unsafeCSS } from 'lit';
+import type { BlockModel } from '@blocksuite/store';
+import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
-import { classMap } from 'lit/directives/class-map.js';
-import { HiddenIcon, SmallArrowDownIcon } from '../../_common/icons.js';
-import type { SelectEvent } from '../utils/custom-events.js';
-
-const styles = css`
- :host {
- display: block;
- position: relative;
- }
-
- .card-container {
- position: relative;
-
- display: flex;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- }
-
- .card-preview {
- position: relative;
-
- width: 100%;
-
- border-radius: 4px;
-
- cursor: default;
- user-select: none;
- }
-
- .card-preview:hover {
- background: var(--affine-hover-color);
- }
-
- .card-header-container {
- padding: 0 8px;
- width: 100%;
- min-height: 28px;
- display: none;
- align-items: center;
- gap: 8px;
- box-sizing: border-box;
- }
-
- .card-header-container.enable-sorting {
- display: flex;
- }
-
- .card-header-container .card-number {
- text-align: center;
- font-size: var(--affine-font-sm);
- font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
- color: var(--affine-brand-color, #1e96eb);
- font-weight: 500;
- line-height: 14px;
- line-height: 20px;
- }
-
- .card-header-container .card-header-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .card-header-container .card-divider {
- height: 1px;
- flex: 1;
- border-top: 1px dashed var(--affine-border-color);
- transform: translateY(50%);
- }
-
- .display-mode-button-group {
- display: none;
- position: absolute;
- right: 8px;
- top: -6px;
- padding-top: 8px;
- padding-bottom: 8px;
- align-items: center;
- gap: 4px;
- font-size: 12px;
- font-weight: 500;
- line-height: 20px;
- }
-
- .card-preview:hover .display-mode-button-group {
- display: flex;
- }
-
- .display-mode-button-label {
- color: var(--affine-text-primary-color);
- }
-
- .display-mode-button {
- display: flex;
- border-radius: 4px;
- background-color: var(--affine-hover-color);
- align-items: center;
- }
-
- .current-mode-label {
- display: flex;
- padding: 2px 0px 2px 4px;
- align-items: center;
- }
-
- note-display-mode-panel {
- position: absolute;
- display: none;
- background: var(--affine-background-overlay-panel-color);
- border-radius: 8px;
- box-shadow: var(--affine-shadow-2);
- box-sizing: border-box;
- padding: 8px;
- font-size: var(--affine-font-sm);
- color: var(--affine-text-primary-color);
- line-height: 22px;
- font-weight: 400;
- font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
- }
-
- note-display-mode-panel[data-show] {
- display: flex;
- }
-
- .card-content {
- font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
- user-select: none;
- color: var(--affine-text-primary-color);
- }
-
- .card-preview .card-content:hover {
- cursor: pointer;
- }
-
- .card-container[data-invisible='false']
- .card-preview
- .card-header-container:hover {
- cursor: grab;
- }
-
- .card-container.placeholder {
- pointer-events: none;
- opacity: 0.5;
- }
-
- .card-container.selected .card-preview {
- background: var(--affine-hover-color);
- }
-
- .card-container.placeholder .card-preview {
- background: var(--affine-hover-color);
- opacity: 0.9;
- }
-
- .card-container[data-sortable='true'] {
- padding: 2px 0;
- }
-
- .card-container[data-invisible='true'] .card-header-container .card-number,
- .card-container[data-invisible='true']
- .card-header-container
- .card-header-icon,
- .card-container[data-invisible='true'] .card-preview .card-content {
- color: var(--affine-text-disable-color);
- pointer-events: none;
- }
-
- .card-preview outline-block-preview:hover {
- color: var(--affine-brand-color);
- }
-`;
+import { HiddenIcon, SmallArrowDownIcon } from '../../_common/icons';
+import type { SelectEvent } from '../utils/custom-events';
+import * as styles from './outline-card.css';
export const AFFINE_OUTLINE_NOTE_CARD = 'affine-outline-note-card';
-export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
- static override styles = styles;
-
+export class OutlineNoteCard extends SignalWatcher(
+ WithDisposable(ShadowlessElement)
+) {
private _displayModePopper: ReturnType
| null =
null;
@@ -283,6 +113,10 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
}
}
+ get invisible() {
+ return this.note.displayMode === NoteDisplayMode.EdgelessOnly;
+ }
+
override updated() {
this._displayModePopper = createButtonPopper(
this._displayModeButtonGroup,
@@ -302,50 +136,49 @@ export class OutlineNoteCard extends SignalWatcher(WithDisposable(LitElement)) {
override render() {
const { children, displayMode } = this.note;
const currentMode = this._getCurrentModeLabel(displayMode);
- const cardHeaderClasses = classMap({
- 'card-header-container': true,
- 'enable-sorting': this.enableNotesSorting,
- });
return html`