yurikhan: (Default)
[personal profile] yurikhan

Понадобилось вот прикрутить к <textarea> фичу Indent/Unindent block. Достаточно простую: выделяем блок, жмём клавишу — в начале каждой строчки в блоке добавляется/удаляется пробел.

Для простоты предполагаем, что браузер реализует стандарты DOM в части выделения :)

Обвязка:

function textarea_keydown(event)
{
  // window.status = event.keyCode;
  if (event.altKey && !event.ctrlKey && !event.shiftKey)
  {
    switch (event.keyCode)
    {
    case 109: // Alt+-
      textarea_unindent(this);
      break;
    case 107: // Alt+=
      textarea_indent(this);
      break;
    }
  }
}

Эту функцию будем вешать на событие onKeyDown жертвы, например, так:

function nodelist_foreach(nodelist, action)
{
  for (var i = 0; i < nodelist.length; ++i)
  {
    action(nodelist[i]);
  }
}

nodelist_foreach(document.getElementsByTagName('textarea'), function(textarea)
{
  textarea.addEventListener('keydown', textarea_keydown, false);
});

Теперь надо реализовать собственно рабочие функции. Начнём с textarea_unindent. Понятно, что нам понадобится текущее выделение, а также текст до и после него.

var value = textarea.value;
var before = value.substring(0, textarea.selectionStart);
var text = value.substring(textarea.selectionStart, textarea.selectionEnd);
var after = value.substring(textarea.selectionEnd);

Собственно unindent — это удаление пробела в начале каждой строки.

text = text.replace(/^ /gm, '');

Теперь соберём обратно текст из частей.

textarea.value = before + text + after;

Итак, что у нас получилось:

function textarea_unindent(textarea)
{
  var value = textarea.value;
  var before = value.substring(0, textarea.selectionStart);
  var text = value.substring(textarea.selectionStart, textarea.selectionEnd);
  var after = value.substring(textarea.selectionEnd);

  text = text.replace(/^ /gm, '');

  textarea.value = before + text + after;
}

Да, но так пропадает выделение. Надо его восстановить.

textarea.setSelectionRange(before.length, before.length + text.length);

И ещё, если текст длинный, то скролл прыгает в начало. Ну правильно — мы же заменяем весь текст.

var scrollTop = textarea.scrollTop;
var scrollLeft = textarea.scrollLeft;

//…

textarea.scrollLeft = scrollLeft;
textarea.scrollTop = scrollTop;

Вот теперь хорошо. Принимаемся за indent и обнаруживаем, что, по сути, будет-то там то же самое, только замена наоборот — начало каждой строки на пробел.

Стало быть, надо вырефакторить общее в отдельную функцию.

function textarea_replace(textarea, what, withWhat)
{
  var scrollTop = textarea.scrollTop;
  var scrollLeft = textarea.scrollLeft;

  var value = textarea.value;
  var before = value.substring(0, textarea.selectionStart);
  var text = value.substring(textarea.selectionStart, textarea.selectionEnd);
  var after = value.substring(textarea.selectionEnd);

  text = text.replace(what, withWhat);

  textarea.value = before + text + after;
  textarea.setSelectionRange(before.length, before.length + text.length);

  textarea.scrollLeft = scrollLeft;
  textarea.scrollTop = scrollTop;
}

function textarea_unindent(textarea)
{
  textarea_replace(textarea, /^ /gm, '');
}

function textarea_indent(textarea)
{
  textarea_replace(textarea, /^/gm, ' ');
}

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

function textarea_indent(textarea)
{
  textarea_replace(textarea, /^(?!$)/gm, ' ');
}

Вот так лучше. Заодно не трогаем пустые строки.

Итого, финальный код:

function textarea_replace(textarea, what, withWhat)
{
  var scrollTop = textarea.scrollTop;
  var scrollLeft = textarea.scrollLeft;

  var value = textarea.value;
  var before = value.substring(0, textarea.selectionStart);
  var text = value.substring(textarea.selectionStart, textarea.selectionEnd);
  var after = value.substring(textarea.selectionEnd);

  text = text.replace(what, withWhat);

  textarea.value = before + text + after;
  textarea.setSelectionRange(before.length, before.length + text.length);

  textarea.scrollLeft = scrollLeft;
  textarea.scrollTop = scrollTop;
}

function textarea_unindent(textarea)
{
  textarea_replace(textarea, /^ /gm, '');
}

function textarea_indent(textarea)
{
  textarea_replace(textarea, /^(?!$)/gm, ' ');
}

function textarea_keydown(event)
{
  // window.status = event.keyCode;
  if (event.altKey && !event.ctrlKey && !event.shiftKey)
  {
    switch (event.keyCode)
    {
    case 109: // Alt+-
      textarea_unindent(this);
      break;
    case 107: // Alt+=
      textarea_indent(this);
      break;
    }
  }
}

function nodelist_foreach(nodelist, action)
{
  for (var i = 0; i < nodelist.length; ++i)
  {
    action(nodelist[i]);
  }
}

nodelist_foreach(document.getElementsByTagName('textarea'), function(textarea)
{
  textarea.addEventListener('keydown', textarea_keydown, false);
});

Чёрт возьми, оно мне нравится. Я, пожалуй, поставлю GreaseMonkey, и пусть привязывает это ко всем <textarea> вообще.

(will be screened)
(will be screened if not validated)
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

Profile

yurikhan: (Default)
Yuri Khan

August 2018

S M T W T F S
   1234
567891011
12131415161718
19202122232425
26 2728293031 

Links

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated 2026-03-01 22:28
Powered by Dreamwidth Studios