Compare commits

...

3 commits

Author SHA1 Message Date
6368c6b54f limpiar App.svelte 2023-03-16 11:30:09 -03:00
baca793e0a mover bubblemenu pa bajo 2023-03-16 11:29:28 -03:00
7668c4a805 mover css de bubblemenu a sus componentes 2023-03-16 10:07:33 -03:00
8 changed files with 127 additions and 272 deletions

View file

@ -1,43 +1,14 @@
<script lang="ts"> <script lang="ts">
import { nanoid } from "nanoid";
import Editor from "./editor/Editor.svelte";
import { getWorldPage, getWorldY } from "./lib/doc";
import { currentRoute } from "./lib/routes"; import { currentRoute } from "./lib/routes";
let worldDescriptor: string = "";
let fileId: string = nanoid();
$: world = worldDescriptor.includes(":")
? {
room: worldDescriptor.split(":")[0],
password: worldDescriptor.split(":")[1],
}
: null;
$: worldY = world && getWorldY(world);
$: doc = worldY && getWorldPage(worldY.ydoc, fileId);
function generateWorld() {
worldDescriptor = `${nanoid()}:${nanoid()}`;
}
</script> </script>
<main> <main>
<svelte:component this={$currentRoute.component} {...$currentRoute.params} /> <svelte:component this={$currentRoute.component} {...$currentRoute.params} />
<!-- <button on:click={generateWorld}>generar mundo</button>
<input
type="text"
bind:value={worldDescriptor}
placeholder="mundo descriptor"
/>
<input type="text" bind:value={fileId} placeholder="world" />
{#if doc}<Editor {doc} />{/if} -->
</main> </main>
<style> <style>
main { main {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem;
} }
</style> </style>

View file

@ -16,6 +16,9 @@
box-sizing: border-box; box-sizing: border-box;
} }
body { body,
#app,
main {
margin: 0; margin: 0;
min-height: 100vh;
} }

View file

@ -1,10 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, tick } from "svelte"; import { onDestroy, onMount, tick } from "svelte";
import type { EditorView } from "prosemirror-view"; import type { EditorView } from "prosemirror-view";
import { EditorState, TextSelection } from "prosemirror-state"; import { EditorState, TextSelection } from "prosemirror-state";
import type { Node as ProsemirrorNode } from "prosemirror-model";
import { toggleMark } from "prosemirror-commands";
import type { Mark } from "prosemirror-model";
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";
@ -19,47 +16,15 @@
updateMark, updateMark,
removeMark, removeMark,
markIsActive, markIsActive,
commandListener,
getFirstMarkInSelection, getFirstMarkInSelection,
} from "./ps-utils"; } from "./ps-utils";
import { refreshCoords as _refreshCoords } from "./bubblemenu/coords";
import SimpleMarkItem from "./bubblemenu/SimpleMarkItem.svelte"; import SimpleMarkItem from "./bubblemenu/SimpleMarkItem.svelte";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import Button from "./bubblemenu/Button.svelte";
export let view: EditorView; export let view: EditorView;
export let state: EditorState; export let state: EditorState;
// == Posicionamiento ==
let bubbleEl: HTMLElement = null;
let left: number = 0;
let bottom: number = 0;
function refreshCoords() {
const coords = _refreshCoords(view, bubbleEl);
left = coords.left;
bottom = coords.bottom;
}
$: {
// refrescar cuando cambia state
view;
state;
if (bubbleEl) refreshCoords();
}
let resizeObserver = new ResizeObserver(() => {
if (bubbleEl) refreshCoords();
});
$: {
view;
resizeObserver.disconnect();
if (bubbleEl) {
resizeObserver.observe(bubbleEl.parentElement);
}
}
onDestroy(() => resizeObserver.disconnect());
// == /Posicionamiento ==
let changingProp: let changingProp:
| false | false
| { type: "link"; url: string } | { type: "link"; url: string }
@ -85,8 +50,7 @@
command(state, view.dispatch); command(state, view.dispatch);
} }
function startEditingLink(event: Event) { function startEditingLink() {
const { to, from } = state.selection;
const match = getFirstMarkInSelection(state, view.state.schema.marks.link); const match = getFirstMarkInSelection(state, view.state.schema.marks.link);
// si no hay un link en la selección, empezar a editar uno sin ningún enlace // si no hay un link en la selección, empezar a editar uno sin ningún enlace
@ -127,14 +91,33 @@
} }
const svgStyle = "width: 100%; height: 100%"; 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> </script>
<div <div class="bubble" hidden={state.selection.empty} style={barStyle}>
bind:this={bubbleEl}
class="bubble"
hidden={state.selection.empty}
style="left: {left}px; bottom: {bottom}px"
>
{#if changingProp === false} {#if changingProp === false}
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strong} <SimpleMarkItem {view} {state} type={view.state.schema.marks.strong}
><BoldIcon style={svgStyle} /></SimpleMarkItem ><BoldIcon style={svgStyle} /></SimpleMarkItem
@ -148,17 +131,13 @@
<SimpleMarkItem {view} {state} type={view.state.schema.marks.strikethrough} <SimpleMarkItem {view} {state} type={view.state.schema.marks.strikethrough}
><StrikethroughIcon style={svgStyle} /></SimpleMarkItem ><StrikethroughIcon style={svgStyle} /></SimpleMarkItem
> >
<button <Button
type="button" active={markIsActive(state, view.state.schema.marks.link)}
class:active={markIsActive(state, view.state.schema.marks.link)} onClick={startEditingLink}><LinkIcon style={svgStyle} /></Button
on:mousedown|preventDefault={startEditingLink}
><LinkIcon style={svgStyle} /></button
> >
<button <Button
type="button" active={markIsActive(state, view.state.schema.marks.internal_link)}
class:active={markIsActive(state, view.state.schema.marks.internal_link)} onClick={createInternalLink}><InternalLinkIcon style={svgStyle} /></Button
on:mousedown|preventDefault={createInternalLink}
><InternalLinkIcon style={svgStyle} /></button
> >
{:else if changingProp.type === "link"} {:else if changingProp.type === "link"}
<input <input
@ -168,11 +147,40 @@
on:change|preventDefault={onChangeLink} on:change|preventDefault={onChangeLink}
value={changingProp.url} value={changingProp.url}
/> />
<button <Button title="Borrar enlace" onClick={removeLink}
type="button" ><CloseIcon style={svgStyle} /></Button
title="Borrar enlace"
on:mousedown|preventDefault={removeLink}
><CloseIcon style={svgStyle} /></button
> >
{/if} {/if}
</div> </div>
<style>
.bubble {
display: flex;
position: fixed;
left: 0px;
bottom: 0px;
padding: 0rem;
/* https://wicg.github.io/visual-viewport/examples/fixed-to-keyboard.html */
transform-origin: left bottom;
border-top: 1px solid #ccc;
width: 100%;
visibility: visible;
opacity: 1;
transition: opacity 0.2s, visibility 0.2s;
}
.bubble[hidden] {
visibility: hidden;
opacity: 0;
}
.bubble input {
appearance: none;
background: none;
color: inherit;
border: none;
font-size: 1.25em;
}
</style>

