Как определить, когда отменить нажатие на ввод файла? - PullRequest
93 голосов
/ 07 января 2011

Как я могу определить, когда пользователь отменяет ввод файла с помощью ввода файла html?

onChange позволяет мне определять, когда они выбирают файл, но я также хотел бы знать, когда они отменяют (закрывают диалог выбора файла, ничего не выбирая).

Ответы [ 28 ]

40 голосов
/ 01 апреля 2013

Хотя это не прямое решение, а также плохое в том смысле, что оно только (насколько я тестировал) работает с onfocus (требует довольно ограничивающей блокировки событий), вы можете добиться этого с помощью следующего:

document.body.onfocus = function(){ /*rock it*/ }

Что приятно в этом, так это то, что вы можете подключить / отсоединить его вовремя к событию file, и это также, кажется, хорошо работает со скрытыми вводами (определенная привилегия, если вы используете визуальный обходной путь для дрянного ввода по умолчаниютип = 'файл').После этого вам просто нужно выяснить, изменилось ли входное значение.

Пример:

var godzilla = document.getElementById('godzilla')

godzilla.onclick = charge

function charge()
{
    document.body.onfocus = roar
    console.log('chargin')
}

function roar()
{
    if(godzilla.value.length) alert('ROAR! FILES!')
    else alert('*empty wheeze*')
    document.body.onfocus = null
    console.log('depleted')
}

Посмотреть это в действии: http://jsfiddle.net/Shiboe/yuK3r/6/

К сожалению, этокажется, работает только в браузерах webkit.Может быть, кто-то еще может найти решение Firefox / IE

17 голосов
/ 07 января 2011

Вы не можете.

Результат диалогового окна файла не отображается браузером.

7 голосов
/ 26 октября 2015
/* Tested on Google Chrome */
$("input[type=file]").bind("change", function() {
    var selected_file_name = $(this).val();
    if ( selected_file_name.length > 0 ) {
        /* Some file selected */
    }
    else {
        /* No file selected or cancel/close
           dialog button clicked */
        /* If user has select a file before,
           when they submit, it will treated as
           no file selected */
    }
});
6 голосов
/ 20 декабря 2011

Когда вы выбираете файл и нажимаете кнопку открыть / отменить, элемент input должен потерять фокус или blur. Предполагая, что начальная value input пуста, любое непустое значение в вашем обработчике blur будет означать ОК, а пустое значение будет означать Отмена.

ОБНОВЛЕНИЕ: blur не срабатывает, когда input скрыто. Поэтому нельзя использовать этот прием с загрузками на основе IFRAME, если только вы не хотите временно отобразить input.

6 голосов
/ 12 апреля 2018

Так что я добавлю свою шляпу в этот вопрос, так как я придумал новое решение. У меня есть Прогрессивное веб-приложение, которое позволяет пользователям снимать фотографии и видео и загружать их. Мы используем WebRTC, когда это возможно, но используем устройства выбора файлов HTML5 для устройств с меньшей поддержкой * кашель Safari * кашель. Если вы работаете специально над мобильным веб-приложением на базе Android / iOS, которое использует встроенную камеру для непосредственного захвата фотографий / видео, то это лучшее решение, с которым я столкнулся.

Суть этой проблемы в том, что при загрузке страницы file равен null, но затем, когда пользователь открывает диалоговое окно и нажимает «Отмена», file по-прежнему null, следовательно, он не «изменить», поэтому событие «изменение» не инициируется. Для настольных компьютеров это не так уж и плохо, потому что большинство пользовательских интерфейсов не зависят от знания, когда вызывается отмена, но мобильные пользовательские интерфейсы, которые поднимают камеру для захвата фото / видео, очень зависят от знания когда нажата отмена.

Первоначально я использовал событие document.body.onfocus, чтобы определить, когда пользователь вернулся из средства выбора файлов, и это работало для большинства устройств, но iOS 11.3 прервала его, поскольку это событие не вызывается.

Концепция

Моё решение - это * вздрогнуть *, чтобы измерить тактовую частоту процессора, чтобы определить, находится ли страница в данный момент на переднем плане или на заднем плане. На мобильных устройствах время обработки отводится приложению, находящемуся на переднем плане. Когда камера видна, она крадет процессорное время и депортирует браузер. Все, что нам нужно сделать, это измерить, сколько времени отводится нашей странице, когда камера запускается, наше доступное время резко сократится. Когда камера отключается (отменяется или иным образом), наше доступное время снова увеличивается.

Осуществление

Мы можем измерить тактирование процессора, используя setTimeout(), чтобы вызвать обратный вызов за X миллисекунд, а затем измерить, сколько времени потребовалось для его фактического вызова. Браузер никогда не вызовет его точно через X миллисекунд, но если это будет разумно близко, то мы должны быть на переднем плане. Если браузер очень далеко (более чем в 10 раз медленнее, чем запрошено), то мы должны быть в фоновом режиме. Базовая реализация этого выглядит так:

