@material-ui/core#InputProps TypeScript Examples

The following examples show how to use @material-ui/core#InputProps. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Autocomplete.tsx    From abacus with GNU General Public License v2.0 6 votes vote down vote up
autocompleteInputProps = (params: AutocompleteRenderInputParams, loading: boolean): InputProps => {
  return {
    ...params.InputProps,
    endAdornment: (
      <>
        {loading ? <CircularProgress color='inherit' size={20} /> : null}
        {params.InputProps.endAdornment}
      </>
    ),
  }
}
Example #2
Source File: DatePicker.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
DatePicker = ({
  label,
  onDateChange,
  ...inputProps
}: InputProps & DatePickerProps) => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <Typography variant="button">{label}</Typography>
      <BootstrapInput
        inputProps={{ 'aria-label': label }}
        type="date"
        fullWidth
        onChange={event => onDateChange?.(event.target.value)}
        {...inputProps}
      />
    </div>
  );
}
Example #3
Source File: RichEditorInternal.tsx    From clearflask with Apache License 2.0 5 votes vote down vote up
render() {
    const { onChange, theme, enqueueSnackbar, closeSnackbar, classes, iAgreeInputIsSanitized, component, ...TextFieldProps } = this.props;

    /**
     * To add single-line support visit https://github.com/quilljs/quill/issues/1432
     * Be careful, when adding keyboard module, handlers somehow stop working.
     */
    if (!TextFieldProps.multiline) {
      throw new Error('RichEditor only supports multiline');
    }

    const shrink = this.state.hasText || this.state.isFocused || false;
    const TextFieldCmpt = component || TextField;
    return (
      <TextFieldCmpt
        className={this.props.classes.textField}
        {...TextFieldProps as any /** Weird issue with variant */}
        InputProps={{
          ...this.props.InputProps || {},
          inputComponent: RichEditorInputRefWrap,
          inputProps: {
            // Anything here will be passed along to RichEditorQuill below
            ...this.props.InputProps?.inputProps || {},
            autoFocusAndSelect: this.props.autoFocusAndSelect,
            uploadImage: this.props.uploadImage,
            classes: this.props.classes,
            theme: theme,
            hidePlaceholder: !shrink && !!this.props.label,
            showControlsImmediately: this.props.showControlsImmediately,
            enqueueSnackbar: enqueueSnackbar,
            closeSnackbar: closeSnackbar,
            onFocus: e => {
              this.setState({ isFocused: true });
              this.props.InputProps?.onFocus?.(e);
            },
            onBlur: e => {
              this.setState({ isFocused: false });
              this.props.InputProps?.onBlur?.(e);
            },
          },
        }}
        onChange={(e) => {
          // Unpack these from the event defined in PropsQuill
          const delta = e.target['delta'];
          const source = e.target['source'];
          const editor = e.target['editor'];

          const hasText = editor.getLength() > 0 ? true : undefined;
          this.props.onChange && this.props.onChange(e, delta, source, editor);
          if (!!this.state.hasText !== !!hasText) {
            this.setState({ hasText });
          }
        }}
        InputLabelProps={{
          shrink,
          ...this.props.InputLabelProps || {},
        }}
      />
    );
  }