View file

@ -67,9 +67,19 @@
<div class="editor"> <div class="editor">
{#if view} {#if view}
<BubbleMenu {view} state={updatedState} />
<MenuBar {view} state={updatedState} /> <MenuBar {view} state={updatedState} />
{/if} {/if}
<!-- this element gets replaced with the editor itself when mounted --> <!-- this element gets replaced with the editor itself when mounted -->
<div bind:this={wrapperEl} /> <div bind:this={wrapperEl} />
{#if view}
<BubbleMenu {view} state={updatedState} />
{/if}
</div> </div>
<style>
.editor,
div,
:global(.ProseMirror) {
min-height: 100vh;
}
</style>

View file

@ -0,0 +1,41 @@
<script lang="ts">
export let active: boolean = false;
export let onClick: (event: Event) => void;
export let title: string = "";
</script>
<button
type="button"
{title}
class:active
on:mousedown|preventDefault={onClick}
>
<slot />
</button>
<style>
button {
appearance: none;
background: none;
color: inherit;
border: none;
border-radius: 2px;
padding: 0.3em;
margin: 0.2em;
width: 2rem;
height: 2rem;
font-size: 1em;
line-height: 1;
transition: background 0.2s;
}
/* https://stackoverflow.com/a/64553121 */
@media (hover: hover) and (pointer: fine) {
button:hover {
background: #eee;
}
}
button.active {
background: #ddd;
}
</style>

View file

@ -5,6 +5,7 @@
import { toggleMark } from "prosemirror-commands"; import { toggleMark } from "prosemirror-commands";
import { commandListener, markIsActive } from "../ps-utils"; import { commandListener, markIsActive } from "../ps-utils";
import Button from "./Button.svelte";
export let view: EditorView; export let view: EditorView;
export let state: EditorState; export let state: EditorState;
@ -15,10 +16,10 @@
$: listener = commandListener(view, toggleMark(type)); $: listener = commandListener(view, toggleMark(type));
</script> </script>
<button type="button" class:active={isActive} on:mousedown={listener}> <Button active={isActive} onClick={listener}>
{#if small} {#if small}
<small><slot /></small> <small><slot /></small>
{:else} {:else}
<slot /> <slot />
{/if} {/if}
</button> </Button>

View file

@ -1,104 +0,0 @@
import type { EditorView } from "prosemirror-view";
function textRange(
node: Node,
from: number = 0,
to: number | null = null
): Range {
const range = document.createRange();
range.setEnd(node, to == null ? node.nodeValue.length : to);
range.setStart(node, Math.max(from, 0));
return range;
}
function singleRect(object: Element | Range, bias: number) {
const rects = object.getClientRects();
return !rects.length
? object.getBoundingClientRect()
: rects[bias < 0 ? 0 : rects.length - 1];
}
interface Coords {
top: number;
bottom: number;
left: number;
right: number;
}
function coordsAtPos(
view: EditorView,
pos: number,
end: boolean = false
): Coords {
const { node, offset } = (view as any).docView.domFromPos(pos);
let side: "left" | "right";
let rect: DOMRect;
if (node.nodeType === 3) {
if (end && offset < node.nodeValue.length) {
rect = singleRect(textRange(node, offset - 1, offset), -1);
side = "right";
} else if (offset < node.nodeValue.length) {
rect = singleRect(textRange(node, offset, offset + 1), -1);
side = "left";
}
} else if (node.firstChild) {
if (offset < node.childNodes.length) {
const child = node.childNodes[offset];
rect = singleRect(child.nodeType === 3 ? textRange(child) : child, -1);
side = "left";
}
if ((!rect || rect.top === rect.bottom) && offset) {
const child = node.childNodes[offset - 1];
rect = singleRect(child.nodeType === 3 ? textRange(child) : child, 1);
side = "right";
}
} else {
rect = node.getBoundingClientRect();
side = "left";
}
const x = rect[side];
return {
top: rect.top,
bottom: rect.bottom,
left: x,
right: x,
};
}
export function refreshCoords(view: EditorView, bubbleEl: HTMLElement) {
// Brutally stolen from https://github.com/ueberdosis/tiptap/blob/d2cf88fd166092d6df079cb47fe2a55520fadf80/packages/tiptap/src/Plugins/MenuBubble.js
const { from, to } = view.state.selection;
// These are in screen coordinates
// We can't use EditorView.coordsAtPos here because it can't handle linebreaks correctly
// See: https://github.com/ProseMirror/prosemirror-view/pull/47
const start = coordsAtPos(view, from);
const end = coordsAtPos(view, to, true);
// The box in which the tooltip is positioned, to use as base
const parent = bubbleEl.offsetParent;
if (!parent) {
console.error(
"Me parece que te falto importar el CSS. `import '@suttyweb/editor/dist/style.css';`"
);
// TODO: i18n
throw new Error(
"¡El editor tuvo un error! Contactar a lxs desarrolladorxs."
);
}
const box = parent.getBoundingClientRect();
const el = bubbleEl.getBoundingClientRect();
const _left = Math.max((start.left + end.left) / 2 - box.left, el.width / 2);
return {
left: Math.round(
_left + el.width / 2 > box.width ? box.width - el.width / 2 : _left
),
bottom: Math.round(box.bottom - start.top),
top: Math.round(end.bottom - box.top),
};
}

View file

@ -23,11 +23,6 @@
border-bottom: 1px solid #bbb; border-bottom: 1px solid #bbb;
} }
.editor .menubar .separator {
border-right: 2px solid #bbb;
margin: 0 0.5rem;
}
.editor .menubar button { .editor .menubar button {
appearance: none; appearance: none;
background: none; background: none;
@ -52,76 +47,6 @@
height: 1.5rem; height: 1.5rem;
} }
.editor .bubble {
display: flex !important;
position: absolute;
z-index: 420;
transform: translateX(-50%);
background: black;
color: white;
border-radius: 5px;
padding: 0rem;
margin-bottom: 0.5rem;
visibility: visible;
opacity: 1;
transition: opacity 0.2s, visibility 0.2s;
}
.editor .bubble[hidden] {
visibility: hidden;
opacity: 0;
}
.editor .bubble input {
appearance: none;
background: none;
color: inherit;
border: none;
font-size: 1.25em;
}
.editor .bubble .separator {
border-right: 1px solid #777;
margin: 0 0.5rem;
}
.editor .bubble button {
appearance: none;
background: none;
color: inherit;
border: none;
border-radius: 2px;
padding: 0.3em;
margin: 0.2em;
width: 1.8rem;
height: 1.8rem;
font-size: 1em;
line-height: 1;
transition: background 0.2s;
}
.editor .bubble button:hover {
background: #333;
}
.editor .bubble button.active {
background: #555;
}
.editor .bubble .color-button {
padding: 0.4em;
}
.editor .bubble .color {
display: inline-block;
width: 1em;
height: 1em;
border-radius: 100%;
}
.editor .bubble p {
margin: 0;
}
.ProseMirror { .ProseMirror {
width: 100%; width: 100%;
padding: 0.5em; padding: 0.5em;