Compare commits
No commits in common. "42a90f99f02d17ce553835475aed8b7fb6aab485" and "26714cb341f52d2060344cd3de07e4cb339969db" have entirely different histories.
42a90f99f0
...
26714cb341
14 changed files with 128 additions and 450 deletions
|
@ -33,7 +33,7 @@
|
||||||
"prosemirror-schema-list": "~1.2.2",
|
"prosemirror-schema-list": "~1.2.2",
|
||||||
"prosemirror-state": "~1.4.2",
|
"prosemirror-state": "~1.4.2",
|
||||||
"prosemirror-transform": "~1.7.1",
|
"prosemirror-transform": "~1.7.1",
|
||||||
"prosemirror-view": "^1.31.7",
|
"prosemirror-view": "~1.29.2",
|
||||||
"svelte": "^3.58.0",
|
"svelte": "^3.58.0",
|
||||||
"svelte-check": "^2.10.3",
|
"svelte-check": "^2.10.3",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
|
@ -42,7 +42,6 @@
|
||||||
"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,9 +5,6 @@ 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
|
||||||
|
@ -31,7 +28,7 @@ dependencies:
|
||||||
version: 9.0.10(yjs@13.5.52)
|
version: 9.0.10(yjs@13.5.52)
|
||||||
y-prosemirror:
|
y-prosemirror:
|
||||||
specifier: ^1.2.1
|
specifier: ^1.2.1
|
||||||
version: 1.2.1(prosemirror-model@1.18.3)(prosemirror-state@1.4.2)(prosemirror-view@1.31.7)(y-protocols@1.0.5)(yjs@13.5.52)
|
version: 1.2.1(prosemirror-model@1.18.3)(prosemirror-state@1.4.2)(prosemirror-view@1.29.2)(y-protocols@1.0.5)(yjs@13.5.52)
|
||||||
y-protocols:
|
y-protocols:
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
|
@ -107,8 +104,8 @@ devDependencies:
|
||||||
specifier: ~1.7.1
|
specifier: ~1.7.1
|
||||||
version: 1.7.1
|
version: 1.7.1
|
||||||
prosemirror-view:
|
prosemirror-view:
|
||||||
specifier: ^1.31.7
|
specifier: ~1.29.2
|
||||||
version: 1.31.7
|
version: 1.29.2
|
||||||
svelte:
|
svelte:
|
||||||
specifier: ^3.58.0
|
specifier: ^3.58.0
|
||||||
version: 3.58.0
|
version: 3.58.0
|
||||||
|
@ -333,23 +330,6 @@ 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'}
|
||||||
|
@ -595,7 +575,7 @@ packages:
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
readdirp: 3.6.0
|
readdirp: 3.6.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/commander@4.1.1:
|
/commander@4.1.1:
|
||||||
|
@ -796,8 +776,8 @@ packages:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/fsevents@2.3.3:
|
/fsevents@2.3.2:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
@ -1307,7 +1287,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-state: 1.4.2
|
prosemirror-state: 1.4.2
|
||||||
prosemirror-transform: 1.7.1
|
prosemirror-transform: 1.7.1
|
||||||
prosemirror-view: 1.31.7
|
prosemirror-view: 1.29.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prosemirror-gapcursor@1.3.1:
|
/prosemirror-gapcursor@1.3.1:
|
||||||
|
@ -1316,7 +1296,7 @@ packages:
|
||||||
prosemirror-keymap: 1.2.1
|
prosemirror-keymap: 1.2.1
|
||||||
prosemirror-model: 1.18.3
|
prosemirror-model: 1.18.3
|
||||||
prosemirror-state: 1.4.2
|
prosemirror-state: 1.4.2
|
||||||
prosemirror-view: 1.31.7
|
prosemirror-view: 1.29.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prosemirror-history@1.3.0:
|
/prosemirror-history@1.3.0:
|
||||||
|
@ -1371,15 +1351,15 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model: 1.18.3
|
prosemirror-model: 1.18.3
|
||||||
prosemirror-transform: 1.7.1
|
prosemirror-transform: 1.7.1
|
||||||
prosemirror-view: 1.31.7
|
prosemirror-view: 1.29.2
|
||||||
|
|
||||||
/prosemirror-transform@1.7.1:
|
/prosemirror-transform@1.7.1:
|
||||||
resolution: {integrity: sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==}
|
resolution: {integrity: sha512-VteoifAfpt46z0yEt6Fc73A5OID9t/y2QIeR5MgxEwTuitadEunD/V0c9jQW8ziT8pbFM54uTzRLJ/nLuQjMxg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model: 1.18.3
|
prosemirror-model: 1.18.3
|
||||||
|
|
||||||
/prosemirror-view@1.31.7:
|
/prosemirror-view@1.29.2:
|
||||||
resolution: {integrity: sha512-Pr7w93yOYmxQwzGIRSaNLZ/1uM6YjnenASzN2H6fO6kGekuzRbgZ/4bHbBTd1u4sIQmL33/TcGmzxxidyPwCjg==}
|
resolution: {integrity: sha512-T4Wm+eTpTH0N9gBJfJR6iecjRX2hYTKewoJUwa92hQOoEz2bYVZy6sYeN+hfnRR506TRvRcuZYqftp4KA8dN+Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model: 1.18.3
|
prosemirror-model: 1.18.3
|
||||||
prosemirror-state: 1.4.2
|
prosemirror-state: 1.4.2
|
||||||
|
@ -1457,7 +1437,7 @@ packages:
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/rope-sequence@1.3.3:
|
/rope-sequence@1.3.3:
|
||||||
|
@ -1797,7 +1777,7 @@ packages:
|
||||||
resolve: 1.22.2
|
resolve: 1.22.2
|
||||||
rollup: 3.20.6
|
rollup: 3.20.6
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitefu@0.2.4(vite@4.2.2):
|
/vitefu@0.2.4(vite@4.2.2):
|
||||||
|
@ -1843,7 +1823,7 @@ packages:
|
||||||
yjs: 13.5.52
|
yjs: 13.5.52
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/y-prosemirror@1.2.1(prosemirror-model@1.18.3)(prosemirror-state@1.4.2)(prosemirror-view@1.31.7)(y-protocols@1.0.5)(yjs@13.5.52):
|
/y-prosemirror@1.2.1(prosemirror-model@1.18.3)(prosemirror-state@1.4.2)(prosemirror-view@1.29.2)(y-protocols@1.0.5)(yjs@13.5.52):
|
||||||
resolution: {integrity: sha512-czMBfB1eL2awqmOSxQM8cS/fsUOGE6fjvyPLInrh4crPxFiw67wDpwIW+EGBYKRa04sYbS0ScGj7ZgvWuDrmBQ==}
|
resolution: {integrity: sha512-czMBfB1eL2awqmOSxQM8cS/fsUOGE6fjvyPLInrh4crPxFiw67wDpwIW+EGBYKRa04sYbS0ScGj7ZgvWuDrmBQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prosemirror-model: ^1.7.1
|
prosemirror-model: ^1.7.1
|
||||||
|
@ -1855,7 +1835,7 @@ packages:
|
||||||
lib0: 0.2.73
|
lib0: 0.2.73
|
||||||
prosemirror-model: 1.18.3
|
prosemirror-model: 1.18.3
|
||||||
prosemirror-state: 1.4.2
|
prosemirror-state: 1.4.2
|
||||||
prosemirror-view: 1.31.7
|
prosemirror-view: 1.29.2
|
||||||
y-protocols: 1.0.5
|
y-protocols: 1.0.5
|
||||||
yjs: 13.5.52
|
yjs: 13.5.52
|
||||||
dev: false
|
dev: false
|
||||||
|
|
10
src/app.css
10
src/app.css
|
@ -48,13 +48,3 @@ a {
|
||||||
--accent-fg: #94a3b8;
|
--accent-fg: #94a3b8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* https://devdojo.com/pines/docs/button */
|
|
||||||
.btn {
|
|
||||||
@apply rounded-md bg-neutral-950 px-4 py-2 text-sm font-medium tracking-wide text-white dark:bg-neutral-200 dark:text-neutral-800;
|
|
||||||
}
|
|
||||||
.btn-outline {
|
|
||||||
@apply rounded-md border border-neutral-200/70 bg-white px-4 py-2 text-sm font-medium tracking-wide text-neutral-500 dark:bg-neutral-900 dark:text-neutral-200;
|
|
||||||
}
|
|
||||||
.input {
|
|
||||||
@apply flex h-10 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm placeholder:text-neutral-500 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700/70 dark:bg-neutral-900;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount, tick } from "svelte";
|
import { onDestroy, onMount, tick } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import type { EditorView } from "prosemirror-view";
|
import type { EditorView } from "prosemirror-view";
|
||||||
import type { EditorState } from "prosemirror-state";
|
import { EditorState, TextSelection } from "prosemirror-state";
|
||||||
|
|
||||||
import BoldIcon from "bootstrap-icons/icons/type-bold.svg";
|
import BoldIcon from "bootstrap-icons/icons/type-bold.svg";
|
||||||
import ItalicIcon from "bootstrap-icons/icons/type-italic.svg";
|
import ItalicIcon from "bootstrap-icons/icons/type-italic.svg";
|
||||||
|
@ -10,6 +9,7 @@
|
||||||
import StrikethroughIcon from "bootstrap-icons/icons/type-strikethrough.svg";
|
import StrikethroughIcon from "bootstrap-icons/icons/type-strikethrough.svg";
|
||||||
import LinkIcon from "eva-icons/outline/svg/external-link-outline.svg";
|
import LinkIcon from "eva-icons/outline/svg/external-link-outline.svg";
|
||||||
import InternalLinkIcon from "eva-icons/outline/svg/menu-arrow-outline.svg";
|
import InternalLinkIcon from "eva-icons/outline/svg/menu-arrow-outline.svg";
|
||||||
|
import CloseIcon from "eva-icons/outline/svg/close-outline.svg";
|
||||||
|
|
||||||
import type { Command } from "./ps-utils";
|
import type { Command } from "./ps-utils";
|
||||||
import {
|
import {
|
||||||
|
@ -17,7 +17,6 @@
|
||||||
removeMark,
|
removeMark,
|
||||||
markIsActive,
|
markIsActive,
|
||||||
getFirstMarkInSelection,
|
getFirstMarkInSelection,
|
||||||
selectMark,
|
|
||||||
} from "./ps-utils";
|
} from "./ps-utils";
|
||||||
import SimpleMarkItem from "./bubblemenu/SimpleMarkItem.svelte";
|
import SimpleMarkItem from "./bubblemenu/SimpleMarkItem.svelte";
|
||||||
import Button from "./bubblemenu/Button.svelte";
|
import Button from "./bubblemenu/Button.svelte";
|
||||||
|
@ -29,7 +28,6 @@
|
||||||
export let view: EditorView;
|
export let view: EditorView;
|
||||||
export let state: EditorState;
|
export let state: EditorState;
|
||||||
export let worldY: WorldY;
|
export let worldY: WorldY;
|
||||||
export let editingLink: Writable<false | "new" | "selection">;
|
|
||||||
|
|
||||||
let changingProp:
|
let changingProp:
|
||||||
| false
|
| false
|
||||||
|
@ -57,19 +55,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditingLink() {
|
function startEditingLink() {
|
||||||
const match = getFirstMarkInSelection(
|
const match = getFirstMarkInSelection(state, view.state.schema.marks.link);
|
||||||
view.state,
|
|
||||||
view.state.schema.marks.link,
|
// si no hay un link en la selección, empezar a editar uno sin ningún enlace
|
||||||
);
|
// TODO: quizás queremos poner algo tipo https://sutty.nl por defecto?
|
||||||
if (match) {
|
if (!match) {
|
||||||
selectMark(match, view.state, view.dispatch);
|
changingProp = { type: "link", url: "" };
|
||||||
$editingLink = "selection";
|
return;
|
||||||
} else if (!view.state.selection.empty) {
|
|
||||||
$editingLink = "selection";
|
|
||||||
} else {
|
|
||||||
runCommand(removeMark(view.state.schema.marks.link));
|
|
||||||
$editingLink = "new";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.dispatch(
|
||||||
|
state.tr.setSelection(
|
||||||
|
TextSelection.create(
|
||||||
|
state.doc,
|
||||||
|
match.position,
|
||||||
|
match.position + match.node.nodeSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
changingProp = { type: "link", url: match.mark.attrs.href };
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLink() {
|
||||||
|
changingProp = false;
|
||||||
|
runCommand(removeMark(view.state.schema.marks.link));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeLink(event: Event) {
|
||||||
|
changingProp = false;
|
||||||
|
const url = (event.target as HTMLInputElement).value;
|
||||||
|
runCommand(updateMark(view.state.schema.marks.link, { href: url }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let makingInternalLink = false;
|
let makingInternalLink = false;
|
||||||
|
@ -119,7 +134,7 @@ transform: scale(${1 / viewport.scale});
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="floating z-40" style={barStyle}>
|
<div class="floating" style={barStyle}>
|
||||||
<Linking {state} />
|
<Linking {state} />
|
||||||
<div class="bubble" hidden={state.selection.empty}>
|
<div class="bubble" hidden={state.selection.empty}>
|
||||||
{#if changingProp === false}
|
{#if changingProp === false}
|
||||||
|
@ -147,6 +162,17 @@ transform: scale(${1 / viewport.scale});
|
||||||
onClick={startMakingInternalLink}
|
onClick={startMakingInternalLink}
|
||||||
><InternalLinkIcon style={svgStyle} /></Button
|
><InternalLinkIcon style={svgStyle} /></Button
|
||||||
>
|
>
|
||||||
|
{:else if changingProp.type === "link"}
|
||||||
|
<input
|
||||||
|
bind:this={linkInputEl}
|
||||||
|
type="text"
|
||||||
|
placeholder="https://"
|
||||||
|
on:change|preventDefault={onChangeLink}
|
||||||
|
value={changingProp.url}
|
||||||
|
/>
|
||||||
|
<Button title="Borrar enlace" onClick={removeLink}
|
||||||
|
><CloseIcon style={svgStyle} /></Button
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,14 +202,19 @@ transform: scale(${1 / viewport.scale});
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
transition:
|
transition: opacity 0.2s, visibility 0.2s, height 0.2s;
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s,
|
|
||||||
height 0.2s;
|
|
||||||
}
|
}
|
||||||
.bubble[hidden] {
|
.bubble[hidden] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble input {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,17 +16,12 @@
|
||||||
// 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,
|
||||||
|
@ -74,16 +69,11 @@
|
||||||
<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
|
<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} />
|
||||||
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} {editingLink} state={updatedState} />
|
<BubbleMenu {view} {worldY} state={updatedState} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,7 @@
|
||||||
export let state: EditorState;
|
export let state: EditorState;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="menubar sticky top-0 flex py-2 px-4 z-50 items-center">
|
||||||
class="sticky top-0 z-50 flex items-center border-b border-neutral-200/60 bg-white px-4 py-2 dark:border-neutral-700 dark:bg-neutral-800"
|
|
||||||
>
|
|
||||||
<BlockSelect {view} {state} />
|
<BlockSelect {view} {state} />
|
||||||
<!-- <UploadItem {view} {state} /> -->
|
<!-- <UploadItem {view} {state} /> -->
|
||||||
<ListItem {view} {state} kind={ListKind.Unordered} />
|
<ListItem {view} {state} kind={ListKind.Unordered} />
|
||||||
|
@ -22,14 +20,9 @@
|
||||||
<BlockQuoteItem {view} {state} />
|
<BlockQuoteItem {view} {state} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style>
|
||||||
div :global(button) {
|
.menubar {
|
||||||
@apply m-1 appearance-none rounded p-2 leading-none transition-colors hover:bg-neutral-600/20 dark:hover:bg-neutral-600/40;
|
background: var(--background, white);
|
||||||
}
|
border-bottom: 1px solid var(--accent-bg);
|
||||||
div :global(button.active) {
|
|
||||||
@apply bg-neutral-300 dark:bg-neutral-700;
|
|
||||||
}
|
|
||||||
div :global(button svg) {
|
|
||||||
@apply h-6 w-6;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
<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 { linkFloatingUi, selectionFloatingUi } from "./floatingUi";
|
|
||||||
import {
|
|
||||||
autoPlacement,
|
|
||||||
shift,
|
|
||||||
offset,
|
|
||||||
type ComputePositionConfig,
|
|
||||||
} from "@floating-ui/dom";
|
|
||||||
|
|
||||||
export let state: EditorState;
|
|
||||||
export let view: EditorView;
|
|
||||||
export let editingLink: Writable<false | "new" | "selection">;
|
|
||||||
|
|
||||||
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 = "";
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
$: style = shown
|
|
||||||
? selectionFloatingUi(formEl, {
|
|
||||||
placement: "top",
|
|
||||||
middleware: [offset(6), autoPlacement(), shift({ padding: 5 })],
|
|
||||||
})
|
|
||||||
: readable("");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:document on:pointerdown={detectFocus} />
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="absolute z-50 w-max rounded-lg border border-neutral-200/70 bg-white p-4 shadow-lg dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
style={$style}
|
|
||||||
hidden={!shown}
|
|
||||||
bind:this={formEl}
|
|
||||||
on:submit|preventDefault={onSubmit}
|
|
||||||
>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for={`link-text-${id}`} class="block">Texto</label>
|
|
||||||
<input class="input" id={`link-text-${id}`} type="text" bind:value={text} />
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<label for={`link-href-${id}`} class="block">Enlace</label>
|
|
||||||
<input
|
|
||||||
class="input"
|
|
||||||
id={`link-href-${id}`}
|
|
||||||
type="url"
|
|
||||||
autofocus
|
|
||||||
bind:value={href}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button class="btn-outline" type="button" on:click={cancel}>Cancelar</button
|
|
||||||
>
|
|
||||||
<button class="btn" type="submit">Guardar</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
|
@ -1,77 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { EditorState } from "prosemirror-state";
|
|
||||||
import {
|
|
||||||
getFirstMarkInSelection,
|
|
||||||
getMarkRange,
|
|
||||||
selectMark,
|
|
||||||
} from "../ps-utils";
|
|
||||||
import type { EditorView } from "prosemirror-view";
|
|
||||||
import { linkFloatingUi, selectionFloatingUi } from "./floatingUi";
|
|
||||||
import { readable, type Writable } from "svelte/store";
|
|
||||||
import { flip, shift, offset } from "@floating-ui/dom";
|
|
||||||
import EditIcon from "eva-icons/outline/svg/edit-outline.svg";
|
|
||||||
import CloseIcon from "eva-icons/fill/svg/close.svg";
|
|
||||||
|
|
||||||
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
|
|
||||||
? linkFloatingUi(view, 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="absolute z-20 w-max items-center overflow-hidden rounded border border-neutral-200/70 bg-white px-1 leading-none shadow-lg dark:border-neutral-700 dark:bg-neutral-900"
|
|
||||||
class:flex={shown}
|
|
||||||
class:hidden={!shown}
|
|
||||||
bind:this={tooltipEl}
|
|
||||||
style={$style}
|
|
||||||
>
|
|
||||||
<span class=" ">
|
|
||||||
<a
|
|
||||||
class="block max-w-[50vw] overflow-x-hidden text-ellipsis whitespace-nowrap p-2 text-blue-500 dark:text-blue-300"
|
|
||||||
href={link?.mark.attrs.href}
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{link?.mark.attrs.href}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="my-1 appearance-none rounded border-none p-1 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
|
||||||
type="button"
|
|
||||||
on:click={editLink}
|
|
||||||
title={"Editar enlace"}
|
|
||||||
>
|
|
||||||
<EditIcon class="h-6 w-6 fill-current" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="my-1 appearance-none rounded border-none p-1 hover:bg-neutral-200 dark:hover:bg-neutral-700"
|
|
||||||
type="button"
|
|
||||||
on:click={removeLink}
|
|
||||||
title={"Borrar enlace"}
|
|
||||||
>
|
|
||||||
<CloseIcon class="h-6 w-6 fill-current" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { readable, type Readable } from "svelte/store";
|
|
||||||
import { computePosition, autoUpdate } from "@floating-ui/dom";
|
|
||||||
import type {
|
|
||||||
ComputePositionConfig,
|
|
||||||
ReferenceElement,
|
|
||||||
} from "@floating-ui/dom/src/types";
|
|
||||||
import type { EditorView } from "prosemirror-view";
|
|
||||||
import { getFirstMarkInSelection } from "../ps-utils";
|
|
||||||
|
|
||||||
export type Style = string;
|
|
||||||
|
|
||||||
export function floatingUi(
|
|
||||||
refEl: ReferenceElement,
|
|
||||||
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 selectionFloatingUi(
|
|
||||||
tooltipEl: HTMLElement,
|
|
||||||
options?: Partial<ComputePositionConfig>,
|
|
||||||
): Readable<Style> {
|
|
||||||
const sel = document.getSelection();
|
|
||||||
const range = sel?.getRangeAt(0);
|
|
||||||
|
|
||||||
if (!range) return readable("");
|
|
||||||
return floatingUi(range, tooltipEl, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function linkFloatingUi(
|
|
||||||
view: EditorView,
|
|
||||||
tooltipEl: HTMLElement,
|
|
||||||
options?: Partial<ComputePositionConfig>,
|
|
||||||
): Readable<Style> {
|
|
||||||
const mark = getFirstMarkInSelection(
|
|
||||||
view.state,
|
|
||||||
view.state.schema.marks.link,
|
|
||||||
);
|
|
||||||
if (!mark) return readable("");
|
|
||||||
let node = view.nodeDOM(mark?.position);
|
|
||||||
if (!node) return readable("");
|
|
||||||
const element = node instanceof Element ? node : node.parentElement;
|
|
||||||
if (!element) return readable("");
|
|
||||||
return floatingUi(element, tooltipEl, options);
|
|
||||||
}
|
|
|
@ -4,6 +4,36 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor *::before,
|
||||||
|
.editor *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .menubar button {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.4em 0.6em;
|
||||||
|
margin: 0.2em;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .menubar button:hover {
|
||||||
|
background: var(--transparentish);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .menubar button.active {
|
||||||
|
background: var(--accent-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor .menubar button svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
<div class="flex min-w-min">
|
<div class="flex min-w-min">
|
||||||
{#each links as link}
|
{#each links as link}
|
||||||
<a
|
<a
|
||||||
class="m-1 flex max-w-[45vw] items-center gap-1 rounded-full border border-neutral-200 bg-white px-4 py-3 no-underline dark:border-neutral-700/70 dark:bg-neutral-800"
|
class="m-1 flex max-w-[45vw] items-center gap-1 rounded-full border border-neutral-200 bg-white px-4 py-3 no-underline dark:border-neutral-500 dark:bg-neutral-800"
|
||||||
href={"href" in link ? link.href : link.id}
|
href={"href" in link ? link.href : link.id}
|
||||||
target={link.type === "external" ? "_blank" : null}
|
target={link.type === "external" ? "_blank" : null}
|
||||||
>
|
>
|
||||||
|
|
|
@ -6,12 +6,7 @@ import type {
|
||||||
ResolvedPos,
|
ResolvedPos,
|
||||||
Node as ProsemirrorNode,
|
Node as ProsemirrorNode,
|
||||||
} from "prosemirror-model";
|
} from "prosemirror-model";
|
||||||
import {
|
import type { EditorState } from "prosemirror-state";
|
||||||
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 = (
|
||||||
|
@ -71,15 +66,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;
|
||||||
|
@ -179,7 +174,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, pos) => {
|
state.doc.nodesBetween(from, to, (node) => {
|
||||||
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];
|
||||||
|
@ -188,63 +183,9 @@ 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 {
|
||||||
const { empty, ranges } = state.selection;
|
let { from, to } = 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 {
|
||||||
|
@ -257,34 +198,16 @@ export function getFirstMarkInSelection(
|
||||||
state: EditorState,
|
state: EditorState,
|
||||||
type: MarkType,
|
type: MarkType,
|
||||||
): MarkMatch | null {
|
): MarkMatch | null {
|
||||||
const { to, from, empty } = state.selection;
|
const { to, from } = 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="px-4 py-2">
|
<nav>
|
||||||
<details>
|
<details>
|
||||||
<summary>Opciones</summary>
|
<summary>Opciones</summary>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
import { derived } from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
import { getTitle } from "../../lib/getTitle";
|
import { getTitle } from "../../lib/getTitle";
|
||||||
import { routes } from "../../lib/routes";
|
import { routes } from "../../lib/routes";
|
||||||
import type { HTMLOlAttributes } from "svelte/elements";
|
|
||||||
|
|
||||||
export let worldId: string;
|
export let worldId: string;
|
||||||
export let pageId: string;
|
export let pageId: string;
|
||||||
|
@ -28,27 +27,22 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
let breadcrumbsEl: HTMLDivElement;
|
let breadcrumbsEl: HTMLDivElement;
|
||||||
let breadcrumbsListEl: HTMLOListElement;
|
const crumbsScrollToEnd = async () => {
|
||||||
const crumbsScrollToEnd = async (behavior: ScrollBehavior = "auto") => {
|
|
||||||
await tick();
|
await tick();
|
||||||
breadcrumbsEl.scroll({
|
breadcrumbsEl?.scroll({
|
||||||
left: breadcrumbsEl.scrollWidth,
|
left: breadcrumbsEl.scrollWidth,
|
||||||
behavior,
|
behavior: "smooth",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
crumbsScrollToEnd("smooth");
|
crumbsScrollToEnd();
|
||||||
const resizeObserver = new ResizeObserver(() =>
|
|
||||||
crumbsScrollToEnd("smooth"),
|
|
||||||
);
|
|
||||||
resizeObserver.observe(breadcrumbsListEl);
|
|
||||||
return () => resizeObserver.disconnect();
|
|
||||||
});
|
|
||||||
onMount(() => {
|
|
||||||
const resizeObserver = new ResizeObserver(() => crumbsScrollToEnd());
|
|
||||||
resizeObserver.observe(breadcrumbsEl);
|
|
||||||
return () => resizeObserver.disconnect();
|
|
||||||
});
|
});
|
||||||
|
$: {
|
||||||
|
$crumbsTitles;
|
||||||
|
$pageBreadcrumbs;
|
||||||
|
crumbsScrollToEnd();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- https://devdojo.com/pines/docs/breadcrumbs -->
|
<!-- https://devdojo.com/pines/docs/breadcrumbs -->
|
||||||
|
@ -58,7 +52,6 @@
|
||||||
>
|
>
|
||||||
<ol
|
<ol
|
||||||
class="inline-flex items-center space-x-1 text-xs text-neutral-500 dark:text-neutral-300 sm:mb-0 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600 dark:[&_.active-breadcrumb]:text-neutral-200"
|
class="inline-flex items-center space-x-1 text-xs text-neutral-500 dark:text-neutral-300 sm:mb-0 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600 dark:[&_.active-breadcrumb]:text-neutral-200"
|
||||||
bind:this={breadcrumbsListEl}
|
|
||||||
>
|
>
|
||||||
{#each $pageBreadcrumbs as crumb, index}
|
{#each $pageBreadcrumbs as crumb, index}
|
||||||
<li>
|
<li>
|
||||||
|
|
Loading…
Reference in a new issue