Ajax / Как защититься от неожиданной отправки комментария по Ctrl+Enter?

в 5:00, , рубрики: greasemonkey, метки:

(Опыт успешной больбы с ветряными мельницами.)
C завидной регулярностью в комментариях встречаются оборванные на полуслове сообщения с приписками о том, что «извините, само отправилось», "сорвалось", и продолжением мысли. Иногда говорят, что разгадали причину такого поведения сайта. Поэтому хочу сообщить, что я не одинок в своей догадке, и более того, около полугода назад я решил эту проблему с помощью юзерскрипта. С тех пор ложные отправки у меня прекратились, но я не мог быть уверен, что причина ложных отправок только в этом, поэтому опыт использования скрипта и догадки других пользователей должны были это подтвердить.
И вот, попалась на глаза заметка о том, что кто-то ещё тоже видит причину ложной отправки в этом:habrahabr.ru/blogs/client_side_optimization/137318/#comment_4572800 (31 янв 2012)
Поиск по Гуглу показал, что причину разгадывали и раньше: habrahabr.ru/qa/9515/#answer_40702 (15 июля 2011)habrahabr.ru/qa/8447/#answer_35939 (12 июня 2011)habrahabr.ru/company/regru/blog/105763/#comment_3318569 (8 окт 2010)habrahabr.ru/blogs/bsdelniki/118485/#comment_3863623 (1 мая 2011)habrahabr.ru/blogs/google/118622/#comment_3873012 (9 мая 2011) и т.д., можно продолжать долго, эти ссылки — из первых 30 результатов поиска (поиск по Сфинксу на сайте не годится, он не ищет по точной фразе; поиск Гугла и Яндекса встроен в тот же скрипт. Надо учитывать, что поиск по поисковику — неполон, не покажет гарантированно всех случаев, а для новых сообщений вообще не будет некоторое время (часы, дни, недели) работать).
Способ решения через техподдержку сайта уже показал свою неэффективность. Например, баг скрипта показа исходного кода программ при появлении тега
не исправлен более полугода. Кроме того, у создателей скрипта может быть противоположное мнение о «полезности» данной функции. Поэтому отключение автоотправки по Ctrl+Enter гораздо быстрее реализовать самостоятельно (некоторые заботы, зато сразу, надёжно (до смены скриптов на сайте) и контролируемо).
До октября 2011 успешно работал юзерскрипт:
var win = (typeof unsafeWindow !='undefined')? unsafeWindow: window;
if(win.commentForm)
win.commentForm.sendOnEnter = function(){};

(кроме браузера Chrome)
С того времени скрипты на сайте обновились, и видно (эксперименты можно безвредно провести в режиме браузера «offline»), что за отправку отвечают уже другие участки кода, вот они:
(файл view.js — для раздела QA)
var answer_form = $('#add_answer_form');
var answer_text = $('#answer_text');
/**
* CTRL + ENTER
*/
answer_text.keydown(function (e) {
if ( (e.ctrlKey || e.metaKey ) && e.keyCode == 13) {
$('input.submit',answer_form).click()
}
});

