Skip to content

Commit 4d46b53

Browse files
committed
fix: allow no validation links url
1 parent 9580d68 commit 4d46b53

8 files changed

Lines changed: 90 additions & 35 deletions

File tree

packages/pluggableWidgets/rich-text-web/src/RichText.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@
171171
<caption>Enable spell checking</caption>
172172
<description />
173173
</property>
174+
<property key="linkValidation" type="boolean" defaultValue="true">
175+
<caption>Enable link URL validation</caption>
176+
<description>If enabled, only valid URLs will be accepted in links.</description>
177+
</property>
174178
<property key="defaultFontFamily" type="textTemplate" required="false">
175179
<caption>Default font family</caption>
176180
<description />

packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,23 @@ import {
1010
useLayoutEffect,
1111
useRef
1212
} from "react";
13-
import { CustomFontsType, RichTextContainerProps } from "../../typings/RichTextProps";
13+
import { RichTextContainerProps } from "../../typings/RichTextProps";
1414
import { EditorDispatchContext } from "../store/EditorProvider";
1515
import { SET_FULLSCREEN_ACTION } from "../store/store";
1616
import "../utils/customPluginRegisters";
17-
import { FontStyleAttributor, formatCustomFonts } from "../utils/formats/fonts";
1817
import "../utils/formats/quill-table-better/assets/css/quill-table-better.scss";
1918
import { getResizeModuleConfig } from "../utils/formats/resizeModuleConfig";
2019
import { ACTION_DISPATCHER } from "../utils/helpers";
2120
import { getKeyboardBindings } from "../utils/modules/keyboard";
2221
import { getIndentHandler } from "../utils/modules/toolbarHandlers";
2322
import MxUploader from "../utils/modules/uploader";
24-
import MxQuill from "../utils/MxQuill";
23+
import MxQuill, { MxQuillModulesOptions } from "../utils/MxQuill";
2524
import { useEmbedModal } from "./CustomToolbars/useEmbedModal";
2625
import Dialog from "./ModalDialog/Dialog";
2726

