michelson / dante2

A complete rewrite of dante editor in draft-js
https://michelson.github.io/dante2/
Other
912 stars 121 forks source link

Safari Focus Bug #221

Open indigofa opened 4 years ago

indigofa commented 4 years ago

I'm having a bug if I insert image in the first block video screenshare , if the image is inserted at the first block the focus is at the body and If i continue to type it crashed the browser.

ERROR in console Screen Shot 2020-04-20 at 12 19 32 PM

This is the code I have

import React from "react"
import { EditorBlock, EditorState } from "draft-js"
import axios from "axios"
import { updateDataOfBlock, addNewBlockAt } from "./model/index.js"
import { image } from "./icons"

export default class ImageBlock extends React.Component {
  constructor(props) {
    super(props)
    let existing_data = this.props.block.getData().toJS()
    this.image_tag = null
    this.config = this.props.blockProps.config
    this.file = this.props.blockProps.data.get("file")
    this.state = {
      loading: false,
      selected: false,
      loading_progress: 0,
      caption: this.defaultPlaceholder(),
      direction: existing_data.direction || "center",
      width: 0,
      height: 0,
      file: null,
      url: this.blockPropsSrc() || this.defaultUrl(existing_data),
      aspect_ratio: this.defaultAspectRatio(existing_data),
    }
  }

  componentDidMount() {
    this.figCaptionNode.focus()
    return this.replaceImg()
  }

  componentWillUnmount() {
    //debugger
  }

  blockPropsSrc = () => {
    return this.props.blockProps.data.src
  }

  defaultUrl = data => {
    if (data.url) {
      return data.url
    }

    if (data.url) {
      if (data.file) {
        return URL.createObjectURL(data.file)
      } else {
        return data.url
      }
    } else {
      return this.props.blockProps.data.src
    }
  }

  defaultPlaceholder = () => {
    return this.props.blockProps.config.image_caption_placeholder
  }

  defaultAspectRatio = data => {
    if (data.aspect_ratio) {
      return {
        width: data.aspect_ratio["width"],
        height: data.aspect_ratio["height"],
        ratio: data.aspect_ratio["ratio"],
      }
    } else {
      return {
        width: 0,
        height: 0,
        ratio: 100,
      }
    }
  }

  getAspectRatio = (w, h) => {
    let maxWidth = 1000
    let maxHeight = 1000
    let ratio = 0
    let width = w // Current image width
    let height = h // Current image height

    // Check if the current width is larger than the max
    if (width > maxWidth) {
      ratio = maxWidth / width // get ratio for scaling image
      height = height * ratio // Reset height to match scaled image
      width = width * ratio // Reset width to match scaled image

      // Check if current height is larger than max
    } else if (height > maxHeight) {
      ratio = maxHeight / height // get ratio for scaling image
      width = width * ratio // Reset width to match scaled image
      height = height * ratio // Reset height to match scaled image
    }

    let fill_ratio = (height / width) * 100
    let result = { width, height, ratio: fill_ratio }
    // console.log result
    return result
  }

  // will update block state
  updateData = () => {
    let { blockProps, block } = this.props
    let { getEditorState } = blockProps
    let { setEditorState } = blockProps
    let data = block.getData()
    let newData = data.merge(this.state).merge({ forceUpload: false })
    return setEditorState(updateDataOfBlock(getEditorState(), block, newData))
  }

  replaceImg = () => {
    this.img = new Image()
    this.img.src = this.image_tag.src
    this.setState({
      url: this.img.src,
    })
    let self = this
    // exit only when not blob and not forceUload
    if (
      !this.img.src.includes("blob:") &&
      !this.props.block.data.get("forceUpload")
    ) {
      return
    }
    return (this.img.onload = () => {
      this.setState({
        width: this.img.width,
        height: this.img.height,
        aspect_ratio: self.getAspectRatio(this.img.width, this.img.height),
      })

      return this.handleUpload()
    })
  }

  startLoader = () => {
    return this.setState({
      loading: true,
    })
  }

  stopLoader = () => {
    return this.setState({
      loading: false,
    })
  }

  handleUpload = () => {
    this.startLoader()
    this.updateData()
    return this.uploadFile()
  }

  aspectRatio = () => {
    return {
      maxWidth: `${this.state.aspect_ratio.width}`,
      maxHeight: `${this.state.aspect_ratio.height}`,
      ratio: `${this.state.aspect_ratio.height}`,
    }
  }

  updateDataSelection = () => {
    const { getEditorState, setEditorState } = this.props.blockProps
    const newselection = getEditorState()
      .getSelection()
      .merge({
        anchorKey: this.props.block.getKey(),
        focusKey: this.props.block.getKey(),
      })

    return setEditorState(
      EditorState.forceSelection(getEditorState(), newselection)
    )
  }

  handleGrafFigureSelectImg = e => {
    e.preventDefault()
    return this.setState({ selected: true }, this.updateDataSelection)
  }

  //main_editor.onChange(main_editor.state.editorState)

  coords = () => {
    return {
      maxWidth: `${this.state.aspect_ratio.width}px`,
      maxHeight: `${this.state.aspect_ratio.height}px`,
    }
  }

  getBase64Image = img => {
    let canvas = document.createElement("canvas")
    canvas.width = img.width
    canvas.height = img.height
    let ctx = canvas.getContext("2d")
    ctx.drawImage(img, 0, 0)
    let dataURL = canvas.toDataURL("image/png")

    return dataURL
  }

  formatData = () => {
    let formData = new FormData()
    if (this.file) {
      let formName = this.config.upload_formName || "file"

      formData.append(formName, this.file)
      return formData
    } else {
      formData.append("url", this.props.blockProps.data.get("url"))
      return formData
    }
  }

  getUploadUrl = () => {
    let url = this.config.upload_url
    if (typeof url === "function") {
      return url()
    } else {
      return url
    }
  }

  getUploadHeaders() {
    return this.config.upload_headers || {}
  }

  uploadFile = () => {
    // custom upload handler
    if (this.config.upload_handler) {
      return this.config.upload_handler(this.formatData().get("file"), this)
    }

    if (!this.config.upload_url) {
      this.stopLoader()
      return
    }

    this.props.blockProps.addLock()

    axios({
      method: "post",
      url: this.getUploadUrl(),
      headers: this.getUploadHeaders(),
      data: this.formatData(),
      onUploadProgress: e => {
        return this.updateProgressBar(e)
      },
    })
      .then(result => {
        this.uploadCompleted(result.data.url)

        if (this.config.upload_callback) {
          return this.config.upload_callback(result, this)
        }
      })
      .catch(error => {
        this.uploadFailed()

        console.log(`ERROR: got error uploading file ${error}`)
        if (this.config.upload_error_callback) {
          return this.config.upload_error_callback(error, this)
        }
      })

    return json_response => {
      return this.uploadCompleted(json_response.url)
    }
  }

  uploadFailed = () => {
    this.props.blockProps.removeLock()
    this.stopLoader()
  }

  uploadCompleted(url) {
    this.setState({ url }, this.updateData)
    this.props.blockProps.removeLock()
    this.stopLoader()
    this.file = null
  }

  updateProgressBar(e) {
    let complete = this.state.loading_progress
    if (e.lengthComputable) {
      complete = (e.loaded / e.total) * 100
      complete = complete != null ? complete : { complete: 0 }
      this.setState({
        loading_progress: complete,
      })
      return console.log(`complete: ${complete}`)
    }
  }

  placeHolderEnabled = () => {
    return this.state.enabled || this.props.block.getText()
  }

  placeholderText = () => {
    return this.config.image_caption_placeholder || "caption here (optional)"
  }

  handleFocus(e) {}

  render = () => {
    return (
      <figure ref="image_tag2" suppressContentEditableWarning={true}>
        <div
          role="button"
          tabIndex="0"
          className="aspectRatioPlaceholder is-locked"
          style={this.coords()}
          onClick={this.handleGrafFigureSelectImg}
          onKeyDown={this.handleGrafFigureSelectImg}
        >
          <div
            style={{ paddingBottom: `${this.state.aspect_ratio.ratio}%` }}
            className="aspect-ratio-fill"
          />
          <img
            src={this.state.url}
            ref={ref => (this.image_tag = ref)}
            height={this.state.aspect_ratio.height}
            width={this.state.aspect_ratio.width}
            className="graf-image"
            contentEditable={false}
            alt={this.state.url}
          />
          <Loader
            toggle={this.state.loading}
            progress={this.state.loading_progress}
          />
        </div>
        <figcaption className="imageCaption">
          {this.props.block.getText().length === 0 ? (
            <span
              className="danteDefaultPlaceholder"
              ref={node => (this.figCaptionNode = node)}
            >
              {this.placeholderText()}
            </span>
          ) : (
            undefined
          )}
          <EditorBlock
            {...Object.assign({}, this.props, {
              editable: true,
              className: "imageCaption",
            })}
          />
        </figcaption>
      </figure>
    )
  }
}

