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(