function waitForCameraDismiss() {
  const REQUESTED_DELAY_MS = 25;
  const ALLOWED_MARGIN_OF_ERROR_MS = 25;
  const MAX_REASONABLE_DELAY_MS =
      REQUESTED_DELAY_MS + ALLOWED_MARGIN_OF_ERROR_MS;
  const MAX_TRIALS_TO_RECORD = 10;

  const triggerDelays = [];
  let lastTriggerTime = Date.now();

  return new Promise((resolve) => {
    const evtTimer = () => {
      // Add the time since the last run
      const now = Date.now();
      triggerDelays.push(now - lastTriggerTime);
      lastTriggerTime = now;

      // Wait until we have enough trials before interpreting them.
      if (triggerDelays.length < MAX_TRIALS_TO_RECORD) {
        window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
        return;
      }

      // Only maintain the last few event delays as trials so as not
      // to penalize a long time in the camera and to avoid exploding
      // memory.
      if (triggerDelays.length > MAX_TRIALS_TO_RECORD) {
        triggerDelays.shift();
      }

      // Compute the average of all trials. If it is outside the
      // acceptable margin of error, then the user must have the
      // camera open. If it is within the margin of error, then the
      // user must have dismissed the camera and returned to the page.
      const averageDelay =
          triggerDelays.reduce((l, r) => l + r) / triggerDelays.length
      if (averageDelay < MAX_REASONABLE_DELAY_MS) {
        // Beyond any reasonable doubt, the user has returned from the
        // camera
        resolve();
      } else {
        // Probably not returned from camera, run another trial.
        window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
      }
    };
    window.setTimeout(evtTimer, REQUESTED_DELAY_MS);
  });
}

Я протестировал это на последних версиях iOS и Android, подняв собственную камеру, установив атрибуты для элемента <input />.

<input type="file" accept="image/*" capture="camera" />
<input type="file" accept="video/*" capture="camcorder" />

На самом деле это работает намного лучше, чем я ожидал. Он запускает 10 испытаний, запрашивая таймер за 25 миллисекунд. Затем он измеряет, сколько времени на самом деле потребовалось для вызова, и если среднее из 10 испытаний составляет менее 50 миллисекунд, мы предполагаем, что мы должны быть на переднем плане, и камера исчезла. Если оно больше 50 миллисекунд, то мы все равно должны быть в фоновом режиме и продолжать ждать.

Некоторые дополнительные детали

Я использовал setTimeout() вместо setInterval(), поскольку последний может ставить в очередь несколько вызовов, которые выполняются сразу после друг друга. Это может значительно увеличить шум в наших данных, поэтому я остановился на setTimeout(), хотя это немного сложнее.

Эти конкретные числа работали для меня хорошо, хотя я видел по крайней мере один раз случай, когда отклонение камеры было обнаружено преждевременно. Я полагаю, что это потому, что камера может медленно открываться, и устройство может выполнить 10 испытаний, прежде чем оно фактически станет фоновым. Обойти это можно, добавив больше испытаний или подождав около 25-50 миллисекунд перед запуском этой функции.

Desktop

К сожалению, это не работает для настольных браузеров. Теоретически возможен тот же трюк, поскольку они устанавливают приоритет текущей страницы над фоновыми страницами. Однако на многих настольных компьютерах достаточно ресурсов, чтобы страница работала на полной скорости, даже если она находится в фоновом режиме, поэтому на практике эта стратегия не работает.

Альтернативные решения

Одним из альтернативных решений, о котором мало кто упоминал, было то, что я издевался над FileList.Мы начинаем с null в <input />, а затем, если пользователь открывает камеру и отменяет, он возвращается к null, что не является изменением и никакое событие не сработает.Одним из решений было бы назначить фиктивный файл для <input /> в начале страницы, поэтому установка на null будет изменением, которое вызовет соответствующее событие.

К сожалению, нет официального способа созданияFileList, а элемент <input /> требует, в частности, FileList и не будет принимать никаких других значений, кроме null.Естественно, FileList объекты не могут быть построены напрямую, это связано с какой-то старой проблемой безопасности, которая, по-видимому, уже не актуальна.Единственный способ получить доступ к одному элементу <input /> заключается в использовании хака, который копирует и вставляет данные, чтобы имитировать событие буфера обмена, которое может содержать объект FileList (в основном вы притворяетесь перетаскиванием-a-file-on-your-website событие).Это возможно в Firefox, но не для iOS Safari, поэтому он не подходит для моего конкретного случая использования.

Браузеры, пожалуйста ...

Излишне говорить, что это явно смешно.Тот факт, что веб-страницы получают нулевое уведомление об изменении критического элемента пользовательского интерфейса, просто смешен.Это действительно ошибка в спецификации, так как она никогда не предназначалась для полноэкранного пользовательского интерфейса захвата мультимедиа, и событие «change» не вызывало технически к спецификации.

