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

Added option to edit the text-content of link node #938

Merged
merged 3 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
151 changes: 90 additions & 61 deletions src/components/Editor/LinkPopOver.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import React, { useEffect, useState, useRef } from "react";

import { getMarkRange, getMarkType } from "@tiptap/react";
import { useOnClickOutside } from "neetocommons/react-utils";
import { Button, Input } from "neetoui";
import { Button } from "neetoui";
import { Form, Input } from "neetoui/formik";
import { equals, isNil } from "ramda";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";

import { LINK_VALIDATION_SCHEMA } from "./constants";
import { validateAndFormatUrl } from "./utils";

const LinkPopOver = ({ editor }) => {
const { view } = editor || {};
const { from } = editor.state.selection;
const initialTextContent = view?.state?.doc?.nodeAt(from)?.text || "";

const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 });
const [urlString, setUrlString] = useState("");
const [error, setError] = useState("");
const [isEditing, setIsEditing] = useState(false);
const [isLinkActive, setIsLinkActive] = useState(editor?.isActive("link"));

const popOverRef = useRef(null);

const { t } = useTranslation();

const { view } = editor || {};
const linkAttributes = editor?.getAttributes("link");

const updatePopoverPosition = () => {
Expand Down Expand Up @@ -48,40 +53,54 @@ const LinkPopOver = ({ editor }) => {
const handleUnlink = () =>
editor.chain().focus().extendMarkRange("link").unsetLink().run();

const handleLink = () => {
const formattedUrl = validateAndFormatUrl(urlString);
const removePopover = () => {
setIsEditing(false);
setIsLinkActive(false);
};

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

if (formattedUrl) {
const handleSubmit = ({ textContent, urlString }) => {
const formattedUrl = validateAndFormatUrl(urlString);
if (equals(textContent, initialTextContent)) {
editor
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: formattedUrl })
.run();
setIsEditing(false);
} else {
setError(t("neetoEditor.error.invalidUrl"));
}
};

const handleKeyDown = event => {
if (event.key === "Escape") {
setIsEditing(false);
} else if (event.key === "Enter") {
handleLink();
return;
}
};
const { state, dispatch } = editor.view;
const type = getMarkType("link", state.schema);
const { $to } = state.selection;
const { from = null, to = null } = getMarkRange($to, type) || {};

const resetLink = () => {
setUrlString(linkAttributes?.href || "");
setError("");
};
if (isNil(from) || isNil(to)) return;

const attrs = { href: formattedUrl };
const linkMark = state.schema.marks.link.create(attrs);
const linkTextWithMark = state.schema.text(textContent, [linkMark]);

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

const removePopover = () => {
setIsEditing(false);
setIsLinkActive(false);
editor.view.focus();
editor.commands.extendMarkRange("link");
};

const handleKeyDown = event =>
equals(event.key, "Escape") && setIsEditing(false);

useOnClickOutside(popOverRef, removePopover);

useEffect(() => {
Expand All @@ -99,48 +118,58 @@ const LinkPopOver = ({ editor }) => {
setIsLinkActive(isActive);
if (isActive) {
updatePopoverPosition();
setUrlString(linkAttributes?.href || "");
}
}, [view?.state?.selection?.$from?.pos, isEditing]);

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

const renderEditingMode = () => (
<>
<Input
autoFocus
{...{ error }}
label={t("neetoEditor.menu.link")}
placeholder={t("neetoEditor.placeholders.url")}
style={{ width: "250px" }}
value={urlString}
onChange={({ target: { value } }) => setUrlString(value)}
onFocus={() => setError("")}
onKeyDown={handleKeyDown}
/>
<div className="ne-link-popover__edit-prompt-buttons">
<Button
label={t("neetoEditor.menu.link")}
size="small"
onClick={handleLink}
/>
<Button
label={t("neetoEditor.common.cancel")}
size="small"
style="text"
onClick={() => {
resetLink();
setIsEditing(false);
}}
/>
</div>
</>
<Form
formikProps={{
initialValues: {
textContent: initialTextContent,
urlString: linkAttributes?.href || "",
},
onSubmit: handleSubmit,
validationSchema: LINK_VALIDATION_SCHEMA,
}}
>
{({ dirty, isSubmitting }) => (
<>
<Input
required
label={t("neetoEditor.common.text")}
name="textContent"
placeholder={t("neetoEditor.placeholders.enterText")}
style={{ width: "250px" }}
onKeyDown={handleKeyDown}
/>
<Input
autoFocus
required
className="ne-link-popover__url-input"
label={t("neetoEditor.common.url")}
name="urlString"
placeholder={t("neetoEditor.placeholders.url")}
style={{ width: "250px" }}
onKeyDown={handleKeyDown}
/>
<div className="ne-link-popover__edit-prompt-buttons">
<Button
disabled={!dirty}
label={t("neetoEditor.menu.link")}
loading={isSubmitting}
size="small"
type="submit"
/>
<Button
label={t("neetoEditor.common.cancel")}
size="small"
style="text"
onClick={() => setIsEditing(false)}
/>
</div>
</>
)}
</Form>
);

const renderViewMode = () => (
Expand Down
12 changes: 11 additions & 1 deletion src/components/Editor/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EDITOR_OPTIONS } from "common/constants";
import * as yup from "yup";

import { EDITOR_OPTIONS, URL_REGEXP } from "common/constants";

export const DEFAULT_EDITOR_OPTIONS = [
EDITOR_OPTIONS.BOLD,
Expand All @@ -25,3 +27,11 @@ export const EMPTY_DIV_REGEX = new RegExp(
/<div[^>]*?>\s*(?:<br[^>]*?>)\s*<\/div>/g
);
export const TRAILING_BR_REGEX = new RegExp(/\s*(?:<br[^>]*?>)+\s*$/);

export const LINK_VALIDATION_SCHEMA = yup.object().shape({
textContent: yup.string().required("Text content is required"),
urlString: yup
.string()
.matches(URL_REGEXP, "Invalid URL")
.required("URL is required"),
});
3 changes: 2 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"cancel": "Cancel",
"text": "Text:",
"done": "Done",
"url": "Url:"
"url": "Url:",
"editLink": "Edit link"
},
"attachments": {
"actionsBlocked": "You are not permitted to update or delete attachments",
Expand Down