Skip to content

Commit

Permalink
Created a popover for adding links to editor content (#904)
Browse files Browse the repository at this point in the history
* Created a popover for adding links to editor content

* Removed unwanted translations

* Optimized function calls

* Fixed all styling related issues

* Fixed bugs

* Reduced length of link edit input

* Updated z index and ui gaps

* Updated positioning logic

* Updated ousideClick logic for link view popOver
  • Loading branch information
gaagul authored Oct 17, 2023
1 parent d563b39 commit 912354a
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 405 deletions.
16 changes: 10 additions & 6 deletions src/components/Editor/LinkPopOver.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useEffect, useState, useRef } from "react";

import { useOnClickOutside } from "neetocommons/react-utils";
import { Button, Input } from "neetoui";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";

import { URL_REGEXP } from "src/common/constants";
import { validateAndFormatUrl } from "./utils";

const LinkPopOver = ({ editor }) => {
const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 });
Expand Down Expand Up @@ -48,12 +49,14 @@ const LinkPopOver = ({ editor }) => {
editor.chain().focus().extendMarkRange("link").unsetLink().run();

const handleLink = () => {
if (URL_REGEXP.test(urlString)) {
const formattedUrl = validateAndFormatUrl(urlString);

if (formattedUrl) {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: urlString })
.setLink({ href: formattedUrl })
.run();
setIsEditing(false);
} else {
Expand All @@ -79,6 +82,8 @@ const LinkPopOver = ({ editor }) => {
setIsLinkActive(false);
};

useOnClickOutside(popOverRef, removePopover);

useEffect(() => {
window.addEventListener("resize", removePopover);
window.addEventListener("wheel", removePopover);
Expand All @@ -103,7 +108,6 @@ const LinkPopOver = ({ editor }) => {
position: "fixed",
top: popoverPosition.top,
left: popoverPosition.left,
zIndex: 999,
transform: `translateY(52px) translateX(${isEditing ? "8px" : "3px"})`,
};

Expand All @@ -114,7 +118,7 @@ const LinkPopOver = ({ editor }) => {
{...{ error }}
label={t("menu.link")}
placeholder={t("placeholders.url")}
style={{ width: "400px" }}
style={{ width: "250px" }}
value={urlString}
onChange={({ target: { value } }) => setUrlString(value)}
onFocus={() => setError("")}
Expand Down Expand Up @@ -163,7 +167,7 @@ const LinkPopOver = ({ editor }) => {
isLinkActive ? (
<div
className="ne-link-popover"
id="ne-link-popover"
id="ne-link-view-popover"
ref={popOverRef}
style={popoverStyle}
>
Expand Down
6 changes: 3 additions & 3 deletions src/components/Editor/Menu/Bubble/TableOption.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Check, Close } from "neetoicons";
import { Button, Dropdown } from "neetoui";
import { useTranslation } from "react-i18next";

import { TABLE_ACTIONS } from "../Fixed/constants";
import { tableActions } from "../Fixed/utils";

const TableOption = ({ editor, handleClose }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -65,8 +65,8 @@ const TableOption = ({ editor, handleClose }) => {
</>
) : (
<Menu className="neeto-editor-bubble-menu__table-options">
{TABLE_ACTIONS({ editor }).map(({ label, command }) => (
<Button key={label} label={label} style="text" onClick={command} />
{tableActions({ editor }).map(({ label, command }) => (
<Button key={label} {...{ label }} style="text" onClick={command} />
))}
</Menu>
)}
Expand Down
178 changes: 178 additions & 0 deletions src/components/Editor/Menu/Fixed/LinkAddPopOver.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useState, useEffect, useRef } from "react";

import { useOnClickOutside } from "neetocommons/react-utils";
import { Button, Input } from "neetoui";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";

import { validateAndFormatUrl } from "components/Editor/utils";
import { URL_REGEXP } from "src/common/constants";
import { isNilOrEmpty } from "utils/common";

const LinkAddPopOver = ({ isAddLinkActive, setIsAddLinkActive, editor }) => {
const { from, to } = editor.state.selection;
const text = editor.state.doc.textBetween(from, to, "");

const [linkText, setLinkText] = useState(text);
const [linkUrl, setLinkUrl] = useState("");
const [error, setError] = useState("");
const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 });
const [arrowPosition, setArrowPosition] = useState({ top: 0, left: 0 });

const popOverRef = useRef(null);

const { t } = useTranslation();

const isLinkTextPresent = !isNilOrEmpty(linkText);
const isLinlUrlPresent = !isNilOrEmpty(linkUrl);
const isSubmitDisabled = !isLinkTextPresent || !isLinlUrlPresent;

const popoverStyle = {
display: "block",
position: "fixed",
top: popoverPosition.top,
left: popoverPosition.left,
transform: "translateY(52px) translateX(8px)",
};

const handleAddLink = () => {
const { state, dispatch } = editor.view;
const { from, to } = state.selection;
const formattedUrl = validateAndFormatUrl(linkUrl);

if (!URL_REGEXP.test(formattedUrl)) {
setError(t("error.invalidUrl"));

return;
}
const attrs = { href: formattedUrl };

const linkMark = state.schema.marks.link.create(attrs);
const linkTextWithMark = state.schema.text(linkText, [linkMark]);

const tr = state.tr.replaceWith(from, to, linkTextWithMark);
dispatch(tr);
removePopover();
};

const removePopover = () => {
editor.view.focus();
setIsAddLinkActive(false);
};

const handleKeyDown = e => {
e.stopPropagation();
if (e.key === "Enter") {
e.preventDefault();
handleAddLink();
} else if (e.key === "Escape") {
removePopover();
}
};

const updatePopoverPosition = () => {
if (popOverRef.current) {
const newPos = editor.view.coordsAtPos(
editor.view.state.selection.$to.pos
);

setArrowPosition({
top: `${newPos.top + 20}px`,
left: `${newPos.left - 10}px`,
});

const popoverRect = popOverRef.current?.getBoundingClientRect();

const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;

const maxLeft = screenWidth - popoverRect.width;
const maxTop = screenHeight - popoverRect.height - 50;

const adjustedLeft = newPos?.left
? Math.min(newPos.left - 50, maxLeft)
: 0;
const adjustedTop = newPos?.top ? Math.min(newPos.top - 22, maxTop) : 0;

setPopoverPosition({
top: `${adjustedTop}px`,
left: `${adjustedLeft}px`,
});
}
};

useOnClickOutside(popOverRef, removePopover);

useEffect(() => {
if (editor && isAddLinkActive) {
updatePopoverPosition();
}
window.addEventListener("resize", removePopover);
window.addEventListener("wheel", removePopover);

return () => {
window.removeEventListener("resize", removePopover);
window.removeEventListener("wheel", removePopover);
};
}, []);

return isAddLinkActive
? createPortal(
<>
<div
className="ne-link-arrow"
style={{ top: arrowPosition.top, left: arrowPosition.left }}
/>
<div
className="ne-link-popover"
id="ne-link-add-popover"
ref={popOverRef}
style={popoverStyle}
>
<Input
required
autoFocus={!isLinkTextPresent}
label={t("common.text")}
placeholder={t("placeholders.enterText")}
size="small"
style={{ width: "250px" }}
value={linkText}
onChange={({ target: { value } }) => setLinkText(value)}
onKeyDown={handleKeyDown}
/>
<Input
required
autoFocus={isLinkTextPresent}
className="ne-link-popover__url-input"
label={t("common.url")}
size="small"
{...{ error }}
placeholder={t("placeholders.url")}
style={{ width: "250px" }}
value={linkUrl}
onChange={({ target: { value } }) => setLinkUrl(value)}
onFocus={() => setError("")}
onKeyDown={handleKeyDown}
/>
<div className="ne-link-popover__edit-prompt-buttons">
<Button
disabled={isSubmitDisabled}
label={t("common.done")}
size="small"
onClick={handleAddLink}
/>
<Button
label={t("common.cancel")}
size="small"
style="text"
onClick={removePopover}
/>
</div>
</div>
</>,
document.body
)
: null;
};

export default LinkAddPopOver;
Loading

0 comments on commit 912354a

Please sign in to comment.