diff --git a/docs/src/components/Docs/Props/CustomizingToolbarProp/defaultToolbar.js b/docs/src/components/Docs/Props/CustomizingToolbarProp/defaultToolbar.js
index ea12e6bca..f3649c565 100644
--- a/docs/src/components/Docs/Props/CustomizingToolbarProp/defaultToolbar.js
+++ b/docs/src/components/Docs/Props/CustomizingToolbarProp/defaultToolbar.js
@@ -104,6 +104,8 @@ export default "{\n" +
" component: undefined,\n" +
" popupClassName: undefined,\n" +
" embedCallback: undefined,\n" +
+ " embedValidator: undefined,\n" +
+ " linkValidator: undefined,\n" +
" defaultSize: {\n" +
" height: 'auto',\n" +
" width: 'auto',\n" +
diff --git a/docs/src/components/Docs/Props/CustomizingToolbarProp/index.js b/docs/src/components/Docs/Props/CustomizingToolbarProp/index.js
index 10b1f3871..9461c91a4 100644
--- a/docs/src/components/Docs/Props/CustomizingToolbarProp/index.js
+++ b/docs/src/components/Docs/Props/CustomizingToolbarProp/index.js
@@ -159,6 +159,14 @@ export default () => (
will be saved iin the link.
+
+ link: linkValidator
+
+ : This is a function to validate the link added by the user.
+ The validator is passed the link text input by the user and is
+ expected to return a boolean.
+
+
emoji: emojis
@@ -181,6 +189,13 @@ export default () => (
passed a url and should return url only.
+
+ embedded: embedValidator
+
+ : This is a function to validate the url added by the user. The validator
+ is passed the url input by the user and is expected to return a boolean.
+
+
image: urlEnabled
diff --git a/src/config/defaultToolbar.js b/src/config/defaultToolbar.js
index 996495bc2..ab7f88869 100644
--- a/src/config/defaultToolbar.js
+++ b/src/config/defaultToolbar.js
@@ -181,7 +181,8 @@ export default {
options: ["link", "unlink"],
link: { icon: link, className: undefined, title: undefined },
unlink: { icon: unlink, className: undefined, title: undefined },
- linkCallback: undefined
+ linkCallback: undefined,
+ linkValidator: undefined
},
emoji: {
icon: emoji,
@@ -327,6 +328,7 @@ export default {
component: undefined,
popupClassName: undefined,
embedCallback: undefined,
+ embedValidator: undefined,
defaultSize: {
height: "auto",
width: "auto"
diff --git a/src/controls/Embedded/Component/index.js b/src/controls/Embedded/Component/index.js
index 041f1aef2..4d5b64401 100644
--- a/src/controls/Embedded/Component/index.js
+++ b/src/controls/Embedded/Component/index.js
@@ -20,6 +20,9 @@ class LayoutComponent extends Component {
embeddedLink: '',
height: this.props.config.defaultSize.height,
width: this.props.config.defaultSize.width,
+ embeddedLinkValid: false,
+ heightValid: !!this.props.config.defaultSize.height,
+ widthValid: !!this.props.config.defaultSize.width,
};
componentDidUpdate(prevProps) {
@@ -30,24 +33,54 @@ class LayoutComponent extends Component {
embeddedLink: '',
height,
width,
+ embeddedLinkValid: false,
+ heightValid: !!height,
+ widthValid: !!width,
});
}
}
+ validateValue = (name, value) => {
+ if (name === 'embeddedLink') {
+ const { embedValidator } = this.props.config;
+ return !!value && (!embedValidator ? true : embedValidator(value));
+ }
+ return !!value;
+ }
+
+ isValid = () => {
+ const {
+ embeddedLinkValid,
+ heightValid,
+ widthValid,
+ } = this.state;
+ return embeddedLinkValid && heightValid && widthValid;
+ }
+
onChange = () => {
+ if (!this.isValid()) return;
const { onChange } = this.props;
const { embeddedLink, height, width } = this.state;
onChange(embeddedLink, height, width);
};
updateValue = event => {
+ const { name, value } = event.target;
this.setState({
- [`${event.target.name}`]: event.target.value,
+ [`${name}`]: value,
+ [`${name}Valid`]: this.validateValue(name, value),
});
};
rendeEmbeddedLinkModal() {
- const { embeddedLink, height, width } = this.state;
+ const {
+ embeddedLink,
+ height,
+ width,
+ embeddedLinkValid,
+ heightValid,
+ widthValid,
+ } = this.state;
const {
config: { popupClassName },
doCollapse,
@@ -67,7 +100,11 @@ class LayoutComponent extends Component {
*
@@ -96,7 +137,11 @@ class LayoutComponent extends Component {
onBlur={this.updateValue}
value={width}
name="width"
- className="rdw-embedded-modal-size-input"
+ className={classNames({
+ 'rdw-embedded-modal-size-input': true,
+ valid: widthValid,
+ invalid: !widthValid,
+ })}
placeholder="Width"
/>
*
@@ -108,7 +153,7 @@ class LayoutComponent extends Component {
type="button"
className="rdw-embedded-modal-btn"
onClick={this.onChange}
- disabled={!embeddedLink || !height || !width}
+ disabled={!this.isValid()}
>
{translations['generic.add']}
diff --git a/src/controls/Embedded/Component/styles.css b/src/controls/Embedded/Component/styles.css
index 84a2727d4..a5ad7dad1 100644
--- a/src/controls/Embedded/Component/styles.css
+++ b/src/controls/Embedded/Component/styles.css
@@ -53,6 +53,10 @@
font-size: 15px;
padding: 0 5px;
}
+.rdw-embedded-modal-link-input.invalid {
+ border: 1px solid #ffadad;
+ background: #fffafa;
+}
.rdw-embedded-modal-link-input-wrapper {
display: flex;
align-items: center;
@@ -99,6 +103,10 @@
border-radius: 2px;
font-size: 12px;
}
+.rdw-embedded-modal-size-input.invalid {
+ border: 1px solid #ffadad;
+ background: #fffafa;
+}
.rdw-embedded-modal-size-input:focus {
outline: none;
}
diff --git a/src/controls/Link/Component/index.js b/src/controls/Link/Component/index.js
index 4ab5d2959..e0312d0d2 100644
--- a/src/controls/Link/Component/index.js
+++ b/src/controls/Link/Component/index.js
@@ -25,6 +25,8 @@ class LayoutComponent extends Component {
linkTarget: '',
linkTitle: '',
linkTargetOption: this.props.config.defaultTargetOption,
+ linkTargetValid: false,
+ linkTitleValid: false,
};
componentDidUpdate(prevProps) {
@@ -34,24 +36,42 @@ class LayoutComponent extends Component {
linkTarget: '',
linkTitle: '',
linkTargetOption: this.props.config.defaultTargetOption,
+ linkTargetValid: false,
+ linkTitleValid: false,
});
}
}
+ validateValue = (name, value) => {
+ if (name === 'linkTarget') {
+ const { linkValidator } = this.props.config;
+ return !!value && (!linkValidator ? true : linkValidator(value));
+ }
+ return !!value
+ }
+
+ isValid = () => {
+ const { linkTitleValid, linkTargetValid } = this.state;
+ return linkTitleValid && linkTargetValid;
+ }
+
removeLink = () => {
const { onChange } = this.props;
onChange('unlink');
};
addLink = () => {
+ if (!this.isValid()) return;
const { onChange } = this.props;
const { linkTitle, linkTarget, linkTargetOption } = this.state;
onChange('link', linkTitle, linkTarget, linkTargetOption);
};
updateValue = event => {
+ const { name, value } = event.target;
this.setState({
- [`${event.target.name}`]: event.target.value,
+ [`${name}`]: value,
+ [`${name}Valid`]: this.validateValue(name, value),
});
};
@@ -73,12 +93,16 @@ class LayoutComponent extends Component {
currentState: { link, selectionText },
} = this.props;
const { linkTargetOption } = this.state;
+ const linkTarget = (link && link.target) || '';
+ const linkTitle = (link && link.title) || selectionText;
onExpandEvent();
this.setState({
showModal: true,
- linkTarget: (link && link.target) || '',
+ linkTarget,
linkTargetOption: (link && link.targetOption) || linkTargetOption,
- linkTitle: (link && link.title) || selectionText,
+ linkTitle,
+ linkTargetValid: this.validateValue('linkTarget', linkTarget),
+ linkTitleValid: this.validateValue('linkTitle', linkTitle),
});
};
@@ -88,12 +112,16 @@ class LayoutComponent extends Component {
currentState: { link, selectionText },
} = this.props;
const { linkTargetOption } = this.state;
+ const linkTarget = (link && link.target);
+ const linkTitle = (link && link.title) || selectionText;
doExpand();
this.setState({
showModal: true,
- linkTarget: link && link.target,
+ linkTarget,
linkTargetOption: (link && link.targetOption) || linkTargetOption,
- linkTitle: (link && link.title) || selectionText,
+ linkTitle,
+ linkTargetValid: this.validateValue('linkTarget', linkTarget),
+ linkTitleValid: this.validateValue('linkTitle', linkTitle),
});
};
@@ -103,7 +131,13 @@ class LayoutComponent extends Component {
doCollapse,
translations,
} = this.props;
- const { linkTitle, linkTarget, linkTargetOption } = this.state;
+ const {
+ linkTitle,
+ linkTarget,
+ linkTargetOption,
+ linkTitleValid,
+ linkTargetValid,
+ } = this.state;
return (
{translations['generic.add']}
diff --git a/src/controls/Link/Component/styles.css b/src/controls/Link/Component/styles.css
index cc2087925..dd328ff8c 100644
--- a/src/controls/Link/Component/styles.css
+++ b/src/controls/Link/Component/styles.css
@@ -45,6 +45,10 @@
.rdw-link-modal-input:focus {
outline: none;
}
+.rdw-link-modal-input.invalid {
+ border: 1px solid #ffadad;
+ background: #fffafa;
+}
.rdw-link-modal-buttonsection {
margin: 0 auto;
}
diff --git a/src/controls/Link/__test__/linkControlTest.js b/src/controls/Link/__test__/linkControlTest.js
index 002676098..9f0b9005e 100644
--- a/src/controls/Link/__test__/linkControlTest.js
+++ b/src/controls/Link/__test__/linkControlTest.js
@@ -113,6 +113,23 @@ describe("LinkControl test suite", () => {
);
});
+ it("should use custom validator if one is set with linkValidator", () => {
+ const onChange = spy();
+ const control = mount( false }} onChange={onChange} editorState={editorState} translations={localeTranslations.en} modalHandler={new ModalHandler()} />);
+ control.setState({ expanded: true });
+ const buttons = control.find(".rdw-option-wrapper");
+ buttons.first().simulate("click");
+ const inputs = control.find(".rdw-link-modal-input");
+ inputs.last().simulate("change", {
+ target: { name: "linkTitle", value: "the google" }
+ });
+ inputs.first().simulate("change", {
+ target: { name: "linkTarget", value: "www.google.com" }
+ });
+ const addButton = control.find(".rdw-link-modal-btn").first();
+ assert.isTrue(addButton.prop('disabled'));
+ });
+
it("should return input value by default", () => {
const onChange = spy();
const control = mount(