Перейти к содержимому

Javascript на что лучше заменить select

  • автор:

Ищем ба­ланс меж­ду на­тив­ным и ка­стом­ным се­лек­том

Есть план! Мы сделаем стилизованный селект. Стилизуем не просто снаружи, но и внутри. Полный контроль над стилизацией. Вдобавок к этому мы сделаем его доступным. Мы не будем пытаться повторить за браузером всё, что он делает по умолчанию при отрисовке нативного . Мы буквально будем использовать нативный , как только используется любая вспомогательная технология. Но когда будет использоваться мышь, мы отрисуем стилизованную версию и заставим ее функционировать как .

Вот что я понимаю под «гибридным» селектом: это одновременно и нативный , и его стилизованная альтернатива.

Сравнение кастомного и нативного селектов: слева кастомный в стиле сайта, справа нативный в стиле ОС.

Селект, выпадающий список, навигация, меню… название имеет значение Скопировать ссылку

Во время изучения данной темы я думала обо всех тех названиях, которыми разбрасываются, когда говорят о селектах. Наиболее общие из них — «выпадающий список» и «меню». Есть два типа ошибок при наименовании, которые мы можем допустить: дать одинаковые названия разным элементам или дать разные названия одинаковым элементам.

Перед тем, как мы двинемся дальше, позвольте мне внести ясность касательно использования термина «выпадающий список». Вот как я его понимаю:

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

Множество интерфейсов могут выглядеть похоже на выпадающий список. Но просто назвать элемент «выпадающим списком» — всё равно что использовать слово «рыба» для описания животного. Какое семейство рыб? Рыба-клоун не то же самое, что и акула. То же касается и выпадающих списков.

Кадр из мультфильма «Спасти Немо»: огромная зубастая акула за спиной двух крошечных испуганных рыбок.

Подобно тому, как в море существует много разных видов рыб, есть множество компонентов, о которых мы можем вести речь, столкнувшись со словосочетанием «выпадающий список».

  • Меню: список команд или действий, которые пользователь может исполнить на странице.
  • Навигация: список ссылок, используемых для перемещения по сайту.
  • Селект: контрол формы , показывающий пользователю список опций, которые он может в ней выбрать.

Решение, о каком типе выпадающих списков мы ведём речь, может быть туманным. Вот несколько примеров из веба, подходящих под мою классификацию вышеупомянутых элементов. Оно основано на моём исследовании, и иногда, когда я не могу найти подходящий ответ, интуитивно основано на моём опыте.

Схема с примерами выпадающих списков: 1. Селект, 2. Меню, 3. Меню или селект, 4. Открывающаяся навигация, 5. Что-то другое.

№ Ожидаемое поведение Тип списка
1 Ожидается, что выбранный вариант отправится внутри формы на сервер, например, возраст. Селект
2 Выпадающему списку не нужен выбранный вариант, например, список действий: копировать, вставить и вырезать. Меню
3 Выбранный вариант влияет на контент, например, сортировка списка. Меню или селект, подробности чуть позже.
4 Выпадающий список содержит ссылки на другие страницы, например, большая навигация со ссылками на разделы сайта. Открывающаяся навигация
5 Содержимое выпадающего меню — не список, например, выбор даты. Что-то другое, что не следует называть выпадающим списком

Не все воспринимают интернет и взаимодействуют с ним одинаково. Именование пользовательских интерфейсов и определение дизайн-паттернов — фундаментальный процесс, хотя и с достаточным пространством для личной интерпретации.

Вот тип выпадающего списка, который определенно можно назвать меню. Его использование является горячей темой при обсуждении доступности. Я не буду много говорить об этом здесь, но позвольте мне просто подчеркнуть, что тег устарел и не рекомендуется к использованию. Вот подробное руководство по инклюзивным меню и меню-кнопкам (в переводе на «Веб-стандартах», прим. редактора), включая объяснение почему ARIA-роль menu не следует использовать для навигации по сайту.

Мы даже не коснулись других элементов, которые попадают в довольно серую зону, что делает классификацию выпадающих списков ещё более туманной из-за недостака практических примеров использования от WCAG.

Уфф… получилось много. Давайте забудем обо всём этом беспорядке с выпадающими списками и сосредоточимся исключительно на элементе .

Давайте поговорим про Скопировать ссылку

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

Для разработчиков — самый разочаровывающий элемент форм, главным образом из-за отстутствия поддержки стилизации. Борьба в UX за это настолько велика, что мы ищем альтернативы. Что ж, я думаю, что первое правило такое же, как с ARIA: избегайте его использования, если можете.

Я могла бы закончить статью прямо сейчас словами «Не используйте , точка». Но давайте посмотрим правде в глаза: селект для нас всё ещё лучшее решение в ряде случаев. Сюда можно отнести сценарии, когда мы работаем со списком, содержащим множество опций, раскладкой, ограниченной в пространстве, или же просто при нехватки времени или бюджета для разработки и реализации пользовательского интерактивного компонента с нуля.

Требования к кастомному Скопировать ссылку

Приняв решение создать кастомный селект — пусть и самый простой — мы сталкиваемся с требованиями, которые мы должны учесть:

  • Должна быть кнопка, содержащая текущий выбранный вариант.
  • Клик по блоку переключает видимость списка опций.
  • Клик по опции, расположенной в списке, обновляет выбранное значение. Текст кнопки меняется и список закрывается.
  • Клик по области вне компонента закрывает список.
  • Переключатель содержит маленький треугольник, направленный вниз, указывающий на то, что есть варианты.

Что-то вроде такого:

