Ответ 1
Ваш предложенный подход (разделение каждого слова в span
и сохранение дополнительных данных в нем) на первый взгляд представляется наиболее разумным подходом. На уровне редактора вам просто нужно обеспечить, чтобы весь текст находился внутри некоторого span
, и каждый из них содержит только одно слово (при необходимости разбивая его). На уровне слов просто слушайте изменения в span
(привязка input
и propertyChange
) и действуйте согласно его классу/данным.
Однако реальная боль заключается в том, чтобы сохранить положение каретки согласованным. Когда вы меняете содержимое либо textarea
, либо элемент с contentEditable
, каретка движется довольно непредсказуемо, и нет простого (кросс-браузерного) способа отслеживания каретки. Я искал решения как здесь, так и в другом месте, и простейшее рабочее решение, которое я нашел, было в этом сообщении в блоге. К сожалению, он применяется только к textarea
, поэтому решение "каждое слово в промежутке" не может быть использовано.
Итак, я предлагаю следующий подход:
- Сохраняйте список слов в
Array
, где каждое слово хранит как текущее значение, так и оригинал; - Когда содержимое
textarea
изменяется, сохраните набор неизменных слов и повторите остальные; - Используйте только проверку орфографии, если каретка сразу после символа без слова (комната для улучшения), и вы не нажимаете
backspace
; - Если пользователь был недоволен коррекцией, нажатие
backspace
однажды отменит его, и он не будет снова проверен , если не будет изменен.- Если было сделано много исправлений сразу (например, если много текста было скопировано), каждый
backspace
отменит одну коррекцию до тех пор, пока никто не останется. - Нажатие любого другого ключа приведет к исправлению, поэтому, если пользователь все еще не удовлетворен, ему придется вернуться и снова изменить его.
- Примечание: в отличие от требований OP измененная версия будет снова автокорректирована, если пользователь вводит символ без слова; ему нужно нажать
backspace
один раз, чтобы "защитить" его.
- Если было сделано много исправлений сразу (например, если много текста было скопировано), каждый
Я создал простую концептуальную концепцию в jsFiddle. Подробности ниже. Обратите внимание, что вы можете комбинировать его с другими подходами (например, обнаруживать клавишу "стрелка вниз" и отображать меню с некоторыми параметрами автоматической коррекции) и т.д.
Шаги доказательств концепции подробно объясняются:
-
Сохраняйте список слов в
Array
, где каждое слово хранит как текущее значение, так и оригинал;var words = [];
Это регулярное выражение разбивает текст на слова (каждое слово имеет свойство
word
иsp
, последнее хранит немедленные символы без слов)delimiter:/^(\w+)(\W+)(.*)$/, ... regexSplit:function(regex,text) { var ret = []; for ( var match = regex.exec(text) ; match ; match = regex.exec(text) ) { ret.push({ word:match[1], sp:match[2], length:match[1].length + match[2].length }); text = match[3]; } if ( text ) ret.push({word:text, sp:'', length:text.length}); return ret; }
-
Когда содержимое
textarea
изменяется, сохраняйте набор неизменных слов и повторите остальные;// Split all the text var split = $.autocorrect.regexSplit(options.delimiter, $this.val()); // Find unchanged words in the beginning of the field var start = 0; while ( start < words.length && start < split.length ) { if ( !words[start].equals(split[start]) ) break; start++; } // Find unchanged words in the end of the field var end = 0; while ( 0 < words.length - end && 0 < split.length - end ) { if ( !words[words.length-end-1].equals(split[split.length-end-1]) || words.length-end-1 < start ) break; end++; } // Autocorrects words in-between var toSplice = [start, words.length-end - start]; for ( var i = start ; i < split.length-end ; i++ ) toSplice.push({ word:check(split[i], i), sp:split[i].sp, original:split[i].word, equals:function(w) { return this.word == w.word && this.sp == w.sp; } }); words.splice.apply(words, toSplice); // Updates the text, preserving the caret position updateText();
-
Используйте только проверку орфографии, если каретка сразу после символа без слова (комната для улучшения), и вы не нажимаете
backspace
;var caret = doGetCaretPosition(this); var atFirstSpace = caret >= 2 && /\w\W/.test($this.val().substring(caret-2,caret)); function check(word, index) { var w = (atFirstSpace && !backtracking ) ? options.checker(word.word) : word.word; if ( w != word.word ) stack.push(index); // stack stores a list of auto-corrections return w; }
-
Если пользователь был неудовлетворен коррекцией, нажатие
backspace
однажды отменит его, и он не будет снова проверен , если не изменено.$(this).keydown(function(e) { if ( e.which == 8 ) { if ( stack.length > 0 ) { var last = stack.pop(); words[last].word = words[last].original; updateText(last); return false; } else backtracking = true; stack = []; } });
-
Код для
updateText
просто снова соединяет все слова в строку и возвращает значение обратно вtextarea
. Каретка сохраняется, если ничего не было изменено или не было размещено сразу после последней выполненной/отмененной автокоррекции, для учета изменений длины текста:function updateText(undone) { var caret = doGetCaretPosition(element); var text = ""; for ( var i = 0 ; i < words.length ; i++ ) text += words[i].word + words[i].sp; $this.val(text); // If a word was autocorrected, put the caret right after it if ( stack.length > 0 || undone !== undefined ) { var last = undone !== undefined ? undone : stack[stack.length-1]; caret = 0; for ( var i = 0 ; i < last ; i++ ) caret += words[i].word.length + words[i].sp.length; caret += words[last].word.length + 1; } setCaretPosition(element,caret); }
-
Окончательная структура плагина:
$.fn.autocorrect = function(options) { options = $.extend({ delimiter:/^(\w+)(\W+)(.*)$/, checker:function(x) { return x; } }, options); return this.each(function() { var element = this, $this = $(this); var words = []; var stack = []; var backtracking = false; function updateText(undone) { ... } $this.bind("input propertyChange", function() { stack = []; // * Only apply the spell check if the caret... // * When the contents of the `textarea` changes... backtracking = false; }); // * If the user was unsatisfied with the correction... }); }; $.autocorrect = { regexSplit:function(regex,text) { ... } };