Однако , могут ли производители браузеров признать реальность этого?Это можно решить с помощью нового события «done», которое запускается, даже если никаких изменений не происходит, или вы можете просто вызвать «change» в любом случае.Да, это было бы против спецификации, но для меня тривиально дедуплировать событие изменения на стороне JavaScript, но принципиально невозможно изобрести мое собственное событие "done".Даже мое решение на самом деле просто эвристическое, если не дают никаких гарантий о состоянии браузера.

В его нынешнем виде этот API принципиально непригоден для мобильных устройств, и я думаю, что относительно простая замена браузера может сделать это бесконечнопроще для веб-разработчиков * сходит с мыла *.

5 голосов
/ 25 июня 2014

Просто послушайте событие click.

Следуя примеру Шибое, вот пример jQuery:

var godzilla = $('#godzilla');
var godzillaBtn = $('#godzilla-btn');

godzillaBtn.on('click', function(){
    godzilla.trigger('click');
});

godzilla.on('change click', function(){

    if (godzilla.val() != '') {
        $('#state').html('You have chosen a Mech!');    
    } else {
        $('#state').html('Choose your Mech!');
    }

});

Вы можете увидеть его в действии здесь: http://jsfiddle.net/T3Vwz

4 голосов
/ 15 декабря 2012

Вы можете отменить отмену, если вы выбрали тот же файл, что и ранее, и нажали кнопку отмены: в этом случае.

Вы можете сделать это следующим образом:

<input type="file" id="myinputfile"/>
<script>
document.getElementById('myinputfile').addEventListener('change', myMethod, false);
function myMethod(evt) {
  var files = evt.target.files; 
  f= files[0];
  if (f==undefined) {
     // the user has clicked on cancel
  }
  else if (f.name.match(".*\.jpg")|| f.name.match(".*\.png")) {
     //.... the user has choosen an image file
     var reader = new FileReader();
     reader.onload = function(evt) { 
        try {
        myimage.src=evt.target.result;
            ...
         } catch (err) {
            ...
         }
     };
  }     
  reader.readAsDataURL(f);          
</script>
4 голосов
/ 07 января 2016

Большинство из этих решений не работают для меня.

Проблема в том, что вы никогда не знаете, какое событие будет вызвано кулаком, это click или change? Вы не можете принять любой заказ, потому что это, вероятно, зависит от реализации браузера.

По крайней мере, в Opera и Chrome (конец 2015 г.) click запускается непосредственно перед «заполнением» ввода файлами, поэтому вы никогда не узнаете длину files.length != 0, пока не задержите запуск click после change.

Вот код:

var inputfile = $("#yourid");

inputfile.on("change click", function(ev){
    if (ev.originalEvent != null){
        console.log("OK clicked");
    }
    document.body.onfocus = function(){
        document.body.onfocus = null;
        setTimeout(function(){
            if (inputfile.val().length === 0) console.log("Cancel clicked");
        }, 1000);
    };
});
4 голосов
/ 16 января 2018

Самый простой способ - проверить, есть ли какие-либо файлы во временной памяти.Если вы хотите получать событие изменения каждый раз, когда пользователь нажимает на ввод файла, вы можете запустить его.

var yourFileInput = $("#yourFileInput");

yourFileInput.on('mouseup', function() {
    $(this).trigger("change");
}).on('change', function() {
    if (this.files.length) {
        //User chose a picture
    } else {
        //User clicked cancel
    }
});
2 голосов
/ 17 января 2014

Решение Shiboe было бы хорошим, если бы оно работало на мобильном webkit, но это не так. Что я могу придумать, так это добавить слушатель события mousemove к некоторому объекту dom во время открытия окна ввода файла, например:

 $('.upload-progress').mousemove(function() {
        checkForFiles(this);
      });
      checkForFiles = function(me) {
        var filefield = $('#myfileinput');
        var files = filefield.get(0).files;
        if (files == undefined || files[0] == undefined) $(me).remove(); // user cancelled the upload
      };

Событие mousemove блокируется со страницы, когда открыто диалоговое окно файла, и когда оно закрыто, проверяется, есть ли какие-либо файлы во входном файле. В моем случае я хочу, чтобы индикатор активности блокировал вещи, пока файл не был загружен, поэтому я хочу удалить свой индикатор только при отмене.

Однако для мобильных устройств это не решает проблему, так как нет мыши, чтобы двигаться. Мое решение там не идеальное, но я думаю, что оно достаточно хорошее.

       $('.upload-progress').bind('touchstart', function() {
        checkForFiles(this);
      });

Теперь мы слушаем прикосновение к экрану, чтобы выполнить ту же проверку файлов. Я вполне уверен, что после отмены пользовательский палец будет выведен на экран довольно быстро и пропустит этот индикатор активности.

Можно также просто добавить индикатор активности к событию изменения ввода файла, но на мобильном телефоне часто наблюдается задержка в несколько секунд между выбором изображения и срабатыванием события изменения, поэтому его гораздо лучший UX для индикатора активности отображается в начале процесса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...