Кто-то из вас подумает: «Работает и хорошо». Но постойте… Разве это работает для всех? Не все используют мышку (или тачскрин). К тому же нативный обладает более широким списком возможностей, которые достаются нам бесплатно и не входят в этот список требований:

  • Выбранный вариант доступен для восприятия всеми пользователями, вне зависимости от их возможностей зрения.
  • С компонентом можно предсказуемо взаимодействовать с помощью клавиатуры во всех браузерах — например, используя клавиши стрелок для навигации, Enter для выбора, Esc для отмены и так далее.
  • Вспомогательные технологии (например, скринридеры) чётко объявляют пользователям элемент, называя его роль, имя и состояние.
  • Положение списка регулируется, то есть он не обрезается за краями экрана.
  • Элемент следует настройкам операционной системы пользователя — например, высокую контрастность, цветовую схему, ограничение движений и другие.

Именно на этом этапе большинство кастомных селектов так или иначе терпят крах. Взгляните на некоторые крупные UI-библиотеки. Я не буду упоминать конкретные, потому что веб достаточно недолговечный, но сходите попробуйте. Вероятно, вы заметите разное поведение селекта в разных фреймворках.

Вот дополнительные характеристики, за которыми нужно следить:

  • Выбирается ли опция списка сразу же при получения фокуса с клавиатуры?
  • Можно ли использовать Enter и Space для выбора варианта?
  • Нажатие на Tab переносит нас к следующему варианут списка или же к следующему элементу формы?
  • Что будет, когда вы достигнете последнего варианта в списке с помощью стрелок? Фокус замрет на последнем варианте, вернется к первому или же, что хуже всего, перейдет к следующему элементу формы?
  • Возможно ли перейти к последней опции списка с помощью клавиши Page Down ?
  • Можно ли прокручивать элементы списка, если их больше, чем в поле видимости в данный момент?

Это был небольшой пример функций нативного селекта.

Решив создать наш собственный кастомный селект, мы обязываем людей пользоваться им определенным образом, который может отличаться от их ожиданий.

Но всё ещё хуже. Даже нативный ведет себя по-разному в разных браузерах и скринридерах.

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

Создаём гибридный селект Скопировать ссылку

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

Что если вместо этого мы зададим нативный селект по умолчанию и заменим его более эстетичным, если это возможно? Вот тут и вступает в игру идея о гибридном селекте. Он гибридный, потому что состоит из двух селектов, каждый из которых показывается в нужный для него момент:

  • Нативный селект, видимый и доступный по умолчанию.
  • Кастомный селект, скрытый до тех пор, пока не произойдёт взаимодействие посредством мыши.

Начнём с разметки. Вначале, добавим нативный с несколькими до кастомного. Чуть позже я объясню почему.

Любой контрол формы должен содержать лейбл. Мы можем прибегнуть к , но фокус будет попадать на нативный селект, когда мы будем кликать на подпись. В целях предотвращения такого поведения используем и свяжем его с селектом с помощью aria-labelledby .

Наконец, с помощью aria-hidden=»true» нужно сообщить вспомогательным технологиям, чтобы те игнорировали кастомный селект. Таким образом, они видят только нативный селект, несмотря ни на что.

Основная рабочая роль

Это приводит нас к стилизации, в ходе которой мы не только заставляем всё выглядеть красивее, но также и управляем переключением между селектами. Нам не хватает лишь пары строк, чтобы начать магию.

Для начала, оба селекта должны обладать одинаковой шириной и высотой. Это позволит пользователям не увидеть серьезного расхождения с макетом при переключении.

.selectNative, .selectCustom

Вот два селекта. Но лишь один может устанавливать пространство, которое они занимают. Второй должен быть спозиционирован абсолютно, чтобы быть вне потока документа. Давайте провернём это с кастомным селектом, так как замена производится только тогда, когда она возможна. Мы спрячем его по умолчанию, чтобы никто пока до него не добрался.

.selectCustom

Вот здесь-то и начинается веселье. Нам нужно определить, использует ли пользователь устройство, в котором наведение — часть основного ввода информации. Например, компьютер с мышью. Хотя мы и думаем о медиавыражениях только как о способе проверки определённых функций или же инструменте адаптивности на брейкпоинтах, их также можно использовать для обнаружения поддержки ховера с помощью hover: hover , который поддерживается всеми основными браузерами. Итак, давайте используем это для отображения кастомного селекта на устройствах, где можно навести курсор.

@media (hover: hover) < .selectCustom < display: block; >> 

Отлично. Но что насчёт людей, которые используют клавиатуру для навигации даже на устройствах, поддерживающих ховер? Что делать? Мы будем прятать кастомный селект, когда нативный находится в состоянии фокуса. Мы можем поймать соседний элемент с помощью комбинирующего селектора + . Как только нативный селект в фокусе, прячем кастомный, который следует сразу за ним в DOM. Вот почему кастомный селект должен следовать за нативным.

@media (hover: hover) < .selectNative:focus + .selectCustom < display: none; >> 

Вот и всё! Трюк переключения между двумя селектами готов. Есть другие способы сделать это через CSS, но и этот прекрасно работает.

Наконец, нам нужно немного JavaScript. Добавим несколько обработчиков событий:

  • Один для события клика, по которому в игру вступает кастомный селект, раскрываясь и показывая варианты выбора.
  • Один, чтобы синхронизировать выбранные варианты. При изменении одного варианта выбора, меняется и второй.
  • И ещё один для установки навигации через клавиатуру с помощью клавиш Up и Down , выбора варианта с помощью клавиш Enter или Space , и закрытия списка через Esc .

Юзабилити-тест Скопировать ссылку

