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> вообще.

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 2025-06-23 09:56
Powered by Dreamwidth Studios