Skip to content
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
9 changes: 9 additions & 0 deletions .changeset/goofy-llamas-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@wso2is/console": patch
"@wso2is/admin.flow-builder-core.v1": patch
"@wso2is/admin.registration-flow-builder.v1": patch
"@wso2is/identity-apps-core": patch
"@wso2is/i18n": patch
---

Add support for configuring link target behavior in Rich Text Editor
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { PenToSquareIcon } from "@oxygen-ui/react-icons";
import {
$getSelection,
$isRangeSelection,
$setSelection,
BaseSelection,
CLICK_COMMAND,
CommandListenerPriority,
Expand All @@ -54,6 +55,13 @@ import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { getSelectedNode } from "../utils/get-selected-node";
import "./link-plugin.scss";
import FormControlLabel from "@oxygen-ui/react/FormControlLabel/FormControlLabel";
import Checkbox from "@oxygen-ui/react/Checkbox";
import Hint from "../../../resources/elements/hint";
import Box from "@oxygen-ui/react/Box/Box";
import Tooltip from "@oxygen-ui/react/Tooltip";

type LinkTarget = "_blank" | "_self";

const LowPriority: CommandListenerPriority = 1;
const HighPriority: CommandListenerPriority = 3;
Expand Down Expand Up @@ -162,7 +170,7 @@ const LinkEditor = (): ReactElement => {
const [ isEditMode, setEditMode ] = useState(false);
const [ lastSelection, setLastSelection ] = useState<BaseSelection | null>(null);
const [ selectedUrlType, setSelectedUrlType ] = useState<string>("CUSTOM");

const [ linkTarget, setLinkTarget ] = useState<LinkTarget>("_blank");
const { t } = useTranslation();

/**
Expand All @@ -178,14 +186,18 @@ const LinkEditor = (): ReactElement => {

if ($isLinkNode(parent)) {
const url: string = parent.getURL();
const target: LinkTarget = (parent.getTarget() ?? "_blank") as LinkTarget;

setLinkUrl(getPlaceholderUrl(url));
setSelectedUrlType(determineUrlType(url));
setLinkTarget(target);
} else if ($isLinkNode(node)) {
const url: string = node.getURL();
const target: LinkTarget = (node.getTarget() ?? "_blank") as LinkTarget;

setLinkUrl(getPlaceholderUrl(url));
setSelectedUrlType(determineUrlType(url));
setLinkTarget(target);
} else {
setLinkUrl("");
setSelectedUrlType("CUSTOM");
Expand Down Expand Up @@ -367,18 +379,21 @@ const LinkEditor = (): ReactElement => {
if (url) {
// First use the default command to handle the link creation/update.
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);

// Then update the link attributes to include safe properties.
const selection: BaseSelection = $getSelection();
const selection: BaseSelection | null = $getSelection();

if ($isRangeSelection(selection)) {
const node: TextNode | ElementNode = getSelectedNode(selection);
const linkNode: ElementNode = $isLinkNode(node) ? node : node.getParent();

if ($isLinkNode(linkNode)) {
// Update the link node with safe attributes.
linkNode.setTarget("_blank");
linkNode.setRel("noopener noreferrer");
linkNode.setTarget(linkTarget);
if(linkTarget === "_blank") {
linkNode.setRel("noopener noreferrer");
} else {
linkNode.setRel("");
}
Comment thread
pavinduLakshan marked this conversation as resolved.
}
}
} else {
Expand All @@ -391,7 +406,7 @@ const LinkEditor = (): ReactElement => {
HighPriority
)
);
}, [ editor, updateLinkEditor, isEditMode ]);
}, [ editor, updateLinkEditor, isEditMode, linkTarget ]);

/**
* Updates the link editor position.
Expand Down Expand Up @@ -465,13 +480,66 @@ const LinkEditor = (): ReactElement => {
}
} }
/>
{/* Link Target Checkbox - With Description */}
<Box sx={ { alignItems: "center", display: "flex", flexDirection: "row", gap: 0 } }>
<FormControlLabel
control={
<Checkbox
checked={ linkTarget === "_blank" }
onChange={ (event: React.ChangeEvent<HTMLInputElement>) => {
const newTarget: LinkTarget =
event.target.checked ? "_blank" : "_self";

setLinkTarget(newTarget);

if (lastSelection !== null) {
const currentUrl: string = getCurrentUrl();

if (currentUrl !== "") {
editor.update(() => {
const selection: BaseSelection | null = $getSelection();

if ($isRangeSelection(selection)) {
const node: TextNode | ElementNode =
getSelectedNode(selection);
const linkNode: ElementNode = $isLinkNode(node)
? node
: node.getParent();

if ($isLinkNode(linkNode)) {
linkNode.setTarget(newTarget);
linkNode.setRel(newTarget === "_blank"
? "noopener noreferrer" : "");
}
}
});
}
}
} }
data-componentid="link-target-checkbox"
/>
}
label={t("flows:core.elements.richText.linkEditor.linkTargetLabel")}
/>
<Tooltip title={
linkTarget === "_blank"
? t("flows:core.elements.richText.linkEditor.newTabHint")
: t("flows:core.elements.richText.linkEditor.sameTabHint")
}>
<span><Hint hint="" /></span>
</Tooltip>

</Box>
<Button
size="small"
variant="outlined"
className="link-input-save-button"
onClick={ (event: ReactMouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (lastSelection !== null) {
editor.update(() => {
$setSelection(lastSelection.clone());
});
const currentUrl: string = getCurrentUrl();

if (currentUrl !== "") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@
{
"category": "DISPLAY",
"config": {
"text": "<p class=\"rich-text-paragraph\"><br></p><p class=\"rich-text-paragraph\"><span class=\"rich-text-pre-wrap\">Already have an account? </span><a href=\"{{application.callbackOrAccessUrl}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"rich-text-link\"><span class=\"rich-text-pre-wrap\">Sign in</span></a></p>"
"text": "<p class=\"rich-text-paragraph\"><br></p><p class=\"rich-text-paragraph\"><span class=\"rich-text-pre-wrap\">Already have an account? </span><a href=\"{{application.callbackOrAccessUrl}}\" class=\"rich-text-link\"><span class=\"rich-text-pre-wrap\">Sign in</span></a></p>",
"linkTarget": "_self"
},
"id": "{{ID}}",
"type": "RICH_TEXT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,31 @@ const RichTextAdapter = ({ component }) => {

// Resolve placeholders in the HTML content before sanitizing.
const sanitizedHtml = useMemo(() => {
const i18nText = resolveElementText(translations, config.text);
const resolvedHtml = resolvePlaceholders(i18nText || "");

return DOMPurify.sanitize(resolvedHtml, {
ADD_ATTR: [ "target" ]
let html = resolveElementText(translations, config.text);
html = resolvePlaceholders(html || "");

// If linkTarget is configured, update links to use that target
if (config.linkTarget) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const links = doc.querySelectorAll('a');

links.forEach(link => {
link.setAttribute('target', config.linkTarget);
if (config.linkTarget === '_blank') {
link.setAttribute('rel', 'noopener noreferrer');
} else {
link.removeAttribute('rel');
}
});
}, [ config.text, contextData ]);

html = doc.body.innerHTML;
}

return DOMPurify.sanitize(html, {
ADD_ATTR: [ "target", "rel" ]
});
}, [ config.text, config.linkTarget, contextData ]);

return (
<div className="rich-text-content">
Expand Down
4 changes: 4 additions & 0 deletions modules/i18n/src/models/namespaces/flows-ns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface flowsNS {
termsOfUseUrl: string;
};
urlTypeLabel: string;
linkTargetLabel: string;
linkTargetHint: string;
newTabHint: string;
sameTabHint: string;
};
placeholder: string;
};
Expand Down
4 changes: 4 additions & 0 deletions modules/i18n/src/translations/en-US/portals/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export const flows: flowsNS = {
elements: {
richText: {
linkEditor: {
linkTargetHint: "Allow admin to control whether links open in the same tab or new tab",
linkTargetLabel: "Open link in new tab",
newTabHint: "Link will open in a new browser tab",
placeholder: "Enter link URL here...",
predefinedUrls: {
applicationAccessUrl: "Application Access URL",
Expand All @@ -55,6 +58,7 @@ export const flows: flowsNS = {
supportEmail: "Contact Support Email",
termsOfUseUrl: "Terms of Use URL"
},
sameTabHint: "Link will open in the same browser tab",
urlTypeLabel: "URL Type"
},
placeholder: "Enter rich text content here..."
Expand Down
Loading