首页 > 解决方案 > 图片上传器在 Safari 中不工作 - 可能的解决方法 (JavaScript)

问题描述

我一直在构建一个理论上已经完成的图像上传器,但它在 Safari 中不起作用。

我最初认为问题是因为DataTransfer,或者更具体地说dataTransfer.files,不受支持,但现在桌面 Safari(版本 14.1 +)支持此功能。尽管在 iOS 中根本没有支持,但我将在 iOS 上仅使用文件选择器(无拖放),所以这不是问题。

桌面 Safari 上的问题/场景

  1. 当用户单击“选择文件”链接并提交图像时,一切正常。

  2. 当用户拖放图像时,文件预览图像被复制(例如,2 个图像显示 4 个预览)。提交时,只上传了 2 张图片(这显然是正确的)。

  3. 当用户单击“选择文件”链接而不是使用拖放功能,然后从图像预览中删除图像时,图像预览会被复制(类似于上面第 2 点中的方式)。

从逻辑上讲,这会让我认为问题出在change事件侦听器上,但我似乎无法修复它。

任何帮助将不胜感激。

const dropZone = document.getElementById("drop-zone"),
  showSelectedImages = document.getElementById("show-selected-images"),
  fileUploader = document.getElementById("standard-upload-files");

dropZone.addEventListener("click", (evt) => {
  // assigns the dropzone to the hidden input element so when you click 'select files' it brings up a file picker window
  fileUploader.click();
});

// Prevent browser default when draging over
dropZone.addEventListener("dragover", (evt) => {
  evt.preventDefault();
});

fileUploader.addEventListener("change", (evt) => {
  // this function is further down but declared here and shows a thumbnail of the image
  [...fileUploader.files].forEach(updateThumbnail);
});

function getFileListItems(files) {
  var transferObject = new ClipboardEvent("").clipboardData || new DataTransfer()
  for (var i = 0; i < files.length; i++) transferObject.items.add(files[i])
  return transferObject.files;
}

dropZone.addEventListener("drop", (evt) => {
  evt.preventDefault();
  // assign dropped files to the hidden input element
  if (evt.dataTransfer.files.length) {
    fileUploader.files = getFileListItems([...fileUploader.files, ...evt.dataTransfer.files]);
  }
  // function is declared here but written further down
  [...evt.dataTransfer.files].forEach(updateThumbnail);
});

// updateThumbnail function that needs to be able to handle multiple files
function updateThumbnail(file) {
  if (file.type.startsWith("image/")) {
    let uploadImageWrapper = document.createElement("article"),
      removeImage = document.createElement("div"),
      thumbnailElement = new Image();

    // 'x' that deletes the image
    removeImage.classList.add("remove-image");
    removeImage.innerHTML =
      '<svg id="remove-x" viewBox="0 0 150 150"><path fill="#000" d="M147.23,133.89a9.43,9.43,0,1,1-13.33,13.34L75,88.34,16.1,147.23A9.43,9.43,0,1,1,2.76,133.89L61.66,75,2.76,16.09A9.43,9.43,0,0,1,16.1,2.77L75,61.66,133.9,2.77a9.42,9.42,0,1,1,13.33,13.32L88.33,75Z"/></svg>';

    // image thumbnail
    thumbnailElement.classList.add("drop-zone__thumb");
    thumbnailElement.src = URL.createObjectURL(file);

    // appending elements
    showSelectedImages.append(uploadImageWrapper); // <article> element
    uploadImageWrapper.append(removeImage); // 'x' to delete
    uploadImageWrapper.append(thumbnailElement); // image thumbnail

    // Delete images
    removeImage.addEventListener("click", (evt) => {
      if (evt.target) {
        var deleteImage = removeImage.parentElement;
        deleteImage.remove();
        fileUploader.files = getFileListItems([...fileUploader.files].filter(f => file !== f));
      }
    });
  }
} // end of 'updateThumbnail' function
body {
  margin: 0;
  display: flex;
  justify-content: center;
  width: 100%;
}

form {
  width: 30%;
}