Example #4
Source File: SQFormTextField.tsx    From SQForm with MIT License 4 votes vote down vote up
function SQFormTextField({
  name,
  label,
  size = 'auto',
  isDisabled = false,
  placeholder = '- -',
  onBlur,
  onChange,
  startAdornment,
  endAdornment,
  type = 'text',
  InputProps,
  inputProps = {},
  maxCharacters,
  muiFieldProps = {},
}: SQFormTextFieldProps): React.ReactElement {
  const {
    formikField: {field},
    fieldState: {isFieldError, isFieldRequired},
    fieldHelpers: {
      handleBlur,
      handleChange: handleChangeHelper,
      HelperTextComponent,
    },
  } = useForm({
    name,
    onBlur,
    onChange,
  });

  const [valueLength, setValueLength] = React.useState(() => {
    if (typeof field.value === 'string') {
      return field.value.length;
    }

    return 0;
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValueLength(e.target.value.length);
    handleChangeHelper(e);
  };

  const maxCharactersValue = inputProps.maxLength || maxCharacters;
  const characterCounter = maxCharactersValue && (
    <small>
      : {valueLength}/{maxCharactersValue}
    </small>
  );

  const labelText = (
    <span>
      {label} {characterCounter}
    </span>
  );

  return (
    <Grid item sm={size}>
      <TextField
        id={toKebabCase(name)}
        color="primary"
        disabled={isDisabled}
        error={isFieldError}
        fullWidth={true}
        InputLabelProps={{shrink: true}}
        InputProps={{
          ...InputProps,
          startAdornment: startAdornment ? (
            <InputAdornment position="start">{startAdornment}</InputAdornment>
          ) : null,
          endAdornment: endAdornment ? (
            <InputAdornment position="end">{endAdornment}</InputAdornment>
          ) : null,
        }}
        inputProps={{
          maxLength: maxCharacters,
          ...inputProps,
        }}
        FormHelperTextProps={{error: isFieldError}}
        name={name}
        type={type}
        label={labelText}
        helperText={!isDisabled && HelperTextComponent}
        placeholder={placeholder}
        onChange={handleChange}
        onBlur={handleBlur}
        required={isFieldRequired}
        value={field.value}
        {...muiFieldProps}
      />
    </Grid>
  );
}
Example #5
Source File: RichEditorInternal.tsx    From clearflask with Apache License 2.0 4 votes vote down vote up
renderEditLinkPopper() {
    const editor = this.editorRef.current?.getEditor();
    var anchorElGetter: AnchorBoundsGetter | undefined;
    if (this.state.editLinkShow && editor) {
      anchorElGetter = () => {
        const editorRect = this.editorContainerRef.current!.getBoundingClientRect();
        const selection = editor.getSelection();
        if (!selection) {
          return;
        }
        const bounds = { ...editor.getBounds(selection.index, selection.length) };
        return {
          height: bounds.height,
          width: bounds.width,
          bottom: editorRect.bottom - editorRect.height + bounds.bottom,
          left: editorRect.left + bounds.left,
          right: editorRect.right - editorRect.width + bounds.right,
          top: editorRect.top + bounds.top,
        }
      }
    }
    return (
      <ClosablePopper
        anchorType='virtual'
        anchor={anchorElGetter}
        zIndex={this.props.theme.zIndex.modal + 1}
        closeButtonPosition='disable'
        arrow
        clickAway
        clickAwayProps={{
          onClickAway: () => {
            if (this.editorRef.current?.editor?.hasFocus()) return;
            this.setState({
              editLinkShow: undefined,
            });
          }
        }}
        placement='top'
        open={!!this.state.editLinkShow}
        onClose={() => this.setState({
          editLinkShow: undefined,
        })}
        className={this.props.classes.editLinkContainer}
        classes={{
          paper: this.props.classes.editLinkContent,
        }}
      >
        {(!this.state.editLinkPrevValue || this.state.editLinkEditing) ? (
          <>
            <div>Enter link:</div>
            <TextField
              autoFocus
              variant='standard'
              size='small'
              margin='none'
              placeholder='https://'
              error={this.state.editLinkError}
              value={(this.state.editLinkValue === undefined
                ? this.state.editLinkPrevValue
                : this.state.editLinkValue) || ''}
              onChange={e => this.setState({
                editLinkValue: e.target.value,
                editLinkError: undefined,
              })}
              InputProps={{
                classes: {
                  input: this.props.classes.editLinkUrlInput,
                },
              }}
              classes={{
                root: this.props.classes.editLinkUrlRoot,
              }}
            />
            <Button
              size='small'
              color='primary'
              classes={{
                root: this.props.classes.editLinkButton,
                label: this.props.classes.editLinkButtonLabel
              }}
              disabled={!this.state.editLinkValue || this.state.editLinkValue === this.state.editLinkPrevValue}
              onClick={e => {
                if (!editor || !this.state.editLinkShow || !this.state.editLinkValue) return;
                const url = sanitize(this.state.editLinkValue);
                if (!url) {
                  this.setState({ editLinkError: true });
                  return;
                }
                if (this.state.editLinkShow.length > 0) {
                  editor.formatText(this.state.editLinkShow, 'link', url, 'user');
                } else {
                  editor.format('link', url, 'user');
                }
                this.setState({
                  editLinkPrevValue: url,
                  editLinkEditing: undefined,
                  editLinkValue: undefined,
                  editLinkError: undefined,
                });
              }}
            >Save</Button>
          </>
        ) : (
          <>
            <div>Visit</div>
            <a
              href={this.state.editLinkPrevValue}
              className={this.props.classes.editLinkA}
              target="_blank"
              rel="noreferrer noopener ugc"
            >{this.state.editLinkPrevValue}</a>
            <Button
              size='small'
              color='primary'
              classes={{
                root: this.props.classes.editLinkButton,
                label: this.props.classes.editLinkButtonLabel
              }}
              onClick={e => {
                this.setState({
                  editLinkEditing: true,
                })
              }}
            >Edit</Button>
          </>
        )}
        {(!!this.state.editLinkPrevValue) && (
          <Button
            size='small'
            color='primary'
            classes={{
              root: this.props.classes.editLinkButton,
              label: this.props.classes.editLinkButtonLabel
            }}
            onClick={e => {
              if (!editor || !this.state.editLinkShow) return;
              const editLinkShow = this.state.editLinkShow;
              this.setState({
                editLinkShow: undefined,
              }, () => {
                const [link, offset] = (editor.scroll as any).descendant(QuillFormatLinkExtended, editLinkShow.index);
                if (link !== null) {
                  editor.formatText(editLinkShow.index - offset, link.length(), 'link', false, 'user');
                } else {
                  editor.formatText(editLinkShow, { link: false }, 'user');
                }
              });
            }}
          >Remove</Button>
        )}
      </ClosablePopper >
    );
  }
Example #6
Source File: RichEditorInternal.tsx    From clearflask with Apache License 2.0 4 votes vote down vote up
class RichEditorQuill extends React.Component<PropsQuill & Omit<InputProps, 'onChange'> & WithStyles<typeof styles, true> & WithSnackbarProps, StateQuill> implements PropsInputRef {
  readonly editorContainerRef: React.RefObject<HTMLDivElement> = React.createRef();
  readonly editorRef: React.RefObject<ReactQuill> = React.createRef();
  readonly dropzoneRef: React.RefObject<DropzoneRef> = React.createRef();
  inputIsFocused: boolean = false;
  isFocused: boolean = false;
  handleFocusBlurDebounced: () => void;

  constructor(props) {
    super(props);

    this.state = {
      showFormats: !!props.showControlsImmediately,
      showFormatsExtended: !!props.showControlsImmediately,
    };

    this.handleFocusBlurDebounced = debounce(() => this.handleFocusBlur(), 100);
  }

  /**
   * Focus and Blur events are tricky in Quill.
   * 
   * See:
   * - https://github.com/quilljs/quill/issues/1680
   * - https://github.com/zenoamaro/react-quill/issues/276
   * 
   * The solution attempts to solve these things:
   * - Focus detection using Quill editor selection AND input selection
   * - Spurious blur/focus flapping during clipboard paste
   * - Spurious blur/focus flapping when link popper open and click into quill editor but not textarea directly
   * - Keep focused during link popper changing
   * 
   * Outstanding issues:
   * - On clipboard paste, editor intermittently loses focus. This is mitigated with a debounce,
   *   but issue still occurrs occassionally if you continuously paste.
   */
  handleFocusBlur() {
    const editor = this.editorRef.current?.getEditor();
    if (!editor) return;
    const inputHasFocus = !!this.inputIsFocused;
    const editorHasSelection = !!editor.getSelection();
    const linkChangeHasFocus = !!this.state.editLinkShow;
    const hasFocus = inputHasFocus || editorHasSelection || linkChangeHasFocus;
    if (!this.isFocused && hasFocus) {
      this.isFocused = true;
      if (!this.state.showFormats) {
        this.setState({ showFormats: true });
      }
      this.props.onFocus?.({
        editor,
        stopPropagation: () => { },
      });
    } else if (this.isFocused && !hasFocus) {
      this.isFocused = false;
      this.props.onBlur?.({
        editor,
        stopPropagation: () => { },
      });
    }
  }

  focus(): void {
    this.editorRef.current?.focus();
  }

  blur(): void {
    this.editorRef.current?.blur();
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (this.state.editLinkShow !== prevState.editLinkShow) {
      this.handleFocusBlurDebounced();
    }
  }

  componentDidMount() {
    const editor = this.editorRef.current!.getEditor();

    editor.root.addEventListener('focus', e => {
      this.inputIsFocused = true;
      this.handleFocusBlurDebounced();
    });
    editor.root.addEventListener('blur', e => {
      this.inputIsFocused = false;
      this.handleFocusBlurDebounced();
    });

    editor.on('editor-change', (type, range) => {
      if (type === 'selection-change') {
        this.updateFormats(editor, range);
        this.handleFocusBlurDebounced();
      }
    });
    editor.on('scroll-optimize' as any, () => {
      const range = editor.getSelection();
      this.updateFormats(editor, range || undefined);
    });
    if (this.props.autoFocus) {
      editor.focus();
    }
    if (this.props.autoFocusAndSelect) {
      editor.setSelection(0, editor.getLength(), 'api');
    }
  }
  counter = 0;
  render() {
    const { value, onChange, onFocus, onBlur, ...otherInputProps } = this.props;

    return (
      <Dropzone
        ref={this.dropzoneRef}
        maxSize={10 * 1024 * 1024}
        multiple
        noClick
        noKeyboard
        onDrop={(acceptedFiles, rejectedFiles, e) => {
          rejectedFiles.forEach(rejectedFile => {
            rejectedFile.errors.forEach(error => {
              this.props.enqueueSnackbar(
                `${rejectedFile.file.name}: ${error.message}`,
                { variant: 'error' });
            })
          })
          this.onDropFiles(acceptedFiles);
        }}
      >
        {({ getRootProps, getInputProps }) => (
          <div
            {...getRootProps()}
            className={this.props.classes.editorContainer}
            ref={this.editorContainerRef}
            onClick={e => this.editorRef.current?.focus()}
          >
            <input {...getInputProps()} />
            <ReactQuill
              {...otherInputProps as any}
              modules={{
                clipboard: {
                  /**
                   * Fixes issue with newlines multiplying
                   * NOTE: When upgrading to Quill V2, this property is deprecated!
                   * https://github.com/KillerCodeMonkey/ngx-quill/issues/357#issuecomment-578138062
                   */
                  matchVisual: false,
                },
                imageResize: {
                  modules: ['Resize', ToolbarExtended],
                  toolbarButtonSvgStyles: {},
                },
              }}
              className={classNames(!!this.props.hidePlaceholder && 'hidePlaceholder', this.props.classes.quill)}
              theme={'' /** core theme */}
              ref={this.editorRef}
              value={value}
              onChange={(valueNew, delta, source, editor) => {
                this.props.onChange && this.props.onChange({
                  target: {
                    value: this.isQuillEmpty(editor) ? '' : valueNew,
                    delta,
                    source,
                    editor,
                  }
                });
                if (delta.ops && source === 'user') {
                  var retainCount = 0;
                  for (const op of delta.ops) {
                    if (op.insert
                      && typeof op.insert === 'object'
                      && op.insert.image
                      && typeof op.insert.image === 'string'
                      && op.insert.image.startsWith('data:')) {
                      this.onDataImageFound(op.insert.image, retainCount - 1);
                    }
                    retainCount += (op.insert ? 1 : 0) + (op.retain || 0) - (op.delete || 0);
                  }
                }
              }}
              formats={[
                'bold',
                'strike',
                'list',
                'link',
                'italic',
                'underline',
                'blockquote',
                'code-block',
                'indent',
                'header',
                'image', 'width', 'align',
              ]}
            />
            <Collapse mountOnEnter in={!!this.state.showFormats} className={this.props.classes.toggleButtonGroups}>
              <div className={this.props.classes.toggleButtonGroup}>
                {this.renderToggleButton(BoldIcon, 'bold', undefined, true)}
                {this.renderToggleButton(ItalicIcon, 'italic', undefined, true)}
                {this.renderToggleButton(UnderlineIcon, 'underline', undefined, true)}
                {this.renderToggleButton(StrikethroughIcon, 'strike', undefined, true)}
                {this.renderToggleButtonLink(LinkIcon)}
                {this.renderToggleButtonImage(ImageIcon)}
                {this.renderToggleButton(CodeIcon, undefined, 'code-block', true)}
                <Fade in={!this.state.showFormatsExtended}>
                  {this.renderToggleButtonCmpt(MoreIcon, false, () => this.setState({ showFormatsExtended: true }))}
                </Fade>
              </div>
              <Collapse mountOnEnter in={!!this.state.showFormatsExtended}>
                <div className={this.props.classes.toggleButtonGroup}>
                  {this.renderToggleButton(Heading2Icon, undefined, 'header', 2)}
                  {this.renderToggleButton(Heading3Icon, undefined, 'header', 3)}
                  {this.renderToggleButton(Heading4Icon, undefined, 'header', 4)}
                  {this.renderToggleButton(ListCheckIcon, undefined, 'list', 'unchecked', ['unchecked', 'checked'])}
                  {this.renderToggleButton(ListUnorderedIcon, undefined, 'list', 'bullet')}
                  {this.renderToggleButton(ListOrderedIcon, undefined, 'list', 'ordered')}
                  {this.renderToggleButton(QuoteIcon, undefined, 'blockquote', true)}
                </div>
              </Collapse>
            </Collapse>
            {this.renderEditLinkPopper()}
          </div>
        )}
      </Dropzone>
    );
  }

  renderEditLinkPopper() {
    const editor = this.editorRef.current?.getEditor();
    var anchorElGetter: AnchorBoundsGetter | undefined;
    if (this.state.editLinkShow && editor) {
      anchorElGetter = () => {
        const editorRect = this.editorContainerRef.current!.getBoundingClientRect();
        const selection = editor.getSelection();
        if (!selection) {
          return;
        }
        const bounds = { ...editor.getBounds(selection.index, selection.length) };
        return {
          height: bounds.height,
          width: bounds.width,
          bottom: editorRect.bottom - editorRect.height + bounds.bottom,
          left: editorRect.left + bounds.left,
          right: editorRect.right - editorRect.width + bounds.right,
          top: editorRect.top + bounds.top,
        }
      }
    }
    return (
      <ClosablePopper
        anchorType='virtual'
        anchor={anchorElGetter}
        zIndex={this.props.theme.zIndex.modal + 1}
        closeButtonPosition='disable'
        arrow
        clickAway
        clickAwayProps={{
          onClickAway: () => {
            if (this.editorRef.current?.editor?.hasFocus()) return;
            this.setState({
              editLinkShow: undefined,
            });
          }
        }}
        placement='top'
        open={!!this.state.editLinkShow}
        onClose={() => this.setState({
          editLinkShow: undefined,
        })}
        className={this.props.classes.editLinkContainer}
        classes={{
          paper: this.props.classes.editLinkContent,
        }}
      >
        {(!this.state.editLinkPrevValue || this.state.editLinkEditing) ? (
          <>
            <div>Enter link:</div>
            <TextField
              autoFocus
              variant='standard'
              size='small'
              margin='none'
              placeholder='https://'
              error={this.state.editLinkError}
              value={(this.state.editLinkValue === undefined
                ? this.state.editLinkPrevValue
                : this.state.editLinkValue) || ''}
              onChange={e => this.setState({
                editLinkValue: e.target.value,
                editLinkError: undefined,
              })}
              InputProps={{
                classes: {
                  input: this.props.classes.editLinkUrlInput,
                },
              }}
              classes={{
                root: this.props.classes.editLinkUrlRoot,
              }}
            />
            <Button
              size='small'
              color='primary'
              classes={{
                root: this.props.classes.editLinkButton,
                label: this.props.classes.editLinkButtonLabel
              }}
              disabled={!this.state.editLinkValue || this.state.editLinkValue === this.state.editLinkPrevValue}
              onClick={e => {
                if (!editor || !this.state.editLinkShow || !this.state.editLinkValue) return;
                const url = sanitize(this.state.editLinkValue);
                if (!url) {
                  this.setState({ editLinkError: true });
                  return;
                }
                if (this.state.editLinkShow.length > 0) {
                  editor.formatText(this.state.editLinkShow, 'link', url, 'user');
                } else {
                  editor.format('link', url, 'user');
                }
                this.setState({
                  editLinkPrevValue: url,
                  editLinkEditing: undefined,
                  editLinkValue: undefined,
                  editLinkError: undefined,
                });
              }}
            >Save</Button>
          </>
        ) : (
          <>
            <div>Visit</div>
            <a
              href={this.state.editLinkPrevValue}
              className={this.props.classes.editLinkA}
              target="_blank"
              rel="noreferrer noopener ugc"
            >{this.state.editLinkPrevValue}</a>
            <Button
              size='small'
              color='primary'
              classes={{
                root: this.props.classes.editLinkButton,
                label: this.props.classes.editLinkButtonLabel
              }}
              onClick={e => {
                this.setState({
                  editLinkEditing: true,
                })
              }}
            >Edit</Button>
          </>
        )}
        {(!!this.state.editLinkPrevValue) && (
          <Button
            size='small'
            color='primary'
            classes={{
              root: this.props.classes.editLinkButton,
              label: this.props.classes.editLinkButtonLabel
            }}
            onClick={e => {
              if (!editor || !this.state.editLinkShow) return;
              const editLinkShow = this.state.editLinkShow;
              this.setState({
                editLinkShow: undefined,
              }, () => {
                const [link, offset] = (editor.scroll as any).descendant(QuillFormatLinkExtended, editLinkShow.index);
                if (link !== null) {
                  editor.formatText(editLinkShow.index - offset, link.length(), 'link', false, 'user');
                } else {
                  editor.formatText(editLinkShow, { link: false }, 'user');
                }
              });
            }}
          >Remove</Button>
        )}
      </ClosablePopper >
    );
  }

  renderToggleButtonLink(IconCmpt) {
    const isActive = !!this.state.activeFormats?.link;
    return this.renderToggleButtonCmpt(
      IconCmpt,
      isActive,
      e => {
        const editor = this.editorRef.current?.getEditor();
        const selection = editor?.getSelection(true);
        if (!editor || !selection) return;
        const range = selection.length > 0
          ? selection
          : this.getWordBoundary(editor, selection.index);
        editor.setSelection(range, 'user');
        this.setState({
          editLinkShow: range,
          editLinkPrevValue: this.state.activeFormats?.link,
          editLinkEditing: true,
          editLinkValue: undefined,
          editLinkError: undefined,
        });
      })
  }

  renderToggleButtonImage(IconCmpt) {
    return this.renderToggleButtonCmpt(
      IconCmpt,
      false,
      e => this.dropzoneRef.current?.open())
  }

  /**
   * Convert data url by uploading and replacing with remote url.
   * 
   * This catches any images drag-n-drop dropzone or uploaded using
   * the image upload button.
   */
  async onDropFiles(files: File[]) {
    const editor = this.editorRef.current?.getEditor();
    if (!editor) return;
    const range = editor.getSelection(true);
    for (const file of files) {
      try {
        const url = await this.props.uploadImage(file);
        editor.insertEmbed(range.index, 'image', url, 'user');
      } catch (e) {
        this.props.enqueueSnackbar(
          `${file.name}: ${e}`,
          { variant: 'error' });
      }
    }
  }

  /**
   * Convert data url by uploading and replacing with remote url.
   * 
   * This catches any images not handled by the dropzone.
   * Particularly if you paste an image into the editor.
   */
  async onDataImageFound(imageDataUrl, retainCount) {
    const editor = this.editorRef.current?.getEditor();
    if (!editor) return;

    const blob = dataImageToBlob(imageDataUrl);

    var imageLink;
    try {
      imageLink = await this.props.uploadImage(blob);
    } catch (e) {
      imageLink = false;
      this.props.enqueueSnackbar(
        `Failed image upload: ${e}`,
        { variant: 'error' });
    };

    // Ensure the image is still in the same spot
    // or find where it was moved to and adjust retainCount
    const isOpOurImage = o => (typeof o?.insert === 'object'
      && typeof o.insert.image === 'string'
      && o.insert.image.startsWith(imageDataUrl.slice(0, Math.min(50, imageDataUrl.length))));
    var op = editor.getContents(retainCount).ops?.[0];
    if (!op || !isOpOurImage(op)) {
      retainCount = -1;
      do {
        retainCount++;
        // There must be a better way to find the index without getting contents
        // for each character here, but:
        // - This is safer than parsing the Delta format
        // - This should be a rare case
        op = editor.getContents(retainCount, 1).ops?.[0];
        if (op === undefined) {
          // The image was deleted while uploaded
          return;
        }
      } while (!isOpOurImage(op));
    }

    if (imageLink) {
      editor.updateContents(new Delta()
        .retain(retainCount)
        .delete(1)
        .insert({ image: imageLink }, op.attributes),
        'silent');
    } else {
      editor.updateContents(new Delta()
        .retain(retainCount)
        .delete(1),
        'silent');
    }
  }

  updateFormats(editor: Quill, range?: RangeStatic) {
    if (!range) {
      if (!!this.state.editLinkShow && !this.state.editLinkEditing) {
        this.setState({
          activeFormats: undefined,
          editLinkShow: undefined,
        });
      } else {
        this.setState({ activeFormats: undefined });
      }
    } else {
      const newActiveFormats = editor.getFormat(range);
      const isLinkActive = !!newActiveFormats.link;
      if (isLinkActive !== !!this.state.editLinkShow) {
        var rangeWord: RangeStatic | undefined;
        const selection = editor.getSelection();
        if (!!selection) {
          rangeWord = selection.length > 0
            ? selection
            : this.getWordBoundary(editor, selection.index);
        }



        this.setState({
          activeFormats: newActiveFormats,
          ...((isLinkActive && !!rangeWord) ? {
            editLinkShow: rangeWord,
            editLinkPrevValue: newActiveFormats.link,
            editLinkEditing: undefined,
            editLinkValue: undefined,
            editLinkError: undefined,
          } : (!this.state.editLinkEditing ? {
            editLinkShow: undefined,
          } : {}))
        });
      } else {
        this.setState({ activeFormats: newActiveFormats });
      }
    }
  }

  renderToggleButton(IconCmpt, format: string | undefined, formatLine: string | undefined, defaultValue: any, valueOpts: any[] = [defaultValue]) {
    const isActiveFormat = !!this.state.activeFormats && !!format && valueOpts.includes(this.state.activeFormats[format]);
    const isActiveFormatLine = !!this.state.activeFormats && !!formatLine && valueOpts.includes(this.state.activeFormats[formatLine]);
    const toggle = e => {
      const editor = this.editorRef.current?.getEditor();
      if (!editor) return;
      const range = editor.getSelection(true);
      const hasSelection = !!range && range.length > 0;
      // Use inline formatting if we have selected text or if there is no line formatting
      if (format && (!formatLine || hasSelection)) {
        if (hasSelection || !range) {
          editor.format(format, isActiveFormat ? false : defaultValue, 'user');
        } else {
          const wordBoundaryRange = this.getWordBoundary(editor, range.index);
          if (wordBoundaryRange.length > 0) {
            editor.formatText(wordBoundaryRange, { [format]: isActiveFormat ? false : defaultValue }, 'user');
          } else {
            editor.format(format, isActiveFormat ? false : defaultValue, 'user');
          }
        }
      } else if (!!formatLine) {
        editor.format(formatLine, isActiveFormatLine ? false : defaultValue, 'user');
      }
    };
    return this.renderToggleButtonCmpt(
      IconCmpt,
      isActiveFormat || isActiveFormatLine,
      toggle);
  }

  renderToggleButtonCmpt(IconCmpt, isActive, onClick) {
    const onChange = e => {
      onClick && onClick(e);
      e.preventDefault();
    };
    return (
      <Button
        className={this.props.classes.toggleButton}
        value='check'
        color={isActive ? 'primary' : 'inherit'}
        style={{ color: isActive ? undefined : this.props.theme.palette.text.hint }}
        onMouseDown={e => e.preventDefault()}
        onChange={onChange}
        onClick={onChange}
      >
        <IconCmpt fontSize='inherit' />
      </Button>
    );
  }

  /** 
   * Empty if contains only whitespace.
   * https://github.com/quilljs/quill/issues/163#issuecomment-561341501
   */
  isQuillEmpty(editor: UnprivilegedEditor): boolean {
    if ((editor.getContents()['ops'] || []).length !== 1) {
      return false;
    }
    return editor.getText().trim().length === 0;
  }


  /**
   * Selects whole word if cursor is inside the word (not at the beginning or end)
   */
  getWordBoundary(editor: Quill, index: number): RangeStatic {
    const [line, offset] = editor.getLine(index);
    if (!line) {
      return { index, length: 0 };
    }
    const text = editor.getText(
      editor.getIndex(line),
      line.length());

    // First check we are surrounded by non-whitespace
    if (offset === 0 || (WhitespaceChars.indexOf(text[offset - 1]) > -1)
      || offset >= text.length || (WhitespaceChars.indexOf(text[offset]) > -1)) {
      return { index, length: 0 };
    }

    // Iterate to the left until we find the beginning of word or start of line
    var boundaryIndex = index - 1;
    for (var x = offset - 2; x >= 0; x--) {
      if (WhitespaceChars.indexOf(text[x]) > -1) {
        break;
      }
      boundaryIndex--;
    }

    // Iterate to the right until we find the end of word or end of line
    var boundaryLength = index + 1 - boundaryIndex;
    for (var y = offset + 1; y < text.length; y++) {
      if (WhitespaceChars.indexOf(text[y]) > -1) {
        break;
      }
      boundaryLength++;
    }

    return { index: boundaryIndex, length: boundaryLength };
  }
}