mirror of
https://gitlab.com/ArkHost/HelixNotes.git
synced 2026-09-19 17:37:29 +02:00
Add Clear Formatting action to editor context menu
This commit is contained in:
@@ -52,6 +52,7 @@
|
|||||||
import { calloutGroup, calloutIcon, calloutLabel, CALLOUT_MENU, transformCalloutBlockquotes, serializeCallout } from '$lib/editor/callouts';
|
import { calloutGroup, calloutIcon, calloutLabel, CALLOUT_MENU, transformCalloutBlockquotes, serializeCallout } from '$lib/editor/callouts';
|
||||||
import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs';
|
import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs';
|
||||||
import { convertListNode, type MixedListName } from '$lib/editor/mixedLists';
|
import { convertListNode, type MixedListName } from '$lib/editor/mixedLists';
|
||||||
|
import { clearFormatting } from '$lib/editor/clearFormatting';
|
||||||
import { serializeInlineMarkdown } from '$lib/editor/markdown';
|
import { serializeInlineMarkdown } from '$lib/editor/markdown';
|
||||||
import { relativePath } from '$lib/utils/paths';
|
import { relativePath } from '$lib/utils/paths';
|
||||||
import GraphView from './GraphView.svelte';
|
import GraphView from './GraphView.svelte';
|
||||||
@@ -5079,6 +5080,11 @@
|
|||||||
closeTextContextMenu();
|
closeTextContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ctxClearFormatting() {
|
||||||
|
if (editor) clearFormatting(editor);
|
||||||
|
closeTextContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
function ctxCode() {
|
function ctxCode() {
|
||||||
editor?.chain().focus().toggleCode().run();
|
editor?.chain().focus().toggleCode().run();
|
||||||
closeTextContextMenu();
|
closeTextContextMenu();
|
||||||
@@ -7201,6 +7207,10 @@
|
|||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
|
||||||
Highlight
|
Highlight
|
||||||
</button>
|
</button>
|
||||||
|
<button onclick={ctxClearFormatting}>
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>
|
||||||
|
Clear Formatting
|
||||||
|
</button>
|
||||||
<div class="text-ctx-sep"></div>
|
<div class="text-ctx-sep"></div>
|
||||||
<button onclick={ctxLink}>
|
<button onclick={ctxLink}>
|
||||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { Editor } from '@tiptap/core';
|
||||||
|
import type { Schema } from '@tiptap/pm/model';
|
||||||
|
import type { Transaction } from '@tiptap/pm/state';
|
||||||
|
import { liftTarget } from '@tiptap/pm/transform';
|
||||||
|
|
||||||
|
// Marks that carry content (URLs, note references) rather than visual
|
||||||
|
// formatting. Clearing formatting must never destroy these.
|
||||||
|
const CONTENT_MARKS: Record<string, true> = { link: true, wikiLink: true };
|
||||||
|
|
||||||
|
// Block types reset to plain paragraphs. Lists, callouts, collapsible
|
||||||
|
// sections and tables keep their structure so no state (checkboxes,
|
||||||
|
// nesting, cells) is lost.
|
||||||
|
const DEMOTE_BLOCKS: Record<string, true> = { heading: true, codeBlock: true };
|
||||||
|
|
||||||
|
// Outermost-first position of the first blockquote intersecting [from, to].
|
||||||
|
function findBlockquote(tr: Transaction, from: number, to: number): number | null {
|
||||||
|
let found: number | null = null;
|
||||||
|
tr.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (found !== null) return false;
|
||||||
|
if (node.type.name === 'blockquote') {
|
||||||
|
found = pos;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips inline formatting and demotes heading/code-block/blockquote
|
||||||
|
* structure over the transaction's selection. An empty (cursor) selection
|
||||||
|
* is expanded to the whole current text block, so a single invocation can
|
||||||
|
* reset one messy line without highlighting it first.
|
||||||
|
*/
|
||||||
|
export function applyClearFormatting(schema: Schema, tr: Transaction): void {
|
||||||
|
const { selection } = tr;
|
||||||
|
const { $from } = selection;
|
||||||
|
const expandToBlock = selection.empty && $from.parent.isTextblock;
|
||||||
|
const from = expandToBlock ? $from.start() : selection.from;
|
||||||
|
const to = expandToBlock ? $from.end() : selection.to;
|
||||||
|
|
||||||
|
// 1. Strip formatting marks, keep content-bearing ones.
|
||||||
|
for (const mark of Object.values(schema.marks)) {
|
||||||
|
if (!CONTENT_MARKS[mark.name]) tr.removeMark(from, to, mark);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Demote headings and code blocks to paragraphs. Mark removal and
|
||||||
|
// retyping never move content, so the original positions stay valid.
|
||||||
|
tr.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (DEMOTE_BLOCKS[node.type.name]) tr.setNodeMarkup(pos, schema.nodes.paragraph);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Lift blockquote contents out of the quote wrapper, one pass per
|
||||||
|
// nesting level. Lifts move positions, so remap and rescan each pass.
|
||||||
|
for (let guard = 0; guard < 20; guard++) {
|
||||||
|
const quotePos = findBlockquote(tr, tr.mapping.map(from, 1), tr.mapping.map(to, -1));
|
||||||
|
if (quotePos === null) break;
|
||||||
|
// The range must span the blockquote's content: a collapsed
|
||||||
|
// blockRange starts one depth up and wraps the quote itself.
|
||||||
|
const quote = tr.doc.nodeAt(quotePos);
|
||||||
|
if (!quote) break;
|
||||||
|
const range = tr.doc.resolve(quotePos + 1).blockRange(tr.doc.resolve(quotePos + quote.nodeSize - 1));
|
||||||
|
if (!range) break;
|
||||||
|
const target = liftTarget(range);
|
||||||
|
if (target === null) break;
|
||||||
|
tr.lift(range, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clears formatting for the editor's current selection (or current line). */
|
||||||
|
export function clearFormatting(editor: Editor): boolean {
|
||||||
|
return editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.command(({ state, tr }) => {
|
||||||
|
applyClearFormatting(state.schema, tr);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user