#drop-zone {
  border: 1px dashed;
  width: 100%;
  padding: 1rem;
  margin-bottom: 1rem;
}

.select-files {
  text-decoration: underline;
  cursor: pointer;
}

/* images that are previewed prior to form submission*/

.drop-zone__thumb {
  width: 200px;
  height: auto;
  display: block;
}

#remove-x {
  width: 1rem;
  height: 1rem;
}

#submit-images {
  margin: 1rem 0;
}

#show-selected-images {
  display: flex;
}
<form id="upload-images-form" enctype="multipart/form-data" method="post">
  <h1>Upload Your Images</h1>
  <div id="drop-zone" class="drop-zone flex">
    <p class="td text-center">DRAG AND DROP IMAGES HERE</p>
    <p class="td text-center" style="margin: 0">Or</p>
    <p class="tl text-center select-files text-bold pointer">Select Files</p>
  </div>
  <input id="standard-upload-files" style="display:none" style="min-width: 100%" type="file" name="standard-upload-files[]" multiple>
  <input type="submit" name="submit-images" id="submit-images" value="SUBMIT IMAGES">
  <div id="show-selected-images"></div>
</form>

标签: javascripthtmlformsdom-eventsdata-transfer

解决方案


问题是 Safari 在您.files设置<input>. (错误 180465

鉴于他们确实同步触发了该事件,您可以使用一个简单的标志来解决这个问题:

let ignoreEvent = false; // our flag

const inp = document.querySelector("input");
inp.onchange = (evt) => {
  if (ignoreEvent) {
    console.log("this event should be ignored");
  }
  else {
    console.log("A true change event");
  }
};
const dT = new DataTransfer();
dT.items.add(new File(['foo'], 'programmatically_created.txt'));

// right before setting the .files
ignoreEvent = true;
inp.files = dT.files;
// the change event fired synchronously,
// lower the flag down
ignoreEvent = false;
<input type="file" id="inp">

在 Safari 中将输出“应忽略此事件”。

现在,我不禁提醒,设置类似FileList的设置<input>仍然是一种黑客行为。你真的不应该在生产中使用它。 (我确实修改了我的答案以使其更清楚。)

因此,请使用简单的 Array 和FormData进行上传。此外,由于您甚至没有显示原始文件选择器<input>


您显然已经复制粘贴了您不理解的先前答案,因此为避免您再次陷入此(可理解的)陷阱,我将仅强调您实施所需的关键点,我不会给出你是一个工作的例子,故意的。

所以首先你需要定义一个 Array ( []) 来存储你的用户选择的文件并且可以被以下所有函数访问。
放置和输入的更改事件中,您将使用新选择的文件更新此数组(您可以附加新文件或替换它们,这是您的决定)。
然后,您将为<form>元素的submit事件添加一个新的事件侦听器。单击提交按钮时将触发此事件。
它的默认操作是将<form>' 的内容发布到您的服务器,但由于我们想要包含我们自己的 Array 而不是<input>' 的文件列表(因为它实际上可能是空的),我们不希望发生此默认操作,所以我们将调用event.preventDefault()为了避免这种情况。
现在,我们仍然想向服务器发送一些东西,所以我们将从头开始构建一个新表单。
这就是我们将创建一个新FormData对象的地方,并使用它的append()方法来存储我们的文件。该方法的第一个参数应该是字段名,也就是<input>name属性值。第二个应该是单个文件,因此我们需要调用此方法的次数与数组中的项目一样多。由于我们处理File对象,我们不需要第三个参数。

一旦我们的 FormData 完成,我们只需将其上传到服务器。
为此,在现代浏览器中,您可以简单地使用该fetch()方法,将 URL 传递给服务器,以及一个看起来大致如下的选项包

{
  method: "POST",
  body: formData // the FormData object we just created
}

如果您需要支持较旧的浏览器,您也可以对 XMLHttpRequest 对象执行相同的操作,在其.send(formData)方法中传递 FormData。

由于尽管逐步解释,OP显然无法摆脱这一点,所以这里有一个 jsfiddle


推荐阅读