class Loader extends React.Component {
  render = () => {
    return (
      <div>
        {this.props.toggle ? (
          <div className="image-upoader-loader">
            <p>
              {this.props.progress === 100 ? (
                "processing image..."
              ) : (
                <span>
                  <span>loading</span>
                </span>
              )}
            </p>
          </div>
        ) : (
          undefined
        )}
      </div>
    )
  }
}

export const ImageBlockConfig = (options = {}) => {
  let config = {
    title: "add an image",
    type: "image",
    icon: image,
    block: ImageBlock,
    editable: true,
    renderable: true,
    breakOnContinuous: true,
    wrapper_class: "graf graf--figure",
    selected_class: "is-selected is-mediaFocused",
    selectedFn: block => {
      const { direction } = block.getData().toJS()
      switch (direction) {
        case "left":
          return "graf--layoutOutsetLeft"
        case "center":
          return ""
        case "wide":
          return "sectionLayout--fullWidth"
        case "fill":
          return "graf--layoutFillWidth"
        default:
          return ""
      }
    },
    handleEnterWithoutText(ctx, block) {
      const { editorState } = ctx.state
      return ctx.onChange(addNewBlockAt(editorState, block.getKey()))
    },
    handleEnterWithText(ctx, block) {
      const { editorState } = ctx.state
      return ctx.onChange(addNewBlockAt(editorState, block.getKey()))
    },
    widget_options: {
      displayOnInlineTooltip: true,
      insertion: "upload",
      insert_block: "image",
    },
    options: {
      upload_url: "",
      upload_headers: null,
      upload_formName: "file",
      upload_callback: null,
      upload_error_callback: null,
      delete_block_callback: null,
      image_caption_placeholder: "type a caption (optional)",
    },
  }

  return Object.assign(config, options)
}

React Component

// Uploader fn used in ImageBlockConfig upload_handler: handleUploadContentMediaHander

const handleUploadContentMediaHander = (file, imageBlock) => {
    setLoadingContentMedia(true)
    imageBlock.startLoader()

    const ext = file.name.substr(file.name.lastIndexOf(".") + 1)
    const filename = `${uuidv4()}.${ext}`

    const getSignedUrl = async cb => {
      const response = await Api.getImageUploadUrl(getToken(), [
        { key: filename },
      ])

      cb(null, response)
    }

    const uploadMedia = async (storage, cb) => {
      try {
        await fetch(storage[0].url, {
          method: "PUT",
          body: file,
        })

        setLoadingContentMedia(false)
        imageBlock.stopLoader()
        cb(null, storage)
      } catch (err) {
        setLoadingContentMedia(false)
        imageBlock.stopLoader()
        cb(err)
      }
    }

<Dante
                        content={editorState.current}
                        body_placeholder="Body"
                        widgets={[
                          ImageBlockConfig({
                            options: {
                              upload_handler: handleUploadContentMediaHander,
                            },
                          })
                        ]}
                        default_wrappers={[
                          { className: "h1-level-1", block: "header-one" },
                        ]}
                        onChange={editor => {
                          const html = convertToHTML({
                            styleToHTML: style => {
                              if (style.startsWith("CUSTOM_COLOR_")) {
                                return (
                                  <span
                                    style={{
                                      color: style.substr(style.length - 7),
                                    }}
                                  />
                                )
                              }
                            },
                            entityToHTML: (entity, originalText) => {
                              if (entity.type === "LINK") {
                                return (
                                  <a href={entity.data.url}>{originalText}</a>
                                )
                              }
                              return originalText
                            },
                            blockToHTML: block => {
                              if (block.type === "image") {
                                return (
                                  <img
                                    src={block.data.url}
                                    alt={block.text}
                                    data-previewsource={block.data.prevUrl}
                                  />
                                )
                              }
                              if (block.type === "code-block") {
                                return (
                                  <code>
                                    <pre>{block.text}</pre>
                                  </code>
                                )
                              }
                            },
                          })(editor.state.editorState._immutable.currentContent)

                        }}
                      />