Система фокусировки

Система фокусировки отслеживает местоположение пользователя (фокус) в редакторе Blockly. Она используется Blockly и пользовательским кодом для определения текущего компонента (блока, поля, категории панели инструментов и т. д.), имеющего фокус, и для переноса фокуса на другой компонент.

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

Архитектура

Система фокусировки состоит из трех частей:

  • FocusManager — это синглтон, координирующий фокус во всем Blockly. Он используется Blockly и пользовательским кодом для определения компонента, находящегося в фокусе Blockly, а также для перемещения фокуса Blockly на другой компонент. Он также отслеживает события фокуса DOM, синхронизирует фокус Blockly и фокус DOM и управляет CSS-классами, указывающими, какой компонент находится в фокусе.

    Менеджер фокуса в основном используется Blockly. Иногда он используется пользовательским кодом для взаимодействия с системой фокуса .

  • IFocusableTree — это независимая область редактора Blockly, например, рабочая область или панель инструментов. Она состоит из фокусируемых узлов, таких как блоки и поля. Деревья также могут иметь поддеревья. Например, рабочая область мутатора блока в основной рабочей области является поддеревом основной рабочей области.

    IFocusableTree в основном используется менеджером фокуса. Если вы не пишете собственный набор инструментов, вам, вероятно, не понадобится его реализовывать .

  • IFocusableNode — это блочный компонент, который может иметь фокус, например, блок, поле или категорию панели инструментов. Фокусируемые узлы содержат DOM-элемент, отображающий узел и получающий DOM-фокус, когда узел имеет блочный фокус. Обратите внимание, что деревья также являются фокусируемыми узлами. Например, вы можете сфокусироваться на рабочей области в целом.

    Методы в IFocusableNode в первую очередь вызываются менеджером фокуса.

    Сам IFocusableNode используется для представления компонента, находящегося в фокусе. Например, когда пользователь выбирает пункт в контекстном меню блока, блок передаётся функции обратного вызова этого элемента как IFocusableNode .

    Если вы пишете пользовательские компоненты, вам может потребоваться реализовать IFocusableNode .

Типы фокусировки

Система фокусировки определяет ряд различных типов фокусировки.

Блочный фокус и фокус DOM

Два основных типа фокуса — это блочный фокус и фокус DOM.

  • Фокус Blockly определяет, какой компонент Blockly (блок, поле, категория панели инструментов и т. д.) находится в фокусе. Это необходимо для работы на уровне компонентов Blockly. Например, плагин навигации с помощью клавиатуры позволяет пользователям использовать клавиши со стрелками для перемещения между компонентами, например, от блока к полю. Аналогичным образом, система контекстного меню формирует меню, подходящее для текущего компонента, то есть различные меню для рабочих областей, блоков и комментариев к рабочим областям.

  • DOM-фокус определяет, какой DOM-элемент находится в фокусе. Это необходимо для работы на уровне DOM-элементов. Например, программы чтения с экрана отображают информацию об элементе, который в данный момент находится в фокусе DOM, а вкладки перемещаются (меняют фокус) с одного DOM-элемента на другой.

Менеджер фокуса синхронизирует фокус Blockly и фокус DOM, поэтому, когда узел (компонент Blockly) имеет фокус Blockly, его базовый элемент DOM имеет фокус DOM, и наоборот.

Активный и пассивный фокус

Блочный фокус также делится на активный и пассивный . Активный фокус означает, что узел получает пользовательский ввод, например, нажатие клавиши. Пассивный фокус означает, что узел ранее был активным, но потерял его, когда пользователь перешёл на узел в другом дереве (например, перешёл из рабочей области на панель инструментов) или вообще покинул редактор Blockly. Если дерево снова получит фокус, пассивно сфокусированный узел снова станет активным.

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

Менеджер фокуса использует разные подсветки (классы CSS) для узлов с активным и пассивным фокусом. Это позволяет пользователям понимать, где они находятся и куда вернутся.

Эфемерный фокус

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

Когда менеджер фокуса предоставляет эфемерный фокус, он переводит активно сфокусированный узел в пассивный фокус. Он восстанавливает активный фокус при возвращении эфемерного фокуса.

Примеры

Следующие примеры иллюстрируют, как Blockly использует систему фокусировки. Они помогут вам понять, как ваш код вписывается в систему фокусировки и как он может её использовать.

