Search code examples
javascriptbootstrap-modalcropper

Why does Cropper.js return null on second attempt when it is used in Bootstraps Nested Model?


I wanted to do followings:

  1. A button pops up firstModal with SUBMISSION FORM.
  2. Within this FORM there is a Document upload input with 2 options, First is normal input file upload, second is screenshot paste option.
  3. if second option is selected, then clipboard image pasted by CTRL+V is captured and the secondModal pops up and initiates cropper to allow users cropping specific area of clipboard pasted image.
  4. When user clicks crop button, it should transfer cropped image to input file (making form submission ready) and also show small preview.

This question has some similarities but it does not provide exact working answer: CropperJS getCroppedCanvas() returns null on second initialization

Below is my html and js codes where I am now. It seems first attempt works properly, but when I close modals and repeat the process then getCroppedCanvas returns null, what might be cause of this ? Alternative JsFiddle: https://jsfiddle.net/51sL06w9/1/

function checkFileUpload() {
  var is_file = $("#isFile").is(":checked");
  var is_ss = $("#isSS").is(":checked");
  if (is_ss) {
    $("#inputSS").prop("disabled", false);
    $("#inputFile").prop("disabled", true);
  } else {
    $("#inputFile").prop("disabled", false);
    $("#inputSS").prop("disabled", true);
    document.getElementById("cropped-image").style.display = "none";
  }
}
checkFileUpload();
$(document).on("click", "#isFile,#isSS", function(e) {
  checkFileUpload();
});
const inputSS = document.getElementById("inputSS");
$(document).off("paste").on("paste", function(e) {
  var clipboardData = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
  if ($("#isSS").is(":checked")) {
    inputSS.files = clipboardData.files;
    console.log("isSS checked and paste captured");
    var $modal = $("#secondModal");
    var cropper;
    var image = document.querySelector("#cropper-image");
    var files = inputSS.files;
    var done = function(url) {
      inputSS.value = "";
      image.src = url;
      $modal.modal("show");
    };
    var reader;
    var file;
    var url;
    if (files && files.length > 0) {
      file = files[0];
      if (URL) {
        done(URL.createObjectURL(file));
      } else if (FileReader) {
        reader = new FileReader();
        reader.onload = function(e) {
          done(reader.result);
        };
        reader.readAsDataURL(file);
      }
    }
    $modal.on("shown.bs.modal", function() {
      cropper = new Cropper(image, {
        viewMode: 3,
      });
    }).on("hidden.bs.modal", function() {
      cropper.destroy();
      cropper = null;
    });
    $("#crop").on("click", function() {
      var canvas;
      console.log(cropper);
      if (cropper) {
        canvas = cropper.getCroppedCanvas({
          width: 400,
          height: 400,
        });
        console.log(canvas);
        var resultImage = document.getElementById("cropped-image");
        resultImage.src = canvas.toDataURL();
        resultImage.style.display = "block";
        canvas.toBlob(blob => {
          const croppedFile = new File([blob], "croppedScreenShot.png");
          const dT = new DataTransfer();
          dT.items.add(croppedFile);
          inputSS.files = dT.files;
        });
      }
      $modal.modal("hide");
    });
  }
});
$("#firstModal").on("hidden.bs.modal", function() {
      document.getElementById("submitForm").reset();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://fengyuanchen.github.io/cropperjs/js/cropper.js"></script>
<link href="https://fengyuanchen.github.io/cropperjs/css/cropper.css" rel="stylesheet"/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<button type="button" class="btn btn-warning" data-toggle="modal" data-target="#firstModal">Update</button>
<div id="firstModal" class="modal" tabindex="-1" role="dialog">
  <form id="submitForm">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">Modal title</h5>
          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
        <div id="firstModalBody" class="modal-body">
          <div class="item form-group">
            <label class="col-form-label col-md-3 col-sm-3 label-align">Document<span class="required">*</span></label>
            <div id="s2File" class="col-md-9 col-sm-9">
              <div class="nested-blocks">
                <input type="radio" id="isFile" name="fileUploadRadio" value="1" class="form-radio" required="required" checked><label for="isFile">File Upload</label> <input type="file" id="inputFile" name="docFile" class="form-control has-feedback-left" value="" required>
              </div>
              <div class="nested-blocks">
                <input type="radio" id="isSS" name="fileUploadRadio" value="1" class="form-radio"><label for="isSS">ScreenShot Paste</label> <input type="file" id="inputSS" name="docFile" class="form-control has-feedback-left" value="" disabled style="background-color:#e9ecef;pointer-events:none">
              </div>
              <img id="cropped-image" src="" alt="SS" style="display:none;margin-top:10px;max-width:500px;max-height:200px">
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-primary">Submit</button>
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
        </div>
      </div>
    </div>
  </form>
</div>
<div id="secondModal" class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <div class="img-container"><img id="cropper-image" src="" alt="SS"></div>
      </div>
      <div class="modal-footer">
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
          <button type="button" class="btn btn-primary" id="crop">Crop</button>
        </div>
      </div>
    </div>
  </div>
</div>


Solution

  • I think your JavaScript codes are loaded with ajax content (ajax brought DOM) and not on parent DOM. When you use JavaScript on dynamic ajax loaded contents, then you need to make sure to uninstall events when modal is closed in order to avoid stacking event executions.

    I have examined your codes, you have used delegation events, but you have not uninstalled them when you close modal. Therefore, on first modal close, cropper is nulled, and when you launch second round, js events are stacked, and run twice and cause canvas return null. Solution:

    1. Uninstall click event as $(document).off("click", "#isFile,#isSS");
    2. Uninstall paste event (you have already used off() but you are uninstalling all paste events attached in document, better to specify DOM id like $(document).off("paste", "#firstModalBody");
    3. You have unbind click event on #crop, similarly you need unbind show/hide event as well: $modal.unbind("shown.bs.modal"); and $modal.unbind("hidden.bs.modal");