Я провела небольшое юзабилити-тестирование, в котором я попросила нескольких людей с ограниченными возможностями воспользоваться гибридным селектом. Были протестированы следующие устройства и инструменты с использованием последних версий Chrome 81, Firefox 76, Safari 13:

  • Компьютер только с мышью.
  • Компьютер только с клавиатурой.
  • VoiceOver на macOS с помощью клавиатуры.
  • NVDA в Windows с помощью клавиатуры.
  • VoiceOver на iPhone и iPad в Safari

Все эти тесты дали желаемый результат, но я уверена, что можно было бы провести ещё больше юзабилити-тестов с более разнообразными устройствами и широким диапазоном лиц. Если у вас есть возможность протестировать на других устройствах или с другими инструментами — такими как JAWS, Dragon и подобным — пожалуйста, расскажите мне, как прошёл тест.

Во время теста была обнаружена проблема. В частности, проблема связана с настройкой VoiceOver «Использовать виртуальный курсор VoiceOver». Если пользователь откроет селект с помощью этого курсора, вместо нативного покажется кастомный селект.

Больше всего мне нравится в этом подходе то, как он совмещает всё самое лучшее из обоих миров без нанесения ущерба функциональности.

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

Данный подход обеспечивает необходимую для каждого функциональность без дополнительного громоздкого кода, реализующего функции нативного селекта.

Не поймите меня неправильно, этот метод не является универсальным решением для всех. Он может являться рабочим для простых селектов, но, вероятно, не будет работать в случаях со сложным взаимодействием. В этих случаях нам нужно использовать ARIA и JavaScript для восполнения пробелов и создания действительно доступного селекта.

Примечание касательно селекта-меню Скопировать ссылку

Давайте вернёмся к третьему сценарию нашего списка селектов. Если вы помните, это выпадающий список, который всегда имеет отмеченный вариант (например, сортировка). Я отнесла его к серой области как и меню или селект.

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

Три выпадающих селекта: кастомная сортировка товаров, кастомный выбор отображения списка, нативная сортировка товаров.

— это вид меню. Оба имеют схожую семантику и поведение, особенно в случае, когда один вариант всегда выбран. Теперь позвольте мне упомянуть критерий из WCAG 3.2.2 о полях (уровень A):

Изменение состояния любого пользовательского элемента не должно влечь за собой автоматическое изменение контекста без уведомления об этом пользователя перед самим изменением.

Давайте применим это на практике. Представьте себе сортируемый список студентов. Может быть визуально очевидно, что сортировка происходит незамедлительно, но это не обязательно так для всех людей. Таким образом, при использовании , мы рискуем нарушить правила WCAG, поскольку контент страницы изменился, а это попадает под понятие «изменение контекста».

Чтобы соблюсти критерий, мы должны уведомить пользователя о действии до того, как он начнёт взаимодействовать с элементом или же поставить сразу после списка, чтобы подтвердить изменения.

  

Тем не менее, использование наряду с созданием пользовательского меню является хорошим подходом, когда речь заходит о несложных меню, требующих изменение содержимого страницы. Просто помните, что от вашего решения зависит объём работ, необходимых для создания полностью доступного компонента. Это как раз тот случай, когда гибридный селект может выручить.

Заключение Скопировать ссылку

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

Создание действительно доступных селектов (или же любого вида выпадающего списка) сложнее, чем может казаться. Руководство WCAG даёт прекрасные инструкции наряду с лучшими практиками, но без конкретных примеров использования эти инструкции носят рекомендательный характер. Не говоря уже о том, что поддержка ARIA слабая, а внешний вид и поведение браузерного отличаются в разных браузерах.

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

P.S. Не забудьте выбрать правильное название при создании выпадающего списка ��

Selection и Range

В этой главе мы рассмотрим выделение как в документе, так и в полях формы, таких как .

JavaScript позволяет получать существующее выделение, выделять и снимать выделение как целиком, так и по частям, убирать выделенную часть из документа, оборачивать её в тег и так далее.

Вы можете получить готовые решения в секции «Итого» в конце статьи, но узнаете гораздо больше, если прочитаете главу целиком. Используемые для выделения встроенные классы Range и Selection просты для понимания, и после их изучения вам уже не понадобятся «готовые рецепты», чтобы сделать всё, что захотите.

Range

В основе выделения лежит Range – диапазон. Он представляет собой пару «граничных точек»: начало и конец диапазона.

Каждая точка представлена как родительский DOM-узел с относительным смещением от начала. Если этот узел – DOM-элемент, то смещение – это номер дочернего элемента, а для текстового узла смещение – позиция в тексте. Скоро будут примеры.

Давайте что-нибудь выделим.

Для начала мы создадим диапазон (конструктор не имеет параметров):

let range = new Range();

Затем мы установим границы выделения, используя range.setStart(node, offset) и range.setEnd(node, offset) .

Например, рассмотрим этот фрагмент HTML-кода:

Взглянем на его DOM-структуру, обратите внимание на текстовые узлы, они важны для нас:

Выделим «Example: italic» . Это первые два дочерних узла тега

(учитывая текстовые узлы):

italic and bold

  • range.setStart(p, 0) – устанавливает начало диапазона на нулевом дочернем элементе тега

    (Это текстовый узел «Example: » ).

  • range.setEnd(p, 2) – расширяет диапазон до 2го (но не включая его) дочернего элемента тега

    (это текстовый узел » and » , но так как конец не включён, последний включённый узел – это тег ).

Ниже представлен расширенный пример, в котором вы можете попробовать другие варианты:

italic and bold

From – To

К примеру, выделение с 1 до 4 возвращает следующий диапазон italic and bold .

Не обязательно использовать один и тот же элемент в setStart и setEnd . Диапазон может охватывать множество не связанных между собой элементов. Важно лишь чтобы конец шёл после начала.

Выделение частей текстовых узлов

Давайте выделим текст частично, как показано ниже:

Это также возможно, нужно просто установить начало и конец как относительное смещение в текстовых узлах.

Нам нужно создать диапазон, который:

  • начинается со второй позиции первого дочернего узла тега

    (захватываем всё, кроме первых двух букв «Example: «)

  • заканчивается на 3 позиции первого дочернего узла тега (захватываем первые три буквы «bold», но не более):

italic and bold

Объект диапазона Range имеет следующие свойства:

  • startContainer , startOffset – узел и начальное смещение,
    • в примере выше: первый текстовый узел внутри тега

      и 2 .

    • в примере выше: первый текстовый узел внутри тега и 3 .
    • в примере выше: false
    • в примере выше:

    Методы Range

    Существует множество удобных методов для манипулирования диапазонами.

    Установить начало диапазона:

    • setStart(node, offset) установить начальную границу в позицию offset в node
    • setStartBefore(node) установить начальную границу прямо перед node
    • setStartAfter(node) установить начальную границу прямо после node

    Установить конец диапазона (похожи на предыдущие методы):

    • setEnd(node, offset) установить конечную границу в позицию offset в node
    • setEndBefore(node) установить конечную границу прямо перед node
    • setEndAfter(node) установить конечную границу прямо после node

    Как было показано, node может быть как текстовым узлом, так и элементом: для текстовых узлов offset пропускает указанное количество символов, в то время как для элементов – указанное количество дочерних узлов.

    • selectNode(node) выделить node целиком
    • selectNodeContents(node) выделить всё содержимое node
    • collapse(toStart) если указано toStart=true , установить конечную границу в начало, иначе установить начальную границу в конец, схлопывая таким образом диапазон
    • cloneRange() создать новый диапазон с идентичными границами

    Чтобы манипулировать содержимым в пределах диапазона:

    • deleteContents() – удалить содержимое диапазона из документа
    • extractContents() – удалить содержимое диапазона из документа и вернуть как DocumentFragment
    • cloneContents() – склонировать содержимое диапазона и вернуть как DocumentFragment
    • insertNode(node) – вставить node в документ в начале диапазона
    • surroundContents(node) – обернуть node вокруг содержимого диапазона. Чтобы этот метод сработал, диапазон должен содержать как открывающие, так и закрывающие теги для всех элементов внутри себя: не допускаются частичные диапазоны по типу abc .

    Используя эти методы, мы можем делать с выделенными узлами что угодно.

    Проверим описанные методы в действии:

    Нажмите на кнопку, чтобы соответствующий метод отработал на выделении, или на "resetExample", чтобы восстановить выделение как было. 

    italic and bold

    , extractContents() < let content = range.extractContents(); result.innerHTML = ""; result.append("Извлечено: ", content); >, cloneContents() < let content = range.cloneContents(); result.innerHTML = ""; result.append("Клонировано: ", content); >, insertNode() < let newNode = document.createElement('u'); newNode.innerHTML = "НОВЫЙ УЗЕЛ"; range.insertNode(newNode); >, surroundContents() < let newNode = document.createElement('u'); try < range.surroundContents(newNode); >catch(e) < alert(e) >>, resetExample() < p.innerHTML = `Example: italic and bold`; result.innerHTML = ""; range.setStart(p.firstChild, 2); range.setEnd(p.querySelector('b').firstChild, 3); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); > >; for(let method in methods) < document.write(`

    ()">$
    `); > methods.resetExample();

    Также существуют методы сравнения диапазонов, но они редко используются. Когда они вам понадобятся, вы можете прочитать о них в спецификации или справочнике MDN.

    Selection

    Range это общий объект для управления диапазонами выделения. Мы можем создавать и передавать подобные объекты. Сами по себе они ничего визуально не выделяют.

    Выделение в документе представлено объектом Selection , который может быть получен как window.getSelection() или document.getSelection() .

    Выделение может включать ноль или более диапазонов. По крайней мере, так утверждается в Спецификации Selection API. На практике же выделить несколько диапазонов в документе можно только в Firefox, используя Ctrl + click ( Cmd + click для Mac).

    Ниже представлен скриншот выделения с 3 диапазонами, сделанный в Firefox:

    Остальные браузеры поддерживают максимум 1 диапазон. Как мы увидим далее, некоторые методы Selection подразумевают, что может быть несколько диапазонов, но, как было сказано ранее, во всех браузерах, кроме Firefox, может быть не более одного диапазона.

    Свойства Selection

    Аналогично диапазону, выделение имеет начальную границу, именуемую «якорем», и конечную, называемую «фокусом».

    Основные свойства выделения:

    • anchorNode – узел, с которого начинается выделение,
    • anchorOffset – смещение в anchorNode , где начинается выделение,
    • focusNode – узел, на котором выделение заканчивается,
    • focusOffset – смещение в focusNode , где выделение заканчивается,
    • isCollapsed – true , если диапазон выделения пуст или не существует.
    • rangeCount – количество диапазонов в выделении, максимум 1 во всех браузерах, кроме Firefox.

    Конец выделения может быть в документе до его начала

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

    Некоторые из них, такие как мышь, позволяют создавать выделение в обоих направлениях: слева направо и справа налево.

    Если начало (якорь) выделения идёт в документе перед концом (фокус), говорят, что такое выделение «направлено вперёд».

    К примеру, если пользователь начинает выделение с помощью мыши в направлении от «Example» до «italic»:

    Иначе, если выделение идёт от «italic» до «Example», выделение идёт в «обратном» направлении, его фокус будет перед якорем:

    Это отличается от объектов Range , которые всегда направлены вперёд: начало диапазона не может стоять после его конца.

    События при выделении

    Существуют события, позволяющие отслеживать выделение:

    • elem.onselectstart – когда с elem начинается выделение, например пользователь начинает двигать мышкой с зажатой кнопкой.
      • preventDefault() отменяет начало выделения.
      • Заметьте: этот обработчик можно поставить только на document .

      Демо отслеживания выделения

      Ниже представлено небольшое демо. В нём границы выделения выводятся динамически по мере того, как оно меняется:

      курсив и жирный

      От – До

      Демо получения выделения

      Чтобы получить всё выделение:

      • Как текст: просто вызовите document.getSelection().toString() .
      • Как DOM-элементы: получите выделенные диапазоны и вызовите их метод cloneContents() (только первый диапазон, если мы не поддерживаем мультивыделение в Firefox).

      Ниже представлено демо получения выделения как в виде текста, так и в виде DOM-узлов:

      курсив и жирный

      Склонировано: // Получаем как текст astext.innerHTML += selection; >;

      Методы Selection

      Методы Selection для добавления и удаления диапазонов:

      • getRangeAt(i) – взять i-ый диапазон, начиная с 0 . Во всех браузерах, кроме Firefox, используется только 0 .
      • addRange(range) – добавить range в выделение. Все браузеры, кроме Firefox, проигнорируют этот вызов, если в выделении уже есть диапазон.
      • removeRange(range) – удалить range из выделения.
      • removeAllRanges() – удалить все диапазоны.
      • empty() – сокращение для removeAllRanges .

      Также существуют методы управления диапазонами выделения напрямую, без обращения к Range:

      • collapse(node, offset) – заменить выделенный диапазон новым, который начинается и заканчивается на node , на позиции offset .
      • setPosition(node, offset) – то же самое, что collapse (дублирующий метод-псевдоним).
      • collapseToStart() – схлопнуть (заменить на пустой диапазон) к началу выделения,
      • collapseToEnd() – схлопнуть диапазон к концу выделения,
      • extend(node, offset) – переместить фокус выделения к данному node , с позиции offset ,
      • setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset) – заменить диапазон выделения на заданные начало anchorNode/anchorOffset и конец focusNode/focusOffset . Будет выделено всё содержимое между этими границами
      • selectAllChildren(node) – выделить все дочерние узлы данного узла node .
      • deleteFromDocument() – удалить содержимое выделения из документа.
      • containsNode(node, allowPartialContainment = false) – проверяет, содержит ли выделение node (частично, если второй аргумент равен true )

      Так что для многих задач мы можем вызывать методы Selection , не обращаясь к связанному объекту Range .

      К примеру, выделение всего параграфа

      :

      курсив и жирный

      То же самое с помощью Range :

      курсив и жирный

      Чтобы что-то выделить, сначала снимите текущее выделение

      Если выделение уже существует, сначала снимите его, используя removeAllRanges() , и только затем добавляйте новые диапазоны. В противном случае все браузеры, кроме Firefox, проигнорируют добавление.

      Исключением являются некоторые методы выделения, которые заменяют существующее выделение, например, setBaseAndExtent .

      Выделение в элементах форм

      Элементы форм, такие как input и textarea , предоставляют отдельное API для выделения. Так как значения полей представляют собой простой текст, а не HTML, и нам не нужны такие сложные объекты, как Range и Selection .

      • input.selectionStart – позиция начала выделения (это свойство можно изменять),
      • input.selectionEnd – позиция конца выделения (это свойство можно изменять),
      • input.selectionDirection – направление выделения, одно из: «forward» (вперёд), «backward» (назад) или «none» (без направления, если, к примеру, выделено с помощью двойного клика мыши).
      • input.onselect – срабатывает, когда выделение завершено.
      • input.select() – выделяет всё содержимое input (может быть textarea вместо input ),
      • input.setSelectionRange(start, end, [direction]) – изменить выделение, чтобы начиналось с позиции start , и заканчивалось end , в данном направлении direction (необязательный параметр).
      • input.setRangeText(replacement, [start], [end], [selectionMode]) – заменяет выделенный текст в диапазоне новым. Если аргументы start и end указаны, то они задают начало и конец диапазона, иначе используется текущее выделение. Последний аргумент, selectionMode , определяет, как будет вести себя выделение после замены текста. Возможные значения:
        • «select» – только что вставленный текст будет выделен.
        • «start» – диапазон выделения схлопнется прямо перед вставленным текстом (так что курсор окажется непосредственно перед ним).
        • «end» – диапазон выделения схлопнется прямо после вставленного текста (курсор окажется сразу после него).
        • «preserve» – пытается сохранить выделение. Значение по умолчанию.

        Давайте посмотрим на эти методы в действии.

        Пример: отслеживание выделения

        К примеру, этот код использует событие onselect , чтобы отслеживать выделение:

          
        От – До
        • onselect срабатывает при выделении чего-либо, но не при снятии выделения.
        • событие document.onselectionchange не должно срабатывать при выделении внутри элемента формы в соответствии со спецификацией, так как оно не является выделением элементов в document . Хотя некоторые браузеры генерируют это событие, полагаться на это не стоит.

        Пример: изменение позиции курсора

        Мы можем изменять selectionStart и selectionEnd , устанавливая выделение.

        Важный граничный случай – когда selectionStart и selectionEnd равны друг другу. В этом случае они указывают на позицию курсора. Иными словами, когда ничего не выбрано, выделение схлопнуто на позиции курсора.

        Таким образом, задавая selectionStart и selectionEnd одно и то же значение, мы можем передвигать курсор.

           

        Пример: изменение выделения

        Чтобы изменять содержимое выделения, мы можем использовать метод input.setRangeText . Конечно, мы можем читать selectionStart/End и, зная позиции выделения, изменять соответствующую подстроку в value , но setRangeText намного мощнее и, зачастую, удобнее.

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

        В этом примере выделенный текст будет обёрнут в *. * :

          

        Передавая больше параметров, мы можем устанавливать start и end .

        В этом примере мы найдём «ЭТО» в поле ввода, заменим его и оставим заменённый текст выделенным:

          

        Пример: вставка на месте курсора

        Если ничего не выделено, или мы указали одинаковые start и end в методе setRangeText , то текст просто вставляется, и ничего не удаляется.

        Мы также можем вставить что-нибудь на текущей позиции курсора, используя setRangeText .

        Кнопка в примере вставляет «ПРИВЕТ» на месте курсора и устанавливает его после вставленного текста. Если какой-то текст был выделен, он будет заменён (мы можем узнать о наличии выделения, проверив selectionStart!=selectionEnd и, если выделение есть, сделать что-то ещё):

          

        Сделать что-то невыделяемым

        Существуют три способа сделать что-то невыделяемым:

          Используйте CSS-свойство user-select: none .

         #elem 
        Можно выделить
        Можно выделить
        Можно выделить
        Можно выделить

        Ссылки

        • Спецификация DOM: Range
        • Selection API
        • Спецификация HTML: API для выделения в элементах управления текстом

        Итого

        Мы подробно рассмотрели два разных API для выделения:

        1. Для документа: объекты Selection и Range .
        2. Для input , textarea : дополнительные методы и свойства.

        Второе API очень простое, так как работает с текстом.

        Самые используемые готовые решения:

        let selection = document.getSelection(); let cloned = /* элемент, в который мы хотим скопировать выделенные узлы */; // затем применяем методы Range к selection.getRangeAt(0) // или, как здесь, ко всем диапазонам, чтобы поддерживать множественное выделение for (let i = 0; i
        let selection = document.getSelection(); // напрямую: selection.setBaseAndExtent(. from. to. ); // или можно создать диапазон range и: selection.removeAllRanges(); selection.addRange(range);

        И пару слов о курсоре. Позиция курсора в редактируемых элементах, таких как , всегда находится в начале или конце выделения.

        Мы можем использовать это, как для того, чтобы получить позицию курсора, так и чтобы переместить его, установив elem.selectionStart и elem.selectionEnd .

        P.S. Если вам нужна поддержка старого IE8-, посмотрите в архивную статью.

        Курсы javascript

        В базе данных есть некая таблица. Так же есть две страницы (код — ниже). Чтобы не «метаться» между мелкими файлами — объединил html+js+css в index.php.

        Что есть сейчас:
        1 — Кнопка «Text» добавляет в .Container текст, который в данный момент выбран в select’е.
        2 — Кнопка «Test» добавляет три предопределённых значения (в теге sсript)в select, затем обращается при помощи XHR в js к файлу test.php. Test.php обращается к базе за списком значений первого столбца (рандомны, уникальны), отдаёт результат обратно и добавляет ответ в .Container без перезагрузки страницы (при помощи innerHTML).
        3 — Кнопка «^W» очищает .Container.

        Что нужно:
        При нажатии кнопки «Test» скрипт должен добавлять в список select значения, полученные из базы.
        Т.е., если select был вначале таким:[«text1», «text2», «text3»], то после нажатия должен стать таким:[«a»,»b»,»c»,»d»,»e»].

           Test  function Test() < Container = document.getElementById('Container'); Selector = document.getElementById('Selector'); Selector.options[0] = new Option('text0','value0'); Selector.options[1] = new Option('text1','value1',false,true); Selector.options[2] = new Option('text2','value2'); req = new XMLHttpRequest(); req.open('POST','/test/test.php',true); req.onreadystatechange = processRequestChange; req.send(null); function processRequestChange() < if (req.readyState == 4) < if (req.status == 200) < Container.innerHTML = req.responseText; >> else < Container.innerHTML = 'Error!
        '+'req.readyState: '+req.readyState+'
        '+'req.statusText: '+req.statusText+'
        '+'req.status: '+req.status; > > Container.innerHTML = req.responseText; > function Text() < Container.innerHTML = Selector.options[Selector.selectedIndex].text; >function Clear() html
        What do you want to see?

        Последний раз редактировалось TrogWar, 15.04.2012 в 15:09 . Причина: Корректировка форматирования и очепяток

        16.04.2012, 13:27
        Регистрация: 08.02.2012
        Сообщений: 34

        ну во первых, у тебя test.php смешной. запусти его отдельно в окне — наверняка поймешь что к чему!
        $result — массив объектов. так его не вывести.
        если чё, погугли как выводить инфу с базы средствами php. тема избита.

        во вторых: может имеет смысл забирать данные посредством JSON,
        мне однажды эту тему популярно разжевали:
        Сколько объектов/массивов содержит JSON
        и соответственно вставлять данные в selectы с возвращенного JSON (ассоциативный массив)
        то бишь в test.php:
        $rows = array();
        while($r = mysql_fetch_assoc($result)) $rows[] = $r;
        >
        print json_encode($rows);
        ну а в Javascript работаешь как с любым другим массивом.

        НО: если будешь так делать, сначала просто выведи полученный JSON, дабы изучить его строение, шоб правильно обращаться к паре ключ:значение, а то я тогда лоханулся. часа 2 искал баг, которого не было
        и еще: не забудь кодировать в utf8 и не выводи массив с тысячами записей, а то нарвешься вот на это —
        Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 22806573 bytes).
        ну или не кодируй, и выводи сколько хош. пока не нарвешься..)))

        Последний раз редактировалось Le-mark43, 16.04.2012 в 13:51 . Причина: правка
        16.04.2012, 13:36
        Регистрация: 05.10.2011
        Сообщений: 85
        Запросы с json ответами можно смотреть в firebug-е, и прочих инспекторах (хром, опера).
        16.04.2012, 21:33
        Интересующийся
        Регистрация: 15.04.2012
        Сообщений: 12

        Конечно же смешной — я и не собираюсь открывать test.php, иначе бы дописал остальную html-заглушку и переделал массив в строку текста через mysql_fetch_array() и цикл. Но мне-то нужно передать обратно массив, а не строку.

        За инфу про JSON спасибо про такой метод я как раз не задумался.
        У меня кириллицы практически никакой нет и не будет — все в utf-8, так что минус проблема.

        Про выделение памяти это очень важное дополнение! Как тогда быть, если в перспективе из мускула будут таким образом вытаскиваться по 7-10 столбцов по 3000-4000 значений в каждом? Должна быть возможность выведения всех этих строк сразу на страницу.
        Что-нибудь можете посоветовать чтобы избежать той ошибки с памятью?

        16.04.2012, 23:15
        Регистрация: 08.02.2012
        Сообщений: 34

        насчет этого не парься пока, практика показывает, что такое появляется, в основном при выводе в кодировке в utf8, ты как нить посмотри, Json строк на 10 при хотя бы 5 столбцах (записи в базе в кириллице), ужаснёшься.
        да поправят меня профессора, но utf8 кодирует кириллические символы длинной аж до 6 байт, для сравнения омериканские символы и символы unicode как и asci — до 2 байт.
        поэтому если выводишь в кодировке 1251, можно особо не париться, 3000 строк на 15 столбцов, нихрена не пустых — выводятся за милую душу.
        но на всякий пожарный, дабы не переборщить делают через try<>catch<>, как не знаю, скоро предстоит и мне с этим разбираться.

        16.04.2012, 23:23
        Регистрация: 08.02.2012
        Сообщений: 34

        Да, кстати, кто-то говорил, что объем памяти можно выставить в профайле Apache, (denwer). погугли! вдруг тебе больше повезёт.

        19.04.2012, 06:17
        Интересующийся
        Регистрация: 15.04.2012
        Сообщений: 12

        Le-mark43, спасибо за информацию! Объём можно, а я и позабыл об этом..

        Таблица из 4х столбцов: один char и три float’а. char будет только где-то в ~10% случаев (не более) написан кириллицей. Апач, мускул, пхп, все страницы принудительно настроены на работу в utf-8 (general).
        Насколько я читал, между cp1251 и utf-8 только в кириллице — на каждую букву юник выделяет два байта вместо одного как в 1251, а вот для латиницы всё одинаково.
        То бишь, если передаётся много кириллицы в utf-8, то, фактически, увеличивается весь трафик в два раза, что не есть гуд.
        Плюс надо учитывать момент, что при урезании кириллической (и только!) строки (например, «text.length-1») будет отрезан только один из двух бит буквы. Последствия очевидны (:

        Но это всё лирика.. Вот чего я боюсь — переполнения памяти, слишком сильного пожирания трафика или урезания данных при передаче на узком месте между test.php и index.php по XHR’у.
        Есть ли разница — передавать с помощью JSON или тупо строкой (echo в test.php и req.responseText в test.js)? Пока что ничего конкретного, чтобы всё в одном месте собрано было, не нашёл =/

        20.04.2012, 20:11
        Регистрация: 08.02.2012
        Сообщений: 34

        как передавать лично я разницы не видел, главное, чтоб на выходе из php у тебя был массив, дабы потом в js его можно было свободно обработать.
        просто массив формировать гораздо удобнее JSONом, он для этого походу и создан был
        по поводу утф8 и кириллицы, я, чем больше нарывался на ошибку с памятью, тем больше понимал, что память тут вообще не причем.
        Allowed memory size of 134217728 bytes exhausted (tried to allocate 22806573 bytes).
        если перевести, то это означает:
        допустимый размер памяти 134217728 байт исчерпан (попытка выделить 22806573 байта),
        первая цифра всегда больше второй, зависит она от настрйки профиля в апаче, максимум 128 метров, как и говорится в сообщении об ошибке.
        вопрос: если максимально можно использовать 128 метров, то какого хрена не можешь выделить 21,7 метра?
        хотя возможно цифра в скобках означает на сколько превышен лимит.
        Причем, я заметил, что так бывает не всегда, иногда при выводе этого же объема информации (не такого же, а именно этого же), такой штуки не вылезает, а почему?
        вообщем, что бы там не говорили, не верю я в нехватку памяти для обработки скрипта, ну а поскольку предложений по решению у меня нет, то и говорю, аккуратнее с количеством записей в утф8.))

        Стилизация Select-Option (почти) без JavaScript

        Стилизация некоторых стандартных элементов — довольно нетривиальная задача.

        Разумеется, хороший специалист может стилизовать что угодно, однако всё упирается в сложность этого действия.

        Для стилизации радиокнопок и чекбоксов потребуется совсем немного времени — идея стилизации label со скрытыми input не нова, и давно и повсеместно используется.

        Для стилизации остального — есть JavaScript.

        Сегодня я хочу рассказать Вам о том, как можно относительно просто стилизовать выпадающие списки, с минимальным кодом JavaScript (от 0 до 26 строк) и минимальной дополнительной разметкой HTML.

        Easy selects withoud JavaScript (almost)

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

        И вот, когда я уже почти закончил писать очередную жуткую конструкцию, призванную заменить несчастный Select, у меня возник вопрос:

        А зачем?

        Обычно этот вопрос ведёт к умудрённой прокрастинации, переосмыслению жизни и т.д., но в этот раз что-то пошло не так, и я понял, что вопрос и правда был хорошим, а главное — полезным.

        Небольшой ликбез:

        Итак, для тех, кто в танке, а также тех, кому выпадающие списки доставили столько боли, что они решили забыть о них, как о страшном сне, я немножко (правда) напомню о том, что это такое.

        Выпадающие списки состоят из 2 основных элементов:

        1. Select — контейнер для всего списка
        2. Option — элемент списка

        Основной функционал выпадающего списка — отправка информации о выбранном(ых) пункте(ах) на сервер.

        Для полноты картины, вот список атрибутов элементов (чтобы можно было оценить, сколько из них будут реализованы):

        1. Select — disabled , form , multiple , name , required , size
        2. Option — disabled , label , selected , value

        Уже было написано немало слов, поэтому — ближе к коду:

         

        Описание 0 пункта

        Итак, чем же хорош этот код? (всё, без чего можно обойтись при объяснении, убрано специально)

        Давайте отметим, какие пункты из функционала Select и Option автоматически стали возможны, за счёт использования input + label:

        1. Select — form, name, required
        2. Option — disabled, label, selected, value
        1. Select — disabled, multiple, size
        2. Option — полностью реализовано

        Чтобы реализовать пункт Select.multiple (множественный выбор) — достаточно сделать так:

         

        Мы всего-навсего меняем тип input’ов с Radio на Checkbox, и получаем практически полный аналог multiple.

        Разница в том, что для множественного выбора не нужно зажимать ctrl (ну а если кто-то хочет полностью имитировать функционал — JavaScript в помощь).

        Что осталось?

        1. Select — disabled, size
        2. Option — полностью реализовано
        • Select.size — какой зададите размер, такой и будет. Необходимо будет просто добавить контейнер.
        • Select.disabled — для контейнера нужно добавить pointer-events: none , чтобы отменить реакцию на любые действия пользователя (наведение, клики и т.д.), ну и можно сделать его чуть прозрачным.

        Нужно также добавить автозаполнение заголовка для выпадающего меню с единственным выбором, а также возникнет проблема при сбросе формы (кнопка Reset), т.к. заголовок не сбросится. Но и это — вполне решаемо (с помощью JavaScript).

        А теперь — примеры:

        Немного реализации (выпадающее меню с единичным выбором):

        И ещё немного (не выпадающее меню с множественным выбором):

        Бонус: не знаю, зачем я это сделал, но… выпадающее меню с единичным выбором и работающим на CSS сбросом заголовка:

        Зачем это нужно?

        В связи с тем, что судя по комментариям, не все понимают, для чего будет полезен данный код, считаю нужным дать некоторые разъяснения.

        При стилизации выпадающих списков вида:

          

        есть серьёзные ограничения стилизации. Поэтому их заменяют на простые списки:

        которым с помощью JavaScript навешивают весь необходимый функционал.

        Поскольку изначально списки никак не взаимодействуют с формами, из-за этого возникают проблемы.

        Мало того, что необходимо реализовать весь основной функционал:

        • переключение пунктов и их взаимодействие
        • состояния (checked, disabled)
        • привязка к форме и отправка данных на сервер
        • инициализация исходных данных (автозаполнение)
        • сброс формы (при нажатии на кнопку «reset» список нужно вернуть в состояние при инициализации)

        Фактически, если в JavaScript что-то ломается, не важно, из-за чего — скрипт не догрузился, возник баг, внесли правки — при возникновении любой проблемы выпадающий список превращается в… что-то, выглядящее как выпадающий список, но не работающее.
        Вообще. Никак.

        Для пользователя это будет выглядеть так: есть выпадающий список, но работать с ним нельзя.

        Для сервера это будет выглядеть ещё «веселее» — пришла форма, в которой не хватает части данных. Хорошо, что такую ситуацию заранее предусмотрели при разработке… Ведь предусмотрели, правда?

        Плюсы получившегося решения:

        • Весь основной функционал (смотри выше), кроме сброса заголовка ВСЕГДА будет работать. Даже если у человека вообще отключён JavaScript (хоть это и стало практически неактуально). Единственное, что может пойти не так — нельзя будет открыть список. Но и в этом случае отправится значение по умолчанию, плюс смену состояний — список открыт/закрыт — можно реализовать прямо в коде html (защита от проблем с файлом JS), или вообще открывать по наведению.
        • Данного функционала достаточно для большинства подобных списков. Да, возникнут проблемы у людей, использующих дополнительный функционал (об этом ниже). Но в случае со списком на ul — li для работы и вовсе требуется библиотека, и много что может пойти не так.

        Минусы (доп. функционал, который не реализован, и добавляется JavaScript’ом / библиотеками):

        • Переключение с помощью tab
        • Управление стрелками (в стандартном select можно менять выбранный пункт кнопками вверх-вниз)
        • Озвучка скринридерами (для людей, у которых проблемы со зрением текст на странице озвучивается)
        • Не работает как нативный select на мобильных (на мобильных select довольно удобно работает сам по себе. При желании можно определять устройство и подменять на стандартный select)
        • Список не умеет выпадать вверх, если снизу край вьюпорта (JS сделает это)
        • Группировка пунктов (аналог OptGroup) (сложно, и очень редко встречается)

        Как видно, ничто из вышеперечисленных минусов не является критичным для подавляющего большинства списков, и при необходимости реализуется с помощью JavaScript, а потому.

        Добро пожаловать в мир, где Ваши библиотеки для стилизации Select-Option-подобных списков станут чуточку меньше, а работа с этими списками без библиотек — почти комфортной!

        UPD: как оказалось, в случае с чекбоксами атрибут required работает некорректно, и его также необходимо эмулировать с помощью JavaScript (на данный момент обязательными становятся все чекбоксы, отмеченные как required, и только они)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *