Input Image

Input Image คือ view ใช้สำหรับรับค่า file รูป เพื่อนำไปอัพโหลดขึ้น server

Source Code

Reactjs Styled Component
Copy
import { useEffect, useRef, useState } from "react";
import ReactCrop, { centerCrop, makeAspectCrop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import styled from "styled-components";
import { fileToBase64, resizeImageBase64 } from "../../util/common";
import Modal from "../layout/Modal";

import IconCaption from "../element/icon/IconCaption";
import IconImage from "../element/icon/IconImage";
import IconPlus from "../element/icon/IconPlus";
import InputButton from "./InputButton";
import InputText from "./InputText";

export default function InputImage({
  initValue,
  onChange,
  placeHolder = "Choose image file",
  maxSize = 1500,
  width = "200px",
  height = "200px",
  className,
  borderRadius = "10px",
  aspect = 600 / 300,
  cropModalMaxWidth = "600px",
  cropModalMaxHeight = "500px",
  isCrop,
  isCaption,
  style,
}) {
  const inputRef = useRef(null);
  const [imgSrc, setImgSrc] = useState("");
  const [isActiveFileChange, setIsActiveFileChange] = useState(true);
  const [captionText, setCaptionText] = useState("");
  const [isShowCaption, setIsShowCaption] = useState(false);

  // crop
  const imgRef = useRef(null);
  const [isShowModalCrop, setIsShowModalCrop] = useState(false);
  const [completedCrop, setCompletedCrop] = useState(null);
  const [crop, setCrop] = useState();

  useEffect(() => {
    if (initValue) {
      setImgSrc(initValue.file);
      setCaptionText(initValue.caption);
    }
  }, [initValue]);

  const imageChange = async (e) => {
    let file = e?.target?.files[0];
    if (file) {
      if (isCrop) {
        setCrop(null);
        const fileBase64 = await fileToBase64(file);
        setImgSrc(fileBase64);
        setIsShowModalCrop(true);
      } else {
        const fileBase64 = await fileToBase64(file);
        const resize = await resizeImageBase64(fileBase64, maxSize, maxSize);
        if (resize) {
          setImgSrc(resize);
        }
        if (onChange) {
          onChange({
            file: resize,
            caption: captionText,
          });
        }
      }
    }
  };

  const onBlurHandler = () => {
    if (onChange) {
      onChange({ file: imgSrc, caption: captionText });
    }
  };

  const handleCaptionChange = (e) => {
    setCaptionText(e.target.value);
  };

  // crop
  const onClickConfirmCrop = async () => {
    if (imgRef.current && crop.width && crop.height) {
      const croppedImageUrl = await getCroppedImg(imgRef.current);
      if (croppedImageUrl) {
        setImgSrc(croppedImageUrl);
      }
      if (onChange) {
        onChange({ file: croppedImageUrl, caption: captionText });
      }
      setIsShowModalCrop(false);
    }
  };
  const getCroppedImg = async (image) => {
    const canvas = document.createElement("canvas");
    const crop = completedCrop;
    if (!crop || !canvas || !image) {
      return;
    }
    const scaleX = image.naturalWidth / image.width;
    const scaleY = image.naturalHeight / image.height;
    const ctx = canvas.getContext("2d");
    const pixelRatio = window.devicePixelRatio;
    canvas.width = crop.width * pixelRatio * scaleX;
    canvas.height = crop.height * pixelRatio * scaleY;
    ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
    ctx.imageSmoothingQuality = "high";
    ctx.drawImage(
      image,
      crop.x * scaleX,
      crop.y * scaleY,
      crop.width * scaleX,
      crop.height * scaleY,
      0,
      0,
      crop.width * scaleX,
      crop.height * scaleY
    );

    const base64Image = canvas.toDataURL("image/png", 1);
    return await resizeImageBase64(base64Image, maxSize, maxSize);
  };
  const centerAspectCrop = (mediaWidth, mediaHeight, aspect) => {
    return centerCrop(
      makeAspectCrop(
        {
          unit: "%",
          width: 90,
        },
        aspect,
        mediaWidth,
        mediaHeight
      ),
      mediaWidth,
      mediaHeight
    );
  };
  const onCloseModal = () => {
    setIsShowModalCrop(false);
    setImgSrc("");
  };
  const onImageLoad = (e) => {
    if (aspect) {
      const { width, height } = e.currentTarget;
      setCrop(centerAspectCrop(width, height, aspect));
    }
  };

  return (
    <Styled
      style={style}
      className={className}
      onBlur={onBlurHandler}
      $width={width}
      $height={height}
      $borderRadius={borderRadius}
    >
      <div className={"outer-bg"}>
        <div className={"outer-wrp"}>
          <div className={"wrp-input-image"}>
            {isActiveFileChange && (
              <input
                type="file"
                ref={inputRef}
                accept={"image/*"}
                style={{ display: "none" }}
                onBlur={onBlurHandler}
                onChange={imageChange}
              />
            )}

            {!imgSrc && (
              <>
                <div className={"img_file_placeholder"}>
                  <IconImage width="50" color="#888" />
                  <InputButton
                    className={"btn_choose_file"}
                    text={placeHolder}
                    onClick={() => {
                      inputRef.current.click();
                    }}
                  />
                </div>
              </>
            )}

            {(isCrop ? !isShowModalCrop : true) && imgSrc && (
              <div className={"wrp_img"}>
                <img src={imgSrc?.file ? imgSrc.file : imgSrc} />
              </div>
            )}
            {imgSrc && (
              <div className="wrp-cc-remove">
                {isCaption && (
                  <InputButton
                    onClick={() => {
                      setIsShowCaption(true);
                    }}
                    design="none"
                    className={"btn-caption"}
                    icon={<IconCaption width="20" />}
                  />
                )}

                <InputButton
                  design="none"
                  onClick={() => {
                    setIsActiveFileChange(false);
                    setTimeout(function () {
                      setImgSrc("");
                      setIsActiveFileChange(true);
                      if (onChange) {
                        onChange({
                          file: "",
                          caption: captionText,
                        });
                      }
                    }, 100);
                  }}
                  className={"btn-remove"}
                  icon={<IconPlus degree="45" width="20" />}
                />
              </div>
            )}
            {isShowCaption && (
              <div className="wrp-caption">
                <InputText
                  value={captionText}
                  onChange={handleCaptionChange}
                  className={"input-caption " + (isShowCaption ? "active" : "")}
                />

                <InputButton
                  onClick={() => {
                    setIsShowCaption((prev) => !prev);
                  }}
                  design="solid"
                  className={"btn-save"}
                  text={"Save"}
                />
              </div>
            )}
          </div>
        </div>
      </div>

      {isShowModalCrop && (
        <Modal
          className="modal-crop"
          width="100%"
          maxWidth={cropModalMaxWidth}
          height="100%"
          maxHeight={cropModalMaxHeight}
          onClosed={onCloseModal}
          body={
            <div className="crop-editor">
              <ReactCrop
                crop={crop}
                onChange={(_, percentCrop) => setCrop(percentCrop)}
                aspect={aspect}
                onComplete={(c) => {
                  setCompletedCrop(c);
                }}
              >
                <img ref={imgRef} src={imgSrc} onLoad={onImageLoad} />
              </ReactCrop>
            </div>
          }
          footer={
            <div className="wrp-btn">
              <InputButton
                text="Confirm"
                design="solid"
                className={"btn-confirm"}
                onClick={onClickConfirmCrop}
              />
              <InputButton
                design="outline"
                text="Cancel"
                onClick={onCloseModal}
              />
            </div>
          }
        />
      )}
    </Styled>
  );
}

const Styled = styled.div`
  .wrp-input-image {
    position: relative;
    display: inline-flex;
    width: ${({ $width }) => $width};
    height: ${({ $height }) => $height};
    border-radius: ${({ $borderRadius }) => $borderRadius};
    justify-content: center;
    align-items: center;
    padding: 20px;
    overflow: hidden;
    background-color: var(--background);
    .wrp-cc-remove {
      position: absolute;
      bottom: 0;
      right: 0;
      display: flex;
      align-items: center;
      background-color: var(--background);
      border-radius: 8px;
      .btn-caption {
        padding: 10px;
      }
      .btn-remove {
        padding: 10px;
      }
    }
    .wrp-caption {
      position: absolute;
      padding: 10px;
      background-color: var(--background);
      inset: 0;
      display: flex;
      align-items: center;
      flex-direction: column;
      justify-content: center;
      .btn-save {
        padding: 10px 20px;
        margin-top: 20px;
      }
      .input-caption {
        display: none;
      }
      .input-caption.active {
        display: flex;
        input {
          flex: 1;
          width: inherit;
        }
      }
    }
    .has_image {
      display: flex;
      position: absolute;
      background-color: var(--background);
      padding: 5px;
      align-items: center;
      justify-content: center;
      bottom: 0;
    }
    .inner_change_wrp {
      position: relative;
      display: inline-block;
    }

    .wrp_upload_image {
      position: relative;
      width: 200px;
      margin: 0 auto;
    }

    .wrp_bg_image {
      position: relative;
    }

    .wrp_img {
      position: absolute;
      inset: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 10px;
      width: 100%;
      height: 100%;
      img {
        display: block;
        max-width: 100%;
        object-fit: contain;
        max-height: 100%;
      }
    }

    .img_file_placeholder {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    .btn_choose_file {
      padding: 10px 20px;
      margin-top: 10px;
      font-weight: bold;
      span {
        font-size: 15px !important;
      }
    }

    .or {
      margin: 0 0 20px 0;
    }
  }
  .crop-editor,
  .ReactCrop,
  .ReactCrop__child-wrapper,
  .ReactCrop__child-wrapper > img {
    width: 100%;
    position: relative;
  }
  .crop-editor {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .ReactCrop__child-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .content {
    display: flex;
  }
  .btn-confirm {
    margin-right: 40px;
  }
`;

Usage

ReactJs Styled Component
Copy
import InputImage from "../../example/InputImage";

export default function TestView() {
  return (
    <div className="test-view">
      <InputImage
        width="250px"
        height="200px"
        isCrop
        onChange={(e) => {
          console.log("e " + JSON.stringify(e));
        }}
      />
    </div>
  );
}

Properties

Property

Description

Type

Default

initValue

ค่าเริ่มต้นของ view

string

onChange

ฟังก์ชันจะทำงานเมื่อทำการเปลี่ยนค่า

function

placeHolder

ข้อความตัวอย่าง

string

"Choose image file"

maxSize

ขนาดกว้างที่สุดของรูป

number

1500

width

ความกว้างภายในเนื้อหาของ view

string

"200px"

height

ความสูงภายในเนื้อหาของ view

string

"200px"

className

กำหนดลักษณะเฉพาะของ view

string

borderRadius

ความโค้งมนของ view

string

aspect

สัดส่วนของรูป

number

cropModalMaxWidth

ความกว้างของ Crop Modal View

string

"600px"

cropModalMaxHeight

ความสูงของ Crop Modal View

string

"500px"

isCrop

กำหนดค่า เป็น true เพื่อใช้งานโหมด Crop Image

boolean

isCaption

กำหนดค่า เป็น true เพื่อกรอก Caption Image

boolean

style

inline style ของ view

CssProperties

Requirements

styled-component react ReactCrop

ยักซ่า (Yakxar)
สร้างสรรค์และแบ่งปันสื่อดิจิทัลง่ายๆ ที่นี่
© 2025 ยักซ่า (Yakxar)