@patternslib/pat-tiptap
Version:
A pattern for tiptap
1,174 lines (977 loc) • 65.6 kB
JavaScript
import Pattern from "./tiptap";
import events from "@patternslib/patternslib/src/core/events";
import utils from "@patternslib/patternslib/src/core/utils";
import tiptap_utils from "./utils";
import PatternModal from "@patternslib/patternslib/src/pat/modal/modal";
// Mock some events which are needed by TipTap
window.ClipboardEvent = jest.fn();
window.DragEvent = jest.fn();
const mockFetch =
(text = "") =>
() =>
Promise.resolve({
text: () => Promise.resolve(text),
});
const SUGGESTION_RESPONSE = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Mentions results</title>
</head>
<body>
<section class="tiptap-items">
<ul>
<li class="tiptap-item" data-tiptap-value="item a">
<a href="https://demo.com/itema"
class="class_item_a"
data-pat-inject="source:#some"
data-mention="jepp">
first
<span class="small">subtext</span>
</a>
</li>
<li class="tiptap-item" data-tiptap-value="item b"><a href="https://demo.com/itemb" class="class_item_b">second</a></li>
<li class="tiptap-item" data-tiptap-value="item c"><a href="https://demo.com/itemc" class="class_item_c" title="okay">third</a></li>
</ul>
</section>
</body>
</html>
`;
describe("pat-tiptap", () => {
afterEach(() => {
document.body.innerHTML = "";
});
it("1.1 - is initialized correctly on textarea elements", async () => {
document.body.innerHTML = `<textarea class="pat-tiptap">hello</textarea>`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
expect(document.querySelector(".pat-tiptap").style.display).toBe("none"); // prettier-ignore
expect(document.querySelector(".tiptap-container [contenteditable]").textContent).toBe("hello"); // prettier-ignore
expect(document.querySelector(".tiptap-container [contenteditable]").innerHTML).toBe("<p>hello</p>"); // prettier-ignore
});
it("1.2 - is initialized correctly on div elements", async () => {
document.body.innerHTML = `<div class="pat-tiptap" contenteditable>hello</div>`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
expect(document.querySelector(".pat-tiptap").style.display).toBe("none"); // prettier-ignore
expect(document.querySelector(".tiptap-container [contenteditable]").textContent).toBe("hello"); // prettier-ignore
expect(document.querySelector(".tiptap-container [contenteditable]").innerHTML).toBe("<p>hello</p>"); // prettier-ignore
});
it("1.3 - Allow multiple instances of pat-tiptap", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar-1">
<a class="button-link pat-modal" href="#modal-link">Link</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar-1;
link-panel: #link-panel
">
</textarea>
<div id="tiptap-external-toolbar-2">
<a class="button-link pat-modal" href="#modal-link">Link</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar-2;
link-panel: #link-panel
">
</textarea>
<template id="modal-link">
<form id="link-panel">
<input name="tiptap-href"/>
<input name="tiptap-text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern1 = new Pattern(document.querySelectorAll(".pat-tiptap")[0]);
await events.await_pattern_init(pattern1);
const pattern2 = new Pattern(document.querySelectorAll(".pat-tiptap")[1]);
await events.await_pattern_init(pattern2);
const containers = document.querySelectorAll(".tiptap-container");
const button_1 = document.querySelector("#tiptap-external-toolbar-1 .button-link"); // prettier-ignore
const button_2 = document.querySelector("#tiptap-external-toolbar-2 .button-link"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_1));
await events.await_pattern_init(new PatternModal(button_2));
containers[0].querySelector("[contenteditable]").focus(); // Set focus to bypass toolbar check
button_1.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#link-panel [name=tiptap-href]").value = "https://url1.com/"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text 1"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
await utils.timeout(1);
containers[1].querySelector("[contenteditable]").focus(); // Set focus to bypass toolbar check
button_2.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#link-panel [name=tiptap-href]").value = "https://url2.com/"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text 2"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
const anchor1 = containers[0].querySelector("a");
expect(anchor1).toBeTruthy();
expect(anchor1.href).toBe("https://url1.com/");
expect(anchor1.textContent).toBe("Link text 1");
const anchor2 = containers[1].querySelector("a");
expect(anchor2).toBeTruthy();
expect(anchor2.href).toBe("https://url2.com/");
expect(anchor2.textContent).toBe("Link text 2");
});
it("1.4 - Allow multiple instances of pat-tiptap, sharing the same toolbar", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-link pat-modal" href="#modal-link">Link</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
link-panel: #link-panel
">
</textarea>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
link-panel: #link-panel
">
</textarea>
<template id="modal-link">
<form id="link-panel">
<input name="tiptap-href"/>
<input name="tiptap-text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern1 = new Pattern(document.querySelectorAll(".pat-tiptap")[0]);
await events.await_pattern_init(pattern1);
const pattern2 = new Pattern(document.querySelectorAll(".pat-tiptap")[1]);
await events.await_pattern_init(pattern2);
const containers = document.querySelectorAll(".tiptap-container");
const button_link = document.querySelector("#tiptap-external-toolbar .button-link"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_link));
containers[0].querySelector("[contenteditable]").focus(); // Set focus to bypass toolbar check
await utils.timeout(1);
button_link.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#link-panel [name=tiptap-href]").value = "https://url1.com/"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text 1"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
await utils.timeout(1);
containers[1].querySelector("[contenteditable]").focus(); // Set focus to bypass toolbar check
await utils.timeout(1);
button_link.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#link-panel [name=tiptap-href]").value = "https://url2.com/"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text 2"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
await utils.timeout(1);
const anchor1 = containers[0].querySelector("a");
expect(anchor1).toBeTruthy();
expect(anchor1.href).toBe("https://url1.com/");
expect(anchor1.textContent).toBe("Link text 1");
const anchor2 = containers[1].querySelector("a");
expect(anchor2).toBeTruthy();
expect(anchor2.href).toBe("https://url2.com/");
expect(anchor2.textContent).toBe("Link text 2");
});
it("1.5 - allows non-paragraph line breaks", async () => {
document.body.innerHTML = `
<textarea class="pat-tiptap">
<p>hello<br><br>there</p>
</textarea>
`;
const instance = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(instance);
expect(
document.querySelector(".tiptap-container [contenteditable]").innerHTML
).toBe("<p>hello<br><br>there</p>");
expect(instance.editor.getHTML()).toBe("<p>hello<br><br>there</p>");
});
it("1.6 - Emits input events on update.", async () => {
document.body.innerHTML = `
<textarea class="pat-tiptap">
</textarea>
`;
const el = document.querySelector(".pat-tiptap");
const pattern = new Pattern(el);
await events.await_pattern_init(pattern);
const tiptap = document.querySelector(".tiptap-container [contenteditable]");
let changed = false;
el.addEventListener("input", () => {
changed = true;
});
tiptap.innerHTML = "<p>hello</p>";
await utils.timeout(1);
expect(el.value).toBe("<p>hello</p>");
expect(changed).toBe(true);
});
it("2.1 - adds a placeholder element.", async () => {
document.body.innerHTML = `
<textarea
class="pat-tiptap"
placeholder="hello there."
>
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
expect(
document.querySelector(
".tiptap-container [contenteditable] [data-placeholder='hello there.']"
)
).toBeTruthy();
});
it("3.1 - sets focus with the autofocus attribute", async () => {
document.body.innerHTML = `
<textarea
class="pat-tiptap"
autofocus
>
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
await utils.timeout(30); // wait some time before tiptap sets focus.
const editor_el = document.querySelector(".tiptap-container *[contenteditable]");
expect(document.querySelector("*:focus")).toBe(editor_el);
});
it("3.2 - sets focus with the pat-autofocus class", async () => {
document.body.innerHTML = `
<textarea
class="pat-tiptap pat-autofocus"
autofocus
>
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
await utils.timeout(30); // wait some time before tiptap sets focus.
const editor_el = document.querySelector(".tiptap-container *[contenteditable]");
expect(document.querySelector("*:focus")).toBe(editor_el);
});
it("4.1 - un/sets focus on the toolbar", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar"></div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
document.querySelector(".tiptap-container [contenteditable]").focus();
expect(document.querySelector("#tiptap-external-toolbar").classList[0]).toBe("tiptap-focus"); // prettier-ignore
document.querySelector(".tiptap-container [contenteditable]").blur();
expect(document.querySelector("#tiptap-external-toolbar").classList.length).toBe(0); // prettier-ignore
});
it("4.2 - un/sets focus on the toolbar with autofocus", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar"></div>
<textarea
autofocus
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
await utils.timeout(30);
expect(document.querySelector("#tiptap-external-toolbar").classList[0]).toBe("tiptap-focus"); // prettier-ignore
});
it("4.3 - un/sets focus on the toolbar when clicking into the toolbar", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar"><button>click</button></div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
document.querySelector("#tiptap-external-toolbar button").focus();
expect(document.querySelector("#tiptap-external-toolbar").classList[0]).toBe("tiptap-focus"); // prettier-ignore
document.querySelector("#tiptap-external-toolbar button").blur();
expect(document.querySelector("#tiptap-external-toolbar").classList.length).toBe(0); // prettier-ignore
});
describe("5 - Link tests", () => {
let button_link;
beforeEach(async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-link pat-modal" href="#modal-link">Link</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
link-panel: #link-panel;
link-menu: #context-menu-link;
link-extra-protocols: fantasy;
">
</textarea>
<template id="modal-link">
<form id="link-panel">
<input name="tiptap-href"/>
<input name="tiptap-text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
<template id="context-menu-link">
<div class="tiptap-link-context-menu">
<a
class="close-panel tiptap-open-new-link"
target="_blank"
href="">Visit linked web page</a>
<button
type="button"
class="close-panel tiptap-edit-link">Edit link</button>
<button
type="button"
class="close-panel tiptap-unlink">Unlink</button>
</div>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
button_link = document.querySelector("#tiptap-external-toolbar .button-link"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_link));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_link.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
});
afterEach(() => {
document.body.innerHTML = "";
});
it("5.1 - Adds a link", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "https://patternslib.com/"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor).toBeTruthy();
expect(anchor.href).toBe("https://patternslib.com/");
expect(anchor.textContent).toBe("Link text");
});
it("5.2 - Corrects a link with missing protocoll.", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor).toBeTruthy();
expect(anchor.href).toBe("https://patternslib.com/");
expect(anchor.textContent).toBe("Link text");
});
it("5.3 - Adds a mailto address.", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "mailto:info@patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Mail text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor.href).toBe("mailto:info@patternslib.com");
expect(anchor.textContent).toBe("Mail text");
});
it("5.4 - Corrects a mailto address with missing protocoll.", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "info@patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Mail text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor.href).toBe("mailto:info@patternslib.com");
expect(anchor.textContent).toBe("Mail text");
});
it("5.5 - Handles exotic protocolls, like ftp://", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "ftp://patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "FTP text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor.href).toBe("ftp://patternslib.com/");
expect(anchor.textContent).toBe("FTP text");
});
it("5.6 - ... or callto:", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "callto:0123456789"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Callto text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor.href).toBe("callto:0123456789");
expect(anchor.textContent).toBe("Callto text");
});
it("5.7 - Supports obscure protocols, when configured.", async () => {
document.querySelector("#link-panel [name=tiptap-href]").value = "fantasy://patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const anchor = document.querySelector(".tiptap-container a");
expect(anchor).toBeTruthy();
expect(anchor.href).toBe("fantasy://patternslib.com");
expect(anchor.textContent).toBe("Link text");
});
it("5.8 - Opens a link context menu", async () => {
// Add a link to test the context menu on.
document.querySelector("#link-panel [name=tiptap-href]").value = "https://patternslib.com"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-text]").value = "Link text"; // prettier-ignore
document.querySelector("#link-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const editable = document.querySelector(
".tiptap-container [contenteditable]",
);
const range = document.createRange();
const sel = window.getSelection();
// Add the triggering character into the content editable
editable.innerHTML = "<p>@</p>";
// Set the cursor right after the @-sign.
range.setStart(editable.childNodes[0].childNodes[0], 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
// Wait some ticks.
await utils.timeout(1);
await utils.timeout(1);
// Context menu not yet opened.
expect(document.querySelector(".tiptap-link-context-menu")).toBeFalsy();
// Context menu opens with a 50ms delay.
await utils.timeout(50);
// Context menu should be opened now.
expect(document.querySelector(".tiptap-link-context-menu")).toBeTruthy();
// Wait two more ticks for the context menu to be fully initialized.
await utils.timeout(1);
await utils.timeout(1);
// UI elements should be initialized now.
expect(
document.querySelector(".tiptap-link-context-menu .tiptap-open-new-link")
.href,
).toBe("https://patternslib.com/");
});
});
it("6.1 - Adds an image within <figure> tags including a <figcaption>", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-image pat-modal" href="#modal-image">Image</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
image-panel: #image-panel
">
</textarea>
<template id="modal-image">
<form id="image-panel">
<input name="tiptap-src" type="text"/>
<input name="tiptap-alt"/>
<input name="tiptap-title"/>
<input name="tiptap-caption"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_image = document.querySelector("#tiptap-external-toolbar .button-image"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_image));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_image.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#image-panel [name=tiptap-src]").value = "https://path/to/image.png"; // prettier-ignore
document.querySelector("#image-panel [name=tiptap-alt]").value = "Alt text for image"; // prettier-ignore
document.querySelector("#image-panel [name=tiptap-title]").value = "Title text for image"; // prettier-ignore
document.querySelector("#image-panel [name=tiptap-caption]").value = "Caption text for image"; // prettier-ignore
document.querySelector("#image-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const img = document.querySelector(".tiptap-container figure img");
expect(img).toBeTruthy();
expect(img.src).toBe("https://path/to/image.png");
expect(img.alt).toBe("Alt text for image");
expect(img.title).toBe("Title text for image");
const figcaption = document.querySelector(".tiptap-container figure figcaption");
expect(figcaption).toBeTruthy();
expect(figcaption.textContent).toBe("Caption text for image");
});
it("6.2 - Adds an image within <figure> tags but without a <figcaption>", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-image pat-modal" href="#modal-image">Image</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
image-panel: #image-panel
">
</textarea>
<template id="modal-image">
<form id="image-panel">
<input name="tiptap-src" type="text"/>
<input name="tiptap-alt"/>
<input name="tiptap-title"/>
<input name="tiptap-caption"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
<template id="modal-image">
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_image = document.querySelector("#tiptap-external-toolbar .button-image"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_image));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_image.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#image-panel [name=tiptap-src]").value = "https://path/to/image.png"; // prettier-ignore
document.querySelector("#image-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const img = document.querySelector(".tiptap-container figure img");
expect(img).toBeTruthy();
expect(img.src).toBe("https://path/to/image.png");
expect(img.alt).toBe("");
expect(img.title).toBe("");
const figcaption = document.querySelector(".tiptap-container figure figcaption");
expect(figcaption).toBeFalsy();
});
it("6.3 - Adds an image with base64 encoded image data", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-image pat-modal" href="#modal-image">Image</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
image-panel: #image-panel
">
</textarea>
<template id="modal-image">
<form id="image-panel">
<input name="tiptap-src" type="text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_image = document.querySelector("#tiptap-external-toolbar .button-image"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_image));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_image.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#image-panel [name=tiptap-src]").value =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2wAAAAAElFTkSuQmCC";
document.querySelector("#image-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const img = document.querySelector(".tiptap-container figure img");
expect(img).toBeTruthy();
expect(img.src).toBe(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2wAAAAAElFTkSuQmCC"
);
});
it("6.4 - Allow to parse inline images", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<button class="button-image">Image</button>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
">
<img src="https://url.to/image-1" />
<p>
some text
<img src="https://url.to/image-2" />
more text
<img src="https://url.to/image-3" />
</p>
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
// Images are parsed and shown in the editor
expect(
document.querySelectorAll(
".tiptap-container img:not(.ProseMirror-separator)"
).length
).toBe(3);
// Also those .ProseMirror-trailingBreak <br>s are added.
// Only two of them - there is inline content after the second image and ProseMirror doesn't add the extra <br> in that case.
// Also see: https://github.com/ProseMirror/prosemirror/issues/1241
expect(document.querySelectorAll(".ProseMirror-trailingBreak").length).toBe(2);
});
it("7.1 - Add a YouTube embed", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-embed pat-modal" href="#modal-embed">Embed</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
embed-panel: #embed-panel
">
</textarea>
<template id="modal-embed">
<form id="embed-panel">
<input name="tiptap-src" type="text"/>
<input name="tiptap-title"/>
<input name="tiptap-caption"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_embed = document.querySelector("#tiptap-external-toolbar .button-embed"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_embed));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_embed.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#embed-panel [name=tiptap-src]").value = "https://www.youtube.com/embed/j8It1z7r1g4"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-title]").value = "Title text for video"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-caption]").value = "Caption text for video"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const iframe = document.querySelector(".tiptap-container figure iframe");
expect(iframe).toBeTruthy();
expect(iframe.src).toBe("https://www.youtube.com/embed/j8It1z7r1g4");
expect(iframe.title).toBe("Title text for video");
const figcaption = document.querySelector(".tiptap-container figure figcaption");
expect(figcaption).toBeTruthy();
expect(figcaption.textContent).toBe("Caption text for video");
});
it("7.2 - Add a YouTube embed and transform to embed URL", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-embed pat-modal" href="#modal-embed">Embed</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
embed-panel: #embed-panel
">
</textarea>
<template id="modal-embed">
<form id="embed-panel">
<input name="tiptap-src" type="text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_embed = document.querySelector("#tiptap-external-toolbar .button-embed"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_embed));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_embed.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#embed-panel [name=tiptap-src]").value = "https://www.youtube.com/watch?v=j8It1z7r1g4"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const iframe = document.querySelector(".tiptap-container figure iframe");
expect(iframe).toBeTruthy();
// Normal YouTube URL is transformed to embed URL.
expect(iframe.src).toBe("https://www.youtube.com/embed/j8It1z7r1g4");
});
it("7.3 - Add a Vimeo embed", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-embed pat-modal" href="#modal-embed">Embed</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
embed-panel: #embed-panel
">
</textarea>
<template id="modal-embed">
<form id="embed-panel">
<input name="tiptap-src" type="text"/>
<input name="tiptap-title"/>
<input name="tiptap-caption"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_embed = document.querySelector("#tiptap-external-toolbar .button-embed"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_embed));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_embed.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#embed-panel [name=tiptap-src]").value = "https://player.vimeo.com/video/9206226"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-title]").value = "Title text for video"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-caption]").value = "Caption text for video"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const iframe = document.querySelector(".tiptap-container figure iframe");
expect(iframe).toBeTruthy();
expect(iframe.src).toBe("https://player.vimeo.com/video/9206226");
expect(iframe.title).toBe("Title text for video");
const figcaption = document.querySelector(".tiptap-container figure figcaption");
expect(figcaption).toBeTruthy();
expect(figcaption.textContent).toBe("Caption text for video");
});
it("7.4 - Add a Vimeo embed and transform to embed URL", async () => {
document.body.innerHTML = `
<div id="tiptap-external-toolbar">
<a class="button-embed pat-modal" href="#modal-embed">Embed</a>
</div>
<textarea
class="pat-tiptap"
data-pat-tiptap="
toolbar-external: #tiptap-external-toolbar;
embed-panel: #embed-panel
">
</textarea>
<template id="modal-embed">
<form id="embed-panel">
<input name="tiptap-src" type="text"/>
<button
type="submit"
name="tiptap-confirm"
class="close-panel">submit</button>
</form>
</template>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const button_embed = document.querySelector("#tiptap-external-toolbar .button-embed"); // prettier-ignore
await events.await_pattern_init(new PatternModal(button_embed));
document.querySelector(".tiptap-container [contenteditable]").focus(); // Set focus to bypass toolbar check
button_embed.click();
await events.await_event(document, "patterns-injected-delayed");
await utils.timeout(1);
document.querySelector("#embed-panel [name=tiptap-src]").value = "https://vimeo.com/9206226"; // prettier-ignore
document.querySelector("#embed-panel [name=tiptap-confirm]").dispatchEvent(new Event("click")); // prettier-ignore
await utils.timeout(1);
const iframe = document.querySelector(".tiptap-container figure iframe");
expect(iframe).toBeTruthy();
// Normal YouTube URL is transformed to embed URL.
expect(iframe.src).toBe("https://player.vimeo.com/video/9206226");
});
it("8.1 - Can use suggestions for mentioning", async () => {
global.fetch = jest.fn().mockImplementation(mockFetch(SUGGESTION_RESPONSE));
document.body.innerHTML = `
<textarea
class="pat-tiptap"
data-pat-tiptap="
mentions-menu: https://demo.at/mentions.html;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const editable = document.querySelector(".tiptap-container [contenteditable]");
const range = document.createRange();
const sel = window.getSelection();
// Add the triggering character into the content editable
editable.innerHTML = "<p>@</p>";
// Set the cursor right after the @-sign.
range.setStart(editable.childNodes[0].childNodes[0], 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
await utils.timeout(1); // Wait a tick for the tooltip to open.
await utils.timeout(1); // Wait a tick for the suggestion-pattern to be initialized.
await utils.timeout(1); // Wait another tick.
await utils.timeout(1); // Wait another tick.
// Check for class ``tiptap-mentions`` set on tooltip container.
expect(
document.querySelector(".tooltip-container.tiptap-mentions")
).toBeTruthy();
expect(document.querySelector(".tiptap-items")).toBeTruthy();
// 1st
const items = document.querySelectorAll(".tiptap-items .tiptap-item");
expect(items.length).toBeGreaterThan(0);
expect(items[0].classList.contains("active")).toBe(true);
// 2nd
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(items[0].classList.contains("active")).toBe(false);
expect(items[1].classList.contains("active")).toBe(true);
expect(items[2].classList.contains("active")).toBe(false);
// 3rd
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(items[0].classList.contains("active")).toBe(false);
expect(items[1].classList.contains("active")).toBe(false);
expect(items[2].classList.contains("active")).toBe(true);
// 1st
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(items[0].classList.contains("active")).toBe(true);
expect(items[1].classList.contains("active")).toBe(false);
expect(items[2].classList.contains("active")).toBe(false);
// select
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
await utils.timeout(1);
expect(document.querySelector(".tiptap-items")).toBeFalsy();
const mention = editable.firstChild.firstChild;
expect(mention.textContent).toBe("@item a");
expect(mention.href).toBe("https://demo.com/itema");
expect(mention.hasAttribute("data-mention")).toBe(true);
expect(mention.getAttribute("data-mention")).toBe("jepp");
expect(mention.getAttribute("data-pat-inject")).toBe("source:#some");
global.fetch.mockClear();
delete global.fetch;
});
it("8.2 - always inserts ``data-NAME`` even if not set", async () => {
global.fetch = jest.fn().mockImplementation(mockFetch(SUGGESTION_RESPONSE));
document.body.innerHTML = `
<textarea
class="pat-tiptap"
data-pat-tiptap="
mentions-menu: https://demo.at/mentions.html;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const editable = document.querySelector(".tiptap-container [contenteditable]");
const range = document.createRange();
const sel = window.getSelection();
// Add the triggering character into the content editable
editable.innerHTML = "<p>@</p>";
// Set the cursor right after the @-sign.
range.setStart(editable.childNodes[0].childNodes[0], 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
await utils.timeout(1); // Wait a tick for the tooltip to open.
await utils.timeout(1); // Wait a tick for the suggestion-pattern to be initialized.
await utils.timeout(1); // Wait another tick.
await utils.timeout(1); // Wait another tick.
// Select 2nd
const items = document.querySelectorAll(".tiptap-items .tiptap-item");
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown" }));
expect(items[1].classList.contains("active")).toBe(true);
// select
editable.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
await utils.timeout(1);
expect(document.querySelector(".tiptap-items")).toBeFalsy();
const mention = editable.firstChild.firstChild;
expect(mention.textContent).toBe("@item b");
expect(mention.href).toBe("https://demo.com/itemb");
expect(mention.getAttribute("class")).toBe("class_item_b");
expect(mention.hasAttribute("data-mention")).toBe(true);
expect(mention.getAttribute("data-mention")).toBe("");
global.fetch.mockClear();
delete global.fetch;
});
it("8.3 - Can use the mouse for suggestions", async () => {
global.fetch = jest.fn().mockImplementation(mockFetch(SUGGESTION_RESPONSE));
document.body.innerHTML = `
<textarea
class="pat-tiptap"
data-pat-tiptap="
mentions-menu: https://demo.at/mentions.html;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const editable = document.querySelector(".tiptap-container [contenteditable]");
const range = document.createRange();
const sel = window.getSelection();
// Add the triggering character into the content editable
editable.innerHTML = "<p>@</p>";
// Set the cursor right after the @-sign.
range.setStart(editable.childNodes[0].childNodes[0], 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
await utils.timeout(1); // Wait a tick for the tooltip to open.
await utils.timeout(1); // Wait a tick for the suggestion-pattern to be initialized.
await utils.timeout(1); // Wait another tick.
await utils.timeout(1); // Wait another tick.
const items = document.querySelectorAll(".tiptap-items .tiptap-item");
// click first item
items[0].querySelector("a").click();
await utils.timeout(1);
expect(document.querySelector(".tiptap-items")).toBeFalsy();
const mention = editable.firstChild.firstChild;
expect(mention.textContent).toBe("@item a");
expect(mention.href).toBe("https://demo.com/itema");
expect(mention.hasAttribute("data-mention")).toBe(true);
expect(mention.getAttribute("data-mention")).toBe("jepp");
expect(mention.getAttribute("data-pat-inject")).toBe("source:#some");
global.fetch.mockClear();
delete global.fetch;
});
it("8.4 - Can use the mouse for suggestions, click within the anchor and still copy correct attributes.", async () => {
global.fetch = jest.fn().mockImplementation(mockFetch(SUGGESTION_RESPONSE));
document.body.innerHTML = `
<textarea
class="pat-tiptap"
data-pat-tiptap="
mentions-menu: https://demo.at/mentions.html;
">
</textarea>
`;
const pattern = new Pattern(document.querySelector(".pat-tiptap"));
await events.await_pattern_init(pattern);
const editable = document.querySelector(".tiptap-container [contenteditable]");
const range = document.createRange();
const sel = window.getSelection();
// Add the triggering character into the content editable
editable.innerHTML = "<p>@</p>";
// Set the cursor right after the @-sign.
range.setStart(editable.childNodes[0].childNodes[0], 1);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
await utils.timeout(1); // Wait a tick for the tooltip to open.
await utils.timeout(1); // Wait a tick for the suggestion-pattern to be initialized.
await utils.timeout(1); // Wait another tick.
await utils.timeout(1); // Wait another tick.
const