diff --git a/packages/editor/src/extensions/callout/__tests__/callout.test.ts b/packages/editor/src/extensions/callout/__tests__/callout.test.ts
new file mode 100644
index 000000000..af73fb03d
--- /dev/null
+++ b/packages/editor/src/extensions/callout/__tests__/callout.test.ts
@@ -0,0 +1,71 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { describe, expect, test } from "vitest";
+import { createEditor } from "../../../../test-utils/index.js";
+import { Callout } from "../callout.js";
+import { Heading } from "../../heading/heading.js";
+import { Paragraph } from "../../paragraph/paragraph.js";
+
+describe("callout", () => {
+ test("tapping the chevron corner of a callout toggles it collapsed", () => {
+ const { editor } = createEditor({
+ initialContent: `
`,
+ extensions: {
+ callout: Callout,
+ heading: Heading,
+ paragraph: Paragraph
+ }
+ });
+
+ const dom = editor.view.dom as HTMLElement;
+ const heading = dom.querySelector(".callout > h4") as HTMLElement;
+ expect(heading).toBeTruthy();
+
+ const collapsed = () => {
+ let value: boolean | undefined;
+ editor.state.doc.descendants((node) => {
+ if (node.type.name === Callout.name) value = node.attrs.collapsed;
+ });
+ return value;
+ };
+
+ const tapAt = (clientX: number) =>
+ heading.dispatchEvent(
+ new MouseEvent("mousedown", {
+ button: 0,
+ clientX,
+ clientY: 5,
+ bubbles: true,
+ cancelable: true
+ })
+ );
+
+ expect(collapsed()).toBe(false);
+ tapAt(-10);
+ expect(collapsed()).toBe(true);
+ tapAt(-10);
+ expect(collapsed()).toBe(false);
+
+ tapAt(8);
+ expect(collapsed()).toBe(true);
+ tapAt(8);
+ expect(collapsed()).toBe(false);
+ });
+});
diff --git a/packages/editor/src/extensions/callout/callout.ts b/packages/editor/src/extensions/callout/callout.ts
index 72f8f79b3..dfcee3fd0 100644
--- a/packages/editor/src/extensions/callout/callout.ts
+++ b/packages/editor/src/extensions/callout/callout.ts
@@ -240,7 +240,13 @@ export const Callout = Node.create({
if (typeof pos !== "number") return;
const resolvedPos = editor.state.doc.resolve(pos);
- if (isClickWithinBounds(e, resolvedPos, "right")) {
+ if (
+ isClickWithinBounds(e, resolvedPos, "right", {
+ width: 40,
+ height: 40,
+ offset: 11
+ })
+ ) {
e.preventDefault();
e.stopImmediatePropagation();
diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts
index d6fbb112b..ff33f7820 100644
--- a/packages/editor/src/extensions/heading/heading.ts
+++ b/packages/editor/src/extensions/heading/heading.ts
@@ -34,6 +34,7 @@ import {
AttributeUpdate,
BatchAttributeStep
} from "../../utils/batch-attribute-step.js";
+import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
const COLLAPSIBLE_BLOCK_TYPES = [
"paragraph",
@@ -223,24 +224,34 @@ export const Heading = TiptapHeading.extend({
const range = document.createRange();
range.selectNodeContents(e.target);
- const hitArea = { height: 40, width: 40 };
-
const rects = range.getClientRects();
const lines = rectsToLines(rects);
const lastLine = lines[lines.length - 1];
if (!lastLine) return;
const targetRect = isRtl ? lastLine[0] : lastLine[lastLine.length - 1];
- const { x, y, width } = targetRect;
+ const { x, y, width, height } = targetRect;
- let xStart = clientX >= x + width;
- let xEnd = clientX <= x + width + hitArea.width;
- const yStart = clientY >= y;
- const yEnd = clientY <= y + hitArea.height;
+ const isMobile = useToolbarStore.getState().isMobile;
- if (isRtl) {
- xStart = clientX >= x - hitArea.width;
- xEnd = clientX <= x;
+ let xStart: boolean, xEnd: boolean, yStart: boolean, yEnd: boolean;
+ if (isMobile) {
+ const hitWidth = 40;
+ const gap = 10;
+ xStart = isRtl
+ ? clientX >= x - gap - hitWidth
+ : clientX >= x + width + gap;
+ xEnd = isRtl ? clientX <= x - gap : clientX <= x + width + gap + hitWidth;
+ yStart = clientY >= y;
+ yEnd = clientY <= y + height;
+ } else {
+ const size = 15;
+ const centerX = isRtl ? x - 20 : x + width + 20;
+ const centerY = y + height / 2;
+ xStart = clientX >= centerX - size / 2;
+ xEnd = clientX <= centerX + size / 2;
+ yStart = clientY >= centerY - size / 2;
+ yEnd = clientY <= centerY + size / 2;
}
if (xStart && xEnd && yStart && yEnd) {
diff --git a/packages/editor/src/utils/prosemirror.ts b/packages/editor/src/utils/prosemirror.ts
index 5eb2af7b9..25b53087f 100644
--- a/packages/editor/src/utils/prosemirror.ts
+++ b/packages/editor/src/utils/prosemirror.ts
@@ -353,12 +353,16 @@ export function isClickWithinBounds(
e: MouseEvent | TouchEvent,
pos: ResolvedPos,
hitPosition: "left" | "right",
- hitArea: { width: number; height: number } = { width: 40, height: 40 }
+ hitArea: { width: number; height: number; offset?: number } = {
+ width: 40,
+ height: 40
+ }
) {
const { target } = e;
if (!(target instanceof HTMLElement)) return false;
const { x, y, right, width } = target.getBoundingClientRect();
+ const offset = hitArea.offset ?? 0;
const clientX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
const clientY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
const isRtl =
@@ -368,27 +372,27 @@ export function isClickWithinBounds(
switch (hitPosition) {
case "left": {
- let xStart = clientX >= x - hitArea.width;
- let xEnd = clientX <= x;
+ let xStart = clientX >= x - hitArea.width - offset;
+ let xEnd = clientX <= x - offset;
const yStart = clientY >= y;
const yEnd = clientY <= y + hitArea.height;
if (isRtl) {
- xEnd = clientX <= right + hitArea.width;
- xStart = clientX >= right;
+ xEnd = clientX <= right + hitArea.width + offset;
+ xStart = clientX >= right + offset;
}
return xStart && xEnd && yStart && yEnd;
}
case "right": {
- let xEnd = clientX <= x + width;
- let xStart = clientX >= x + width - hitArea.width;
+ let xEnd = clientX <= x + width + offset;
+ let xStart = clientX >= x + width - hitArea.width + offset;
const yStart = clientY >= y;
const yEnd = clientY <= y + hitArea.height;
if (isRtl) {
- xStart = clientX >= x;
- xEnd = clientX <= x + hitArea.width;
+ xStart = clientX >= x - offset;
+ xEnd = clientX <= x + hitArea.width - offset;
}
return xStart && xEnd && yStart && yEnd;
diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css
index d20fdc0d7..a865facf4 100644
--- a/packages/editor/styles/styles.css
+++ b/packages/editor/styles/styles.css
@@ -819,6 +819,7 @@ p > *::selection {
position: absolute;
top: calc((1lh - 18px) / 2);
right: 0px;
+ margin: 0;
cursor: pointer;
content: "";
background-size: 18px;
@@ -841,6 +842,22 @@ p > *::selection {
left: 0px;
}
+.ProseMirror div.callout > :first-child::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: -11px;
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ z-index: 1;
+}
+
+.ProseMirror div.callout > :first-child[dir="rtl"]::before {
+ right: unset;
+ left: -11px;
+}
+
.ProseMirror div.callout.collapsed > :first-child::after {
transform: rotate(-90deg);
}
@@ -930,13 +947,14 @@ del.diffdel {
cursor: pointer;
content: "";
background-size: var(--icon-size, 18px);
- width: var(--icon-size, 18px);
+ width: 40px;
height: var(--icon-size, 18px);
+ margin-inline-end: calc(var(--icon-size, 18px) - 40px);
background-color: var(--icon);
mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+)
no-repeat 50% 50%;
- mask-size: cover;
+ mask-size: var(--icon-size, 18px);
transform: rotate(0);
transition: transform 250ms ease, opacity 200ms ease;