И в другом месте (файл comments.js, для остальных разделов):
$(document).ready(function(){
var comments_form = $('#comments_form');
var comment_text = $('#comment_text');
comment_text.keydown(function (e) {
if ( (e.ctrlKey || e.metaKey ) && e.keyCode == 13) {
$('input.submit',comments_form).click()
}
});
...

Забавны эти скрипты. Если бы у нас был рутовый доступ к сайту или хотя бы проксомитрон (известный инструмент правки потока на прокси-сервере), дело бы было за малым. Сейчас же надо сделать в несколько раз больше дополнительных движений, чтобы исправить разрушительные последствия нескольких строчек. С другой стороны, в образовательных целях, это хорошо. Нам усложняют задачу, а мы крепчаем © (в знании способов решения). И здесь покажем, как решить задачу, «раскрутив» проблему от источника.
Видим навешивание события с анонимной функцией через jQuery. Насколько известно, событие можно снять, если написать $('#comment_text').unbind('keydown', та же самая функция), а с функцией — проблема, она анонимная. Повторение текста функции, например — не решает вопроса. Отключать и переделывать следствие — $('input.submit', $('#comments_form')).click()? Вопроса не решает, потому что в неё не передаётся «e» — объект события, из которого взяли бы данные о нажатой клавише. Значит, переделываем «keydown» — метод jQuery, обрабатывающий нажатие на клавишу в таком виде (текст из сжатого jQuery):
function (a, c) {
c == null && (c = a, a = null);
return arguments.length > 0 ? this.bind(b, a, c) : this.trigger(b);
}

Здесь b — очевидно, константа «keydown», с — (в соответствии с описанием в документации) — handler(eventObject) — тот самый пользовательский обработчик, тело которого нам надо изменить, a — необязательный хеш — данные для обработчика. И этот участок кода в библиотеке — общий для многих событий. Нужно не нарушить эту функцию для прочих применений в jQuery и внести проверку, когда this равен $('textarea'). Метод нужно переопределить, обратившись не к самой функции, а к её прототипу.
И, поскольку действия происходят в юзерскрипте, эти операции непросто будет заставить исполняться в Хроме (там разделены окружения для юзерскриптов и скриптов), а в остальных — в начале надо писать объект окружения — win, который (typeof unsafeWindow !='undefined')? unsafeWindow: window.
Частичное решение задачи (Firefox, Opera, Safari) будет выглядеть так, и на нём пока остановимся, потому что общее решение — вне пределов сути статьи.
var win = (typeof unsafeWindow !='undefined')? unsafeWindow: window;
if(win.$){ //удалить отправку по Ctrl+Enter (не Chrome)
var comT = win.$('#comment_text');
if(comT.length==0)
comT = win.$('#answer_text');
if(comT){ //если найдена Textarea с вредным действием...
comT.constructor.prototype.keydown = function(ha, fKeyD){
fKeyD == null && (fKeyD = ha, ha = null); //повторяем прототип функции
var k ='keydown';
if(this[0].tagName=='TEXTAREA' && /^(comment|answer)_text$/.test(this[0].id)){
fKeyD = function(ev){
if((ev.ctrlKey || ev.metaKey)&& ev.keyCode == 13) //для демонстрации
win.console.log('Ctrl-enter не пройдёт');
};
}
return arguments.length > 0 ? this.bind(k, ha, fKeyD) : this.trigger(k);
}
}
}

Что получили с точки зрения красивости решения? Функцию библиотеки jQuery пришлось нагрузить специальной проверкой, потому что нет другой возможности отменить срабатывание действия отправки, и внутри неё подменить функцию, делающую клик по кнопке, пустой функцией (у нас она не пустая исключительно для демонстрации). Исходный текст библиотеки, разумеется, не патчится, потому что к нему у нас доступа как не было, так и нет.
На базе этого кода несложно сделать маленький юзерскрипт, решающий только эту задачу. Но, так как имеется общий юзерскрипт, решающий 2-3 десятка задач, под названием HabrAjax, создадим для него эту функцию. Она начнёт действовать у скрипта с версией 2.06 и выше.
Чтобы проверить неотправку сообщения по Ctrl-Enter в указанных браузерах, нужно установить юзерскрипт (короткий, приведённый выше, или HabrAjax), загрузить страницу сайта с полем ввода комментария, перевести браузер в режим «оффлайн» и убедиться, что изнутри поля ввода по Ctrl-Enter форма не отправляется. Если отключить скрипт, такое сочетание приведёт к анимации кнопки «Отправить», сопровождающей попытку отправки по Ajax (видна в консоли Firebug, например).
Задача решена — теперь случайное нажатие Ctrl + Enter, которое иногда случается при быстром наборе, не приведёт к неожиданной отправке (неисправляемого!) сообщения на сервер.
Для общего решения, включающего Chrome, проще всего сделать обходной манёвр. Создать новую клавишу отправки. Удалить старую клавишу со всеми навешанными на неё событиями. По клику на новой — форма будет автоматически отправляться во всех браузерах. Заодно, в Firefox можно будет исправить стили кнопки отправки, которые не поддавались исправлению юзерстилями другими способами вследствие особого порядка приоритетов применения стилей.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js