28-
export interface EditorProps extends Pick<
29-
RichTextContainerProps,
30-
"imageSource" | "imageSourceContent" | "enableDefaultUpload"
31-
> {
32-
customFonts: CustomFontsType[];
27+
export interface EditorProps
28+
extends Pick<RichTextContainerProps, "imageSource" | "imageSourceContent" | "enableDefaultUpload"> {
29+
options: MxQuillModulesOptions;
3330
defaultValue?: string;
3431
onTextChange?: (...args: [delta: Delta, oldContent: Delta, source: EmitterSource]) => void;
3532
onSelectionChange?: (...args: [range: Range, oldRange: Range, source: EmitterSource]) => void;
@@ -43,10 +40,17 @@ export interface EditorProps extends Pick<
4340

4441
// Editor is an uncontrolled React component
4542
const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | null>) => {
46-
const fonts = formatCustomFonts(props.customFonts);
47-
const FontStyle = new FontStyleAttributor(fonts);
48-
Quill.register(FontStyle, true);
49-
const { theme, defaultValue, style, className, toolbarId, onTextChange, onSelectionChange, readOnly } = props;
43+
const {
44+
theme,
45+
defaultValue,
46+
style,
47+
className,
48+
toolbarId,
49+
onTextChange,
50+
onSelectionChange,
51+
readOnly,
52+
options: mxOptions
53+
} = props;
5054
const containerRef = useRef<HTMLDivElement>(null);
5155
const modalRef = useRef<HTMLDivElement>(null);
5256
const onTextChangeRef = useRef(onTextChange);
@@ -127,6 +131,7 @@ const Editor = forwardRef((props: EditorProps, ref: MutableRefObject<Quill | nul
127131

128132
const quill = new MxQuill(editorContainer, options);
129133
ref.current = quill;
134+
quill.registerCustomModules(mxOptions);
130135

131136
const delta = quill.clipboard.convert({ html: defaultValue ?? "" });
132137
quill.updateContents(delta, Quill.sources.SILENT);

packages/pluggableWidgets/rich-text-web/src/components/EditorWrapper.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EditorContext, EditorProvider } from "../store/EditorProvider";
1111
import { useActionEvents } from "../store/useActionEvents";
1212
import { updateLegacyQuillFormats } from "../utils/helpers";
1313
import MendixTheme from "../utils/themes/mxTheme";
14+
import { MxQuillModulesOptions } from "../utils/MxQuill";
1415
import { createPreset } from "./CustomToolbars/presets";
1516
import Editor from "./Editor";
1617
import { StickySentinel } from "./StickySentinel";
@@ -195,7 +196,14 @@ function EditorWrapperInner(props: EditorWrapperProps): ReactElement {
195196
className={"widget-rich-text-container"}
196197
readOnly={stringAttribute.readOnly}
197198
key={`${toolbarId}_${stringAttribute.readOnly}`}
198-
customFonts={props.customFonts}
199+
options={
200+
{
201+
fonts: props.customFonts,
202+
links: {
203+
validate: props.linkValidation
204+
}
205+
} as MxQuillModulesOptions
206+
}
199207
imageSource={imageSource}
200208
imageSourceContent={imageSourceContent}
201209
enableDefaultUpload={enableDefaultUpload}

packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ import Quill, { EmitterSource, QuillOptions } from "quill";
4141
import TextBlot, { escapeText } from "quill/blots/text";
4242
import { Delta, Op } from "quill/core";
4343
import Editor from "quill/core/editor";
44-
import { STANDARD_LIST_TYPES } from "./formats/customList";
44+
import { CustomFontsType } from "../../typings/RichTextProps";
4545
import MxBlock from "./formats/block";
46+
import { STANDARD_LIST_TYPES } from "./formats/customList";
47+
import { FontStyleAttributor, formatCustomFonts } from "./formats/fonts";
48+
import CustomLink, { CustomLinkNoValidation } from "./formats/link";
4649

4750
interface ListItem {
4851
child: Blot;
@@ -62,6 +65,9 @@ class MxEditor extends Editor {
6265
* https://github.com/slab/quill/blob/main/packages/quill/src/core/editor.ts
6366
*/
6467
getHTML(index: number, length: number): string {
68+
if (this.isBlank()) {
69+
return "";
70+
}
6571
const [line, lineOffset] = this.scroll.line(index);
6672
if (line) {
6773
const lineLength = line.length();
@@ -75,8 +81,15 @@ class MxEditor extends Editor {
7581
}
7682
}
7783

84+
export interface MxQuillModulesOptions {
85+
fonts: CustomFontsType[];
86+
links: {
87+
validate: boolean;
88+
};
89+
}
90+
7891
/**
79-
* Extension's of quill to allow us replacing the editor instance.
92+
* Extension's of quill to allow us to replace the editor instance.
8093
*/
8194
export default class MxQuill extends Quill {
8295
constructor(container: HTMLElement | string, options: QuillOptions = {}) {
@@ -88,6 +101,18 @@ export default class MxQuill extends Quill {
88101
super.setContents(new Delta(), Quill.sources.SILENT);
89102
return this.updateContents(this.getContents().transform(dlta as Delta, false), source);
90103
}
104+
105+
registerCustomModules(props: MxQuillModulesOptions): void {
106+
const { fonts, links } = props;
107+
const customFonts = formatCustomFonts(fonts);
108+
const FontStyle = new FontStyleAttributor(customFonts);
109+
Quill.register(FontStyle, true);
110+
if (links.validate) {
111+
Quill.register(CustomLink, true);
112+
} else {
113+
Quill.register(CustomLinkNoValidation, true);
114+
}
115+
}
91116
}
92117

93118
/**
@@ -136,7 +161,14 @@ function findEmptyTailBlock(blot: Blot): Blot | null {
136161

137162
if (blot instanceof ScrollBlot && blot.statics.blotName === "scroll" && !blot.parent) {
138163
if (MxBlock.IsMxBlock(blot.children.tail) && (blot.children.tail as MxBlock).isEmptyTailBlock()) {
139-
if (MxBlock.IsMxBlock(blot.children.tail.prev) && (blot.children.tail.prev as MxBlock).isEmptyTailBlock()) {
164+
if (blot.children.tail.prev) {
165+
if (
166+
MxBlock.IsMxBlock(blot.children.tail.prev) &&
167+
(blot.children.tail.prev as MxBlock).isEmptyTailBlock()
168+
) {
169+
skippedBlots = blot.children.tail;
170+
}
171+
} else {
140172
skippedBlots = blot.children.tail;
141173
}
142174
}

packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import MendixTheme from "./themes/mxTheme";
33
import "./formats/fonts";
44
import "./formats/fontsize";
55
import CustomListItem from "./formats/customList";
6-
import CustomLink from "./formats/link";
76
import CustomVideo from "./formats/video";
87
import CustomImage from "./formats/image";
98
import SoftBreak from "./formats/softBreak";
@@ -31,7 +30,6 @@ Quill.debug("error");
3130
Quill.register({ "themes/snow": MendixTheme }, true);
3231
Quill.register(CustomListItem, true);
3332
Quill.register(WhiteSpaceStyle, true);
34-
Quill.register(CustomLink, true);
3533
Quill.register(CustomVideo, true);
3634
Quill.register(CustomImage, true);
3735
Quill.register({ "formats/softbreak": SoftBreak }, true);

packages/pluggableWidgets/rich-text-web/src/utils/formats/block.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ class MxBlock extends Block {
55
isEmptyTailBlock(): boolean {
66
const hasNoValidChildren =
77
this.children.length === 0 ||
8-
(this.children.length === 1 && this.children.head?.statics.tagName.toString().toUpperCase() === "BR");
9-
return this.prev !== null && hasNoValidChildren;
8+
(this.children.length === 1 && this.children.head?.statics.tagName?.toString().toUpperCase() === "BR");
9+
return hasNoValidChildren;
1010
}
1111

1212
html(): string {

packages/pluggableWidgets/rich-text-web/src/utils/formats/link.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,6 @@ import * as linkify from "linkifyjs";
22
import Link from "quill/formats/link";
33
import { linkConfigType } from "../formats";
44

5-
function getLink(url: string): string {
6-
const foundLinks = linkify.find(url, {
7-
defaultProtocol: "https"
8-
});
9-
let results = url;
10-
if (foundLinks && foundLinks.length > 0) {
11-
results = foundLinks[0].href;
12-
}
13-
14-
return results;
15-
}
16-
175
/**
186
* Custom Link handler, allowing extra config: target and default protocol.
197
*/
@@ -24,7 +12,7 @@ export default class CustomLink extends Link {
2412
} else if ((value as linkConfigType)?.href !== undefined) {
2513
const linkConfig = value as linkConfigType;
2614
// @ts-expect-error the constructor is generic function, ts will consider sanitize not exist
27-
this.domNode.setAttribute("href", getLink(this.constructor.sanitize(linkConfig.href)));
15+
this.domNode.setAttribute("href", this.constructor.getLink(this.constructor.sanitize(linkConfig.href)));
2816
this.domNode.textContent = linkConfig.text ?? linkConfig.href;
2917
if (linkConfig.target) {
3018
this.domNode.setAttribute("target", linkConfig.target);
@@ -34,15 +22,15 @@ export default class CustomLink extends Link {
3422
}
3523
} else {
3624
// @ts-expect-error the constructor is generic function, ts will consider sanitize not exist
37-
this.domNode.setAttribute("href", getLink(this.constructor.sanitize(value)));
25+
this.domNode.setAttribute("href", this.constructor.getLink(this.constructor.sanitize(value)));
3826
}
3927
}
4028

4129
static create(value: unknown): HTMLElement {
4230
if ((value as linkConfigType)?.href !== undefined) {
4331
const linkConfig = value as linkConfigType;
4432
const node = super.create(linkConfig.href) as HTMLElement;
45-
node.setAttribute("href", getLink(this.sanitize(linkConfig.href)));
33+
node.setAttribute("href", this.getLink(this.sanitize(linkConfig.href)));
4634
node.setAttribute("rel", "noopener noreferrer");
4735
node.setAttribute("title", linkConfig.title ?? linkConfig.href);
4836
node.setAttribute("target", linkConfig.target || "_blank");
@@ -52,4 +40,22 @@ export default class CustomLink extends Link {
5240
return super.create(value);
5341
}
5442
}
43+
44+
static getLink(url: string): string {
45+
const foundLinks = linkify.find(url, {
46+
defaultProtocol: "https"
47+
});
48+
let results = url;
49+
if (foundLinks && foundLinks.length > 0) {
50+
results = foundLinks[0].href;
51+
}
52+
53+
return results;
54+
}
55+
}
56+
57+
export class CustomLinkNoValidation extends CustomLink {
58+
static getLink(url: string): string {
59+
return url;
60+
}
5561
}

packages/pluggableWidgets/rich-text-web/typings/RichTextProps.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface RichTextContainerProps {
7575
onLoad?: ActionValue;
7676
onChangeType: OnChangeTypeEnum;
7777
spellCheck: boolean;
78+
linkValidation: boolean;
7879
defaultFontFamily?: DynamicValue<string>;
7980
defaultFontSize?: DynamicValue<string>;
8081
customFonts: CustomFontsType[];
@@ -124,6 +125,7 @@ export interface RichTextPreviewProps {
124125
onLoad: {} | null;
125126
onChangeType: OnChangeTypeEnum;
126127
spellCheck: boolean;
128+
linkValidation: boolean;
127129
defaultFontFamily: string;
128130
defaultFontSize: string;
129131
customFonts: CustomFontsPreviewType[];

0 commit comments

Comments
 (0)