Статьи

Paste Wasteland (или, почему событие onPaste — беспорядок)

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

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

Самая простая и наиболее полная реализация — это Google Chrome (все примеры предполагают, что мы используем jQuery, чтобы уменьшить многословность):

$( 'body' ).bind( 'paste', function( evt ) {
    var items = evt.originalEvent.clipboardData.items
      , paste;

    // Items have a "kind" (string, file) and a MIME type
    console.log( 'First item "kind":', items[0].kind );
    console.log( 'First item MIME type:', items[0].type );

    // If a user pastes image data, there are 2 items: the file name (at index 0) and the file (at index 1)
    // but if the user pastes plain text or HTML, index 0 is the data with markup and index 1 is the plain, unadorned text.

    if( items[0].kind === 'string' && items[1].kind === 'file' && items[1].type.match( /^image/ ) ) {
        // If the user copied a file from Finder (OS X) and pasted it in the window, this is the result. This is also the result if a user takes a screenshot and pastes it.
        // Unfortunately, you can't copy & paste a file from the desktop. It just returns the file's icon image data & filename (on OS X).
        item = items[0];
    } else if( items[0].kind === 'string' && items[1].kind === 'string' ) {
        // Get the plain text
        item = items[1];
    }

    // From here, you can use a FileReader object to read the item with item.getAsFile(), or evt.originalEvent.clipboardData.getData(item.type) to get the plain text. Confused yet?
} );

Вау. И вышеупомянутое даже не всесторонне; Я все еще нахожу множество крайних случаев, которые возвращают результаты не в виде простого текста, когда нам нужен простой текст.

Firefox, с другой стороны, имеет совершенно неутешительную реализацию. Вы не можете извлечь вставленный контент из события, и, в отличие от Chrome, где вы можете привязать что-либо, событие, кажется, работает только при привязке к полю, которое «обычно» принимает событие вставки, например поле ввода или текстовое поле.

var $pasteField = $( '#pasteField' );

function onPaste( evt, retries ) {
    // No clipboardData object, so we fake it and just return the field's content, when we receive it.
    var text = $pasteField.val();
    $pasteField.val( '' ); // reset the field
    clipboardObj = { getData: function() { return text; } };
    items = [ { kind: 'string', type: 'text' } ];

    // If we didn't find any content on this attempt, and we haven't tried > 3 times,
    // try again in 100 ms to find content in the textarea.
    if( !text.length && ( retries < 3 || typeof retries === 'undefined' ) ) {
        if( typeof retries === 'undefined' )
             retries = 0;

         setTimeout( function() {
             onPaste( evt, ++retries );
         }, 100 );
         return true;
    }

    // Now you finally have the item's content, with an API similar to that of Chrome -- at least, for plain text.
}

$pasteField.bind( 'paste', onPaste );

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

А как насчет IE?

// We need a textarea for IE, too: <textarea id="pasteField"></textarea>

$( '#pasteField' ).bind( 'paste', function( evt ) {
    // In true Microsoft form, the type to get plain text is Text with a captial T.
    console.log( window.clipboardData.getData( 'Text' ) );
} );

Довольно простой. Примерно с IE5. Не очень универсальный. Но, по крайней мере, мы можем получить данные.

И последнее, но не менее важное, Safari, который имеет самую странную реализацию из всех:

// Safe to say...textarea is a good idea: <textarea id="pasteField"></textarea>

$( '#pasteField' ).on( 'paste', function( evt ) {
    // Safari has a weird "types" list that we need to loop through and no "items" array.
    var i = 0, items = [], item, key = 'text/plain', kind = 'string';
    while (i < evt.originalEvent.clipboardData.types.length) {
        var key = evt.originalEvent.clipboardData.types[i];
        if( !key.match( /(text\/)|(plain-text)/i ) ) {
            kind = 'file';
        }
        items.push( { kind: kind, type: key } );
        i++;
    }

    // Let's just get content of the first item.
    item = items[0];
    console.log( evt.originalEvent.clipboardData.getData( item.type ) );
} );

Это даже не касается различий между ОС (OS X и Windows) и того, как они обрабатывают метаданные в содержимом вставки.

Итак, как нам согласовать всю эту информацию и разнообразие реализаций? Кажется, самым безопасным решением является наличие закадрового или скрытого текстового поля, в которое можно при необходимости поместить фокус пользователя и соответствующим образом связать все события вставки. Затем объедините описанные выше методы (я оставлю читателю в качестве упражнения объединить их все в кросс-браузерную реализацию).

Вам также нужно будет протестировать различные варианты вставки — элементы, вставленные из Word, браузера и других редакторов форматированного текста, все реагируют по-разному и могут возвращать разные метаданные / разметку.

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

Постскриптум: Недавно мне сообщил веб-разработчик Джоэл Бесада, что, несмотря на отсутствие объекта clipboardData в Firefox, вы все равно можете получать изображения из буфера обмена . Спасибо, Джоэл! Я обнаружил, что вариант этой техники также работает в IE, что довольно интересно.