Перемещайте фокус с помощью клавиатуры

Предположим, блок с двумя полями имеет блочный фокус, на что указывает подсветка (класс CSS) на DOM-элементе блока. Теперь предположим, что пользователь нажимает стрелку вправо:

  1. Плагин навигации с помощью клавиатуры :
    • Получает событие нажатия клавиши.
    • Отдает запрос навигационной системе (часть ядра Blockly) на перемещение фокуса на «следующий» компонент.
  2. Навигационная система:
    • Запрашивает у менеджера фокуса, какой компонент имеет блочный фокус. Менеджер фокуса возвращает блок как IFocusableNode .
    • Определяет, что IFocusableNode является BlockSvg , и проверяет его правила навигации по блокам, которые гласят, что он должен переместить фокус Blockly с блока в целом на первое поле в блоке.
    • Дает указание менеджеру фокуса переместить блочный фокус на первое поле.
  3. Менеджер фокуса:
    • Обновляет свое состояние, устанавливая блочный фокус на первом поле.
    • Устанавливает фокус DOM на элемент DOM поля.
    • Перемещает класс выделения из элемента блока в элемент поля.

Перемещайте фокус с помощью мыши

Теперь предположим, что пользователь нажимает на второе поле блока. Менеджер фокуса:

  1. Получает событие DOM focusout для элемента DOM первого поля и событие focusin для элемента DOM второго поля.
  2. Определяет, что элемент DOM, получивший фокус, соответствует второму полю.
  3. Обновляет свое состояние, устанавливая блочный фокус на второе поле. (Менеджеру фокуса не нужно устанавливать фокус DOM, поскольку браузер уже сделал это.)
  4. Перемещает класс выделения из элемента первого поля в элемент второго поля.

Другие примеры

Вот еще несколько примеров:

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

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

  • Сочетания клавиш используют IFocusableNode для идентификации компонента Blockly, к которому применяется сочетание клавиш.

  • Контекстные меню используют IFocusableNode для идентификации компонента Blockly, на котором меню было вызвано.

Настройки и система фокусировки

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

Пользовательские блоки и содержимое набора инструментов

Самый распространённый способ настройки Blockly — это создание собственных блоков и настройка содержимого панели инструментов. Ни одно из этих действий не влияет на систему фокусировки.

Пользовательские классы

Пользовательские классы могут нуждаться в реализации одного или обоих интерфейсов фокусировки ( IFocusableTree и IFocusableNode ). В таких случаях это не всегда очевидно.

Некоторые классы явно нуждаются в реализации интерфейсов фокусировки. К ним относятся:

  • Класс, реализующий пользовательский набор инструментов. Этот класс должен реализовывать IFocusableTree и IFocusableNode .

  • Классы, создающие видимый компонент (например, поле или значок), к которому пользователи могут перемещаться. Эти классы должны реализовывать интерфейс IFocusableNode .

Некоторым классам необходимо реализовать IFocusableNode , даже если они не создают видимый компонент или создают видимый компонент, к которому пользователи не могут перейти. К ним относятся:

  • Классы, реализующие интерфейс, расширяющий IFocusableNode .

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

  • Классы, используемые в API, требующем IFocusableNode .

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

  • Классы, расширяющие класс, реализующий IFocusableNode .

    Например, ToolboxSeparator расширяет ToolboxItem , который реализует IFocusableNode . Хотя разделители панели инструментов имеют видимый компонент, пользователи не могут перейти к ним, поскольку с ними нельзя выполнять действия и они не содержат полезного содержимого.

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

  • Классы, создающие видимый компонент, который управляет собственным фокусом, например, редактор полей или диалоговое окно. (Обратите внимание, что такие классы должны принимать эфемерный фокус при запуске и возвращать его при завершении. Использование WidgetDiv или DropDownDiv решит эту проблему.)

Наконец, некоторые классы не взаимодействуют с системой фокусировки и не требуют реализации IFocusableTree или IFocusableNode . К ним относятся:

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

  • Классы, совершенно не связанные с системой фокусировки, например классы, реализующие IMetricsManager или IVariableMap .

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

Реализуйте интерфейсы фокусировки

