traer cambios de enlaces de sutty/editor
This commit is contained in:
parent
26714cb341
commit
91cbf2260b
7 changed files with 406 additions and 12 deletions
|
@ -42,6 +42,7 @@
|
||||||
"vite": "^4.2.2"
|
"vite": "^4.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.5.1",
|
||||||
"bootstrap-icons": "^1.10.4",
|
"bootstrap-icons": "^1.10.4",
|
||||||
"eva-icons": "^1.1.3",
|
"eva-icons": "^1.1.3",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
|
|
|
@ -5,6 +5,9 @@ settings:
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@floating-ui/dom':
|
||||||
|
specifier: ^1.5.1
|
||||||
|
version: 1.5.1
|
||||||
bootstrap-icons:
|
bootstrap-icons:
|
||||||
specifier: ^1.10.4
|
specifier: ^1.10.4
|
||||||
version: 1.10.4
|
version: 1.10.4
|
||||||
|
@ -330,6 +333,23 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@floating-ui/core@1.4.1:
|
||||||
|
resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@floating-ui/dom@1.5.1:
|
||||||
|
resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==}
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.4.1
|
||||||
|
'@floating-ui/utils': 0.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@floating-ui/utils@0.1.1:
|
||||||
|
resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.3:
|
/@jridgewell/gen-mapping@0.3.3:
|
||||||
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
|
@ -16,12 +16,17 @@
|
||||||
// import { placeholderPlugin } from "./upload";
|
// import { placeholderPlugin } from "./upload";
|
||||||
import { baseKeymap } from "./keymap";
|
import { baseKeymap } from "./keymap";
|
||||||
import type { WorldY } from "../lib/doc";
|
import type { WorldY } from "../lib/doc";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import EditLinkMenu from "./bubblemenu/EditLinkMenu.svelte";
|
||||||
|
import LinkTooltip from "./bubblemenu/LinkTooltip.svelte";
|
||||||
|
|
||||||
export let doc: XmlFragment;
|
export let doc: XmlFragment;
|
||||||
export let worldY: WorldY;
|
export let worldY: WorldY;
|
||||||
|
|
||||||
let wrapperEl: HTMLElement;
|
let wrapperEl: HTMLElement;
|
||||||
|
|
||||||
|
const editingLink = writable<false | "new" | "selection">(false);
|
||||||
|
|
||||||
function createState(doc: XmlFragment): EditorState {
|
function createState(doc: XmlFragment): EditorState {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
schema,
|
schema,
|
||||||
|
@ -69,9 +74,14 @@
|
||||||
<div class="editor min-h-screen">
|
<div class="editor min-h-screen">
|
||||||
{#if view}
|
{#if view}
|
||||||
<MenuBar {view} state={updatedState} />
|
<MenuBar {view} state={updatedState} />
|
||||||
|
<EditLinkMenu state={updatedState} {view} {editingLink} />
|
||||||
|
<LinkTooltip state={updatedState} {view} {editingLink} />
|
||||||
{/if}
|
{/if}
|
||||||
<!-- this element gets replaced with the editor itself when mounted -->
|
<!-- this element gets replaced with the editor itself when mounted -->
|
||||||
<div class="prose dark:prose-invert before:prose-p:content-none after:prose-p:content-none prose-blockquote:font-normal prose-blockquote:not-italic max-w-none min-h-screen" bind:this={wrapperEl} />
|
<div
|
||||||
|
class="prose min-h-screen max-w-none dark:prose-invert before:prose-p:content-none after:prose-p:content-none prose-blockquote:font-normal prose-blockquote:not-italic"
|
||||||
|
bind:this={wrapperEl}
|
||||||
|
/>
|
||||||
{#if view}
|
{#if view}
|
||||||
<BubbleMenu {view} {worldY} state={updatedState} />
|
<BubbleMenu {view} {worldY} state={updatedState} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
142
src/editor/bubblemenu/EditLinkMenu.svelte
Normal file
142
src/editor/bubblemenu/EditLinkMenu.svelte
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { EditorState } from "prosemirror-state";
|
||||||
|
import type { EditorView } from "prosemirror-view";
|
||||||
|
import { getFirstMarkInSelection } from "../ps-utils";
|
||||||
|
import { readable, type Writable } from "svelte/store";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { markSelectionFloatingUi } from "./floatingUi";
|
||||||
|
import { autoPlacement, shift, offset } from "@floating-ui/dom";
|
||||||
|
|
||||||
|
export let state: EditorState;
|
||||||
|
export let view: EditorView;
|
||||||
|
export let editingLink: Writable<false | "new" | "selection">;
|
||||||
|
|
||||||
|
let hrefInputEl: HTMLInputElement;
|
||||||
|
let formEl: HTMLFormElement;
|
||||||
|
|
||||||
|
// ref: https://gitlab.com/gitlab-org/gitlab-foss/-/blob/13d851c795a48b670b859a7ec5bd6e2886d2789e/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue#L69
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
let href = "";
|
||||||
|
|
||||||
|
function onChangeEditingLink(editingLink: false | "new" | "selection") {
|
||||||
|
if (!editingLink) return;
|
||||||
|
text = "";
|
||||||
|
href = "";
|
||||||
|
|
||||||
|
// el timeout... ugh
|
||||||
|
hrefInputEl?.focus();
|
||||||
|
setTimeout(() => hrefInputEl?.focus(), 50);
|
||||||
|
setTimeout(() => hrefInputEl?.focus(), 100);
|
||||||
|
|
||||||
|
if (editingLink === "selection") {
|
||||||
|
// https://gitlab.com/gitlab-org/gitlab-foss/-/blob/13d851c795a48b670b859a7ec5bd6e2886d2789e/app/assets/javascripts/content_editor/components/bubble_menus/link_bubble_menu.vue#L69
|
||||||
|
text = getSelectionText(view.state);
|
||||||
|
const match = getFirstMarkInSelection(
|
||||||
|
view.state,
|
||||||
|
view.state.schema.marks.link,
|
||||||
|
);
|
||||||
|
if (!match) return;
|
||||||
|
href = match.mark.attrs.href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: onChangeEditingLink($editingLink);
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const { from, to } = view.state.selection;
|
||||||
|
const mark = view.state.schema.marks.link.create({ href });
|
||||||
|
|
||||||
|
if ($editingLink === "new") {
|
||||||
|
view.dispatch(
|
||||||
|
view.state.tr
|
||||||
|
.ensureMarks([mark])
|
||||||
|
.insertText(text, from, from)
|
||||||
|
.removeStoredMark(mark),
|
||||||
|
);
|
||||||
|
} else if ($editingLink === "selection") {
|
||||||
|
let tr = view.state.tr;
|
||||||
|
tr.addMark(from, to, mark);
|
||||||
|
const currentText = getSelectionText(view.state);
|
||||||
|
if (currentText !== text)
|
||||||
|
tr.ensureMarks([mark])
|
||||||
|
.insertText(text, from, to)
|
||||||
|
.removeStoredMark(mark);
|
||||||
|
|
||||||
|
view.dispatch(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
$editingLink = false;
|
||||||
|
}
|
||||||
|
function cancel() {
|
||||||
|
$editingLink = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFocus(event: any) {
|
||||||
|
if (!formEl.contains(event.target)) cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectionText(state: EditorState) {
|
||||||
|
return state.doc.textBetween(state.selection.from, state.selection.to, " ");
|
||||||
|
}
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
|
$: shown = !!$editingLink;
|
||||||
|
|
||||||
|
$: linkMatch =
|
||||||
|
state && getFirstMarkInSelection(view.state, view.state.schema.marks.link);
|
||||||
|
$: style =
|
||||||
|
shown && linkMatch
|
||||||
|
? markSelectionFloatingUi(view, linkMatch, formEl, {
|
||||||
|
placement: "top",
|
||||||
|
middleware: [offset(6), autoPlacement(), shift({ padding: 5 })],
|
||||||
|
})
|
||||||
|
: readable("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document on:pointerdown={detectFocus} />
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="rounded-lg bg-white shadow-lg"
|
||||||
|
style={$style}
|
||||||
|
hidden={!shown}
|
||||||
|
bind:this={formEl}
|
||||||
|
on:submit|preventDefault={onSubmit}
|
||||||
|
>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for={`link-text-${id}`} class="col-form-label d-block">Texto</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id={`link-text-${id}`}
|
||||||
|
type="text"
|
||||||
|
bind:value={text}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group was-validated">
|
||||||
|
<label for={`link-href-${id}`} class="col-form-label d-block">Enlace</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id={`link-href-${id}`}
|
||||||
|
type="url"
|
||||||
|
bind:this={hrefInputEl}
|
||||||
|
bind:value={href}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end buttons">
|
||||||
|
<button type="button" class="btn btn-secondary" on:click={cancel}
|
||||||
|
>Cancelar</button
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn btn-primary">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
form {
|
||||||
|
width: max-content;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 420;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
103
src/editor/bubblemenu/LinkTooltip.svelte
Normal file
103
src/editor/bubblemenu/LinkTooltip.svelte
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { EditorState } from "prosemirror-state";
|
||||||
|
import {
|
||||||
|
getFirstMarkInSelection,
|
||||||
|
getMarkRange,
|
||||||
|
removeMark,
|
||||||
|
selectMark,
|
||||||
|
} from "../ps-utils";
|
||||||
|
import type { EditorView } from "prosemirror-view";
|
||||||
|
import { markSelectionFloatingUi } from "./floatingUi";
|
||||||
|
import { readable, type Writable } from "svelte/store";
|
||||||
|
import { flip, shift, offset } from "@floating-ui/dom";
|
||||||
|
|
||||||
|
export let state: EditorState;
|
||||||
|
export let view: EditorView;
|
||||||
|
export let editingLink: Writable<false | "new" | "selection">;
|
||||||
|
|
||||||
|
let tooltipEl: HTMLElement;
|
||||||
|
$: markType = view.state.schema.marks.link;
|
||||||
|
$: link = state && getFirstMarkInSelection(view.state, markType);
|
||||||
|
$: shown = !!link && !$editingLink;
|
||||||
|
|
||||||
|
$: style =
|
||||||
|
shown && link
|
||||||
|
? markSelectionFloatingUi(view, link, tooltipEl, {
|
||||||
|
placement: "bottom",
|
||||||
|
middleware: [offset(6), flip(), shift({ padding: 5 })],
|
||||||
|
})
|
||||||
|
: readable("");
|
||||||
|
|
||||||
|
function editLink() {
|
||||||
|
if (!link) return;
|
||||||
|
selectMark(link, view.state, view.dispatch);
|
||||||
|
$editingLink = "selection";
|
||||||
|
}
|
||||||
|
function removeLink() {
|
||||||
|
const range = getMarkRange(state.selection.$from, markType);
|
||||||
|
if (!range)
|
||||||
|
throw new Error("removeLink: Couldn't get mark range to remove");
|
||||||
|
view.dispatch(state.tr.removeMark(range.from, range.to, markType));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="x-tooltip bg-gray"
|
||||||
|
class:flex={shown}
|
||||||
|
class:hidden={!shown}
|
||||||
|
bind:this={tooltipEl}
|
||||||
|
style={$style}
|
||||||
|
>
|
||||||
|
<!-- TODO: hacer que el clickeable sea relativo a la url final del sitio? -->
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
href={link?.mark.attrs.href}
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{link?.mark.attrs.href}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<button type="button" on:click={editLink}>
|
||||||
|
<i class={`fa fa-edit`} title={"Editar enlace"} />
|
||||||
|
</button>
|
||||||
|
<button type="button" on:click={removeLink}>
|
||||||
|
<i class={`fa fa-remove`} title={"Borrar enlace"} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.x-tooltip {
|
||||||
|
width: max-content;
|
||||||
|
position: absolute;
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 420;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.x-tooltip > * {
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
color: lightblue;
|
||||||
|
max-width: 50vw;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
appearance: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
</style>
|
41
src/editor/bubblemenu/floatingUi.ts
Normal file
41
src/editor/bubblemenu/floatingUi.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { readable, type Readable } from "svelte/store";
|
||||||
|
import {
|
||||||
|
computePosition,
|
||||||
|
autoUpdate,
|
||||||
|
autoPlacement,
|
||||||
|
shift,
|
||||||
|
offset,
|
||||||
|
} from "@floating-ui/dom";
|
||||||
|
import type { ComputePositionConfig } from "@floating-ui/dom/src/types";
|
||||||
|
import type { MarkMatch } from "../ps-utils";
|
||||||
|
import type { EditorView } from "prosemirror-view";
|
||||||
|
|
||||||
|
export type Style = string;
|
||||||
|
|
||||||
|
export function floatingUi(
|
||||||
|
refEl: Element,
|
||||||
|
tooltipEl: HTMLElement,
|
||||||
|
options?: Partial<ComputePositionConfig>,
|
||||||
|
): Readable<Style> {
|
||||||
|
return {
|
||||||
|
subscribe(run, invalidate) {
|
||||||
|
return autoUpdate(refEl, tooltipEl, () => {
|
||||||
|
computePosition(refEl, tooltipEl, options).then(({ x, y }) => {
|
||||||
|
run(`left: ${x}px; top: ${y}px`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markSelectionFloatingUi(
|
||||||
|
view: EditorView,
|
||||||
|
mark: MarkMatch,
|
||||||
|
tooltipEl: HTMLElement,
|
||||||
|
options?: Partial<ComputePositionConfig>,
|
||||||
|
): Readable<Style> {
|
||||||
|
let { node } = (view as any).docView.domFromPos(view.state.selection.from);
|
||||||
|
if (!node || !mark || !tooltipEl) return readable("");
|
||||||
|
if (!(node instanceof Element)) node = node.parentElement;
|
||||||
|
return floatingUi(node, tooltipEl, options);
|
||||||
|
}
|
|
@ -6,7 +6,12 @@ import type {
|
||||||
ResolvedPos,
|
ResolvedPos,
|
||||||
Node as ProsemirrorNode,
|
Node as ProsemirrorNode,
|
||||||
} from "prosemirror-model";
|
} from "prosemirror-model";
|
||||||
import type { EditorState } from "prosemirror-state";
|
import {
|
||||||
|
EditorState,
|
||||||
|
Selection,
|
||||||
|
TextSelection,
|
||||||
|
Transaction,
|
||||||
|
} from "prosemirror-state";
|
||||||
import type { EditorView } from "prosemirror-view";
|
import type { EditorView } from "prosemirror-view";
|
||||||
|
|
||||||
export type Command = (
|
export type Command = (
|
||||||
|
@ -66,15 +71,15 @@ export function updateMark(type: MarkType, attrs: any): Command {
|
||||||
const { ranges, empty } = selection;
|
const { ranges, empty } = selection;
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
|
if (doc.rangeHasMark(selection.from, selection.to, type)) {
|
||||||
const range = getMarkRange(selection.$from, type);
|
const range = getMarkRange(selection.$from, type);
|
||||||
if (!range) throw new Error("What the fuck");
|
if (!range) throw new Error("What the fuck");
|
||||||
const { from, to } = range;
|
const { from, to } = range;
|
||||||
|
|
||||||
if (doc.rangeHasMark(from, to, type)) {
|
|
||||||
tr.removeMark(from, to, type);
|
tr.removeMark(from, to, type);
|
||||||
|
} else {
|
||||||
|
tr.addStoredMark(type.create(attrs));
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.addMark(from, to, type.create(attrs));
|
|
||||||
} else {
|
} else {
|
||||||
ranges.forEach((ref$1) => {
|
ranges.forEach((ref$1) => {
|
||||||
const { $to, $from } = ref$1;
|
const { $to, $from } = ref$1;
|
||||||
|
@ -174,7 +179,7 @@ export function getAttrFn(attrKey: string): (state: EditorState) => any {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
let { from, to } = state.selection;
|
let { from, to } = state.selection;
|
||||||
let value: any = undefined;
|
let value: any = undefined;
|
||||||
state.doc.nodesBetween(from, to, (node) => {
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
if (value !== undefined) return false;
|
if (value !== undefined) return false;
|
||||||
if (!node.isTextblock) return;
|
if (!node.isTextblock) return;
|
||||||
if (attrKey in node.attrs) value = node.attrs[attrKey];
|
if (attrKey in node.attrs) value = node.attrs[attrKey];
|
||||||
|
@ -183,9 +188,63 @@ export function getAttrFn(attrKey: string): (state: EditorState) => any {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adaptado de
|
||||||
|
// https://github.com/ueberdosis/tiptap/blob/8c6751f0c638effb22110b62b40a1632ea6867c9/packages/core/src/helpers/isMarkActive.ts#L18
|
||||||
export function markIsActive(state: EditorState, type: MarkType): boolean {
|
export function markIsActive(state: EditorState, type: MarkType): boolean {
|
||||||
let { from, to } = state.selection;
|
const { empty, ranges } = state.selection;
|
||||||
return state.doc.rangeHasMark(from, to, type);
|
|
||||||
|
if (empty) {
|
||||||
|
return !!(state.storedMarks || state.selection.$from.marks()).some(
|
||||||
|
(mark) => type.name === mark.type.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionRange = 0;
|
||||||
|
const markRanges: {
|
||||||
|
mark: Mark;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
ranges.forEach(({ $from, $to }) => {
|
||||||
|
const from = $from.pos;
|
||||||
|
const to = $to.pos;
|
||||||
|
|
||||||
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (!node.isText && !node.marks.length) return;
|
||||||
|
|
||||||
|
const relativeFrom = Math.max(from, pos);
|
||||||
|
const relativeTo = Math.min(to, pos + node.nodeSize);
|
||||||
|
const range = relativeTo - relativeFrom;
|
||||||
|
|
||||||
|
selectionRange += range;
|
||||||
|
|
||||||
|
markRanges.push(
|
||||||
|
...node.marks.map((mark) => ({
|
||||||
|
mark,
|
||||||
|
from: relativeFrom,
|
||||||
|
to: relativeTo,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectionRange === 0) return false;
|
||||||
|
|
||||||
|
const matchedRange = markRanges
|
||||||
|
.filter((markRange) => type.name === markRange.mark.type.name)
|
||||||
|
.reduce((sum, markRange) => sum + markRange.to - markRange.from, 0);
|
||||||
|
|
||||||
|
const excludedRange = markRanges
|
||||||
|
.filter(
|
||||||
|
(markRange) =>
|
||||||
|
markRange.mark.type !== type && markRange.mark.type.excludes(type),
|
||||||
|
)
|
||||||
|
.reduce((sum, markRange) => sum + markRange.to - markRange.from, 0);
|
||||||
|
|
||||||
|
const range = matchedRange > 0 ? matchedRange + excludedRange : matchedRange;
|
||||||
|
|
||||||
|
return range >= selectionRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkMatch {
|
export interface MarkMatch {
|
||||||
|
@ -198,16 +257,34 @@ export function getFirstMarkInSelection(
|
||||||
state: EditorState,
|
state: EditorState,
|
||||||
type: MarkType,
|
type: MarkType,
|
||||||
): MarkMatch | null {
|
): MarkMatch | null {
|
||||||
const { to, from } = state.selection;
|
const { to, from, empty } = state.selection;
|
||||||
|
|
||||||
let match: MarkMatch | null = null;
|
let match: MarkMatch | null = null;
|
||||||
|
|
||||||
state.selection.$from.doc.nodesBetween(from, to, (node, position) => {
|
state.selection.$from.doc.nodesBetween(from, to, (node, position) => {
|
||||||
if (!match) {
|
if (!match) {
|
||||||
const mark = type.isInSet(node.marks);
|
const mark = type.isInSet(node.marks);
|
||||||
if (!mark) return;
|
if (!mark) return;
|
||||||
|
if (!markIsActive(state, type)) return;
|
||||||
match = { node, position, mark };
|
match = { node, position, mark };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectMark(
|
||||||
|
mark: MarkMatch,
|
||||||
|
state: EditorState,
|
||||||
|
dispatch: EditorView["dispatch"],
|
||||||
|
) {
|
||||||
|
dispatch(
|
||||||
|
state.tr.setSelection(
|
||||||
|
TextSelection.create(
|
||||||
|
state.doc,
|
||||||
|
mark.position,
|
||||||
|
mark.position + mark.node.nodeSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue