Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feedback from Seth #178

Merged
merged 5 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-berries-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rhino-editor": minor
---

Add `getHTMLContentFromRange()` and `getTextContentFromRange()` functions
5 changes: 5 additions & 0 deletions .changeset/six-jars-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rhino-editor": patch
---

Fixed a bug where the current focused figure did not have an outline
5 changes: 5 additions & 0 deletions .changeset/soft-books-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rhino-editor": patch
---

Close link dialogs when clicking outside the editor or on other toolbar items
5 changes: 5 additions & 0 deletions .changeset/twenty-hornets-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rhino-editor": patch
---

Fixed a bug where figcaptions would never update if you did not interact with the editor
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
title: Getting HTML and Text content of the editor
permalink: /how-tos/getting-html-and-text-content/
---

Sometimes you may want to grab either a plain text representation or an HTML representation of Rhino Editor's content, or even the currently selected content.

Rhino Editor exposes 2 functions to help with this.

## getHTMLContentFromRange

The first is `getHTMLContentFromRange(from, to)` where `from` and `to` are `number`. They are optional. If no parameters are passed, it will read from the user's current selection.

### getHTMLContentFromRange examples

#### Getting the HTML content of the user's current selection

To get the HTML content of what the user has highlighted, you can do so by calling `getHTMLContentFromRange()` with no parameters. Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
rhinoEditor.getHTMLContentFromRange()
```

#### Getting HTML content for a given range

Sometimes you may want to get the HTML content for a given range, to do so, pass in a `from` and `to` parameters that are a `number`. Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
rhinoEditor.getHTMLContentFromRange(0, 50)
```

#### Getting selected content and falling back to full editor content

Sometimes you may want to get the current selection of the user, or fall back to the entire editor's HTML if the user has not selected anything. To do so, we can conditionally check if the `getHTMLContentFromRange()` is "empty". Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
let html = rhinoEditor.getHTMLContentFromRange()

// If the user has nothing currently highlighted, fallback to getting the full HTML of the editor.
if (!html) {
html = rhinoEditor.editor.getHTML()
}
```

## getTextContentFromRange

`getTextContentFromRange()` has much the same API as `getHTMLContentFromRange()`. You can either pass 2 numbers as a range, or you can pass nothing it will return the user's currently selected text. Let's look at the previous example but instead now grabbing text.

### getTextContentFromRange examples

#### Getting the text content of the user's current selection

To get the Text content of what the user has highlighted, you can do so by calling `getTextContentFromRange()` with no parameters. Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
rhinoEditor.getTextContentFromRange()
```

#### Getting Text content for a given range

Sometimes you may want to get the Text content for a given range, to do so, pass in a `from` and `to` parameters that are a `number`. Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
rhinoEditor.getTextContentFromRange(0, 50)
```

#### Getting selected content and falling back to full editor content

Sometimes you may want to get the current selection of the user, or fall back to the entire editor's Text if the user has not selected anything. To do so, we can conditionally check if the `getTextContentFromRange()` is "empty". Like so:

```js
const rhinoEditor = document.querySelector("rhino-editor")
let text = rhinoEditor.getTextContentFromRange()

// If the user has nothing currently highlighted, fallback to getting the full Text of the editor.
if (!text) {
text = rhinoEditor.editor.getText()
}
```

13 changes: 7 additions & 6 deletions esbuild.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ const deps = [

const watchMode = process.argv.includes("--watch")

import {
hostStyles,
toolbarButtonStyles
} from "./src/exports/styles/editor.js"


/**
* @return {import("esbuild").Plugin}
*/
Expand All @@ -33,6 +27,13 @@ function AppendCssStyles () {
{ encoding: "utf8" }
)

let date = new Date()

const {
hostStyles,
toolbarButtonStyles
} = await import(`./src/exports/styles/editor.js?cache=${date.toString()}`)

const finalString = `/* THIS FILE IS AUTO-GENERATED. DO NOT EDIT BY HAND! */
${styles.toString()}
/* src/exports/styles/editor.js:hostStyles */
Expand Down
116 changes: 115 additions & 1 deletion src/exports/elements/tip-tap-editor-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { RhinoBlurEvent } from "../events/rhino-blur-event.js";
import { RhinoChangeEvent } from "../events/rhino-change-event.js";
import { SelectionChangeEvent } from "../events/selection-change-event.js";
import { RhinoPasteEvent } from "../events/rhino-paste-event.js";
import { Slice } from "@tiptap/pm/model";
import { DOMSerializer, Slice } from "@tiptap/pm/model";
import { EditorView } from "@tiptap/pm/view";

export type Serializer = "" | "html" | "json";
Expand Down Expand Up @@ -191,6 +191,120 @@ export class TipTapEditorBase extends BaseElement {
this.requestUpdate();
}

/**
* Grabs HTML content based on a given range. If no range is given, it will return the contents
* of the current editor selection. If the current selection is empty, it will return an empty string.
* @param from - The start of the selection
* @param to - The end of the selection
* @example Getting the HTML content of the current selection
* const rhinoEditor = document.querySelector("rhino-editor")
* rhinoEditor.getHTMLContentFromRange()
*
* @example Getting the HTML content of node range
* const rhinoEditor = document.querySelector("rhino-editor")
* rhinoEditor.getHTMLContentFromRange(0, 50)
*
* @example Getting the HTML content and falling back to entire editor HTML
* const rhinoEditor = document.querySelector("rhino-editor")
* let html = rhinoEditor.getHTMLContentFromRange()
* if (!html) {
* html = rhinoEditor.editor.getHTML()
* }
*/
getHTMLContentFromRange(from?: number, to?: number) {
const editor = this.editor;

if (!editor) return "";

let empty;

if (!from && !to) {
const currentSelection = editor.state.selection;

from = currentSelection.from;
to = currentSelection.to;
}

if (empty) {
return "";
}
if (from == null) {
return "";
}
if (to == null) {
return "";
}

const { state } = editor;
const htmlArray: string[] = [];

const tempScript = document.createElement("script");
// We want plain text so we don't parse.
tempScript.type = "text/plain";

state.doc.nodesBetween(from, to, (node, _pos, parent) => {
if (parent === state.doc) {
tempScript.innerHTML = "";
const serializer = DOMSerializer.fromSchema(editor.schema);
const dom = serializer.serializeNode(node);
tempScript.appendChild(dom);
htmlArray.push(tempScript.innerHTML);
tempScript.innerHTML = "";
}
});

return htmlArray.join("");
}

/**
* Grabs plain text representation based on a given range. If no parameters are given, it will return the contents
* of the current selection. If the current selection is empty, it will return an empty string.
* @param from - The start of the selection
* @param to - The end of the selection
* @example Getting the Text content of the current selection
* const rhinoEditor = document.querySelector("rhino-editor")
* rhinoEditor.getTextContentFromRange()
*
* @example Getting the Text content of node range
* const rhinoEditor = document.querySelector("rhino-editor")
* rhinoEditor.getTextContentFromRange(0, 50)
*
* @example Getting the Text content and falling back to entire editor Text
* const rhinoEditor = document.querySelector("rhino-editor")
* let text = rhinoEditor.getTextContentFromRange()
* if (!text) {
* text = rhinoEditor.editor.getText()
* }
*/
getTextContentFromRange(from?: number, to?: number) {
const editor = this.editor;

if (!editor) {
return "";
}

let empty;

if (!from && !to) {
const selection = editor.state.selection;
from = selection.from;
to = selection.to;
empty = selection.empty;
}

if (empty) {
return "";
}
if (from == null) {
return "";
}
if (to == null) {
return "";
}

return editor.state.doc.textBetween(from, to, " ");
}

protected willUpdate(
changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
): void {
Expand Down
60 changes: 42 additions & 18 deletions src/exports/elements/tip-tap-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ export class TipTapEditor extends TipTapEditorBase {
if (this.editor) {
this.editor.on("focus", this.closeLinkDialog);
}

document.addEventListener("click", this.__handleLinkDialogClick);
}

disconnectedCallback() {
super.disconnectedCallback();
document.removeEventListener("click", this.__handleLinkDialogClick);
}

get icons(): typeof icons {
Expand Down Expand Up @@ -221,7 +228,6 @@ export class TipTapEditor extends TipTapEditorBase {
if (this.linkDialog == null) return;

this.linkDialogExpanded = false;
this.linkDialog.setAttribute("hidden", "");
}

showLinkDialog(): void {
Expand All @@ -235,16 +241,13 @@ export class TipTapEditor extends TipTapEditorBase {

this.__invalidLink__ = false;
this.linkDialogExpanded = true;
this.linkDialog.removeAttribute("hidden");
setTimeout(() => {
if (inputElement != null) inputElement.focus();
});
}

get linkDialog(): Maybe<HTMLAnchorElement> {
return this.shadowRoot?.querySelector(
".link-dialog",
) as Maybe<HTMLAnchorElement>;
get linkDialog(): Maybe<HTMLDivElement> {
return this.shadowRoot?.querySelector<HTMLDivElement>("#link-dialog");
}

attachFiles(): void {
Expand Down Expand Up @@ -1074,24 +1077,45 @@ export class TipTapEditor extends TipTapEditorBase {
`;
}

/**
* @private
*/
private __handleLinkDialogClick = (e: Event) => {
const linkDialogContainer = this.shadowRoot?.querySelector(
".link-dialog__container",
);

if (!linkDialogContainer) {
this.linkDialogExpanded = false;
return;
}

const composedPath = e.composedPath();

const linkButton = this.shadowRoot?.querySelector("[name='link-button']");

if (composedPath.includes(linkDialogContainer as EventTarget)) {
return;
}

if (linkButton && composedPath.includes(linkButton as EventTarget)) {
return;
}

this.linkDialogExpanded = false;
};

/** @TODO: Lets think of a more friendly way to render dialogs for users to extend. */
renderDialog(): TemplateResult {
if (this.readonly) return html``;
if (this.readonly) {
return html``;
}

return html` <div
id="link-dialog"
class="link-dialog"
part="link-dialog"
hidden
@click=${(event: MouseEvent) => {
const target = event.target as HTMLElement;
const currentTarget = event.currentTarget as HTMLElement;

if (currentTarget.contains(target) && currentTarget !== target) {
return;
}

this.closeLinkDialog();
}}
?hidden=${!this.linkDialogExpanded}
>
<div class="link-dialog__container" part="link-dialog__container">
<input
Expand Down
7 changes: 1 addition & 6 deletions src/exports/extensions/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,6 @@ function handleCaptions(

if (figureTypes.includes(node.type.name) === false) return modified;

// Make sure the user isn't selecting multiple nodes.
if (newState.selection.from !== newState.selection.to) {
return modified;
}

// @see https://discuss.prosemirror.net/t/saving-content-containing-dom-generated-by-nodeview/2594/5
let scratch = document.createElement("div");
scratch.appendChild(
Expand Down Expand Up @@ -775,7 +770,7 @@ export const Attachment = Node.create<AttachmentOptions>({
)}

<figcaption
style="display: ${Boolean(content) ? "none" : ""};"
style="${Boolean(content) ? "display: none;" : ""}"
class=${`attachment__caption ${
caption ? "attachment__caption--edited" : "is-empty"
}`}
Expand Down
Loading
Loading