mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
44
blocksuite/affine/blocks/note/src/adapters/html.ts
Normal file
44
blocksuite/affine/blocks/note/src/adapters/html.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
/**
|
||||
* Create a html adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to html when current editor mode is `Doc(Page)`.
|
||||
* @returns The html adapter matcher.
|
||||
*/
|
||||
const createNoteBlockHtmlAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockHtmlAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockHtmlAdapterMatcher = createNoteBlockHtmlAdapterMatcher(
|
||||
NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
|
||||
export const edgelessNoteBlockHtmlAdapterMatcher =
|
||||
createNoteBlockHtmlAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
docNoteBlockHtmlAdapterMatcher
|
||||
);
|
||||
|
||||
export const EdgelessNoteBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
edgelessNoteBlockHtmlAdapterMatcher
|
||||
);
|
||||
30
blocksuite/affine/blocks/note/src/adapters/index.ts
Normal file
30
blocksuite/affine/blocks/note/src/adapters/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
DocNoteBlockHtmlAdapterExtension,
|
||||
EdgelessNoteBlockHtmlAdapterExtension,
|
||||
} from './html';
|
||||
import {
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
EdgelessNoteBlockMarkdownAdapterExtension,
|
||||
} from './markdown';
|
||||
import {
|
||||
DocNoteBlockPlainTextAdapterExtension,
|
||||
EdgelessNoteBlockPlainTextAdapterExtension,
|
||||
} from './plain-text';
|
||||
|
||||
export * from './html';
|
||||
export * from './markdown';
|
||||
export * from './plain-text';
|
||||
|
||||
export const DocNoteBlockAdapterExtensions: ExtensionType[] = [
|
||||
DocNoteBlockMarkdownAdapterExtension,
|
||||
DocNoteBlockHtmlAdapterExtension,
|
||||
DocNoteBlockPlainTextAdapterExtension,
|
||||
];
|
||||
|
||||
export const EdgelessNoteBlockAdapterExtensions: ExtensionType[] = [
|
||||
EdgelessNoteBlockMarkdownAdapterExtension,
|
||||
EdgelessNoteBlockHtmlAdapterExtension,
|
||||
EdgelessNoteBlockPlainTextAdapterExtension,
|
||||
];
|
||||
123
blocksuite/affine/blocks/note/src/adapters/markdown.ts
Normal file
123
blocksuite/affine/blocks/note/src/adapters/markdown.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
FOOTNOTE_DEFINITION_PREFIX,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { FootnoteDefinition, Root } from 'mdast';
|
||||
|
||||
const isRootNode = (node: MarkdownAST): node is Root => node.type === 'root';
|
||||
const isFootnoteDefinitionNode = (
|
||||
node: MarkdownAST
|
||||
): node is FootnoteDefinition => node.type === 'footnoteDefinition';
|
||||
|
||||
const createFootnoteDefinition = (
|
||||
identifier: string,
|
||||
content: string
|
||||
): MarkdownAST => ({
|
||||
type: 'footnoteDefinition',
|
||||
label: identifier,
|
||||
identifier,
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: content,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a markdown adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to markdown when current editor mode is `Doc`.
|
||||
* @returns The markdown adapter matcher.
|
||||
*/
|
||||
const createNoteBlockMarkdownAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockMarkdownAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: o => isRootNode(o.node),
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!isRootNode(o.node)) {
|
||||
return;
|
||||
}
|
||||
const noteAst = o.node;
|
||||
// Find all the footnoteDefinition in the noteAst
|
||||
const { configs } = context;
|
||||
noteAst.children.forEach(child => {
|
||||
if (isFootnoteDefinitionNode(child)) {
|
||||
const identifier = child.identifier;
|
||||
const definitionKey = `${FOOTNOTE_DEFINITION_PREFIX}${identifier}`;
|
||||
// Get the text content of the footnoteDefinition
|
||||
const textContent = child.children
|
||||
.find(child => child.type === 'paragraph')
|
||||
?.children.find(child => child.type === 'text')?.value;
|
||||
if (textContent) {
|
||||
configs.set(definitionKey, textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove the footnoteDefinition node from the noteAst
|
||||
noteAst.children = noteAst.children.filter(
|
||||
child => !isFootnoteDefinitionNode(child)
|
||||
);
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext, configs } = context;
|
||||
// Get all the footnote definitions config starts with FOOTNOTE_DEFINITION_PREFIX
|
||||
// And create footnoteDefinition AST node for each of them
|
||||
Array.from(configs.keys())
|
||||
.filter(key => key.startsWith(FOOTNOTE_DEFINITION_PREFIX))
|
||||
.forEach(key => {
|
||||
const hasFootnoteDefinition = !!walkerContext.getGlobalContext(key);
|
||||
// If the footnoteDefinition node is already in md ast, skip it
|
||||
// In markdown file, we only need to create footnoteDefinition once
|
||||
if (hasFootnoteDefinition) {
|
||||
return;
|
||||
}
|
||||
const definition = configs.get(key);
|
||||
const identifier = key.slice(FOOTNOTE_DEFINITION_PREFIX.length);
|
||||
if (definition && identifier) {
|
||||
walkerContext
|
||||
.openNode(
|
||||
createFootnoteDefinition(identifier, definition),
|
||||
'children'
|
||||
)
|
||||
.closeNode();
|
||||
// Set the footnoteDefinition node as global context to avoid duplicate creation
|
||||
walkerContext.setGlobalContext(key, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockMarkdownAdapterMatcher =
|
||||
createNoteBlockMarkdownAdapterMatcher(NoteDisplayMode.EdgelessOnly);
|
||||
|
||||
export const edgelessNoteBlockMarkdownAdapterMatcher =
|
||||
createNoteBlockMarkdownAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(docNoteBlockMarkdownAdapterMatcher);
|
||||
|
||||
export const EdgelessNoteBlockMarkdownAdapterExtension =
|
||||
BlockMarkdownAdapterExtension(edgelessNoteBlockMarkdownAdapterMatcher);
|
||||
41
blocksuite/affine/blocks/note/src/adapters/plain-text.ts
Normal file
41
blocksuite/affine/blocks/note/src/adapters/plain-text.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
/**
|
||||
* Create a plain text adapter matcher for note block.
|
||||
*
|
||||
* @param displayModeToSkip - The note with specific display mode to skip.
|
||||
* For example, the note with display mode `EdgelessOnly` should not be converted to plain text when current editor mode is `Doc(Page)`.
|
||||
* @returns The plain text adapter matcher.
|
||||
*/
|
||||
const createNoteBlockPlainTextAdapterMatcher = (
|
||||
displayModeToSkip: NoteDisplayMode
|
||||
): BlockPlainTextAdapterMatcher => ({
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
toMatch: () => false,
|
||||
fromMatch: o => o.node.flavour === NoteBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const node = o.node;
|
||||
if (node.props.displayMode === displayModeToSkip) {
|
||||
context.walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docNoteBlockPlainTextAdapterMatcher =
|
||||
createNoteBlockPlainTextAdapterMatcher(NoteDisplayMode.EdgelessOnly);
|
||||
|
||||
export const edgelessNoteBlockPlainTextAdapterMatcher =
|
||||
createNoteBlockPlainTextAdapterMatcher(NoteDisplayMode.DocOnly);
|
||||
|
||||
export const DocNoteBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(docNoteBlockPlainTextAdapterMatcher);
|
||||
|
||||
export const EdgelessNoteBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(edgelessNoteBlockPlainTextAdapterMatcher);
|
||||
247
blocksuite/affine/blocks/note/src/commands/block-type.ts
Normal file
247
blocksuite/affine/blocks/note/src/commands/block-type.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
CodeBlockModel,
|
||||
ListBlockModel,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
asyncSetInlineRange,
|
||||
focusTextModel,
|
||||
onModelTextUpdated,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
getBlockSelectionsCommand,
|
||||
getSelectedBlocksCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
matchModels,
|
||||
mergeToCodeModel,
|
||||
transformModel,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
type UpdateBlockConfig = {
|
||||
flavour: string;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const updateBlockType: Command<
|
||||
UpdateBlockConfig & {
|
||||
selectedBlocks?: BlockComponent[];
|
||||
},
|
||||
{
|
||||
updatedBlocks: BlockModel[];
|
||||
}
|
||||
> = (ctx, next) => {
|
||||
const { std, flavour, props } = ctx;
|
||||
const host = std.host;
|
||||
const doc = std.store;
|
||||
|
||||
const getSelectedBlocks = () => {
|
||||
let { selectedBlocks } = ctx;
|
||||
|
||||
if (selectedBlocks == null) {
|
||||
const [result, ctx] = std.command
|
||||
.chain()
|
||||
.tryAll(chain => [
|
||||
chain.pipe(getTextSelectionCommand),
|
||||
chain.pipe(getBlockSelectionsCommand),
|
||||
])
|
||||
.pipe(getSelectedBlocksCommand, { types: ['text', 'block'] })
|
||||
.run();
|
||||
if (result) {
|
||||
selectedBlocks = ctx.selectedBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedBlocks;
|
||||
};
|
||||
|
||||
const selectedBlocks = getSelectedBlocks();
|
||||
if (!selectedBlocks || selectedBlocks.length === 0) return false;
|
||||
|
||||
const blockModels = selectedBlocks.map(ele => ele.model);
|
||||
|
||||
const hasSameDoc = selectedBlocks.every(block => block.doc === doc);
|
||||
if (!hasSameDoc) {
|
||||
// doc check
|
||||
console.error(
|
||||
'Not all models have the same doc instance, the result for update text type may not be correct',
|
||||
selectedBlocks
|
||||
);
|
||||
}
|
||||
|
||||
const mergeToCode: Command<{}, { updatedBlocks: BlockModel[] }> = (
|
||||
_,
|
||||
next
|
||||
) => {
|
||||
if (flavour !== 'affine:code') return;
|
||||
const id = mergeToCodeModel(blockModels);
|
||||
if (!id) return;
|
||||
const model = doc.getModelById(id);
|
||||
if (!model) return;
|
||||
asyncSetInlineRange(std, model, {
|
||||
index: model.text?.length ?? 0,
|
||||
length: 0,
|
||||
}).catch(console.error);
|
||||
return next({ updatedBlocks: [model] });
|
||||
};
|
||||
const appendDivider: Command<{}, { updatedBlocks: BlockModel[] }> = (
|
||||
_,
|
||||
next
|
||||
) => {
|
||||
if (flavour !== 'affine:divider') {
|
||||
return false;
|
||||
}
|
||||
const model = blockModels.at(-1);
|
||||
if (!model) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
const index = parent.children.indexOf(model);
|
||||
const nextSibling = doc.getNext(model);
|
||||
let nextSiblingId = nextSibling?.id as string;
|
||||
const id = doc.addBlock('affine:divider', {}, parent, index + 1);
|
||||
if (!nextSibling) {
|
||||
nextSiblingId = doc.addBlock('affine:paragraph', {}, parent);
|
||||
}
|
||||
focusTextModel(host.std, nextSiblingId);
|
||||
const newModel = doc.getModelById(id);
|
||||
if (!newModel) {
|
||||
return next({ updatedBlocks: [] });
|
||||
}
|
||||
return next({ updatedBlocks: [newModel] });
|
||||
};
|
||||
|
||||
const focusText: Command<{ updatedBlocks: BlockModel[] }> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const firstNewModel = updatedBlocks[0];
|
||||
const lastNewModel = updatedBlocks[updatedBlocks.length - 1];
|
||||
|
||||
const allTextUpdated = updatedBlocks.map(model =>
|
||||
onModelTextUpdated(std, model)
|
||||
);
|
||||
const selectionManager = host.selection;
|
||||
const textSelection = selectionManager.find(TextSelection);
|
||||
if (!textSelection) {
|
||||
return false;
|
||||
}
|
||||
const newTextSelection = selectionManager.create(TextSelection, {
|
||||
from: {
|
||||
blockId: firstNewModel.id,
|
||||
index: textSelection.from.index,
|
||||
length: textSelection.from.length,
|
||||
},
|
||||
to: textSelection.to
|
||||
? {
|
||||
blockId: lastNewModel.id,
|
||||
index: textSelection.to.index,
|
||||
length: textSelection.to.length,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
Promise.all(allTextUpdated)
|
||||
.then(() => {
|
||||
selectionManager.setGroup('note', [newTextSelection]);
|
||||
})
|
||||
.catch(console.error);
|
||||
return next();
|
||||
};
|
||||
|
||||
const focusBlock: Command<{ updatedBlocks: BlockModel[] }> = (ctx, next) => {
|
||||
const { updatedBlocks } = ctx;
|
||||
if (!updatedBlocks || updatedBlocks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectionManager = host.selection;
|
||||
|
||||
const blockSelections = selectionManager.filter(BlockSelection);
|
||||
if (blockSelections.length === 0) {
|
||||
return false;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const selections = updatedBlocks.map(model => {
|
||||
return selectionManager.create(BlockSelection, {
|
||||
blockId: model.id,
|
||||
});
|
||||
});
|
||||
|
||||
selectionManager.setGroup('note', selections);
|
||||
});
|
||||
return next();
|
||||
};
|
||||
|
||||
const [result, resultCtx] = std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
doc.captureSync();
|
||||
return next();
|
||||
})
|
||||
// update block type
|
||||
.try<{ updatedBlocks: BlockModel[] }>(chain => [
|
||||
chain.pipe(mergeToCode),
|
||||
chain.pipe(appendDivider),
|
||||
chain.pipe((_, next) => {
|
||||
const newModels: BlockModel[] = [];
|
||||
blockModels.forEach(model => {
|
||||
if (
|
||||
!matchModels(model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (model.flavour === flavour) {
|
||||
doc.updateBlock(model, props ?? {});
|
||||
newModels.push(model);
|
||||
return;
|
||||
}
|
||||
const newId = transformModel(model, flavour, props);
|
||||
if (!newId) {
|
||||
return;
|
||||
}
|
||||
const newModel = doc.getModelById(newId);
|
||||
if (newModel) {
|
||||
newModels.push(newModel);
|
||||
}
|
||||
});
|
||||
return next({ updatedBlocks: newModels });
|
||||
}),
|
||||
])
|
||||
// focus
|
||||
.try(chain => [
|
||||
chain.pipe((_, next) => {
|
||||
if (['affine:code', 'affine:divider'].includes(flavour)) {
|
||||
return next();
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
chain.pipe(focusText),
|
||||
chain.pipe(focusBlock),
|
||||
chain.pipe((_, next) => next()),
|
||||
])
|
||||
.run();
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return next({ updatedBlocks: resultCtx.updatedBlocks });
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
export const changeNoteDisplayMode: Command<{
|
||||
noteId: string;
|
||||
mode: NoteDisplayMode;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
const { std, noteId, mode, stopCapture } = ctx;
|
||||
|
||||
const noteBlockModel = std.store.getBlock(noteId)?.model;
|
||||
if (!noteBlockModel || !matchModels(noteBlockModel, [NoteBlockModel])) return;
|
||||
|
||||
const currentMode = noteBlockModel.props.displayMode;
|
||||
if (currentMode === mode) return;
|
||||
|
||||
if (stopCapture) std.store.captureSync();
|
||||
|
||||
// Update the order in the note list in its parent, for example
|
||||
// 1. Both mode note | 1. Both mode note
|
||||
// 2. Page mode note | 2. Page mode note
|
||||
// ---------------------------- |-> 3. the changed note (Both or Page)
|
||||
// 3. Edgeless mode note | ---------------------------
|
||||
// 4. the changing edgeless note -| 4. Edgeless mode note
|
||||
const parent = std.store.getParent(noteBlockModel);
|
||||
if (parent) {
|
||||
const notes = parent.children.filter(child =>
|
||||
matchModels(child, [NoteBlockModel])
|
||||
);
|
||||
const firstEdgelessOnlyNote = notes.find(
|
||||
note => note.props.displayMode === NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
const lastPageVisibleNote = notes.findLast(
|
||||
note => note.props.displayMode !== NoteDisplayMode.EdgelessOnly
|
||||
);
|
||||
|
||||
if (currentMode === NoteDisplayMode.EdgelessOnly) {
|
||||
std.store.moveBlocks(
|
||||
[noteBlockModel],
|
||||
parent,
|
||||
lastPageVisibleNote ?? firstEdgelessOnlyNote,
|
||||
lastPageVisibleNote ? false : true
|
||||
);
|
||||
} else if (mode === NoteDisplayMode.EdgelessOnly) {
|
||||
std.store.moveBlocks(
|
||||
[noteBlockModel],
|
||||
parent,
|
||||
firstEdgelessOnlyNote ?? lastPageVisibleNote,
|
||||
firstEdgelessOnlyNote ? true : false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std.store.updateBlock(noteBlockModel, {
|
||||
displayMode: mode,
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlock } from './dedent-block';
|
||||
|
||||
export const dedentBlockToRoot: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
let parent = store.getParent(model);
|
||||
let changed = false;
|
||||
while (parent && !matchModels(parent, [NoteBlockModel])) {
|
||||
if (!changed) {
|
||||
if (stopCapture) store.captureSync();
|
||||
changed = true;
|
||||
}
|
||||
std.command.exec(dedentBlock, { blockId: model.id, stopCapture: true });
|
||||
parent = store.getParent(model);
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
67
blocksuite/affine/blocks/note/src/commands/dedent-block.ts
Normal file
67
blocksuite/affine/blocks/note/src/commands/dedent-block.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after unindent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const dedentBlock: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const parent = store.getParent(model);
|
||||
const grandParent = parent && store.getParent(parent);
|
||||
if (store.readonly || !parent || parent.role !== 'content' || !grandParent) {
|
||||
// Top most, can not unindent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
store.moveBlocks([model, ...collapsedSiblings], grandParent, parent, false);
|
||||
return next();
|
||||
}
|
||||
|
||||
try {
|
||||
const nextSiblings = store.getNexts(model);
|
||||
store.moveBlocks(nextSiblings, model);
|
||||
store.moveBlocks([model], grandParent, parent, false);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlockToRoot } from './dedent-block-to-root';
|
||||
|
||||
export const dedentBlocksToRoot: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const text = std.selection.find(TextSelection);
|
||||
if (text) {
|
||||
// If the text selection is not at the beginning of the block, use default behavior
|
||||
if (text.from.index !== 0) return;
|
||||
|
||||
blockIds = [text.from.blockId, text.to?.blockId].filter(
|
||||
(x): x is string => !!x
|
||||
);
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || store.readonly) return;
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
for (let i = blockIds.length - 1; i >= 0; i--) {
|
||||
const model = blockIds[i];
|
||||
const parent = store.getParent(model);
|
||||
if (parent && !matchModels(parent, [NoteBlockModel])) {
|
||||
std.command.exec(dedentBlockToRoot, {
|
||||
blockId: model,
|
||||
stopCapture: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
87
blocksuite/affine/blocks/note/src/commands/dedent-blocks.ts
Normal file
87
blocksuite/affine/blocks/note/src/commands/dedent-blocks.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { dedentBlock } from './dedent-block';
|
||||
|
||||
export const dedentBlocks: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store, selection, range, host } = std;
|
||||
const { schema } = store;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || store.readonly) return;
|
||||
|
||||
// Find the first model that can be unindented
|
||||
let firstDedentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const model = store.getBlock(blockIds[i])?.model;
|
||||
if (!model) continue;
|
||||
const parent = store.getParent(blockIds[i]);
|
||||
if (!parent) continue;
|
||||
const grandParent = store.getParent(parent);
|
||||
if (!grandParent) continue;
|
||||
|
||||
if (schema.isValid(model.flavour, grandParent.flavour)) {
|
||||
firstDedentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDedentIndex === -1) return;
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstDedentIndex).forEach(id => {
|
||||
const model = store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be dedented
|
||||
const dedentIds = blockIds
|
||||
.slice(firstDedentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
dedentIds.reverse().forEach(id => {
|
||||
std.command.exec(dedentBlock, { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
74
blocksuite/affine/blocks/note/src/commands/indent-block.ts
Normal file
74
blocksuite/affine/blocks/note/src/commands/indent-block.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { ListBlockModel, ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
/**
|
||||
* @example
|
||||
* before indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*
|
||||
* after indent:
|
||||
* - aaa
|
||||
* - bbb
|
||||
* - ccc|
|
||||
* - ddd
|
||||
* - eee
|
||||
*/
|
||||
export const indentBlock: Command<{
|
||||
blockId?: string;
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockId } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store } = std;
|
||||
const { schema } = store;
|
||||
if (!blockId) {
|
||||
const sel = std.selection.getGroup('note').at(0);
|
||||
blockId = sel?.blockId;
|
||||
}
|
||||
if (!blockId) return;
|
||||
const model = std.store.getBlock(blockId)?.model;
|
||||
if (!model) return;
|
||||
|
||||
const previousSibling = store.getPrev(model);
|
||||
if (
|
||||
store.readonly ||
|
||||
!previousSibling ||
|
||||
!schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
// can not indent, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
store.moveBlocks([model, ...collapsedSiblings], previousSibling);
|
||||
} else {
|
||||
store.moveBlocks([model], previousSibling);
|
||||
}
|
||||
|
||||
// update collapsed state of affine list
|
||||
if (
|
||||
matchModels(previousSibling, [ListBlockModel]) &&
|
||||
previousSibling.props.collapsed
|
||||
) {
|
||||
store.updateBlock(previousSibling, {
|
||||
collapsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
125
blocksuite/affine/blocks/note/src/commands/indent-blocks.ts
Normal file
125
blocksuite/affine/blocks/note/src/commands/indent-blocks.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ParagraphBlockModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { type Command, TextSelection } from '@blocksuite/std';
|
||||
|
||||
import { indentBlock } from './indent-block';
|
||||
|
||||
export const indentBlocks: Command<{
|
||||
blockIds?: string[];
|
||||
stopCapture?: boolean;
|
||||
}> = (ctx, next) => {
|
||||
let { blockIds } = ctx;
|
||||
const { std, stopCapture = true } = ctx;
|
||||
const { store, selection, range, host } = std;
|
||||
const { schema } = store;
|
||||
|
||||
if (!blockIds || !blockIds.length) {
|
||||
const nativeRange = range.value;
|
||||
if (nativeRange) {
|
||||
const topBlocks = range.getSelectedBlockComponentsByRange(nativeRange, {
|
||||
match: el => el.model.role === 'content',
|
||||
mode: 'highest',
|
||||
});
|
||||
if (topBlocks.length > 0) {
|
||||
blockIds = topBlocks.map(block => block.blockId);
|
||||
}
|
||||
} else {
|
||||
blockIds = std.selection.getGroup('note').map(sel => sel.blockId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!blockIds || !blockIds.length || store.readonly) return;
|
||||
|
||||
// Find the first model that can be indented
|
||||
let firstIndentIndex = -1;
|
||||
for (let i = 0; i < blockIds.length; i++) {
|
||||
const previousSibling = store.getPrev(blockIds[i]);
|
||||
const model = store.getBlock(blockIds[i])?.model;
|
||||
if (
|
||||
model &&
|
||||
previousSibling &&
|
||||
schema.isValid(model.flavour, previousSibling.flavour)
|
||||
) {
|
||||
firstIndentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No model can be indented
|
||||
if (firstIndentIndex === -1) return;
|
||||
|
||||
if (stopCapture) store.captureSync();
|
||||
|
||||
const collapsedIds: string[] = [];
|
||||
blockIds.slice(firstIndentIndex).forEach(id => {
|
||||
const model = store.getBlock(id)?.model;
|
||||
if (!model) return;
|
||||
if (
|
||||
matchModels(model, [ParagraphBlockModel]) &&
|
||||
model.props.type.startsWith('h') &&
|
||||
model.props.collapsed
|
||||
) {
|
||||
const collapsedSiblings = calculateCollapsedSiblings(model);
|
||||
collapsedIds.push(...collapsedSiblings.map(sibling => sibling.id));
|
||||
}
|
||||
});
|
||||
// Models waiting to be indented
|
||||
const indentIds = blockIds
|
||||
.slice(firstIndentIndex)
|
||||
.filter(id => !collapsedIds.includes(id));
|
||||
const firstModel = store.getBlock(indentIds[0])?.model;
|
||||
if (!firstModel) return;
|
||||
|
||||
{
|
||||
// > # 123
|
||||
// > # 456
|
||||
// > # 789
|
||||
//
|
||||
// we need to update 123 collapsed state to false when indent 456 and 789
|
||||
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchModels(nearestHeading, [ParagraphBlockModel]) &&
|
||||
nearestHeading.props.collapsed
|
||||
) {
|
||||
store.updateBlock(nearestHeading, { collapsed: false });
|
||||
}
|
||||
}
|
||||
|
||||
indentIds.forEach(id => {
|
||||
std.command.exec(indentBlock, { blockId: id, stopCapture: false });
|
||||
});
|
||||
|
||||
{
|
||||
// 123
|
||||
// > # 456
|
||||
// 789
|
||||
// 012
|
||||
//
|
||||
// we need to update 456 collapsed state to false when indent 789 and 012
|
||||
const nearestHeading = getNearestHeadingBefore(firstModel);
|
||||
if (
|
||||
nearestHeading &&
|
||||
matchModels(nearestHeading, [ParagraphBlockModel]) &&
|
||||
nearestHeading.props.collapsed
|
||||
) {
|
||||
store.updateBlock(nearestHeading, { collapsed: false });
|
||||
}
|
||||
}
|
||||
|
||||
const textSelection = selection.find(TextSelection);
|
||||
if (textSelection) {
|
||||
host.updateComplete
|
||||
.then(() => {
|
||||
range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
10
blocksuite/affine/blocks/note/src/commands/index.ts
Normal file
10
blocksuite/affine/blocks/note/src/commands/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { updateBlockType } from './block-type.js';
|
||||
export { changeNoteDisplayMode } from './change-note-display-mode.js';
|
||||
export { dedentBlock } from './dedent-block.js';
|
||||
export { dedentBlockToRoot } from './dedent-block-to-root.js';
|
||||
export { dedentBlocks } from './dedent-blocks.js';
|
||||
export { dedentBlocksToRoot } from './dedent-blocks-to-root.js';
|
||||
export { indentBlock } from './indent-block.js';
|
||||
export { indentBlocks } from './indent-blocks.js';
|
||||
export { selectBlock } from './select-block.js';
|
||||
export { selectBlocksBetween } from './select-blocks-between.js';
|
||||
22
blocksuite/affine/blocks/note/src/commands/select-block.ts
Normal file
22
blocksuite/affine/blocks/note/src/commands/select-block.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
export const selectBlock: Command<{
|
||||
focusBlock?: BlockComponent;
|
||||
}> = (ctx, next) => {
|
||||
const { focusBlock, std } = ctx;
|
||||
if (!focusBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { selection } = std;
|
||||
|
||||
selection.setGroup('note', [
|
||||
selection.create(BlockSelection, { blockId: focusBlock.blockId }),
|
||||
]);
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type Command,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
export const selectBlocksBetween: Command<{
|
||||
focusBlock?: BlockComponent;
|
||||
anchorBlock?: BlockComponent;
|
||||
tail: boolean;
|
||||
}> = (ctx, next) => {
|
||||
const { focusBlock, anchorBlock, tail } = ctx;
|
||||
if (!focusBlock || !anchorBlock) {
|
||||
return;
|
||||
}
|
||||
const selection = ctx.std.selection;
|
||||
|
||||
// In same block
|
||||
if (anchorBlock.blockId === focusBlock.blockId) {
|
||||
const blockId = focusBlock.blockId;
|
||||
selection.setGroup('note', [selection.create(BlockSelection, { blockId })]);
|
||||
return next();
|
||||
}
|
||||
|
||||
// In different blocks
|
||||
const selections = [...selection.value];
|
||||
if (selections.every(sel => sel.blockId !== focusBlock.blockId)) {
|
||||
if (tail) {
|
||||
selections.push(
|
||||
selection.create(BlockSelection, { blockId: focusBlock.blockId })
|
||||
);
|
||||
} else {
|
||||
selections.unshift(
|
||||
selection.create(BlockSelection, { blockId: focusBlock.blockId })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let start = false;
|
||||
const sel = selections.filter(sel => {
|
||||
if (
|
||||
sel.blockId === anchorBlock.blockId ||
|
||||
sel.blockId === focusBlock.blockId
|
||||
) {
|
||||
start = !start;
|
||||
return true;
|
||||
}
|
||||
return start;
|
||||
});
|
||||
|
||||
selection.setGroup('note', sel);
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
import {
|
||||
ACTIVE_NOTE_EXTRA_PADDING,
|
||||
edgelessNoteContainer,
|
||||
} from '../note-edgeless-block.css';
|
||||
|
||||
export const background = style({
|
||||
position: 'absolute',
|
||||
borderColor: cssVar('black10'),
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
selectors: {
|
||||
[`${edgelessNoteContainer}[data-editing="true"] &`]: {
|
||||
left: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
|
||||
top: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
|
||||
width: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
|
||||
height: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
|
||||
transition: 'left 0.3s, top 0.3s, width 0.3s, height 0.3s',
|
||||
boxShadow: cssVar('activeShadow'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
DefaultTheme,
|
||||
ListBlockModel,
|
||||
NoteBlockModel,
|
||||
ParagraphBlockModel,
|
||||
StrokeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
getClosestBlockComponentByPoint,
|
||||
handleNativeRangeAtPoint,
|
||||
matchModels,
|
||||
stopPropagation,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { clamp, Point } from '@blocksuite/global/gfx';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import {
|
||||
type BlockComponent,
|
||||
type BlockStdScope,
|
||||
PropTypes,
|
||||
requiredProperties,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { consume } from '@lit/context';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html, nothing } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { NoteConfigExtension } from '../config';
|
||||
import * as styles from './edgeless-note-background.css';
|
||||
|
||||
@requiredProperties({
|
||||
note: PropTypes.instanceOf(NoteBlockModel),
|
||||
})
|
||||
export class EdgelessNoteBackground extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
readonly backgroundStyle$ = computed(() => {
|
||||
const themeProvider = this.std.get(ThemeProvider);
|
||||
const theme = themeProvider.theme$.value;
|
||||
const backgroundColor = themeProvider.generateColorProperty(
|
||||
this.note.props.background$.value,
|
||||
DefaultTheme.noteBackgrounColor,
|
||||
theme
|
||||
);
|
||||
|
||||
const { borderRadius, borderSize, borderStyle, shadowType } =
|
||||
this.note.props.edgeless$.value.style;
|
||||
|
||||
return {
|
||||
borderRadius: borderRadius + 'px',
|
||||
backgroundColor: backgroundColor,
|
||||
borderWidth: `${borderSize}px`,
|
||||
borderStyle: borderStyle === StrokeStyle.Dash ? 'dashed' : borderStyle,
|
||||
boxShadow: !shadowType ? 'none' : `var(${shadowType})`,
|
||||
};
|
||||
});
|
||||
|
||||
get gfx() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
get doc() {
|
||||
return this.std.host.doc;
|
||||
}
|
||||
|
||||
private _tryAddParagraph(x: number, y: number) {
|
||||
const nearest = getClosestBlockComponentByPoint(
|
||||
new Point(x, y)
|
||||
) as BlockComponent | null;
|
||||
if (!nearest) return;
|
||||
|
||||
const nearestBBox = nearest.getBoundingClientRect();
|
||||
const yRel = y - nearestBBox.top;
|
||||
|
||||
const insertPos: 'before' | 'after' =
|
||||
yRel < nearestBBox.height / 2 ? 'before' : 'after';
|
||||
|
||||
const nearestModel = nearest.model as BlockModel;
|
||||
const nearestModelIdx = this.note.children.indexOf(nearestModel);
|
||||
|
||||
const children = this.note.children;
|
||||
const siblingModel =
|
||||
children[
|
||||
clamp(
|
||||
nearestModelIdx + (insertPos === 'before' ? -1 : 1),
|
||||
0,
|
||||
children.length
|
||||
)
|
||||
];
|
||||
|
||||
if (
|
||||
(!nearestModel.text ||
|
||||
!matchModels(nearestModel, [ParagraphBlockModel, ListBlockModel])) &&
|
||||
(!siblingModel ||
|
||||
!siblingModel.text ||
|
||||
!matchModels(siblingModel, [ParagraphBlockModel, ListBlockModel]))
|
||||
) {
|
||||
const [pId] = this.doc.addSiblingBlocks(
|
||||
nearestModel,
|
||||
[{ flavour: 'affine:paragraph' }],
|
||||
insertPos
|
||||
);
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
this.std.selection.setGroup('note', [
|
||||
this.std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: pId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleClickAtBackground(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (!this.editing) return;
|
||||
|
||||
const { zoom } = this.gfx.viewport;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
const offsetY = 16 * zoom;
|
||||
const offsetX = 2 * zoom;
|
||||
const x = clamp(e.x, rect.left + offsetX, rect.right - offsetX);
|
||||
const y = clamp(e.y, rect.top + offsetY, rect.bottom - offsetY);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
|
||||
if (this.std.host.doc.readonly) return;
|
||||
|
||||
this._tryAddParagraph(x, y);
|
||||
}
|
||||
|
||||
private _renderHeader() {
|
||||
const header = this.std
|
||||
.getOptional(NoteConfigExtension.identifier)
|
||||
?.edgelessNoteHeader({ note: this.note, std: this.std });
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div
|
||||
class=${styles.background}
|
||||
style=${styleMap(this.backgroundStyle$.value)}
|
||||
@pointerdown=${stopPropagation}
|
||||
@click=${this._handleClickAtBackground}
|
||||
>
|
||||
${this.note.isPageBlock() ? this._renderHeader() : nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@consume({ context: stdContext })
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editing: boolean = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor note!: NoteBlockModel;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-background': EdgelessNoteBackground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import { LineWidth, type StrokeStyle } from '@blocksuite/affine-model';
|
||||
import { LineStyleIcon } from '@blocksuite/icons/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class EdgelessNoteBorderDropdownMenu extends ShadowlessElement {
|
||||
override render() {
|
||||
const { lineSize, lineStyle } = this;
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Border style"
|
||||
.tooltip="${'Border style'}"
|
||||
>
|
||||
${LineStyleIcon()} ${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<edgeless-line-styles-panel
|
||||
.lineSize=${lineSize}
|
||||
.lineStyle=${lineStyle}
|
||||
></edgeless-line-styles-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineStyle!: StrokeStyle;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor lineSize: LineWidth = LineWidth.Two;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-border-dropdown-menu': EdgelessNoteBorderDropdownMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { ShadowlessElement } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
const DisplayModeMap = {
|
||||
[NoteDisplayMode.DocAndEdgeless]: 'Both',
|
||||
[NoteDisplayMode.EdgelessOnly]: 'Edgeless',
|
||||
[NoteDisplayMode.DocOnly]: 'Page',
|
||||
} as const satisfies Record<NoteDisplayMode, string>;
|
||||
|
||||
export class EdgelessNoteDisplayModeDropdownMenu extends ShadowlessElement {
|
||||
get mode() {
|
||||
return DisplayModeMap[this.displayMode];
|
||||
}
|
||||
|
||||
select(detail: NoteDisplayMode) {
|
||||
this.dispatchEvent(new CustomEvent('select', { detail }));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { displayMode, mode } = this;
|
||||
|
||||
return html`
|
||||
<span class="display-mode-button-label">Show in</span>
|
||||
<editor-menu-button
|
||||
.contentPadding=${'8px'}
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Mode"
|
||||
.tooltip="${'Display mode'}"
|
||||
.justify="${'space-between'}"
|
||||
.labelHeight="${'20px'}"
|
||||
>
|
||||
<span class="label">${mode}</span>
|
||||
${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<note-display-mode-panel
|
||||
.displayMode=${displayMode}
|
||||
.onSelect=${(newMode: NoteDisplayMode) => this.select(newMode)}
|
||||
>
|
||||
</note-display-mode-panel>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor displayMode!: NoteDisplayMode;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-display-mode-dropdown-menu': EdgelessNoteDisplayModeDropdownMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { almostEqual, Bound } from '@blocksuite/global/gfx';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { type EditorHost, ShadowlessElement } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { ACTIVE_NOTE_EXTRA_PADDING } from '../note-edgeless-block.css';
|
||||
|
||||
export class EdgelessNoteMask extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
protected override firstUpdated() {
|
||||
const maskDOM = this.renderRoot!.querySelector('.affine-note-mask');
|
||||
const observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (!this.model.props.edgeless.collapse) {
|
||||
const bound = Bound.deserialize(this.model.xywh);
|
||||
const scale = this.model.props.edgeless.scale ?? 1;
|
||||
const height = entry.contentRect.height * scale;
|
||||
|
||||
if (!height || almostEqual(bound.h, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bound.h = height;
|
||||
this.model.stash('xywh');
|
||||
this.model.xywh = bound.serialize();
|
||||
this.model.pop('xywh');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(maskDOM!);
|
||||
|
||||
this._disposables.add(() => {
|
||||
observer.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
const extra = this.editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
return html`
|
||||
<div
|
||||
class="affine-note-mask"
|
||||
style=${styleMap({
|
||||
position: 'absolute',
|
||||
top: `${-extra}px`,
|
||||
left: `${-extra}px`,
|
||||
bottom: `${-extra}px`,
|
||||
right: `${-extra}px`,
|
||||
zIndex: '1',
|
||||
pointerEvents: this.editing || this.disableMask ? 'none' : 'auto',
|
||||
borderRadius: `${
|
||||
this.model.props.edgeless.style.borderRadius * this.zoom
|
||||
}px`,
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor disableMask!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editing!: boolean;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor host!: EditorHost;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor model!: NoteBlockModel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor zoom!: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-mask': EdgelessNoteMask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { EditorChevronDown } from '@blocksuite/affine-components/toolbar';
|
||||
import { ColorScheme, NoteShadow } from '@blocksuite/affine-model';
|
||||
import { NoteShadowDuotoneIcon } from '@blocksuite/icons/lit';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { NoteNoShadowIcon, NoteShadowSampleIcon } from './icons';
|
||||
|
||||
const SHADOWS = [
|
||||
{
|
||||
type: NoteShadow.None,
|
||||
styles: {
|
||||
light: '',
|
||||
dark: '',
|
||||
},
|
||||
tooltip: 'No shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Box,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0.2px 4.8px 0px rgba(66, 65, 73, 0.2), 0px 0px 1.6px 0px rgba(66, 65, 73, 0.2)',
|
||||
dark: '0px 0.2px 6px 0px rgba(0, 0, 0, 0.44), 0px 0px 2px 0px rgba(0, 0, 0, 0.66)',
|
||||
},
|
||||
tooltip: 'Box shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Sticker,
|
||||
styles: {
|
||||
light:
|
||||
'0px 9.6px 10.4px -4px rgba(66, 65, 73, 0.07), 0px 10.4px 7.2px -8px rgba(66, 65, 73, 0.22)',
|
||||
dark: '0px 9.6px 10.4px -4px rgba(0, 0, 0, 0.66), 0px 10.4px 7.2px -8px rgba(0, 0, 0, 0.44)',
|
||||
},
|
||||
tooltip: 'Sticker shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Paper,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0px 0px 4px rgba(255, 255, 255, 1), 0px 1.2px 2.4px 4.8px rgba(66, 65, 73, 0.16)',
|
||||
dark: '0px 1.2px 2.4px 4.8px rgba(0, 0, 0, 0.36), 0px 0px 0px 3.4px rgba(75, 75, 75, 1)',
|
||||
},
|
||||
tooltip: 'Paper shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Float,
|
||||
styles: {
|
||||
light:
|
||||
'0px 5.2px 12px 0px rgba(66, 65, 73, 0.13), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.06)',
|
||||
dark: '0px 5.2px 12px 0px rgba(0, 0, 0, 0.66), 0px 0px 0.4px 1px rgba(0, 0, 0, 0.44)',
|
||||
},
|
||||
tooltip: 'Floation shadow',
|
||||
},
|
||||
{
|
||||
type: NoteShadow.Film,
|
||||
styles: {
|
||||
light:
|
||||
'0px 0px 0px 1.4px rgba(0, 0, 0, 1), 2.4px 2.4px 0px 1px rgba(0, 0, 0, 1)',
|
||||
dark: '0px 0px 0px 1.4px rgba(178, 178, 178, 1), 2.4px 2.4px 0px 1px rgba(178, 178, 178, 1)',
|
||||
},
|
||||
tooltip: 'Film shadow',
|
||||
},
|
||||
];
|
||||
|
||||
export class EdgelessNoteShadowDropdownMenu extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon svg rect:first-of-type {
|
||||
fill: var(--background);
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
|
||||
.item[data-selected] {
|
||||
border: 1px solid var(--affine-brand-color);
|
||||
}
|
||||
`;
|
||||
|
||||
select(value: NoteShadow) {
|
||||
this.dispatchEvent(new CustomEvent('select', { detail: value }));
|
||||
}
|
||||
|
||||
override render() {
|
||||
const { value, background, theme } = this;
|
||||
const isDark = theme === ColorScheme.Dark;
|
||||
|
||||
return html`
|
||||
<editor-menu-button
|
||||
.contentPadding="${'8px'}"
|
||||
.button=${html`
|
||||
<editor-icon-button
|
||||
aria-label="Shadow style"
|
||||
.tooltip="${'Shadow style'}"
|
||||
>
|
||||
${NoteShadowDuotoneIcon()} ${EditorChevronDown}
|
||||
</editor-icon-button>
|
||||
`}
|
||||
>
|
||||
<div
|
||||
data-orientation="horizontal"
|
||||
style=${styleMap({
|
||||
'--background': background.startsWith('--')
|
||||
? `var(${background})`
|
||||
: background,
|
||||
})}
|
||||
>
|
||||
${repeat(
|
||||
SHADOWS,
|
||||
shadow => shadow.type,
|
||||
({ type, tooltip, styles: { dark, light } }, index) =>
|
||||
html`<div
|
||||
class="item"
|
||||
?data-selected="${value === type}"
|
||||
@click=${() => this.select(type)}
|
||||
>
|
||||
<editor-icon-button
|
||||
class="item-icon"
|
||||
data-testid="${type.replace('--', '')}"
|
||||
.tooltip=${tooltip}
|
||||
.tipPosition="${'bottom'}"
|
||||
.iconContainerPadding=${0}
|
||||
.hover=${false}
|
||||
style=${styleMap({
|
||||
boxShadow: `${isDark ? dark : light}`,
|
||||
})}
|
||||
>
|
||||
${index === 0 ? NoteNoShadowIcon : NoteShadowSampleIcon}
|
||||
</editor-icon-button>
|
||||
</div>`
|
||||
)}
|
||||
</div>
|
||||
</editor-menu-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor background!: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor value!: NoteShadow;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-note-shadow-dropdown-menu': EdgelessNoteShadowDropdownMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const pageBlockTitle = style({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
globalStyle(`${pageBlockTitle} .doc-title-container`, {
|
||||
padding: '26px 0px',
|
||||
marginLeft: 'unset',
|
||||
marginRight: 'unset',
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
PropTypes,
|
||||
requiredProperties,
|
||||
ShadowlessElement,
|
||||
stdContext,
|
||||
} from '@blocksuite/std';
|
||||
import { consume } from '@lit/context';
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import { NoteConfigExtension } from '../config';
|
||||
import * as styles from './edgeless-page-block-title.css';
|
||||
|
||||
@requiredProperties({
|
||||
note: PropTypes.instanceOf(NoteBlockModel),
|
||||
})
|
||||
export class EdgelessPageBlockTitle extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
override render() {
|
||||
if (!this.note.isPageBlock()) return;
|
||||
|
||||
const title = this.std
|
||||
.getOptional(NoteConfigExtension.identifier)
|
||||
?.pageBlockTitle({
|
||||
note: this.note,
|
||||
std: this.std,
|
||||
});
|
||||
|
||||
return html`<div class=${styles.pageBlockTitle}>${title}</div>`;
|
||||
}
|
||||
|
||||
@consume({ context: stdContext })
|
||||
accessor std!: BlockStdScope;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor note!: NoteBlockModel;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'edgeless-page-block-title': EdgelessPageBlockTitle;
|
||||
}
|
||||
}
|
||||
84
blocksuite/affine/blocks/note/src/components/icons.ts
Normal file
84
blocksuite/affine/blocks/note/src/components/icons.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const NoteNoShadowIcon = html`
|
||||
<svg
|
||||
width="60"
|
||||
height="72"
|
||||
viewBox="0 0 60 72"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="60" height="72" />
|
||||
<rect
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
width="58.0769"
|
||||
height="71"
|
||||
stroke="black"
|
||||
stroke-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M21.9576 26.8962L38.6423 43.5809C42.5269 38.9268 42.2845 31.993 37.9149 27.6235C33.5454 23.254 26.6117 23.0115 21.9576 26.8962ZM37.1193 45.1038L20.4346 28.4192C16.55 33.0732 16.7924 40.007 21.162 44.3765C25.5315 48.746 32.4652 48.9885 37.1193 45.1038ZM19.639 26.1005C25.1063 20.6332 33.9706 20.6332 39.4379 26.1005C44.9053 31.5678 44.9053 40.4322 39.4379 45.8995C33.9706 51.3668 25.1063 51.3668 19.639 45.8995C14.1716 40.4322 14.1716 31.5678 19.639 26.1005Z"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
export const NoteShadowSampleIcon = html`
|
||||
<svg
|
||||
width="60"
|
||||
height="72"
|
||||
viewBox="0 0 60 72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="60" height="72" />
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="12.0771"
|
||||
width="32.3077"
|
||||
height="4.61538"
|
||||
rx="2"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="25.8462"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="35.6152"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="45.3843"
|
||||
width="40.6154"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<rect
|
||||
x="9.23071"
|
||||
y="55.1533"
|
||||
width="13.8462"
|
||||
height="2.76923"
|
||||
rx="1.38462"
|
||||
fill="black"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { html } from 'lit';
|
||||
|
||||
export const MoreIndicator = html`<svg
|
||||
width="34"
|
||||
height="29"
|
||||
viewBox="0 0 34 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 14.4292L16.8345 18.9019C17.0345 18.9665 17.2498 18.9665 17.4498 18.9019L31.2843 14.4292"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
stroke-width="5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>`;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const viewInPageNotifyFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '12px',
|
||||
});
|
||||
|
||||
export const viewInPageNotifyFooterButton = style({
|
||||
padding: '0px 6px',
|
||||
borderRadius: '4px',
|
||||
color: cssVarV2('text/primary'),
|
||||
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
|
||||
':hover': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
});
|
||||
20
blocksuite/affine/blocks/note/src/config.ts
Normal file
20
blocksuite/affine/blocks/note/src/config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { type BlockStdScope, ConfigExtensionFactory } from '@blocksuite/std';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
type NoteBlockContext = {
|
||||
note: NoteBlockModel;
|
||||
std: BlockStdScope;
|
||||
};
|
||||
|
||||
export type NoteConfig = {
|
||||
edgelessNoteHeader: (context: NoteBlockContext) => TemplateResult;
|
||||
pageBlockTitle: (context: NoteBlockContext) => TemplateResult;
|
||||
/**
|
||||
* @returns if the viewport fit animation executed
|
||||
*/
|
||||
pageBlockViewportFitAnimation?: (context: NoteBlockContext) => boolean;
|
||||
};
|
||||
|
||||
export const NoteConfigExtension =
|
||||
ConfigExtensionFactory<NoteConfig>('affine:note');
|
||||
121
blocksuite/affine/blocks/note/src/configs/slash-menu.ts
Normal file
121
blocksuite/affine/blocks/note/src/configs/slash-menu.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
formatBlockCommand,
|
||||
type TextFormatConfig,
|
||||
textFormatConfigs,
|
||||
} from '@blocksuite/affine-inline-preset';
|
||||
import {
|
||||
type TextConversionConfig,
|
||||
textConversionConfigs,
|
||||
} from '@blocksuite/affine-rich-text';
|
||||
import { isInsideBlockByFlavour } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type SlashMenuActionItem,
|
||||
type SlashMenuConfig,
|
||||
SlashMenuConfigExtension,
|
||||
type SlashMenuItem,
|
||||
} from '@blocksuite/affine-widget-slash-menu';
|
||||
import { HeadingsIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
|
||||
import { updateBlockType } from '../commands';
|
||||
import { tooltips } from './tooltips';
|
||||
|
||||
let basicIndex = 0;
|
||||
const noteSlashMenuConfig: SlashMenuConfig = {
|
||||
items: [
|
||||
...textConversionConfigs
|
||||
.filter(i => i.type && ['h1', 'h2', 'h3', 'text'].includes(i.type))
|
||||
.map(config => createConversionItem(config, `0_Basic@${basicIndex++}`)),
|
||||
{
|
||||
name: 'Other Headings',
|
||||
icon: HeadingsIcon(),
|
||||
group: `0_Basic@${basicIndex++}`,
|
||||
subMenu: textConversionConfigs
|
||||
.filter(i => i.type && ['h4', 'h5', 'h6'].includes(i.type))
|
||||
.map(config => createConversionItem(config)),
|
||||
},
|
||||
...textConversionConfigs
|
||||
.filter(i => i.flavour === 'affine:code')
|
||||
.map(config => createConversionItem(config, `0_Basic@${basicIndex++}`)),
|
||||
|
||||
...textConversionConfigs
|
||||
.filter(i => i.type && ['divider', 'quote'].includes(i.type))
|
||||
.map(
|
||||
config =>
|
||||
({
|
||||
...createConversionItem(config, `0_Basic@${basicIndex++}`),
|
||||
when: ({ model }) =>
|
||||
model.doc.schema.flavourSchemaMap.has(config.flavour) &&
|
||||
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text'),
|
||||
}) satisfies SlashMenuActionItem
|
||||
),
|
||||
|
||||
...textConversionConfigs
|
||||
.filter(i => i.flavour === 'affine:list')
|
||||
.map((config, index) =>
|
||||
createConversionItem(config, `1_List@${index++}`)
|
||||
),
|
||||
|
||||
...textFormatConfigs
|
||||
.filter(i => !['Code', 'Link'].includes(i.name))
|
||||
.map((config, index) =>
|
||||
createTextFormatItem(config, `2_Style@${index++}`)
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
function createConversionItem(
|
||||
config: TextConversionConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
): SlashMenuActionItem {
|
||||
const { name, description, icon, flavour, type } = config;
|
||||
return {
|
||||
name,
|
||||
group,
|
||||
description,
|
||||
icon,
|
||||
tooltip: tooltips[name],
|
||||
when: ({ model }) => model.doc.schema.flavourSchemaMap.has(flavour),
|
||||
action: ({ std }) => {
|
||||
std.command.exec(updateBlockType, {
|
||||
flavour,
|
||||
props: { type },
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createTextFormatItem(
|
||||
config: TextFormatConfig,
|
||||
group?: SlashMenuItem['group']
|
||||
): SlashMenuActionItem {
|
||||
const { name, icon, id, action } = config;
|
||||
return {
|
||||
name,
|
||||
icon,
|
||||
group,
|
||||
tooltip: tooltips[name],
|
||||
action: ({ std, model }) => {
|
||||
const { host } = std;
|
||||
|
||||
if (model.text?.length !== 0) {
|
||||
std.command.exec(formatBlockCommand, {
|
||||
blockSelections: [
|
||||
std.selection.create(BlockSelection, {
|
||||
blockId: model.id,
|
||||
}),
|
||||
],
|
||||
styles: { [id]: true },
|
||||
});
|
||||
} else {
|
||||
// like format bar when the line is empty
|
||||
action(host);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const NoteSlashMenuConfigExtension = SlashMenuConfigExtension(
|
||||
'affine:note',
|
||||
noteSlashMenuConfig
|
||||
);
|
||||
593
blocksuite/affine/blocks/note/src/configs/toolbar.ts
Normal file
593
blocksuite/affine/blocks/note/src/configs/toolbar.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
EdgelessLegacySlotIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
packColor,
|
||||
type PickColorEvent,
|
||||
} from '@blocksuite/affine-components/color-picker';
|
||||
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||
import {
|
||||
DefaultTheme,
|
||||
NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
type NoteShadow,
|
||||
resolveColor,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
NotificationProvider,
|
||||
SidebarExtensionIdentifier,
|
||||
type ToolbarAction,
|
||||
type ToolbarContext,
|
||||
type ToolbarModuleConfig,
|
||||
ToolbarModuleExtension,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonResolvedValue } from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
AutoHeightIcon,
|
||||
CornerIcon,
|
||||
CustomizedHeightIcon,
|
||||
LinkedPageIcon,
|
||||
ScissorsIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import {
|
||||
BlockFlavourIdentifier,
|
||||
EditorLifeCycleExtension,
|
||||
} from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
|
||||
import { changeNoteDisplayMode } from '../commands';
|
||||
import * as styles from '../components/view-in-page-notify.css';
|
||||
import { NoteConfigExtension } from '../config';
|
||||
|
||||
const trackBaseProps = {
|
||||
category: 'note',
|
||||
};
|
||||
|
||||
const CORNER_LIST = [
|
||||
{ key: 'None', value: 0 },
|
||||
{ key: 'Small', value: 8 },
|
||||
{ key: 'Medium', value: 16 },
|
||||
{ key: 'Large', value: 24 },
|
||||
{ key: 'Huge', value: 32 },
|
||||
] as const;
|
||||
|
||||
const builtinSurfaceToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.show-in',
|
||||
when(ctx) {
|
||||
return (
|
||||
ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 &&
|
||||
ctx.features.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { displayMode } = firstModel.props;
|
||||
const onSelect = (e: CustomEvent<NoteDisplayMode>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const newMode = e.detail;
|
||||
setDisplayMode(ctx, firstModel, newMode);
|
||||
};
|
||||
|
||||
return html`
|
||||
<edgeless-note-display-mode-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.displayMode="${displayMode}"
|
||||
>
|
||||
</edgeless-note-display-mode-dropdown-menu>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.display-in-page',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length === 1 &&
|
||||
!elements[0].isPageBlock() &&
|
||||
!ctx.features.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
generate(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const shouldShowTooltip$ = computed(
|
||||
() =>
|
||||
firstModel.props.displayMode$.value ===
|
||||
NoteDisplayMode.DocAndEdgeless
|
||||
);
|
||||
const label$ = computed(() =>
|
||||
firstModel.props.displayMode$.value === NoteDisplayMode.EdgelessOnly
|
||||
? 'Display in Page'
|
||||
: 'Displayed in Page'
|
||||
);
|
||||
const onSelect = () => {
|
||||
const newMode =
|
||||
firstModel.props.displayMode === NoteDisplayMode.EdgelessOnly
|
||||
? NoteDisplayMode.DocAndEdgeless
|
||||
: NoteDisplayMode.EdgelessOnly;
|
||||
setDisplayMode(ctx, firstModel, newMode);
|
||||
};
|
||||
|
||||
return {
|
||||
content: html`<editor-icon-button
|
||||
aria-label="${label$.value}"
|
||||
.showTooltip="${shouldShowTooltip$.value}"
|
||||
.tooltip="${'This note is part of Page Mode. Click to remove it from the page.'}"
|
||||
data-testid="display-in-page"
|
||||
@click=${() => onSelect()}
|
||||
>
|
||||
${LinkedPageIcon()}
|
||||
<span class="label">${label$.value}</span>
|
||||
</editor-icon-button>`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.color-picker',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
elements[0].props.displayMode !== NoteDisplayMode.DocOnly
|
||||
);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const enableCustomColor = ctx.features.getFlag('enable_color_picker');
|
||||
const theme = ctx.theme.edgeless$.value;
|
||||
|
||||
const firstModel = models[0];
|
||||
const background =
|
||||
getMostCommonResolvedValue(
|
||||
models.map(model => model.props),
|
||||
'background',
|
||||
background => resolveColor(background, theme)
|
||||
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
|
||||
const onPick = (e: PickColorEvent) => {
|
||||
const field = 'background';
|
||||
|
||||
if (e.type === 'pick') {
|
||||
const color = e.detail.value;
|
||||
for (const model of models) {
|
||||
const props = packColor(field, color);
|
||||
ctx.std
|
||||
.get(EdgelessCRUDIdentifier)
|
||||
.updateElement(model.id, props);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
model[e.type === 'start' ? 'stash' : 'pop'](field);
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<edgeless-color-picker-button
|
||||
class="background"
|
||||
.label="${'Background'}"
|
||||
.pick=${onPick}
|
||||
.color=${background}
|
||||
.colorPanelClass="${'small'}"
|
||||
.theme=${theme}
|
||||
.palettes=${DefaultTheme.NoteBackgroundColorPalettes}
|
||||
.originalColor=${firstModel.props.background}
|
||||
.enableCustomColor=${enableCustomColor}
|
||||
>
|
||||
</edgeless-color-picker-button>
|
||||
`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'd.style',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
elements[0].props.displayMode !== NoteDisplayMode.DocOnly
|
||||
);
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 'a.shadow-style',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const theme = ctx.theme.edgeless$.value;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { shadowType } = firstModel.props.edgeless.style;
|
||||
const background =
|
||||
getMostCommonResolvedValue(
|
||||
models.map(model => model.props),
|
||||
'background',
|
||||
background => resolveColor(background, theme)
|
||||
) ?? resolveColor(DefaultTheme.noteBackgrounColor, theme);
|
||||
const onSelect = (e: CustomEvent<NoteShadow>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const shadowType = e.detail;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
shadowType,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<edgeless-note-shadow-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.value="${shadowType}"
|
||||
.background="${background}"
|
||||
.theme="${theme}"
|
||||
></edgeless-note-shadow-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
{
|
||||
id: 'b.border-style',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { borderSize, borderStyle } = firstModel.props.edgeless.style;
|
||||
const onSelect = (e: CustomEvent<LineDetailType>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const { type, value } = e.detail;
|
||||
|
||||
if (type === 'size') {
|
||||
const borderSize = value;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderSize,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const borderStyle = value;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderStyle,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`
|
||||
<edgeless-note-border-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.lineSize=${borderSize}
|
||||
.lineStyle=${borderStyle}
|
||||
></edgeless-note-border-dropdown-menu>
|
||||
`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
{
|
||||
id: 'c.corners',
|
||||
label: 'Corners',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const label = this.label;
|
||||
const firstModel = models[0];
|
||||
const borderRadius$ = computed(
|
||||
() => firstModel.props.edgeless$.value.style.borderRadius
|
||||
);
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const borderRadius = e.detail;
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
ctx.std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
|
||||
edgeless: {
|
||||
...edgeless,
|
||||
style: {
|
||||
...edgeless.style,
|
||||
borderRadius,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return html`${keyed(
|
||||
firstModel,
|
||||
html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
.label="${label}"
|
||||
.icon=${CornerIcon()}
|
||||
.sizes=${CORNER_LIST}
|
||||
.size$=${borderRadius$}
|
||||
></affine-size-dropdown-menu>`
|
||||
)}`;
|
||||
},
|
||||
} satisfies ToolbarAction,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'e.slicer',
|
||||
label: 'Slicer',
|
||||
icon: ScissorsIcon(),
|
||||
tooltip: html`<affine-tooltip-content-with-shortcut
|
||||
data-tip="${'Cutting mode'}"
|
||||
data-shortcut="${'-'}"
|
||||
></affine-tooltip-content-with-shortcut>`,
|
||||
active: false,
|
||||
when(ctx) {
|
||||
return (
|
||||
ctx.getSurfaceModelsByType(NoteBlockModel).length === 1 &&
|
||||
ctx.features.getFlag('enable_advanced_block_visibility')
|
||||
);
|
||||
},
|
||||
run(ctx) {
|
||||
ctx.std.get(EdgelessLegacySlotIdentifier).toggleNoteSlicer.next();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'f.auto-height',
|
||||
label: 'Size',
|
||||
when(ctx) {
|
||||
const elements = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
return (
|
||||
elements.length > 0 &&
|
||||
(!elements[0].isPageBlock() ||
|
||||
!ctx.std.getOptional(NoteConfigExtension.identifier)
|
||||
?.edgelessNoteHeader)
|
||||
);
|
||||
},
|
||||
generate(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const { collapse } = firstModel.props.edgeless$.value;
|
||||
const options: Pick<ToolbarAction, 'tooltip' | 'icon'> = collapse
|
||||
? {
|
||||
tooltip: 'Auto height',
|
||||
icon: AutoHeightIcon(),
|
||||
}
|
||||
: {
|
||||
tooltip: 'Customized height',
|
||||
icon: CustomizedHeightIcon(),
|
||||
};
|
||||
|
||||
return {
|
||||
...options,
|
||||
run(ctx) {
|
||||
ctx.store.captureSync();
|
||||
|
||||
for (const model of models) {
|
||||
const edgeless = model.props.edgeless;
|
||||
|
||||
if (edgeless.collapse) {
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.props.edgeless.collapse = false;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (edgeless.collapsedHeight) {
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
bounds.h = edgeless.collapsedHeight * (edgeless.scale ?? 1);
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.xywh = xywh;
|
||||
model.props.edgeless.collapse = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'g.scale',
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(NoteBlockModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
const firstModel = models[0];
|
||||
const scale$ = computed(() => {
|
||||
const scale = firstModel.props.edgeless$.value.scale ?? 1;
|
||||
return Math.round(100 * scale);
|
||||
});
|
||||
const onSelect = (e: CustomEvent<number>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const scale = e.detail / 100;
|
||||
|
||||
models.forEach(model => {
|
||||
const bounds = Bound.deserialize(model.xywh);
|
||||
const oldScale = model.props.edgeless.scale ?? 1;
|
||||
const ratio = scale / oldScale;
|
||||
bounds.w *= ratio;
|
||||
bounds.h *= ratio;
|
||||
const xywh = bounds.serialize();
|
||||
|
||||
ctx.store.updateBlock(model, () => {
|
||||
model.xywh = xywh;
|
||||
model.props.edgeless.scale = scale;
|
||||
});
|
||||
});
|
||||
|
||||
ctx.track('SelectedCardScale', {
|
||||
...trackBaseProps,
|
||||
control: 'select card scale',
|
||||
});
|
||||
};
|
||||
const onToggle = (e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const opened = e.detail;
|
||||
if (!opened) return;
|
||||
|
||||
ctx.track('OpenedCardScaleSelector', {
|
||||
...trackBaseProps,
|
||||
control: 'switch card scale',
|
||||
});
|
||||
};
|
||||
const format = (value: number) => `${value}%`;
|
||||
|
||||
return html`<affine-size-dropdown-menu
|
||||
@select=${onSelect}
|
||||
@toggle=${onToggle}
|
||||
.format=${format}
|
||||
.size$=${scale$}
|
||||
></affine-size-dropdown-menu>`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
when: ctx => ctx.getSurfaceModelsByType(NoteBlockModel).length > 0,
|
||||
} as const satisfies ToolbarModuleConfig;
|
||||
|
||||
function setDisplayMode(
|
||||
ctx: ToolbarContext,
|
||||
model: NoteBlockModel,
|
||||
newMode: NoteDisplayMode
|
||||
) {
|
||||
const displayMode = model.props.displayMode;
|
||||
|
||||
ctx.command.exec(changeNoteDisplayMode, {
|
||||
noteId: model.id,
|
||||
mode: newMode,
|
||||
stopCapture: true,
|
||||
});
|
||||
|
||||
// if change note to page only, should clear the selection
|
||||
if (newMode === NoteDisplayMode.DocOnly) {
|
||||
ctx.selection.clear();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const clear = () => {
|
||||
ctx.history.off('stack-item-added', addHandler);
|
||||
ctx.history.off('stack-item-popped', popHandler);
|
||||
disposable.unsubscribe();
|
||||
};
|
||||
const closeNotify = () => {
|
||||
abortController.abort();
|
||||
clear();
|
||||
};
|
||||
|
||||
const addHandler = ctx.history.on('stack-item-added', closeNotify);
|
||||
const popHandler = ctx.history.on('stack-item-popped', closeNotify);
|
||||
const disposable = ctx.std
|
||||
.get(EditorLifeCycleExtension)
|
||||
.slots.unmounted.subscribe(closeNotify);
|
||||
|
||||
const undo = () => {
|
||||
ctx.store.undo();
|
||||
closeNotify();
|
||||
};
|
||||
|
||||
const viewInToc = () => {
|
||||
const sidebar = ctx.std.getOptional(SidebarExtensionIdentifier);
|
||||
sidebar?.open('outline');
|
||||
closeNotify();
|
||||
};
|
||||
|
||||
const data =
|
||||
newMode === NoteDisplayMode.EdgelessOnly
|
||||
? {
|
||||
title: 'Note removed from Page Mode',
|
||||
message: 'Content removed from your page.',
|
||||
}
|
||||
: {
|
||||
title: 'Note displayed in Page Mode',
|
||||
message: 'Content added to your page.',
|
||||
};
|
||||
|
||||
const notification = ctx.std.getOptional(NotificationProvider);
|
||||
notification?.notify({
|
||||
title: data.title,
|
||||
message: `${data.message} Find it in the TOC for quick navigation.`,
|
||||
accent: 'success',
|
||||
duration: 5 * 1000,
|
||||
footer: html`<div class=${styles.viewInPageNotifyFooter}>
|
||||
<button
|
||||
class=${styles.viewInPageNotifyFooterButton}
|
||||
@click=${undo}
|
||||
data-testid="undo-display-in-page"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<button
|
||||
class=${styles.viewInPageNotifyFooterButton}
|
||||
@click=${viewInToc}
|
||||
data-testid="view-in-toc"
|
||||
>
|
||||
View in Toc
|
||||
</button>
|
||||
</div>`,
|
||||
abort: abortController.signal,
|
||||
onClose: () => {
|
||||
clear();
|
||||
},
|
||||
});
|
||||
|
||||
ctx.track('NoteDisplayModeChanged', {
|
||||
...trackBaseProps,
|
||||
control: 'display mode',
|
||||
other: `from ${displayMode} to ${newMode}`,
|
||||
});
|
||||
}
|
||||
|
||||
export const createBuiltinToolbarConfigExtension = (
|
||||
flavour: string
|
||||
): ExtensionType[] => {
|
||||
const name = flavour.split(':').pop();
|
||||
|
||||
return [
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier(`affine:surface:${name}`),
|
||||
config: builtinSurfaceToolbarConfig,
|
||||
}),
|
||||
];
|
||||
};
|
||||
330
blocksuite/affine/blocks/note/src/configs/tooltips.ts
Normal file
330
blocksuite/affine/blocks/note/src/configs/tooltips.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import type { SlashMenuTooltip } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { html } from 'lit';
|
||||
// prettier-ignore
|
||||
const TextTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_868" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_868)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading1Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_873" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_873)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="28" font-weight="bold" letter-spacing="-0.24px"><tspan x="8" y="34.1818">Heading 1</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="51.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="63.6364">complexity to our data. </tspan><tspan x="8" y="79.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="91.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="107.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="119.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="131.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="143.636">other users. </tspan><tspan x="8" y="159.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="171.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading2Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_880" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_880)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="26" font-weight="600" letter-spacing="-0.24px"><tspan x="8" y="33.4545">Heading 2</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="51.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="63.6364">complexity to our data. </tspan><tspan x="8" y="79.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="91.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="107.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="119.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="131.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="143.636">other users. </tspan><tspan x="8" y="159.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="171.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading3Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_887" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_887)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="24" font-weight="600" letter-spacing="-0.24px"><tspan x="8" y="30.7273">Heading 3</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="47.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="59.6364">complexity to our data. </tspan><tspan x="8" y="75.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="87.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="103.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="115.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="127.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="139.636">other users. </tspan><tspan x="8" y="155.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="167.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading4Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_894" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_894)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="22" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="29">Heading 4</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="45.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="57.6364">complexity to our data. </tspan><tspan x="8" y="73.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="85.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="101.636">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="113.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="125.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="137.636">other users. </tspan><tspan x="8" y="153.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="165.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading5Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_901" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_901)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="20" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="27.2727">Heading 5</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="55.6364">complexity to our data. </tspan><tspan x="8" y="71.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="83.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="99.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="111.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="123.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="135.636">other users. </tspan><tspan x="8" y="151.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="163.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const Heading6Tooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_908" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_908)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="18" font-weight="600" letter-spacing="0.24px"><tspan x="8" y="25.5455">Heading 6</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="41.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="53.6364">complexity to our data. </tspan><tspan x="8" y="69.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="81.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="97.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="109.636">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="121.636">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="133.636">other users. </tspan><tspan x="8" y="149.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="161.636">those changes to their version of the document.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const CodeBlockTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_915" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_915)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="47.5742" y="17.46"> </tspan><tspan x="126.723" y="17.46">: </tspan><tspan x="166.297" y="17.46"> { </tspan><tspan x="8" y="32.46"> </tspan><tspan x="100.34" y="32.46"> helloTo </tspan><tspan x="166.297" y="32.46"> "World" </tspan><tspan x="8" y="47.46"> </tspan><tspan x="54.1699" y="47.46"> body: </tspan><tspan x="126.723" y="47.46"> </tspan><tspan x="159.701" y="47.46"> { </tspan><tspan x="8" y="62.46"> </tspan><tspan x="87.1484" y="62.46">(</tspan><tspan x="219.062" y="62.46">) </tspan><tspan x="8" y="77.46">} </tspan><tspan x="8" y="92.46">}</tspan></text>
|
||||
<text fill="#0782A0" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="8" y="17.46">struct</tspan><tspan x="73.957" y="32.46"> var</tspan><tspan x="159.701" y="32.46">=</tspan><tspan x="34.3828" y="47.46">var</tspan><tspan x="100.34" y="47.46">some</tspan><tspan x="139.914" y="62.46">\(</tspan></text>
|
||||
<text fill="#842ED3" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="54.1699" y="17.46">ContentView</tspan></text>
|
||||
<text fill="#C62222" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="139.914" y="17.46">View</tspan><tspan x="34.3828" y="32.46">@State</tspan><tspan x="133.318" y="47.46">View</tspan></text>
|
||||
<text fill="#2159D3" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="60.7656" y="62.46">Text</tspan></text>
|
||||
<text fill="#D34F0B" xml:space="preserve" style="white-space: pre" font-family="var(--affine-font-code-family)" font-size="11" letter-spacing="0em"><tspan x="93.7441" y="62.46">"Hello </tspan><tspan x="153.105" y="62.46">helloTo)!"</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const QuoteTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_920" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_920)">
|
||||
<rect x="12" y="14" width="2" height="33" rx="1" fill="#C2C1C5"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="24" y="26.6364">In a decentralized system, we can have a </tspan><tspan x="24" y="40.6364">kaleidoscopic complexity to our data. …</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const DividerTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_928" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_928)">
|
||||
<text fill="black" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="16.6364">In a decentralized system, we can have a </tspan><tspan x="8" y="30.6364">kaleidoscopic complexity to our data. </tspan><tspan x="8" y="54.6364">Any user may have a different perspective </tspan><tspan x="8" y="68.6364">on what data they either have, choose to </tspan><tspan x="8" y="82.6364">share, or accept. </tspan><tspan x="8" y="106.636">For example, one user’s edits to a </tspan><tspan x="8" y="120.636">document might be on their laptop on an </tspan><tspan x="8" y="134.636">airplane; when the plane lands and the </tspan><tspan x="8" y="148.636">computer reconnects, those changes are </tspan><tspan x="8" y="162.636">distributed to other users. </tspan><tspan x="8" y="186.636">Other users might choose to accept all, </tspan><tspan x="8" y="200.636">some, or none of those changes to their </tspan><tspan x="8" y="214.636">version of the document.</tspan></text>
|
||||
<line x1="8.25" y1="40.75" x2="169.75" y2="40.75" stroke="#E3E2E4" stroke-width="0.5" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const BulletedListTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_934" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_934)">
|
||||
<circle cx="14" cy="26" r="1.5" fill="#1C81D9"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="22" y="29.6364">Here's an example of a bulleted list.</tspan></text>
|
||||
<circle cx="14" cy="42" r="1.5" fill="#1C81D9"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="22" y="45.6364">You can list your plans such as this</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
const NumberedListTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_947" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_947)">
|
||||
<g clip-path="url(#clip0_16460_947)">
|
||||
<text fill="#1C81D9" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="10" y="29.6364">1.</tspan></text>
|
||||
</g>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="24" y="29.6364">Here's an example of a numbered list.</tspan></text>
|
||||
<g clip-path="url(#clip1_16460_947)">
|
||||
<text fill="#1C81D9" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="10" y="45.6364">2.</tspan></text>
|
||||
</g>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="24" y="45.6364">You can list your plans such as this</tspan></text>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_16460_947">
|
||||
<rect width="16" height="16" fill="white" transform="translate(10 18)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_16460_947">
|
||||
<rect width="16" height="16" fill="white" transform="translate(10 34)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const BoldTextTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_971" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_971)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" font-weight="bold" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const ItalicTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_976" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_976)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" font-style="italic" letter-spacing="0px"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const StrikethroughTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_986" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_986)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px" text-decoration="line-through"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const UnderlineTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_16460_981" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_16460_981)">
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="8" y="43.6364">Any user may have a different perspective on what data they </tspan><tspan x="8" y="55.6364">either have, choose to share, or accept. </tspan><tspan x="8" y="71.6364">For example, one user’s edits to a document might be on </tspan><tspan x="8" y="83.6364">their laptop on an airplane; when the plane lands and the </tspan><tspan x="8" y="95.6364">computer reconnects, those changes are distributed to </tspan><tspan x="8" y="107.636">other users. </tspan><tspan x="8" y="123.636">Other users might choose to accept all, some, or none of </tspan><tspan x="8" y="135.636">those changes to their version of the document.</tspan></text>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px" text-decoration="underline"><tspan x="8" y="15.6364">In a decentralized system, we can have a kaleidoscopic </tspan><tspan x="8" y="27.6364">complexity to our data. </tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// prettier-ignore
|
||||
export const TodoTooltip = html`<svg width="170" height="68" viewBox="0 0 170 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
<mask id="mask0_5604_203551" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="170" height="68">
|
||||
<rect width="170" height="68" rx="2" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_5604_203551)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.6667 19C12.7462 19 12 19.7462 12 20.6667V27.3333C12 28.2538 12.7462 29 13.6667 29H20.3333C21.2538 29 22 28.2538 22 27.3333V20.6667C22 19.7462 21.2538 19 20.3333 19H13.6667ZM12.9091 20.6667C12.9091 20.2483 13.2483 19.9091 13.6667 19.9091H20.3333C20.7517 19.9091 21.0909 20.2483 21.0909 20.6667V27.3333C21.0909 27.7517 20.7517 28.0909 20.3333 28.0909H13.6667C13.2483 28.0909 12.9091 27.7517 12.9091 27.3333V20.6667Z" fill="#77757D"/>
|
||||
<text fill="#121212" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="27.6364">Here is an example of todo list.</tspan></text>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 40.6667C12 39.7462 12.7462 39 13.6667 39H20.3333C21.2538 39 22 39.7462 22 40.6667V47.3333C22 48.2538 21.2538 49 20.3333 49H13.6667C12.7462 49 12 48.2538 12 47.3333V40.6667ZM19.7457 42.5032C19.9232 42.3257 19.9232 42.0379 19.7457 41.8604C19.5681 41.6829 19.2803 41.6829 19.1028 41.8604L16.0909 44.8723L15.2002 43.9816C15.0227 43.8041 14.7349 43.8041 14.5574 43.9816C14.3799 44.1591 14.3799 44.4469 14.5574 44.6244L15.7695 45.8366C15.947 46.0141 16.2348 46.0141 16.4123 45.8366L19.7457 42.5032Z" fill="#1E96EB"/>
|
||||
<text fill="#8E8D91" xml:space="preserve" style="white-space: pre" font-family="Inter" font-size="10" letter-spacing="0px"><tspan x="28" y="47.6364">Make a list for building preview.</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
|
||||
export const tooltips: Record<string, SlashMenuTooltip> = {
|
||||
Text: {
|
||||
figure: TextTooltip,
|
||||
caption: 'Text',
|
||||
},
|
||||
|
||||
'Heading 1': {
|
||||
figure: Heading1Tooltip,
|
||||
caption: 'Heading #1',
|
||||
},
|
||||
|
||||
'Heading 2': {
|
||||
figure: Heading2Tooltip,
|
||||
caption: 'Heading #2',
|
||||
},
|
||||
|
||||
'Heading 3': {
|
||||
figure: Heading3Tooltip,
|
||||
caption: 'Heading #3',
|
||||
},
|
||||
|
||||
'Heading 4': {
|
||||
figure: Heading4Tooltip,
|
||||
caption: 'Heading #4',
|
||||
},
|
||||
|
||||
'Heading 5': {
|
||||
figure: Heading5Tooltip,
|
||||
caption: 'Heading #5',
|
||||
},
|
||||
|
||||
'Heading 6': {
|
||||
figure: Heading6Tooltip,
|
||||
caption: 'Heading #6',
|
||||
},
|
||||
|
||||
'Code Block': {
|
||||
figure: CodeBlockTooltip,
|
||||
caption: 'Code Block',
|
||||
},
|
||||
|
||||
Quote: {
|
||||
figure: QuoteTooltip,
|
||||
caption: 'Quote',
|
||||
},
|
||||
|
||||
Divider: {
|
||||
figure: DividerTooltip,
|
||||
caption: 'Divider',
|
||||
},
|
||||
|
||||
'Bulleted List': {
|
||||
figure: BulletedListTooltip,
|
||||
caption: 'Bulleted List',
|
||||
},
|
||||
|
||||
'Numbered List': {
|
||||
figure: NumberedListTooltip,
|
||||
caption: 'Numbered List',
|
||||
},
|
||||
|
||||
Bold: {
|
||||
figure: BoldTextTooltip,
|
||||
caption: 'Bold Text',
|
||||
},
|
||||
|
||||
Italic: {
|
||||
figure: ItalicTooltip,
|
||||
caption: 'Italic',
|
||||
},
|
||||
|
||||
Underline: {
|
||||
figure: UnderlineTooltip,
|
||||
caption: 'Underline',
|
||||
},
|
||||
|
||||
Strikethrough: {
|
||||
figure: StrikethroughTooltip,
|
||||
caption: 'Strikethrough',
|
||||
},
|
||||
|
||||
'To-do List': {
|
||||
figure: TodoTooltip,
|
||||
caption: 'To-do List',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { EdgelessClipboardConfig } from '@blocksuite/affine-block-surface';
|
||||
import { type BlockSnapshot } from '@blocksuite/store';
|
||||
|
||||
export class EdgelessClipboardNoteConfig extends EdgelessClipboardConfig {
|
||||
static override readonly key = 'affine:note';
|
||||
|
||||
override async createBlock(note: BlockSnapshot): Promise<null | string> {
|
||||
const oldId = note.id;
|
||||
|
||||
delete note.props.index;
|
||||
if (!note.props.xywh) {
|
||||
console.error(`Note block(id: ${oldId}) does not have xywh property`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const newId = await this.onBlockSnapshotPaste(
|
||||
note,
|
||||
this.std.store,
|
||||
this.std.store.root!.id
|
||||
);
|
||||
if (!newId) {
|
||||
console.error(`Failed to paste note block(id: ${oldId})`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
}
|
||||
31
blocksuite/affine/blocks/note/src/effects.ts
Normal file
31
blocksuite/affine/blocks/note/src/effects.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { EdgelessNoteBackground } from './components/edgeless-note-background';
|
||||
import { EdgelessNoteBorderDropdownMenu } from './components/edgeless-note-border-dropdown-menu';
|
||||
import { EdgelessNoteDisplayModeDropdownMenu } from './components/edgeless-note-display-mode-dropdown-menu';
|
||||
import { EdgelessNoteMask } from './components/edgeless-note-mask';
|
||||
import { EdgelessNoteShadowDropdownMenu } from './components/edgeless-note-shadow-dropdown-menu';
|
||||
import { EdgelessPageBlockTitle } from './components/edgeless-page-block-title';
|
||||
import { NoteBlockComponent } from './note-block';
|
||||
import {
|
||||
AFFINE_EDGELESS_NOTE,
|
||||
EdgelessNoteBlockComponent,
|
||||
} from './note-edgeless-block';
|
||||
|
||||
export function effects() {
|
||||
customElements.define('affine-note', NoteBlockComponent);
|
||||
customElements.define(AFFINE_EDGELESS_NOTE, EdgelessNoteBlockComponent);
|
||||
customElements.define('edgeless-note-mask', EdgelessNoteMask);
|
||||
customElements.define('edgeless-note-background', EdgelessNoteBackground);
|
||||
customElements.define('edgeless-page-block-title', EdgelessPageBlockTitle);
|
||||
customElements.define(
|
||||
'edgeless-note-shadow-dropdown-menu',
|
||||
EdgelessNoteShadowDropdownMenu
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-note-border-dropdown-menu',
|
||||
EdgelessNoteBorderDropdownMenu
|
||||
);
|
||||
customElements.define(
|
||||
'edgeless-note-display-mode-dropdown-menu',
|
||||
EdgelessNoteDisplayModeDropdownMenu
|
||||
);
|
||||
}
|
||||
8
blocksuite/affine/blocks/note/src/index.ts
Normal file
8
blocksuite/affine/blocks/note/src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export * from './adapters';
|
||||
export * from './commands';
|
||||
export * from './components/edgeless-note-background';
|
||||
export * from './config';
|
||||
export * from './edgeless-clipboard-config';
|
||||
export * from './note-block';
|
||||
export * from './note-edgeless-block';
|
||||
export * from './note-spec';
|
||||
129
blocksuite/affine/blocks/note/src/move-block.ts
Normal file
129
blocksuite/affine/blocks/note/src/move-block.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
BlockSelection,
|
||||
type BlockStdScope,
|
||||
TextSelection,
|
||||
} from '@blocksuite/std';
|
||||
|
||||
const getSelection = (std: BlockStdScope) => std.selection;
|
||||
|
||||
function getBlockSelectionBySide(std: BlockStdScope, tail: boolean) {
|
||||
const selection = getSelection(std);
|
||||
const selections = selection.filter(BlockSelection);
|
||||
const sel = selections.at(tail ? -1 : 0) as BlockSelection | undefined;
|
||||
return sel ?? null;
|
||||
}
|
||||
|
||||
function getTextSelection(std: BlockStdScope) {
|
||||
const selection = getSelection(std);
|
||||
return selection.find(TextSelection);
|
||||
}
|
||||
|
||||
const pathToBlock = (std: BlockStdScope, blockId: string) =>
|
||||
std.view.getBlock(blockId);
|
||||
|
||||
interface MoveBlockConfig {
|
||||
name: string;
|
||||
hotkey: string[];
|
||||
action: (std: BlockStdScope) => void;
|
||||
}
|
||||
|
||||
export const moveBlockConfigs: MoveBlockConfig[] = [
|
||||
{
|
||||
name: 'Move Up',
|
||||
hotkey: ['Mod-Alt-ArrowUp', 'Mod-Shift-ArrowUp'],
|
||||
action: std => {
|
||||
const doc = std.store;
|
||||
const textSelection = getTextSelection(std);
|
||||
if (textSelection) {
|
||||
const currentModel = pathToBlock(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const previousSiblingModel = doc.getPrev(currentModel);
|
||||
if (!previousSiblingModel) return;
|
||||
|
||||
const parentModel = std.store.getParent(previousSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
std.store.moveBlocks(
|
||||
[currentModel],
|
||||
parentModel,
|
||||
previousSiblingModel,
|
||||
true
|
||||
);
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
std.range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
return true;
|
||||
}
|
||||
const blockSelection = getBlockSelectionBySide(std, true);
|
||||
if (blockSelection) {
|
||||
const currentModel = pathToBlock(std, blockSelection.blockId)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const previousSiblingModel = doc.getPrev(currentModel);
|
||||
if (!previousSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(previousSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks(
|
||||
[currentModel],
|
||||
parentModel,
|
||||
previousSiblingModel,
|
||||
false
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Move Down',
|
||||
hotkey: ['Mod-Alt-ArrowDown', 'Mod-Shift-ArrowDown'],
|
||||
action: std => {
|
||||
const doc = std.store;
|
||||
const textSelection = getTextSelection(std);
|
||||
if (textSelection) {
|
||||
const currentModel = pathToBlock(
|
||||
std,
|
||||
textSelection.from.blockId
|
||||
)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const nextSiblingModel = doc.getNext(currentModel);
|
||||
if (!nextSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(nextSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks([currentModel], parentModel, nextSiblingModel, false);
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
std.range.syncTextSelectionToRange(textSelection);
|
||||
})
|
||||
.catch(console.error);
|
||||
return true;
|
||||
}
|
||||
const blockSelection = getBlockSelectionBySide(std, true);
|
||||
if (blockSelection) {
|
||||
const currentModel = pathToBlock(std, blockSelection.blockId)?.model;
|
||||
if (!currentModel) return;
|
||||
|
||||
const nextSiblingModel = doc.getNext(currentModel);
|
||||
if (!nextSiblingModel) return;
|
||||
|
||||
const parentModel = doc.getParent(nextSiblingModel);
|
||||
if (!parentModel) return;
|
||||
|
||||
doc.moveBlocks([currentModel], parentModel, nextSiblingModel, false);
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
];
|
||||
34
blocksuite/affine/blocks/note/src/note-block.ts
Normal file
34
blocksuite/affine/blocks/note/src/note-block.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { NoteBlockModel } from '@blocksuite/affine-model';
|
||||
import { BlockComponent } from '@blocksuite/std';
|
||||
import { css, html } from 'lit';
|
||||
|
||||
export class NoteBlockComponent extends BlockComponent<NoteBlockModel> {
|
||||
static override styles = css`
|
||||
.affine-note-block-container {
|
||||
display: flow-root;
|
||||
}
|
||||
.affine-note-block-container.selected {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
return html`
|
||||
<div class="affine-note-block-container">
|
||||
<div class="affine-block-children-container">
|
||||
${this.renderChildren(this.model)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-note': NoteBlockComponent;
|
||||
}
|
||||
}
|
||||
74
blocksuite/affine/blocks/note/src/note-edgeless-block.css.ts
Normal file
74
blocksuite/affine/blocks/note/src/note-edgeless-block.css.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const ACTIVE_NOTE_EXTRA_PADDING = 20;
|
||||
|
||||
export const edgelessNoteContainer = style({
|
||||
height: '100%',
|
||||
padding: `${EDGELESS_BLOCK_CHILD_PADDING}px`,
|
||||
boxSizing: 'border-box',
|
||||
pointerEvents: 'all',
|
||||
transformOrigin: '0 0',
|
||||
fontWeight: '400',
|
||||
lineHeight: cssVar('lineHeight'),
|
||||
});
|
||||
|
||||
export const collapseButton = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
zIndex: 2,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
opacity: 0.2,
|
||||
transition: 'opacity 0.3s',
|
||||
|
||||
':hover': {
|
||||
opacity: 1,
|
||||
},
|
||||
selectors: {
|
||||
'&.flip': {
|
||||
transform: 'translateX(-50%) rotate(180deg)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const noteBackground = style({
|
||||
position: 'absolute',
|
||||
borderColor: cssVar('black10'),
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
selectors: {
|
||||
[`${edgelessNoteContainer}[data-editing="true"] &`]: {
|
||||
left: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
|
||||
top: `${-ACTIVE_NOTE_EXTRA_PADDING}px`,
|
||||
width: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
|
||||
height: `calc(100% + ${ACTIVE_NOTE_EXTRA_PADDING * 2}px)`,
|
||||
transition: 'left 0.3s, top 0.3s, width 0.3s, height 0.3s',
|
||||
boxShadow: cssVar('activeShadow'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const clipContainer = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const collapsedContent = style({
|
||||
position: 'absolute',
|
||||
background: cssVar('white'),
|
||||
opacity: 0.5,
|
||||
pointerEvents: 'none',
|
||||
border: `2px ${cssVar('blue')} solid`,
|
||||
borderTop: 'unset',
|
||||
borderRadius: '0 0 8px 8px',
|
||||
});
|
||||
401
blocksuite/affine/blocks/note/src/note-edgeless-block.ts
Normal file
401
blocksuite/affine/blocks/note/src/note-edgeless-block.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import type { DocTitle } from '@blocksuite/affine-fragment-doc-title';
|
||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
handleNativeRangeAtPoint,
|
||||
stopPropagation,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||
import type { SelectedContext } from '@blocksuite/std/gfx';
|
||||
import { html, nothing, type PropertyValues } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
|
||||
import { MoreIndicator } from './components/more-indicator';
|
||||
import { NoteConfigExtension } from './config';
|
||||
import { NoteBlockComponent } from './note-block';
|
||||
import { ACTIVE_NOTE_EXTRA_PADDING } from './note-edgeless-block.css';
|
||||
import * as styles from './note-edgeless-block.css';
|
||||
|
||||
export const AFFINE_EDGELESS_NOTE = 'affine-edgeless-note';
|
||||
|
||||
export class EdgelessNoteBlockComponent extends toGfxBlockComponent(
|
||||
NoteBlockComponent
|
||||
) {
|
||||
private get _isShowCollapsedContent() {
|
||||
return (
|
||||
this.model.props.edgeless.collapse &&
|
||||
this.gfx.selection.has(this.model.id) &&
|
||||
!this._dragging &&
|
||||
(this._isResizing || this._isHover)
|
||||
);
|
||||
}
|
||||
|
||||
private get _dragging() {
|
||||
return this._isHover && this.gfx.tool.dragging$.value;
|
||||
}
|
||||
|
||||
private _collapsedContent() {
|
||||
if (!this._isShowCollapsedContent) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { xywh, edgeless } = this.model.props;
|
||||
const { borderSize } = edgeless.style;
|
||||
|
||||
const extraPadding = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
const extraBorder = this._editing ? borderSize : 0;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const scale = edgeless.scale ?? 1;
|
||||
const width = bound.w / scale + extraPadding * 2 + extraBorder;
|
||||
const height = bound.h / scale;
|
||||
|
||||
const rect = this._noteContent?.getBoundingClientRect();
|
||||
if (!rect) return nothing;
|
||||
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
this._noteFullHeight =
|
||||
rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING;
|
||||
|
||||
if (height >= this._noteFullHeight) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${styles.collapsedContent}
|
||||
style=${styleMap({
|
||||
width: `${width}px`,
|
||||
height: `${this._noteFullHeight - height}px`,
|
||||
left: `${-(extraPadding + extraBorder / 2)}px`,
|
||||
top: `${height + extraPadding + extraBorder / 2}px`,
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'ArrowUp') {
|
||||
this._docTitle?.inlineEditor?.focusEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private _hovered() {
|
||||
if (
|
||||
this.selection.value.some(
|
||||
sel => sel.type === 'surface' && sel.blockId === this.model.id
|
||||
)
|
||||
) {
|
||||
this._isHover = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _leaved() {
|
||||
if (this._isHover) {
|
||||
this._isHover = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _setCollapse(event: MouseEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const { collapse, collapsedHeight } = this.model.props.edgeless;
|
||||
|
||||
if (collapse) {
|
||||
this.model.doc.updateBlock(this.model, () => {
|
||||
this.model.props.edgeless.collapse = false;
|
||||
});
|
||||
} else if (collapsedHeight) {
|
||||
const { xywh, edgeless } = this.model.props;
|
||||
const bound = Bound.deserialize(xywh);
|
||||
bound.h = collapsedHeight * (edgeless.scale ?? 1);
|
||||
this.model.doc.updateBlock(this.model, () => {
|
||||
this.model.props.edgeless.collapse = true;
|
||||
this.model.props.xywh = bound.serialize();
|
||||
});
|
||||
}
|
||||
|
||||
this.selection.clear();
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
const selection = this.gfx.selection;
|
||||
|
||||
this._editing = selection.has(this.model.id) && selection.editing;
|
||||
this._disposables.add(
|
||||
selection.slots.updated.subscribe(() => {
|
||||
if (selection.has(this.model.id) && selection.editing) {
|
||||
this._editing = true;
|
||||
} else {
|
||||
this._editing = false;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.disposables.addFromEvent(this, 'keydown', this._handleKeyDown);
|
||||
}
|
||||
|
||||
get edgelessSlots() {
|
||||
return this.std.get(EdgelessLegacySlotIdentifier);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
const { _disposables } = this;
|
||||
const selection = this.gfx.selection;
|
||||
|
||||
_disposables.add(
|
||||
this.edgelessSlots.elementResizeStart.subscribe(() => {
|
||||
if (selection.selectedElements.includes(this.model)) {
|
||||
this._isResizing = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
this.edgelessSlots.elementResizeEnd.subscribe(() => {
|
||||
this._isResizing = false;
|
||||
})
|
||||
);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const rect = this._noteContent?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const zoom = this.gfx.viewport.zoom;
|
||||
const scale = this.model.props.edgeless.scale ?? 1;
|
||||
this._noteFullHeight =
|
||||
rect.height / scale / zoom + 2 * EDGELESS_BLOCK_CHILD_PADDING;
|
||||
});
|
||||
if (this._noteContent) {
|
||||
observer.observe(this, { childList: true, subtree: true });
|
||||
_disposables.add(() => observer.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
override updated(changedProperties: PropertyValues) {
|
||||
if (changedProperties.has('_editing') && this._editing) {
|
||||
this.std.getOptional(TelemetryProvider)?.track('EdgelessNoteEditing', {
|
||||
page: 'edgeless',
|
||||
segment: this.model.isPageBlock() ? 'page' : 'note',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override getRenderingRect() {
|
||||
const { xywh, edgeless } = this.model.props;
|
||||
const { collapse, scale = 1 } = edgeless;
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const width = bound.w / scale;
|
||||
const height = bound.h / scale;
|
||||
|
||||
return {
|
||||
x: bound.x,
|
||||
y: bound.y,
|
||||
w: width,
|
||||
h: collapse ? height : 'unset',
|
||||
zIndex: this.toZIndex(),
|
||||
};
|
||||
}
|
||||
|
||||
override renderGfxBlock() {
|
||||
const { model } = this;
|
||||
const { displayMode } = model.props;
|
||||
if (!!displayMode && displayMode === NoteDisplayMode.DocOnly)
|
||||
return nothing;
|
||||
|
||||
const { xywh, edgeless } = model.props;
|
||||
const { borderRadius } = edgeless.style;
|
||||
const { collapse = false, collapsedHeight, scale = 1 } = edgeless;
|
||||
|
||||
const { tool } = this.gfx;
|
||||
|
||||
const bound = Bound.deserialize(xywh);
|
||||
const height = bound.h / scale;
|
||||
|
||||
const style = {
|
||||
borderRadius: borderRadius + 'px',
|
||||
transform: `scale(${scale})`,
|
||||
};
|
||||
|
||||
const extra = this._editing ? ACTIVE_NOTE_EXTRA_PADDING : 0;
|
||||
|
||||
const isCollapsable =
|
||||
collapse != null &&
|
||||
collapsedHeight != null &&
|
||||
collapsedHeight !== this._noteFullHeight;
|
||||
|
||||
const isCollapseArrowUp = collapse
|
||||
? this._noteFullHeight < height
|
||||
: !!collapsedHeight && collapsedHeight < height;
|
||||
|
||||
const hasHeader = !!this.std.getOptional(NoteConfigExtension.identifier)
|
||||
?.edgelessNoteHeader;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${styles.edgelessNoteContainer}
|
||||
style=${styleMap(style)}
|
||||
data-model-height="${bound.h}"
|
||||
data-editing=${this._editing}
|
||||
data-collapse=${ifDefined(collapse)}
|
||||
data-testid="edgeless-note-container"
|
||||
@mouseleave=${this._leaved}
|
||||
@mousemove=${this._hovered}
|
||||
data-scale="${scale}"
|
||||
>
|
||||
<edgeless-note-background
|
||||
.editing=${this._editing}
|
||||
.note=${this.model}
|
||||
></edgeless-note-background>
|
||||
|
||||
<div
|
||||
data-testid="edgeless-note-clip-container"
|
||||
class=${styles.clipContainer}
|
||||
style=${styleMap({
|
||||
'overflow-y': this._isShowCollapsedContent ? 'initial' : 'clip',
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<edgeless-page-block-title
|
||||
.note=${this.model}
|
||||
></edgeless-page-block-title>
|
||||
<div
|
||||
contenteditable=${String(!this.doc.readonly$.value)}
|
||||
class="edgeless-note-page-content"
|
||||
>
|
||||
${this.renderPageContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edgeless-note-mask
|
||||
.model=${this.model}
|
||||
.host=${this.host}
|
||||
.zoom=${this.gfx.viewport.zoom ?? 1}
|
||||
.disableMask=${this.hideMask}
|
||||
.editing=${this._editing}
|
||||
></edgeless-note-mask>
|
||||
|
||||
${isCollapsable &&
|
||||
tool.currentToolName$.value !== 'frameNavigator' &&
|
||||
(!this.model.isPageBlock() || !hasHeader)
|
||||
? html`<div
|
||||
class="${classMap({
|
||||
[styles.collapseButton]: true,
|
||||
flip: isCollapseArrowUp,
|
||||
})}"
|
||||
style=${styleMap({
|
||||
bottom: this._editing ? `${-extra}px` : '0',
|
||||
})}
|
||||
data-testid="edgeless-note-collapse-button"
|
||||
@mousedown=${stopPropagation}
|
||||
@mouseup=${stopPropagation}
|
||||
@click=${this._setCollapse}
|
||||
>
|
||||
${MoreIndicator}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._collapsedContent()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override onSelected(context: SelectedContext) {
|
||||
const { selected, multiSelect, event: e } = context;
|
||||
const { editing } = this.gfx.selection;
|
||||
const alreadySelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
if (!multiSelect && selected && (alreadySelected || editing)) {
|
||||
if (this.model.isLocked()) return;
|
||||
|
||||
if (alreadySelected && editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
editing: true,
|
||||
});
|
||||
|
||||
this.updateComplete
|
||||
.then(() => {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model.children.length === 0) {
|
||||
const blockId = this.doc.addBlock(
|
||||
'affine:paragraph',
|
||||
{ type: 'text' },
|
||||
this.model.id
|
||||
);
|
||||
|
||||
if (blockId) {
|
||||
focusTextModel(this.std, blockId);
|
||||
}
|
||||
} else {
|
||||
const rect = this.querySelector(
|
||||
'.affine-block-children-container'
|
||||
)?.getBoundingClientRect();
|
||||
|
||||
if (rect) {
|
||||
const offsetY = 8 * this.gfx.viewport.zoom;
|
||||
const offsetX = 2 * this.gfx.viewport.zoom;
|
||||
const x = clamp(
|
||||
e.clientX,
|
||||
rect.left + offsetX,
|
||||
rect.right - offsetX
|
||||
);
|
||||
const y = clamp(
|
||||
e.clientY,
|
||||
rect.top + offsetY,
|
||||
rect.bottom - offsetY
|
||||
);
|
||||
handleNativeRangeAtPoint(x, y);
|
||||
} else {
|
||||
handleNativeRangeAtPoint(e.clientX, e.clientY);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
super.onSelected(context);
|
||||
}
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _editing = false;
|
||||
|
||||
@state()
|
||||
private accessor _isHover = false;
|
||||
|
||||
@state()
|
||||
private accessor _isResizing = false;
|
||||
|
||||
@state()
|
||||
private accessor _noteFullHeight = 0;
|
||||
|
||||
@state()
|
||||
accessor hideMask = false;
|
||||
|
||||
@query(`.${styles.clipContainer} > div`)
|
||||
private accessor _noteContent: HTMLElement | null = null;
|
||||
|
||||
@query('doc-title')
|
||||
private accessor _docTitle: DocTitle | null = null;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_EDGELESS_NOTE]: EdgelessNoteBlockComponent;
|
||||
}
|
||||
}
|
||||
611
blocksuite/affine/blocks/note/src/note-keymap.ts
Normal file
611
blocksuite/affine/blocks/note/src/note-keymap.ts
Normal file
@@ -0,0 +1,611 @@
|
||||
import {
|
||||
CodeBlockModel,
|
||||
ListBlockModel,
|
||||
NoteBlockModel,
|
||||
NoteBlockSchema,
|
||||
ParagraphBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { textConversionConfigs } from '@blocksuite/affine-rich-text';
|
||||
import {
|
||||
focusBlockEnd,
|
||||
focusBlockStart,
|
||||
getBlockSelectionsCommand,
|
||||
getNextBlockCommand,
|
||||
getPrevBlockCommand,
|
||||
getTextSelectionCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import {
|
||||
asyncGetBlockComponent,
|
||||
matchModels,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
type BlockComponent,
|
||||
BlockSelection,
|
||||
type BlockStdScope,
|
||||
type Chain,
|
||||
KeymapExtension,
|
||||
TextSelection,
|
||||
type UIEventHandler,
|
||||
type UIEventStateContext,
|
||||
} from '@blocksuite/std';
|
||||
import type { BaseSelection } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
dedentBlocks,
|
||||
dedentBlocksToRoot,
|
||||
indentBlocks,
|
||||
selectBlock,
|
||||
selectBlocksBetween,
|
||||
updateBlockType,
|
||||
} from './commands';
|
||||
import { moveBlockConfigs } from './move-block';
|
||||
import { quickActionConfig } from './quick-action';
|
||||
|
||||
class NoteKeymap {
|
||||
constructor(readonly std: BlockStdScope) {}
|
||||
|
||||
private _anchorSel: BlockSelection | null = null;
|
||||
|
||||
private readonly _bindMoveBlockHotKey = () => {
|
||||
return moveBlockConfigs.reduce(
|
||||
(acc, config) => {
|
||||
const keys = config.hotkey.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
return config.action(this.std);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
return {
|
||||
...acc,
|
||||
...keys,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindQuickActionHotKey = () => {
|
||||
return quickActionConfig
|
||||
.filter(config => config.hotkey)
|
||||
.reduce(
|
||||
(acc, config) => {
|
||||
return {
|
||||
...acc,
|
||||
[config.hotkey!]: ctx => {
|
||||
if (!config.showWhen(this.std)) return;
|
||||
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
config.action(this.std);
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _bindTextConversionHotKey = () => {
|
||||
return textConversionConfigs
|
||||
.filter(item => item.hotkey)
|
||||
.reduce(
|
||||
(acc, item) => {
|
||||
const keymap = item.hotkey!.reduce(
|
||||
(acc, key) => {
|
||||
return {
|
||||
...acc,
|
||||
[key]: ctx => {
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(updateBlockType, {
|
||||
flavour: item.flavour,
|
||||
props: {
|
||||
type: item.type,
|
||||
},
|
||||
})
|
||||
.pipe((ctx, next) => {
|
||||
const newModels = ctx.updatedBlocks;
|
||||
if (!newModels) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.flavour !== 'affine:code') {
|
||||
return;
|
||||
}
|
||||
|
||||
const [codeModel] = newModels;
|
||||
asyncGetBlockComponent(ctx.std, codeModel.id)
|
||||
.then(codeElement => {
|
||||
if (!codeElement) {
|
||||
return;
|
||||
}
|
||||
this._std.selection.setGroup('note', [
|
||||
this._std.selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId: codeElement.blockId,
|
||||
index: 0,
|
||||
length: codeModel.text?.length ?? 0,
|
||||
},
|
||||
to: null,
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
|
||||
return {
|
||||
...acc,
|
||||
...keymap,
|
||||
};
|
||||
},
|
||||
{} as Record<string, UIEventHandler>
|
||||
);
|
||||
};
|
||||
|
||||
private _focusBlock: BlockComponent | null = null;
|
||||
|
||||
private readonly _getClosestNoteByBlockId = (blockId: string) => {
|
||||
const doc = this._std.store;
|
||||
let parent = doc.getBlock(blockId)?.model ?? null;
|
||||
while (parent) {
|
||||
if (matchModels(parent, [NoteBlockModel])) {
|
||||
return parent;
|
||||
}
|
||||
parent = doc.getParent(parent);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
private readonly _onArrowDown = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the next block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the next block (use block selection instead of text selection)
|
||||
cmd
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchModels(nextBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
}
|
||||
|
||||
return next({});
|
||||
}),
|
||||
|
||||
// block selection - select the next block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe<{ focusBlock: BlockComponent }>((ctx, next) => {
|
||||
const { nextBlock } = ctx;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (
|
||||
matchModels(nextBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(focusBlockStart, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: nextBlock,
|
||||
});
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onArrowUp = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe((_, next) => {
|
||||
this._reset();
|
||||
return next();
|
||||
})
|
||||
.try(cmd => [
|
||||
// text selection - select the previous block
|
||||
// 1. is paragraph, list, code block - follow the default behavior
|
||||
// 2. is not - select the previous block (use block selection instead of text selection)
|
||||
cmd
|
||||
.pipe(getTextSelectionCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentTextSelection = ctx.currentTextSelection;
|
||||
if (!currentTextSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: currentTextSelection.blockId });
|
||||
})
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!matchModels(prevBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
// block selection - select the previous block
|
||||
// 1. is paragraph, list, code block - focus it
|
||||
// 2. is not - select it using block selection
|
||||
cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string }>((ctx, next) => {
|
||||
const currentBlockSelections = ctx.currentBlockSelections;
|
||||
const blockSelection = currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
return next({ currentSelectionPath: blockSelection.blockId });
|
||||
})
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const { prevBlock } = ctx;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
matchModels(prevBlock.model, [
|
||||
ParagraphBlockModel,
|
||||
ListBlockModel,
|
||||
CodeBlockModel,
|
||||
])
|
||||
) {
|
||||
event.preventDefault();
|
||||
this._std.command.exec(focusBlockEnd, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
this._std.command.exec(selectBlock, {
|
||||
focusBlock: prevBlock,
|
||||
});
|
||||
return next();
|
||||
}),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftDown = (cmd: Chain) => {
|
||||
return cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string; anchorBlock: BlockComponent }>(
|
||||
(ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(-1) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
}
|
||||
)
|
||||
.pipe(getNextBlockCommand)
|
||||
.pipe<{ focusBlock: BlockComponent }>((ctx, next) => {
|
||||
const nextBlock = ctx.nextBlock;
|
||||
if (!nextBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = nextBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.pipe(selectBlocksBetween, { tail: true });
|
||||
};
|
||||
|
||||
private readonly _onBlockShiftUp = (cmd: Chain) => {
|
||||
return cmd
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe<{ currentSelectionPath: string; anchorBlock: BlockComponent }>(
|
||||
(ctx, next) => {
|
||||
const blockSelections = ctx.currentBlockSelections;
|
||||
if (!blockSelections) {
|
||||
return;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
this._anchorSel = blockSelections.at(0) ?? null;
|
||||
}
|
||||
if (!this._anchorSel) {
|
||||
return;
|
||||
}
|
||||
const anchorBlock = ctx.std.view.getBlock(this._anchorSel.blockId);
|
||||
if (!anchorBlock) {
|
||||
return;
|
||||
}
|
||||
return next({
|
||||
anchorBlock,
|
||||
currentSelectionPath:
|
||||
this._focusBlock?.blockId ?? anchorBlock?.blockId,
|
||||
});
|
||||
}
|
||||
)
|
||||
.pipe(getPrevBlockCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const prevBlock = ctx.prevBlock;
|
||||
if (!prevBlock) {
|
||||
return;
|
||||
}
|
||||
this._focusBlock = prevBlock;
|
||||
return next({
|
||||
focusBlock: this._focusBlock,
|
||||
});
|
||||
})
|
||||
.pipe(selectBlocksBetween, { tail: false });
|
||||
};
|
||||
|
||||
private readonly _onEnter = (ctx: UIEventStateContext) => {
|
||||
const event = ctx.get('defaultState').event;
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { view, store, selection } = ctx.std;
|
||||
|
||||
const element = view.getBlock(blockSelection.blockId);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { model } = element;
|
||||
const parent = store.getParent(model);
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parent.children.indexOf(model) ?? undefined;
|
||||
|
||||
const blockId = store.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
|
||||
const sel = selection.create(TextSelection, {
|
||||
from: {
|
||||
blockId,
|
||||
index: 0,
|
||||
length: 0,
|
||||
},
|
||||
to: null,
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
selection.setGroup('note', [sel]);
|
||||
this._reset();
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onEsc = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.pipe(getBlockSelectionsCommand)
|
||||
.pipe((ctx, next) => {
|
||||
const blockSelection = ctx.currentBlockSelections?.at(-1);
|
||||
if (!blockSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.std.selection.update(selList => {
|
||||
return selList.filter(sel => !sel.is(BlockSelection));
|
||||
});
|
||||
|
||||
return next();
|
||||
})
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onSelectAll: UIEventHandler = ctx => {
|
||||
const selection = this._std.selection;
|
||||
const block = selection.find(BlockSelection);
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
const note = this._getClosestNoteByBlockId(block.blockId);
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
ctx.get('defaultState').event.preventDefault();
|
||||
const children = note.children;
|
||||
const blocks: BlockSelection[] = children.map(child => {
|
||||
return selection.create(BlockSelection, {
|
||||
blockId: child.id,
|
||||
});
|
||||
});
|
||||
selection.update(selList => {
|
||||
return selList
|
||||
.filter<BaseSelection>(sel => !sel.is(BlockSelection))
|
||||
.concat(blocks);
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowDown = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftDown(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _onShiftArrowUp = () => {
|
||||
const [result] = this._std.command
|
||||
.chain()
|
||||
.try(cmd => [
|
||||
// block selection
|
||||
this._onBlockShiftUp(cmd),
|
||||
])
|
||||
.run();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
private readonly _reset = () => {
|
||||
this._anchorSel = null;
|
||||
this._focusBlock = null;
|
||||
};
|
||||
|
||||
private get _std() {
|
||||
return this.std;
|
||||
}
|
||||
|
||||
get hotKeys(): Record<string, UIEventHandler> {
|
||||
return {
|
||||
...this._bindMoveBlockHotKey(),
|
||||
...this._bindQuickActionHotKey(),
|
||||
...this._bindTextConversionHotKey(),
|
||||
Tab: ctx => {
|
||||
const [success] = this.std.command.exec(indentBlocks);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Shift-Tab': ctx => {
|
||||
const [success] = this.std.command.exec(dedentBlocks);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
'Mod-Backspace': ctx => {
|
||||
const [success] = this.std.command.exec(dedentBlocksToRoot);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
ctx.get('keyboardState').raw.preventDefault();
|
||||
return true;
|
||||
},
|
||||
ArrowDown: this._onArrowDown,
|
||||
ArrowUp: this._onArrowUp,
|
||||
'Shift-ArrowDown': this._onShiftArrowDown,
|
||||
'Shift-ArrowUp': this._onShiftArrowUp,
|
||||
Escape: this._onEsc,
|
||||
Enter: this._onEnter,
|
||||
'Mod-a': this._onSelectAll,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const NoteKeymapExtension = KeymapExtension(
|
||||
std => new NoteKeymap(std).hotKeys,
|
||||
{
|
||||
flavour: NoteBlockSchema.model.flavour,
|
||||
}
|
||||
);
|
||||
31
blocksuite/affine/blocks/note/src/note-spec.ts
Normal file
31
blocksuite/affine/blocks/note/src/note-spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NoteBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import {
|
||||
DocNoteBlockAdapterExtensions,
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
} from './adapters/index';
|
||||
import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
import { NoteKeymapExtension } from './note-keymap.js';
|
||||
|
||||
const flavour = NoteBlockSchema.model.flavour;
|
||||
|
||||
export const NoteBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension(flavour),
|
||||
BlockViewExtension(flavour, literal`affine-note`),
|
||||
DocNoteBlockAdapterExtensions,
|
||||
NoteSlashMenuConfigExtension,
|
||||
NoteKeymapExtension,
|
||||
].flat();
|
||||
|
||||
export const EdgelessNoteBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension(flavour),
|
||||
BlockViewExtension(flavour, literal`affine-edgeless-note`),
|
||||
EdgelessNoteBlockAdapterExtensions,
|
||||
NoteSlashMenuConfigExtension,
|
||||
createBuiltinToolbarConfigExtension(flavour),
|
||||
NoteKeymapExtension,
|
||||
].flat();
|
||||
66
blocksuite/affine/blocks/note/src/quick-action.ts
Normal file
66
blocksuite/affine/blocks/note/src/quick-action.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
convertSelectedBlocksToLinkedDoc,
|
||||
getTitleFromSelectedModels,
|
||||
notifyDocCreated,
|
||||
promptDocTitle,
|
||||
} from '@blocksuite/affine-block-embed';
|
||||
import {
|
||||
draftSelectedModelsCommand,
|
||||
getSelectedModelsCommand,
|
||||
} from '@blocksuite/affine-shared/commands';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { toDraftModel } from '@blocksuite/store';
|
||||
|
||||
export interface QuickActionConfig {
|
||||
id: string;
|
||||
hotkey?: string;
|
||||
showWhen: (std: BlockStdScope) => boolean;
|
||||
action: (std: BlockStdScope) => void;
|
||||
}
|
||||
|
||||
export const quickActionConfig: QuickActionConfig[] = [
|
||||
{
|
||||
id: 'convert-to-linked-doc',
|
||||
hotkey: `Mod-Shift-l`,
|
||||
showWhen: std => {
|
||||
const [_, ctx] = std.command.exec(getSelectedModelsCommand, {
|
||||
types: ['block'],
|
||||
});
|
||||
const { selectedModels } = ctx;
|
||||
return !!selectedModels && selectedModels.length > 0;
|
||||
},
|
||||
action: std => {
|
||||
const [_, ctx] = std.command
|
||||
.chain()
|
||||
.pipe(getSelectedModelsCommand, {
|
||||
types: ['block'],
|
||||
mode: 'flat',
|
||||
})
|
||||
.pipe(draftSelectedModelsCommand)
|
||||
.run();
|
||||
const { selectedModels, draftedModels } = ctx;
|
||||
if (!selectedModels) return;
|
||||
|
||||
if (!selectedModels.length || !draftedModels) return;
|
||||
|
||||
std.selection.clear();
|
||||
|
||||
const doc = std.store;
|
||||
const autofill = getTitleFromSelectedModels(
|
||||
selectedModels.map(toDraftModel)
|
||||
);
|
||||
promptDocTitle(std, autofill)
|
||||
.then(title => {
|
||||
if (title === null) return;
|
||||
convertSelectedBlocksToLinkedDoc(
|
||||
std,
|
||||
doc,
|
||||
draftedModels,
|
||||
title
|
||||
).catch(console.error);
|
||||
notifyDocCreated(std, doc);
|
||||
})
|
||||
.catch(console.error);
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user