Самый простой способ реализовать IFocusableTree или IFocusableNode — расширить класс, реализующий эти интерфейсы. Например, если вы создаёте пользовательский набор инструментов, расширьте Toolbox , который реализует IFocusableTree и IFocusableNode . Если вы создаёте пользовательское поле, расширьте Field , который реализует IFocusableNode . Убедитесь, что ваш код не конфликтует с кодом интерфейса фокуса в базовом классе.

Если вы расширяете класс, реализующий интерфейс фокуса, вам, как правило, не потребуется переопределять какие-либо методы. Наиболее распространённое исключение — IFocusableNode.canBeFocused , который необходимо переопределить, если вы не хотите, чтобы пользователи переходили к вашему компоненту.

Реже возникает необходимость переопределять методы обратного вызова фокуса ( onTreeFocus и onTreeBlur в IFocusableTree и onNodeFocus и onNodeBlur в IFocusableNode ). Обратите внимание, что попытка изменить фокус (вызов FocusManager.focusNode или FocusManager.focusTree ) из этих методов приводит к исключению.

Если вы пишете собственный компонент с нуля, вам потребуется реализовать интерфейсы фокусировки самостоятельно. Подробнее см. в справочной документации по IFocusableTree и IFocusableNode .

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

Используйте менеджер фокуса

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

const focusManager = Blockly.getFocusManager();

Чтобы получить текущий сфокусированный узел, вызовите getFocusedNode :

const focusedNode = focusManager.getFocusedNode();
// Do something with the focused node.

Чтобы переместить фокус на другой узел, вызовите focusNode :

// Move focus to a different block.
focusManager.focusNode(myOtherBlock);

Чтобы переместить фокус на дерево, вызовите функцию focusTree . Это также установит фокус на корневой узел дерева.

// Move focus to the main workspace.
focusManager.focusTree(myMainWorkspace);

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

const returnEphemeralFocus = focusManager.takeEphemeralFocus();
// Do something.
returnEphemeralFocus();

Если вы используете WidgetDiv или DropDownDiv , они будут управлять эфемерным фокусом автоматически.

Табуляция

Система фокусировки устанавливает табулятор ( tabindex , равным 0 ) на корневом элементе всех деревьев (основной рабочей области, панели инструментов и выпадающих рабочих областей). Это позволяет пользователям использовать клавишу Tab для навигации по основным областям редактора Blockly, а затем (с помощью плагина навигации с помощью клавиатуры) использовать клавиши со стрелками для навигации внутри этих областей. Не изменяйте эти табуляторы, так как это помешает менеджеру фокусировки управлять ими.

Как правило, следует избегать установки табуляции на других элементах DOM, используемых Blockly, поскольку это противоречит модели Blockly, где для навигации между областями редактора используется клавиша Tab, а клавиши со стрелками внутри этих областей. Кроме того, такие табуляции могут работать некорректно. Это связано с тем, что каждый фокусируемый узел объявляет элемент DOM своим фокусируемым элементом. Если табуляция установлена ​​на потомке фокусируемого элемента, и пользователь переходит к этому элементу, менеджер фокусировки переместит фокус DOM на объявленный фокусируемый элемент.

Можно безопасно устанавливать позиции табуляции для элементов вашего приложения, находящихся за пределами редактора Blockly. Когда пользователь переходит из редактора к такому элементу с помощью клавиши Tab, менеджер фокуса меняет фокус Blockly с активного на пассивный. Для обеспечения доступности следует установить свойство tabindex равным 0 или -1 , как рекомендовано в предупреждении MDN в описании атрибута tabindex .

DOM-фокус

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

Дополнительная проблема заключается в том, что менеджер фокуса реагирует на события фокуса, устанавливая фокус DOM на ближайшего предка или себя самого элемента, на котором был объявлен фокусируемый элемент. Этот элемент может отличаться от элемента, на котором был установлен focus . (Если ближайшего предка или себя самого, на котором был установлен фокус, нет, например, когда focus установлен на элементе вне редактора Blockly, менеджер фокуса просто переводит активно сфокусированный узел в пассивный.)

Позиционируемые

Позиционируемые элементы — это компоненты, которые размещаются поверх рабочего пространства и реализуют интерфейс IPositionable . Примерами служат мусорная корзина и рюкзак в плагине «Рюкзак» . Позиционируемые элементы пока не интегрированы в систему фокусировки.