cambiar forma de navegación mucho
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
230c6a8732
commit
dc773b14ff
17 changed files with 219 additions and 291 deletions
|
@ -48,6 +48,7 @@
|
|||
"idb-keyval": "^6.2.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"navaid": "^1.2.0",
|
||||
"prosemirror-inputrules": "^1.2.1",
|
||||
"regexparam": "^2.0.1",
|
||||
"y-indexeddb": "^9.0.10",
|
||||
"y-prosemirror": "^1.2.1",
|
||||
|
|
|
@ -23,6 +23,9 @@ dependencies:
|
|||
navaid:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
prosemirror-inputrules:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
regexparam:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
|
@ -1317,6 +1320,13 @@ packages:
|
|||
rope-sequence: 1.3.3
|
||||
dev: true
|
||||
|
||||
/prosemirror-inputrules@1.2.1:
|
||||
resolution: {integrity: sha512-3LrWJX1+ULRh5SZvbIQlwZafOXqp1XuV21MGBu/i5xsztd+9VD15x6OtN6mdqSFI7/8Y77gYUbQ6vwwJ4mr6QQ==}
|
||||
dependencies:
|
||||
prosemirror-state: 1.4.2
|
||||
prosemirror-transform: 1.7.1
|
||||
dev: false
|
||||
|
||||
/prosemirror-keymap@1.2.1:
|
||||
resolution: {integrity: sha512-kVK6WGC+83LZwuSJnuCb9PsADQnFZllt94qPP3Rx/vLcOUV65+IbBeH2nS5cFggPyEVJhGkGrgYFRrG250WhHQ==}
|
||||
dependencies:
|
||||
|
|
41
src/components/BottomFloatingBar.svelte
Normal file
41
src/components/BottomFloatingBar.svelte
Normal file
|
@ -0,0 +1,41 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
|
||||
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
|
||||
let barStyle = "";
|
||||
function updateBar() {
|
||||
const viewport = window.visualViewport!;
|
||||
// Since the bar is position: fixed we need to offset it by the
|
||||
// visual viewport's offset from the layout viewport origin.
|
||||
const offsetY = window.innerHeight - viewport.height - viewport.offsetTop;
|
||||
|
||||
barStyle = `
|
||||
left: ${viewport.offsetLeft}px;
|
||||
bottom: ${offsetY}px;
|
||||
transform: scale(${1 / viewport.scale});
|
||||
`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.visualViewport!.addEventListener("resize", updateBar);
|
||||
window.visualViewport!.addEventListener("scroll", updateBar);
|
||||
});
|
||||
onDestroy(() => {
|
||||
window.visualViewport!.removeEventListener("resize", updateBar);
|
||||
window.visualViewport!.removeEventListener("scroll", updateBar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="transform-origin- fixed bottom-0 left-0 z-40 flex w-full flex-col"
|
||||
style={barStyle}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
|
||||
.transform-origin- {
|
||||
transform-origin: left bottom;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import CloseIcon from "eva-icons/fill/svg/close.svg";
|
||||
import bodyScroll from "../lib/bodyScroll";
|
||||
|
||||
export let onClose: () => void;
|
||||
|
||||
|
@ -10,6 +11,8 @@
|
|||
function keydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") onClose();
|
||||
}
|
||||
|
||||
$: $bodyScroll;
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -20,10 +23,16 @@
|
|||
on:click={click}
|
||||
on:keydown={keydown}
|
||||
>
|
||||
<div class="backdrop" />
|
||||
<div class="fixed inset-0 bg-neutral-100/70 dark:bg-neutral-950/70" />
|
||||
|
||||
<div class="content-alignment" on:click={click} on:keydown={keydown}>
|
||||
<div class="content shadow-xl">
|
||||
<div
|
||||
class="fixed inset-0 z-[269] flex h-screen items-center justify-center overflow-y-auto"
|
||||
on:click={click}
|
||||
on:keydown={keydown}
|
||||
>
|
||||
<div
|
||||
class="h-full w-full overflow-y-auto rounded-2xl bg-neutral-100 px-5 py-4 shadow-xl dark:bg-neutral-800 sm:h-auto sm:w-auto"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-2xl" id="modal-title">
|
||||
<slot name="title" />
|
||||
|
@ -109,29 +118,4 @@
|
|||
position: relative;
|
||||
z-index: 169;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--background);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content-alignment {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 269;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
padding: 16px 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
|
@ -10,6 +9,8 @@
|
|||
import StrikethroughIcon from "bootstrap-icons/icons/type-strikethrough.svg";
|
||||
import LinkIcon from "eva-icons/outline/svg/external-link-outline.svg";
|
||||
import InternalLinkIcon from "eva-icons/outline/svg/menu-arrow-outline.svg";
|
||||
import H2Icon from "bootstrap-icons/icons/type-h2.svg";
|
||||
import H3Icon from "bootstrap-icons/icons/type-h3.svg";
|
||||
|
||||
import type { Command } from "./ps-utils";
|
||||
import {
|
||||
|
@ -23,35 +24,16 @@
|
|||
import Button from "./bubblemenu/Button.svelte";
|
||||
import Modal from "../components/Modal.svelte";
|
||||
import PagePicker from "../components/PagePicker.svelte";
|
||||
import BottomFloatingBar from "../components/BottomFloatingBar.svelte";
|
||||
import type { WorldY } from "../lib/doc";
|
||||
import Linking from "./menubar/Linking.svelte";
|
||||
import Linking from "./bubblemenu/Linking.svelte";
|
||||
import HeadingButton from "./bubblemenu/HeadingButton.svelte";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
export let worldY: WorldY;
|
||||
export let editingLink: Writable<false | "new" | "selection">;
|
||||
|
||||
let changingProp:
|
||||
| false
|
||||
| { type: "link"; url: string }
|
||||
| { type: "mark" }
|
||||
| { type: "mark-custom"; color: string } = false;
|
||||
|
||||
let linkInputEl: HTMLElement;
|
||||
|
||||
$: {
|
||||
if (state.selection.empty) {
|
||||
changingProp = false;
|
||||
}
|
||||
if (changingProp && changingProp.type === "link") {
|
||||
tick().then(() => {
|
||||
linkInputEl.focus();
|
||||
});
|
||||
} else {
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand(command: Command) {
|
||||
command(state, view.dispatch);
|
||||
}
|
||||
|
@ -86,30 +68,6 @@
|
|||
}
|
||||
|
||||
const svgStyle = "width: 100%; height: 100%";
|
||||
|
||||
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
|
||||
let barStyle = "";
|
||||
function updateBar() {
|
||||
const viewport = window.visualViewport!;
|
||||
// Since the bar is position: fixed we need to offset it by the
|
||||
// visual viewport's offset from the layout viewport origin.
|
||||
const offsetY = window.innerHeight - viewport.height - viewport.offsetTop;
|
||||
|
||||
barStyle = `
|
||||
left: ${viewport.offsetLeft}px;
|
||||
bottom: ${offsetY}px;
|
||||
transform: scale(${1 / viewport.scale});
|
||||
`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.visualViewport!.addEventListener("resize", updateBar);
|
||||
window.visualViewport!.addEventListener("scroll", updateBar);
|
||||
});
|
||||
onDestroy(() => {
|
||||
window.visualViewport!.removeEventListener("resize", updateBar);
|
||||
window.visualViewport!.removeEventListener("scroll", updateBar);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if makingInternalLink}
|
||||
|
@ -119,55 +77,47 @@ transform: scale(${1 / viewport.scale});
|
|||
</Modal>
|
||||
{/if}
|
||||
|
||||
<div class="floating z-40" style={barStyle}>
|
||||
<BottomFloatingBar>
|
||||
<Linking {state} />
|
||||
<div class="bubble" hidden={state.selection.empty}>
|
||||
{#if changingProp === false}
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strong}
|
||||
><BoldIcon style={svgStyle} /></SimpleMarkItem
|
||||
>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.em}
|
||||
><ItalicIcon style={svgStyle} /></SimpleMarkItem
|
||||
>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.underline}
|
||||
><UnderlineIcon style={svgStyle} /></SimpleMarkItem
|
||||
>
|
||||
<SimpleMarkItem
|
||||
{view}
|
||||
{state}
|
||||
type={view.state.schema.marks.strikethrough}
|
||||
><StrikethroughIcon style={svgStyle} /></SimpleMarkItem
|
||||
>
|
||||
<div class="bubble flex items-center" hidden={state.selection.empty}>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strong}>
|
||||
<BoldIcon style={svgStyle} />
|
||||
</SimpleMarkItem>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.em}>
|
||||
<ItalicIcon style={svgStyle} />
|
||||
</SimpleMarkItem>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.underline}>
|
||||
<UnderlineIcon style={svgStyle} />
|
||||
</SimpleMarkItem>
|
||||
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strikethrough}>
|
||||
<StrikethroughIcon style={svgStyle} />
|
||||
</SimpleMarkItem>
|
||||
<Button
|
||||
active={markIsActive(state, view.state.schema.marks.link)}
|
||||
onClick={startEditingLink}><LinkIcon style={svgStyle} /></Button
|
||||
onClick={startEditingLink}
|
||||
>
|
||||
<LinkIcon style={svgStyle} />
|
||||
</Button>
|
||||
<Button
|
||||
active={markIsActive(state, view.state.schema.marks.internal_link)}
|
||||
onClick={startMakingInternalLink}
|
||||
><InternalLinkIcon style={svgStyle} /></Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<InternalLinkIcon style={svgStyle} />
|
||||
</Button>
|
||||
|
||||
<span class="mx-2 block h-6 border-r border-neutral-400" />
|
||||
|
||||
<HeadingButton {view} {state} level={2}>
|
||||
<H2Icon style={svgStyle} />
|
||||
</HeadingButton>
|
||||
<HeadingButton {view} {state} level={3}>
|
||||
<H3Icon style={svgStyle} />
|
||||
</HeadingButton>
|
||||
</div>
|
||||
</BottomFloatingBar>
|
||||
|
||||
<style>
|
||||
.floating {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
padding: 0rem;
|
||||
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
|
||||
transform-origin: left bottom;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
display: flex;
|
||||
|
||||
background: var(--background);
|
||||
border-top: 1px solid var(--accent-bg);
|
||||
width: 100%;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import { dropCursor } from "prosemirror-dropcursor";
|
||||
import { gapCursor } from "prosemirror-gapcursor";
|
||||
import { history } from "prosemirror-history";
|
||||
import { inputRules, wrappingInputRule } from "prosemirror-inputrules";
|
||||
import type { XmlFragment } from "yjs";
|
||||
import { ySyncPlugin } from "y-prosemirror";
|
||||
|
||||
|
@ -12,7 +13,6 @@
|
|||
|
||||
import { schema } from "./schema";
|
||||
import BubbleMenu from "./BubbleMenu.svelte";
|
||||
import MenuBar from "./MenuBar.svelte";
|
||||
// import { placeholderPlugin } from "./upload";
|
||||
import { baseKeymap } from "./keymap";
|
||||
import type { WorldY } from "../lib/doc";
|
||||
|
@ -56,6 +56,14 @@
|
|||
// yCursorPlugin(doc.webrtcProvider.awareness),
|
||||
// yUndoPlugin(),
|
||||
keymap(baseKeymap),
|
||||
inputRules({
|
||||
rules: [
|
||||
// https://github.com/ueberdosis/tiptap/blob/6f218be6e439603c85559c6d77ec93a205003bf5/packages/extension-bullet-list/src/bullet-list.ts#L24C27-L24C43
|
||||
wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
|
||||
wrappingInputRule(/^\s*([0-9]\.)\s$/, schema.nodes.ordered_list),
|
||||
wrappingInputRule(/^>\s$/, schema.nodes.blockquote),
|
||||
],
|
||||
}),
|
||||
// placeholderPlugin,
|
||||
],
|
||||
});
|
||||
|
@ -73,7 +81,6 @@
|
|||
|
||||
<div class="editor min-h-screen">
|
||||
{#if view}
|
||||
<MenuBar {view} state={updatedState} />
|
||||
<EditLinkMenu state={updatedState} {view} {editingLink} />
|
||||
<LinkTooltip state={updatedState} {view} {editingLink} />
|
||||
{/if}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
|
||||
import BlockSelect from "./menubar/BlockSelect.svelte";
|
||||
// import UploadItem from "./menubar/UploadItem.svelte";
|
||||
import ListItem from "./menubar/ListItem.svelte";
|
||||
import BlockQuoteItem from "./menubar/BlockQuoteItem.svelte";
|
||||
import { ListKind } from "./utils";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
</script>
|
||||
|
||||
<div
|
||||
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} />
|
||||
<!-- <UploadItem {view} {state} /> -->
|
||||
<ListItem {view} {state} kind={ListKind.Unordered} />
|
||||
<ListItem {view} {state} kind={ListKind.Ordered} />
|
||||
<BlockQuoteItem {view} {state} />
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
div :global(button) {
|
||||
@apply m-1 appearance-none rounded p-2 leading-none transition-colors hover:bg-neutral-600/20 dark:hover:bg-neutral-600/40;
|
||||
}
|
||||
div :global(button.active) {
|
||||
@apply bg-neutral-300 dark:bg-neutral-700;
|
||||
}
|
||||
div :global(button svg) {
|
||||
@apply h-6 w-6;
|
||||
}
|
||||
</style>
|
29
src/editor/bubblemenu/HeadingButton.svelte
Normal file
29
src/editor/bubblemenu/HeadingButton.svelte
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
|
||||
import { commandListener } from "../ps-utils";
|
||||
import Button from "./Button.svelte";
|
||||
import { chainCommands, setBlockType } from "prosemirror-commands";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
export let level: number;
|
||||
|
||||
$: type = state.schema.nodes.heading;
|
||||
$: paragraphType = state.schema.nodes.paragraph;
|
||||
|
||||
$: isActive =
|
||||
state.selection.to <= state.selection.$from.end() &&
|
||||
state.selection.$from.parent.type === type &&
|
||||
state.selection.$from.parent.attrs.level === level;
|
||||
|
||||
$: listener = commandListener(
|
||||
view,
|
||||
chainCommands(setBlockType(type, { level }), setBlockType(paragraphType)),
|
||||
);
|
||||
</script>
|
||||
|
||||
<Button active={isActive} onClick={listener}>
|
||||
<slot />
|
||||
</Button>
|
|
@ -15,7 +15,7 @@ export function floatingUi(
|
|||
options?: Partial<ComputePositionConfig>,
|
||||
): Readable<Style> {
|
||||
return {
|
||||
subscribe(run, invalidate) {
|
||||
subscribe(run) {
|
||||
return autoUpdate(refEl, tooltipEl, () => {
|
||||
computePosition(refEl, tooltipEl, options).then(({ x, y }) => {
|
||||
run(`left: ${x}px; top: ${y}px`);
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { wrapIn } from "prosemirror-commands";
|
||||
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
|
||||
import BlockquoteIcon from "bootstrap-icons/icons/blockquote-left.svg";
|
||||
|
||||
import { commandListener, nodeIsActiveFn } from "../ps-utils";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
|
||||
const type = state.schema.nodes.blockquote;
|
||||
|
||||
$: isActive = nodeIsActiveFn(type, null, true);
|
||||
$: command = wrapIn(type);
|
||||
$: isPossible = command(state);
|
||||
$: actionListener = commandListener(view, command);
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class:active={isActive(state)}
|
||||
on:mousedown={actionListener}
|
||||
disabled={!isPossible}><BlockquoteIcon /></button
|
||||
>
|
|
@ -1,49 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { setBlockType } from "prosemirror-commands";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
|
||||
const paragraphType = state.schema.nodes.paragraph;
|
||||
const headingType = state.schema.nodes.heading;
|
||||
|
||||
$: currentValue =
|
||||
state.selection.to <= state.selection.$from.end() &&
|
||||
(state.selection.$from.parent.type == headingType
|
||||
? `heading:${state.selection.$from.parent.attrs.level}`
|
||||
: state.selection.$from.parent.type == paragraphType
|
||||
? "paragraph"
|
||||
: null);
|
||||
|
||||
const onChange = (
|
||||
event: Event & { currentTarget: EventTarget & HTMLSelectElement }
|
||||
) => {
|
||||
event.preventDefault();
|
||||
|
||||
const [type, param] = event.currentTarget.value.split(":");
|
||||
if (type === "paragraph") {
|
||||
setBlockType(paragraphType, {
|
||||
align: state.selection.$from.parent.attrs.align,
|
||||
})(state, view.dispatch);
|
||||
} else if (type === "heading") {
|
||||
setBlockType(headingType, {
|
||||
level: parseInt(param),
|
||||
align: state.selection.$from.parent.attrs.align,
|
||||
})(state, view.dispatch);
|
||||
} else {
|
||||
console.error(`¡type no es heading ni paragraph! Es`, type);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<select value={currentValue} on:change={onChange} class="relative flex items-center justify-between py-3 pl-3 pr-10 text-left bg-white dark:bg-neutral-800 border rounded-md shadow-sm cursor-default border-neutral-200/70 dark:border-neutral-700/70 text-sm">
|
||||
<option value="paragraph">Párrafo</option>
|
||||
<option value="heading:1">Titulo grande</option>
|
||||
<option value="heading:2">Titulo mediano</option>
|
||||
<option value="heading:3">Subtitulo</option>
|
||||
<option value="heading:4">Subsubtitulo</option>
|
||||
<option value="heading:5">Subsubsubtitulo</option>
|
||||
<option value="heading:6">Subsubsubsubtitulo</option>
|
||||
</select>
|
|
@ -1,44 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { chainCommands } from "prosemirror-commands";
|
||||
|
||||
import { wrapInList } from "prosemirror-schema-list";
|
||||
import { liftListItem } from "prosemirror-schema-list";
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
|
||||
import UlIcon from "bootstrap-icons/icons/list-ul.svg";
|
||||
import OlIcon from "bootstrap-icons/icons/list-ol.svg";
|
||||
|
||||
import { ListKind } from "../utils";
|
||||
import { commandListener, nodeIsActiveFn } from "../ps-utils";
|
||||
|
||||
export let view: EditorView;
|
||||
export let state: EditorState;
|
||||
export let kind: ListKind;
|
||||
|
||||
$: type =
|
||||
kind === ListKind.Unordered
|
||||
? state.schema.nodes.bullet_list
|
||||
: kind === ListKind.Ordered
|
||||
? state.schema.nodes.ordered_list
|
||||
: (null as never);
|
||||
const listItemType = state.schema.nodes.list_item;
|
||||
$: iconComponent =
|
||||
kind === ListKind.Unordered
|
||||
? UlIcon
|
||||
: kind === ListKind.Ordered
|
||||
? OlIcon
|
||||
: (console.error("adsfadsf"), UlIcon);
|
||||
|
||||
$: isActive = nodeIsActiveFn(type, null, true);
|
||||
$: command = chainCommands(liftListItem(listItemType), wrapInList(type));
|
||||
$: isPossible = command(state);
|
||||
$: actionListener = commandListener(view, command);
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class:active={isActive(state)}
|
||||
on:mousedown={actionListener}
|
||||
disabled={!isPossible}><svelte:component this={iconComponent} /></button
|
||||
>
|
24
src/lib/bodyScroll.ts
Normal file
24
src/lib/bodyScroll.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { Readable } from "svelte/store";
|
||||
|
||||
// weird way to stop scroll in the body
|
||||
// when the store is mounted, scroll stops in the body
|
||||
class BodyScroll implements Readable<boolean> {
|
||||
num: number = 0;
|
||||
|
||||
refresh() {
|
||||
if (this.num > 0) document.body.style.overflow = "hidden";
|
||||
else document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
subscribe() {
|
||||
this.num++;
|
||||
this.refresh();
|
||||
return () => {
|
||||
this.num--;
|
||||
this.refresh();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const bodyScroll = new BodyScroll();
|
||||
export default bodyScroll;
|
|
@ -1,11 +1,6 @@
|
|||
import {
|
||||
derived,
|
||||
type Readable,
|
||||
type Subscriber,
|
||||
type Unsubscriber,
|
||||
} from "svelte/store";
|
||||
import { derived, type Readable } from "svelte/store";
|
||||
import type { Doc, Transaction, XmlFragment } from "yjs";
|
||||
import { loadWorlds, worldsStore } from "./worldStorage";
|
||||
import { worldsStore } from "./worldStorage";
|
||||
import { getWorldPage, getWorldY } from "./doc";
|
||||
|
||||
export function makeYdocStore<T>(
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { XmlFragment } from "yjs";
|
||||
import { inject } from "regexparam";
|
||||
import ArrowBackIcon from "eva-icons/fill/svg/arrow-back.svg";
|
||||
import Editor from "../editor/Editor.svelte";
|
||||
import Breadcrumbs from "./Page/Breadcrumbs.svelte";
|
||||
import { getWorldPage, getWorldY, type WorldY } from "../lib/doc";
|
||||
import { routes } from "../lib/routes";
|
||||
import { loadWorlds } from "../lib/worldStorage";
|
||||
import Breadcrumbs from "./Page/Breadcrumbs.svelte";
|
||||
|
||||
export let worldId: string;
|
||||
export let pageId: string;
|
||||
|
@ -33,9 +34,13 @@
|
|||
})
|
||||
.catch((error) => (state = { error }));
|
||||
}
|
||||
|
||||
function pop() {
|
||||
history.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="px-4 py-2">
|
||||
<div class="px-4">
|
||||
<details>
|
||||
<summary>Opciones</summary>
|
||||
<ul>
|
||||
|
@ -47,7 +52,13 @@
|
|||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
<nav
|
||||
class="sticky top-0 z-10 flex h-10 items-stretch gap-4 bg-white text-neutral-700 shadow dark:bg-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
<button title="Ir a la página anterior" on:click={pop}>
|
||||
<ArrowBackIcon class="w-10 shrink-0 fill-current pl-2" />
|
||||
</button>
|
||||
<Breadcrumbs {pageId} {worldId} />
|
||||
</nav>
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
import ChevronRight from "eva-icons/fill/svg/chevron-right.svg";
|
||||
import ArrowDown from "eva-icons/fill/svg/arrow-down.svg";
|
||||
import { inject } from "regexparam";
|
||||
import breadcrumbs from "../../lib/breadcrumbs";
|
||||
import { pageStore } from "../../lib/makeYdocStore";
|
||||
import { derived } from "svelte/store";
|
||||
import { getTitle } from "../../lib/getTitle";
|
||||
import { routes } from "../../lib/routes";
|
||||
import type { HTMLOlAttributes } from "svelte/elements";
|
||||
import Modal from "../../components/Modal.svelte";
|
||||
|
||||
export let worldId: string;
|
||||
export let pageId: string;
|
||||
|
@ -26,6 +27,7 @@
|
|||
).subscribe(set);
|
||||
},
|
||||
);
|
||||
$: currentTitle = $crumbsTitles[$crumbsTitles.length - 1];
|
||||
|
||||
let breadcrumbsEl: HTMLDivElement;
|
||||
let breadcrumbsListEl: HTMLOListElement;
|
||||
|
@ -49,22 +51,51 @@
|
|||
resizeObserver.observe(breadcrumbsEl);
|
||||
return () => resizeObserver.disconnect();
|
||||
});
|
||||
|
||||
let breadcrumbsModalOpen = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="flex items-center overflow-hidden text-lg sm:hidden"
|
||||
on:click={() => (breadcrumbsModalOpen = true)}
|
||||
>
|
||||
<span class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>{currentTitle}</span
|
||||
>
|
||||
<ArrowDown class="h-6 w-6 flex-shrink-0 fill-current" /></button
|
||||
>
|
||||
|
||||
{#if breadcrumbsModalOpen}
|
||||
<Modal onClose={() => (breadcrumbsModalOpen = false)}>
|
||||
<ol class="h-full w-full">
|
||||
{#each $pageBreadcrumbs as crumb, index}
|
||||
<li>
|
||||
<a
|
||||
href={inject(routes.Page, { worldId, pageId: crumb })}
|
||||
class="flex items-center text-ellipsis whitespace-nowrap p-4"
|
||||
class:active-breadcrumb={crumb === pageId}
|
||||
>{$crumbsTitles[index] || crumb}</a
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<!-- https://devdojo.com/pines/docs/breadcrumbs -->
|
||||
<div
|
||||
class="flex justify-between overflow-x-auto rounded-md border border-neutral-200/60 px-3.5 py-1 dark:border-neutral-700"
|
||||
class="hidden justify-between overflow-x-hidden leading-none sm:flex"
|
||||
bind:this={breadcrumbsEl}
|
||||
>
|
||||
<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="mb-0 inline-flex items-center space-x-1 text-ellipsis text-sm text-neutral-500 dark:text-neutral-300 [&_.active-breadcrumb]:font-medium [&_.active-breadcrumb]:text-neutral-600 dark:[&_.active-breadcrumb]:text-neutral-200"
|
||||
bind:this={breadcrumbsListEl}
|
||||
>
|
||||
{#each $pageBreadcrumbs as crumb, index}
|
||||
<li>
|
||||
<a
|
||||
href={inject(routes.Page, { worldId, pageId: crumb })}
|
||||
class="font-norma inline-flex items-center text-ellipsis whitespace-nowrap py-1 focus:outline-none"
|
||||
class="inline-flex items-center text-ellipsis whitespace-nowrap py-1 font-normal focus:outline-none"
|
||||
class:active-breadcrumb={crumb === pageId}
|
||||
>{$crumbsTitles[index] || crumb}</a
|
||||
>
|
||||
|
|
Loading…
Reference in a new issue