Skip to content

Commit

Permalink
feat: clear formatting command
Browse files Browse the repository at this point in the history
  • Loading branch information
umaranis committed May 14, 2024
1 parent 9911304 commit 337615f
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 0 deletions.
2 changes: 2 additions & 0 deletions demos/playground/src/ToolbarPlayground.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
StrikethroughDropDownItem,
SubscriptDropDownItem,
SuperscriptDropDownItem,
ClearFormattingDropDownItem,
} from 'svelte-lexical';
import InsertImageDialog from './InsertImageDialog.svelte';
Expand Down Expand Up @@ -69,6 +70,7 @@
<StrikethroughDropDownItem />
<SubscriptDropDownItem />
<SuperscriptDropDownItem />
<ClearFormattingDropDownItem />
</MoreStylesDropDown>
<Divider />
<InsertDropDown>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script lang="ts">
import DropDownItem from '$lib/components/generic/dropdown/DropDownItem.svelte';
import {getActiveEditor} from '$lib/core/composerContext.js';
import {$isDecoratorBlockNode as isDecoratorBlockNode} from '$lib/core/plugins/DecoratorBlockNode.js';
import {
$isQuoteNode as isQuoteNode,
$isHeadingNode as isHeadingNode,
} from '@lexical/rich-text';
import {$isTableSelection as isTableSelection} from '@lexical/table';
import {$getNearestBlockElementAncestorOrThrow as getNearestBlockElementAncestorOrThrow} from '@lexical/utils';
import {
$isTextNode as isTextNode,
$getSelection as getSelection,
$isRangeSelection as isRangeSelection,
$createParagraphNode as createParagraphNode,
} from 'lexical';
const activeEditor = getActiveEditor();
function clearFormatting() {
$activeEditor.update(() => {
const selection = getSelection();
if (isRangeSelection(selection) || isTableSelection(selection)) {
const anchor = selection.anchor;
const focus = selection.focus;
const nodes = selection.getNodes();
if (anchor.key === focus.key && anchor.offset === focus.offset) {
return;
}
nodes.forEach((node, idx) => {
// We split the first and last node by the selection
// So that we don't format unselected text inside those nodes
if (isTextNode(node)) {
// Use a separate variable to ensure TS does not lose the refinement
let textNode = node;
if (idx === 0 && anchor.offset !== 0) {
textNode = textNode.splitText(anchor.offset)[1] || textNode;
}
if (idx === nodes.length - 1) {
textNode = textNode.splitText(focus.offset)[0] || textNode;
}
/**
* If the selected text has one format applied
* selecting a portion of the text, could
* clear the format to the wrong portion of the text.
*
* The cleared text is based on the length of the selected text.
*/
// We need this in case the selected text only has one format
const extractedTextNode = selection.extract()[0];
if (nodes.length === 1 && isTextNode(extractedTextNode)) {
textNode = extractedTextNode;
}
if (textNode.__style !== '') {
textNode.setStyle('');
}
if (textNode.__format !== 0) {
textNode.setFormat(0);
getNearestBlockElementAncestorOrThrow(textNode).setFormat('');
}
node = textNode;
} else if (isHeadingNode(node) || isQuoteNode(node)) {
node.replace(createParagraphNode(), true);
} else if (isDecoratorBlockNode(node)) {
node.setFormat('');
}
});
}
});
}
</script>

<DropDownItem
on:click={clearFormatting}
class="item"
title="Clear text formatting"
ariaLabel="Clear all text formatting">
<i class="icon clear" />
<span class="text">Clear Formatting</span>
</DropDownItem>
64 changes: 64 additions & 0 deletions packages/svelte-lexical/src/lib/core/plugins/DecoratorBlockNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import type {
ElementFormatType,
LexicalNode,
NodeKey,
SerializedLexicalNode,
Spread,
} from 'lexical';

import {DecoratorNode} from 'lexical';

export type SerializedDecoratorBlockNode = Spread<
{
format: ElementFormatType;
},
SerializedLexicalNode
>;

export class DecoratorBlockNode extends DecoratorNode<unknown> {
__format: ElementFormatType;

constructor(format?: ElementFormatType, key?: NodeKey) {
super(key);
this.__format = format || '';
}

exportJSON(): SerializedDecoratorBlockNode {
return {
format: this.__format || '',
type: 'decorator-block',
version: 1,
};
}

createDOM(): HTMLElement {
return document.createElement('div');
}

updateDOM(): false {
return false;
}

setFormat(format: ElementFormatType): void {
const self = this.getWritable();
self.__format = format;
}

isInline(): false {
return false;
}
}

export function $isDecoratorBlockNode(
node: LexicalNode | null | undefined,
): node is DecoratorBlockNode {
return node instanceof DecoratorBlockNode;
}
1 change: 1 addition & 0 deletions packages/svelte-lexical/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export {default as MoreStylesDropDown} from './components/toolbar/MoreStylesDrop
export {default as StrikethroughDropDownItem} from './components/toolbar/MoreStylesDropDown/StrikethroughDropDownItem.svelte';
export {default as SubscriptDropDownItem} from './components/toolbar/MoreStylesDropDown/SubscriptDropDownItem.svelte';
export {default as SuperscriptDropDownItem} from './components/toolbar/MoreStylesDropDown/SuperscriptDropDownItem.svelte';
export {default as ClearFormattingDropDownItem} from './components/toolbar/MoreStylesDropDown/ClearFormattingDropDownItem.svelte';
// dialogs
export {default as InsertImageDialog} from './components/toolbar/dialogs/InsertImageDialog.svelte';
export {default as InsertImageUploadedDialogBody} from './components/toolbar/dialogs/InsertImageUploadedDialogBody.svelte';
Expand Down

0 comments on commit 337615f

Please sign in to comment.