Прежде чем создавать новый тип поля, подумайте, подходит ли вам другой метод настройки полей. Если вашему приложению требуется хранить новый тип значения или вы хотите создать новый пользовательский интерфейс для существующего типа значения, вам, вероятно, потребуется создать новый тип поля.
Чтобы создать новое поле, выполните следующие действия:
- Реализовать конструктор .
- Зарегистрируйте ключ JSON и реализуйте
fromJson
. - Выполнять инициализацию блочного пользовательского интерфейса и прослушивателей событий .
- Управлять удалением прослушивателей событий (утилизация пользовательского интерфейса выполняется автоматически).
- Реализовать обработку значений .
- Добавьте текстовое представление значения вашего поля для удобства .
- Добавьте дополнительные функции, такие как:
- Настройте дополнительные аспекты вашей области, такие как:
В этом разделе предполагается, что вы прочитали и знакомы с содержанием книги «Анатомия поля» .
Пример настраиваемого поля смотрите в демоверсии пользовательских полей .
Реализация конструктора
Конструктор поля отвечает за установку начального значения поля и, при необходимости, за настройку локального валидатора . Конструктор настраиваемого поля вызывается во время инициализации исходного блока независимо от того, определён ли исходный блок в JSON или JavaScript. Таким образом, настраиваемое поле не имеет доступа к исходному блоку во время его создания.
Следующий пример кода создает настраиваемое поле с именем GenericField
:
class GenericField extends Blockly.Field {
constructor(value, validator) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
Сигнатура метода
Конструкторы полей обычно принимают значение и локальный валидатор. Значение необязательно, и если вы не передадите значение (или передадите значение, которое не пройдёт валидацию класса), будет использовано значение по умолчанию суперкласса. Для класса Field
по умолчанию это значение — null
. Если вам не нужно это значение по умолчанию, обязательно передайте подходящее значение. Параметр валидатора присутствует только для редактируемых полей и обычно помечен как необязательный. Подробнее о валидаторах читайте в документации по валидаторам .
Структура
Логика внутри вашего конструктора должна следовать следующему потоку:
- Вызовите унаследованный суперконструктор (все настраиваемые поля должны наследоваться от
Blockly.Field
или одного из его подклассов), чтобы правильно инициализировать значение и установить локальный валидатор для вашего поля. - Если ваше поле сериализуемо, задайте соответствующее свойство в конструкторе. Редактируемые поля должны быть сериализуемыми, и поля доступны для редактирования по умолчанию, поэтому, вероятно, следует установить это свойство в значение true, если только вы не уверены, что оно не должно быть сериализуемым.
- Необязательно: примените дополнительную настройку (например, поля метки позволяют передавать класс css, который затем применяется к тексту).
JSON и регистрация
В определениях блоков JSON поля описываются строкой (например, field_number
или field_textinput
). Blockly поддерживает сопоставление этих строк с объектами полей и вызывает fromJson
для соответствующего объекта во время построения.
Вызовите Blockly.fieldRegistry.register
, чтобы добавить тип поля к этой карте, передав класс поля в качестве второго аргумента:
Blockly.fieldRegistry.register('field_generic', GenericField);
Вам также необходимо определить функцию fromJson
. Ваша реализация должна сначала разыменовать все ссылки на токены локализации с помощью replaceMessageReferences , а затем передать значения конструктору.
GenericField.fromJson = function(options) {
const value = Blockly.utils.parsing.replaceMessageReferences(
options['value']);
return new CustomFields.GenericField(value);
};
Инициализация
При создании поля оно, по сути, содержит только значение. Инициализация — это этап, на котором строится DOM, модель (если поле имеет модель) и привязываются события.
Наблочный дисплей
Во время инициализации вы несете ответственность за создание всего необходимого для отображения на блоке поля.
Параметры по умолчанию, фон и текст
Функция initView
по умолчанию создаёт светлый rect
и text
элемент. Если вы хотите, чтобы ваше поле содержало оба этих элемента, а также некоторые дополнительные возможности, вызовите функцию initView
суперкласса перед добавлением остальных DOM-элементов. Если вы хотите, чтобы ваше поле содержало один, но не оба, из этих элементов, вы можете использовать функции createBorderRect_
или createTextElement_
.
Настройка конструкции DOM
Если ваше поле представляет собой обычное текстовое поле (например, Text Input ), построение DOM будет выполнено автоматически. В противном случае вам потребуется переопределить функцию initView
для создания DOM-элементов, которые понадобятся при последующем отображении поля.
Например, раскрывающееся поле может содержать как изображения, так и текст. В initView
создаётся один элемент изображения и один текстовый элемент. Затем во время render_
активный элемент отображается, а другой скрывается, в зависимости от типа выбранного варианта.
Создание элементов DOM можно осуществлять либо с помощью метода Blockly.utils.dom.createSvgElement
, либо с помощью традиционных методов создания DOM.
Требования к отображению информации на блоке поля:
- Все элементы DOM должны быть дочерними элементами
fieldGroup_
поля. Группа полей создаётся автоматически. - Все элементы DOM должны оставаться в пределах указанных размеров поля.
Более подробную информацию о настройке и обновлении отображения на блоке см. в разделе «Рендеринг» .
Добавление текстовых символов
Если вы хотите добавить символы в текст поля (например, символ градуса в поле «Угол »), вы можете добавить элемент символа (обычно содержащийся в <tspan>
) непосредственно к textElement_
поля.
Входные события
По умолчанию поля регистрируют события подсказок и нажатия мыши (для отображения редакторов ). Если вы хотите отслеживать другие типы событий (например, обработку перетаскивания по полю), следует переопределить функцию bindEvents_
поля.
bindEvents_() {
// Call the superclass function to preserve the default behavior as well.
super.bindEvents_();
// Then register your own additional event listeners.
this.mouseDownWrapper_ =
Blockly.browserEvents.conditionalBind(this.getClickTarget_(), 'mousedown', this,
function(event) {
this.originalMouseX_ = event.clientX;
this.isMouseDown_ = true;
this.originalValue_ = this.getValue();
event.stopPropagation();
}
);
this.mouseMoveWrapper_ =
Blockly.browserEvents.conditionalBind(document, 'mousemove', this,
function(event) {
if (!this.isMouseDown_) {
return;
}
var delta = event.clientX - this.originalMouseX_;
this.setValue(this.originalValue_ + delta);
}
);
this.mouseUpWrapper_ =
Blockly.browserEvents.conditionalBind(document, 'mouseup', this,
function(_event) {
this.isMouseDown_ = false;
}
);
}
Для привязки к событию обычно следует использовать функцию Blockly.utils.browserEvents.conditionalBind
. Этот метод привязки событий отфильтровывает вторичные касания во время перетаскивания. Если вы хотите, чтобы обработчик запускался даже в процессе перетаскивания, можно использовать функцию Blockly.browserEvents.bind
.
Утилизация
Если вы зарегистрировали какие-либо пользовательские прослушиватели событий внутри функции bindEvents_
поля, их необходимо будет отменить внутри функции dispose
.
Если вы правильно инициализировали представление своего поля (добавив все элементы DOM к fieldGroup_
), то DOM поля будет автоматически удален.
Обработка ценностей
→ Информацию о значении поля и его тексте см. в разделе Анатомия поля .
Порядок проверки
Реализация валидатора класса
Поля должны принимать только определённые значения. Например, числовые поля должны принимать только числа, цветовые поля — только цвета и т. д. Это обеспечивается валидаторами класса и локальными валидаторами . Валидатор класса следует тем же правилам, что и локальные валидаторы, за исключением того, что он также запускается в конструкторе и, следовательно, не должен ссылаться на исходный блок.
Чтобы реализовать валидатор класса поля, переопределите функцию doClassValidation_
.
doClassValidation_(newValue) {
if (typeof newValue != 'string') {
return null;
}
return newValue;
};
Обработка допустимых значений
Если значение, переданное в поле с помощью setValue
допустимо, вы получите обратный вызов doValueUpdate_
. По умолчанию функция doValueUpdate_
:
- Устанавливает свойство
value_
вnewValue
. - Устанавливает свойство
isDirty_
вtrue
.
Если вам просто нужно сохранить значение и вы не хотите выполнять какую-либо специальную обработку, вам не нужно переопределять doValueUpdate_
.
В противном случае, если вы хотите сделать что-то вроде:
- Пользовательское хранилище
newValue
. - Измените другие свойства на основе
newValue
. - Сохраните, является ли текущее значение допустимым или нет.
Вам необходимо переопределить doValueUpdate_
:
doValueUpdate_(newValue) {
super.doValueUpdate_(newValue);
this.displayValue_ = newValue;
this.isValueValid_ = true;
}
Обработка недопустимых значений
Если значение, переданное в поле с помощью setValue
, недопустимо, вы получите функцию обратного вызова doValueInvalid_
. По умолчанию функция doValueInvalid_
ничего не делает. Это означает, что по умолчанию недопустимые значения не будут отображаться. Это также означает, что поле не будет перерисовываться, поскольку свойство isDirty_
не будет установлено.
Если вы хотите отобразить недопустимые значения, следует переопределить doValueInvalid_
. В большинстве случаев следует установить свойство displayValue_
на недопустимое значение, установить isDirty_
на true
и переопределить render_ , чтобы отображение на блоке обновлялось на основе displayValue_
, а не value_
.
doValueInvalid_(newValue) {
this.displayValue_ = newValue;
this.isDirty_ = true;
this.isValueValid_ = false;
}
Многосоставные значения
Если поле содержит составное значение (например, списки, векторы, объекты), вам может потребоваться, чтобы части обрабатывались как отдельные значения.
doClassValidation_(newValue) {
if (FieldTurtle.PATTERNS.indexOf(newValue.pattern) == -1) {
newValue.pattern = null;
}
if (FieldTurtle.HATS.indexOf(newValue.hat) == -1) {
newValue.hat = null;
}
if (FieldTurtle.NAMES.indexOf(newValue.turtleName) == -1) {
newValue.turtleName = null;
}
if (!newValue.pattern || !newValue.hat || !newValue.turtleName) {
this.cachedValidatedValue_ = newValue;
return null;
}
return newValue;
}
В приведённом выше примере каждое свойство newValue
проверяется индивидуально. Затем, в конце функции doClassValidation_
, если какое-либо свойство оказывается недопустимым, его значение кэшируется в свойстве cacheValidatedValue_
, после чего возвращается null
(недопустимо). Кэширование объекта с индивидуально проверенными свойствами позволяет функции doValueInvalid_
обрабатывать их по отдельности, просто выполняя проверку !this.cacheValidatedValue_.property
, вместо повторной проверки каждого свойства по отдельности.
Этот шаблон для проверки многокомпонентных значений также может использоваться в локальных валидаторах, но в настоящее время нет способа принудительно реализовать этот шаблон.
isDirty_
isDirty_
— это флаг, используемый в функции setValue
, а также в других частях поля, чтобы указать, требуется ли повторная отрисовка поля. Если отображаемое значение поля изменилось, isDirty_
обычно следует установить в true
.
Текст
→ Информацию о том, где используется текст поля и чем он отличается от значения поля, см. в разделе Анатомия поля .
Если текст вашего поля отличается от значения вашего поля, вам следует переопределить функцию getText
, чтобы предоставить правильный текст.
getText() {
let text = this.value_.turtleName + ' wearing a ' + this.value_.hat;
if (this.value_.hat == 'Stovepipe' || this.value_.hat == 'Propeller') {
text += ' hat';
}
return text;
}
Создание редактора
Если вы определите функцию showEditor_
, Blockly будет автоматически отслеживать нажатия и вызывать showEditor_
в нужный момент. Вы можете отобразить любой HTML-код в редакторе, заключив его в один из двух специальных div-элементов, называемых DropDownDiv и WidgetDiv, которые располагаются поверх остального пользовательского интерфейса Blockly.
DropDownDiv против WidgetDiv
Элемент DropDownDiv
используется для создания редакторов, расположенных внутри блока, соединённого с полем. Он автоматически позиционируется рядом с полем, оставаясь в пределах видимых границ. Примерами элементов DropDownDiv
являются элементы выбора угла и цвета.
Элемент WidgetDiv
используется для создания редакторов, которые не находятся внутри блока. В числовых полях элемент WidgetDiv используется для закрытия поля полем ввода HTML-текста. В то время как DropDownDiv автоматически позиционирует поле, WidgetDiv этого не делает. Элементы необходимо позиционировать вручную. Система координат задаётся в пикселях относительно левого верхнего угла окна. Редактор ввода текста — хороший пример использования элемента WidgetDiv
.
Пример кода DropDownDiv
showEditor_() {
// Create the widget HTML
this.editor_ = this.dropdownCreate_();
Blockly.DropDownDiv.getContentDiv().appendChild(this.editor_);
// Set the dropdown's background colour.
// This can be used to make it match the colour of the field.
Blockly.DropDownDiv.setColour('white', 'silver');
// Show it next to the field. Always pass a dispose function.
Blockly.DropDownDiv.showPositionedByField(
this, this.disposeWidget_.bind(this));
}
Пример кода WidgetDiv
showEditor_() {
// Show the div. This automatically closes the dropdown if it is open.
// Always pass a dispose function.
Blockly.WidgetDiv.show(
this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));
// Create the widget HTML.
var widget = this.createWidget_();
Blockly.WidgetDiv.getDiv().appendChild(widget);
}
Уборка
Оба класса, DropDownDiv и WidgetDiv, управляют уничтожением HTML-элементов виджета, но вам необходимо вручную удалить все прослушиватели событий, которые вы применили к этим элементам.
widgetDispose_() {
for (let i = this.editorListeners_.length, listener;
listener = this.editorListeners_[i]; i--) {
Blockly.browserEvents.unbind(listener);
this.editorListeners_.pop();
}
}
Функция dispose
вызывается в null
контексте для DropDownDiv
. Для WidgetDiv
она вызывается в контексте WidgetDiv
. В любом случае при передаче функции dispose лучше всего использовать функцию bind , как показано в примерах DropDownDiv
и WidgetDiv
выше.
→ Информацию об утилизации, не относящуюся к утилизации редакторов, см. в разделе Утилизация .
Обновление отображения на блоке
Функция render_
используется для обновления отображения поля на блоке в соответствии с его внутренним значением.
Вот типичные примеры:
- Изменить текст (раскрывающийся список)
- Изменить цвет (цвет)
Настройки по умолчанию
Функция render_
по умолчанию устанавливает отображаемый текст равным результату функции getDisplayText_
. Функция getDisplayText_
возвращает свойство value_
поля, преобразованное в строку, после того как оно было усечено в соответствии с максимальной длиной текста.
Если вы используете отображение блока по умолчанию и поведение текста по умолчанию подходит для вашего поля, вам не нужно переопределять render_
.
Если поведение текста по умолчанию подходит для вашего поля, но на блочном отображении поля имеются дополнительные статические элементы, вы можете вызвать функцию render_
по умолчанию, но вам все равно придется переопределить ее, чтобы обновить размер поля .
Если поведение текста по умолчанию не подходит для вашего поля или отображение на блоке вашего поля содержит дополнительные динамические элементы, вам потребуется настроить функцию render_
.
Настройка рендеринга
Если стандартное поведение рендеринга не подходит для вашего поля, вам потребуется определить индивидуальное поведение рендеринга. Это может включать в себя любые действия: от настройки отображаемого текста до изменения элементов изображения и обновления цветов фона.
Все изменения атрибутов DOM допустимы, нужно помнить только две вещи:
- Создание DOM следует выполнять во время инициализации , так как это более эффективно.
- Всегда следует обновлять свойство
size_
в соответствии с размером отображаемого блока.
render_() {
switch(this.value_.hat) {
case 'Stovepipe':
this.stovepipe_.style.display = '';
break;
case 'Crown':
this.crown_.style.display = '';
break;
case 'Mask':
this.mask_.style.display = '';
break;
case 'Propeller':
this.propeller_.style.display = '';
break;
case 'Fedora':
this.fedora_.style.display = '';
break;
}
switch(this.value_.pattern) {
case 'Dots':
this.shellPattern_.setAttribute('fill', 'url(#polkadots)');
break;
case 'Stripes':
this.shellPattern_.setAttribute('fill', 'url(#stripes)');
break;
case 'Hexagons':
this.shellPattern_.setAttribute('fill', 'url(#hexagons)');
break;
}
this.textContent_.nodeValue = this.value_.turtleName;
this.updateSize_();
}
Обновление размера
Обновление свойства size_
поля очень важно, поскольку оно сообщает коду отрисовки блока, как расположить поле. Лучший способ точно определить значение size_
— экспериментировать.
updateSize_() {
const bbox = this.movableGroup_.getBBox();
let width = bbox.width;
let height = bbox.height;
if (this.borderRect_) {
width += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
height += this.constants_.FIELD_BORDER_RECT_X_PADDING * 2;
this.borderRect_.setAttribute('width', width);
this.borderRect_.setAttribute('height', height);
}
// Note how both the width and the height can be dynamic.
this.size_.width = width;
this.size_.height = height;
}
Соответствие цветов блоков
Если вы хотите, чтобы элементы поля соответствовали цветам блока, к которому они прикреплены, следует переопределить метод applyColour
. Доступ к цвету можно получить через свойство style блока.
applyColour() {
const sourceBlock = this.sourceBlock_;
if (sourceBlock.isShadow()) {
this.arrow_.style.fill = sourceBlock.style.colourSecondary;
} else {
this.arrow_.style.fill = sourceBlock.style.colourPrimary;
}
}
Обновление возможности редактирования
Функция updateEditable
позволяет изменить внешний вид поля в зависимости от того, доступно ли оно для редактирования. Функция по умолчанию делает так, что фон отображается при наведении курсора (граница) или нет, независимо от того, доступно ли оно для редактирования. Размер отображаемого на блоке элемента не должен меняться в зависимости от его редактируемости, но все остальные изменения разрешены.
updateEditable() {
if (!this.fieldGroup_) {
// Not initialized yet.
return;
}
super.updateEditable();
const group = this.getClickTarget_();
if (!this.isCurrentlyEditable()) {
group.style.cursor = 'not-allowed';
} else {
group.style.cursor = this.CURSOR;
}
}
Сериализация
Сериализация заключается в сохранении состояния поля, чтобы его можно было загрузить в рабочую область позже.
Состояние вашей рабочей области всегда включает значение поля, но может включать и другие состояния, например, состояние пользовательского интерфейса поля. Например, если ваше поле представляет собой масштабируемую карту, позволяющую пользователю выбирать страны, вы также можете сериализовать уровень масштабирования.
Если ваше поле сериализуемо, необходимо установить свойство SERIALIZABLE
в true
.
Blockly предоставляет два набора хуков сериализации для полей. Одна пара хуков работает с новой системой сериализации JSON, а другая — со старой системой сериализации XML.
saveState
и loadState
saveState
и loadState
— это хуки сериализации, которые работают с новой системой сериализации JSON.
В некоторых случаях вам не нужно предоставлять эти данные, поскольку реализации по умолчанию будут работать. Если (1) ваше поле является прямым подклассом базового класса Blockly.Field
, (2) ваше значение имеет сериализуемый тип JSON и (3) вам нужно только сериализовать значение, то реализация по умолчанию будет работать отлично!
В противном случае функция saveState
должна возвращать сериализуемый объект/значение JSON, представляющее состояние поля. А функция loadState
должна принимать тот же сериализуемый объект/значение JSON и применять его к полю.
saveState() {
return {
'country': this.getValue(), // Value state
'zoom': this.getZoomLevel(), // UI state
};
}
loadState(state) {
this.setValue(state['country']);
this.setZoomLevel(state['zoom']);
}
Полная сериализация и резервные данные
saveState
также принимает необязательный параметр doFullSerialization
. Он используется полями, которые обычно ссылаются на состояние, сериализованное другим сериализатором (например, резервными моделями данных). Этот параметр сигнализирует, что ссылающееся состояние не будет доступно при десериализации блока, поэтому поле должно выполнить всю сериализацию самостоятельно. Например, это справедливо при сериализации отдельного блока или при копировании и вставке блока.
Два распространенных варианта использования этого:
- Когда отдельный блок загружается в рабочую область, в которой не существует базовой модели данных, поле имеет достаточно информации в своем собственном состоянии для создания новой модели данных.
- При копировании и вставке блока поле всегда создает новую резервную модель данных, а не ссылается на существующую.
Одним из полей, использующих этот метод, является встроенное поле переменной. Обычно оно сериализует идентификатор переменной, на которую ссылается, но если doFullSerialization
имеет значение true, сериализуется всё её состояние.
saveState(doFullSerialization) {
const state = {'id': this.variable_.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
}
return state;
}
loadState(state) {
const variable = Blockly.Variables.getOrCreateVariablePackage(
this.getSourceBlock().workspace,
state['id'],
state['name'], // May not exist.
state['type']); // May not exist.
this.setValue(variable.getId());
}
Поле переменной делает это для того, чтобы при загрузке в рабочую область, где его переменная не существует, можно было создать новую переменную для ссылки.
toXml
и fromXml
toXml
и fromXml
— это хуки сериализации, работающие со старой системой сериализации XML. Используйте эти хуки только в случае крайней необходимости (например, при работе со старой кодовой базой, которая ещё не перенесена), в противном случае используйте saveState
и loadState
.
Функция toXml
должна возвращать XML-узел, представляющий состояние поля. Функция fromXml
должна принимать тот же XML-узел и применять его к полю.
toXml(fieldElement) {
fieldElement.textContent = this.getValue();
fieldElement.setAttribute('zoom', this.getZoomLevel());
return fieldElement;
}
fromXml(fieldElement) {
this.setValue(fieldElement.textContent);
this.setZoomLevel(fieldElement.getAttribute('zoom'));
}
Редактируемые и сериализуемые свойства
Свойство EDITABLE
определяет, должно ли поле иметь пользовательский интерфейс, указывающий на возможность взаимодействия с ним. По умолчанию оно равно true
.
Свойство SERIALIZABLE
определяет, следует ли сериализовать поле. По умолчанию оно равно false
. Если это свойство равно true
, может потребоваться предоставить функции сериализации и десериализации (см. раздел Сериализация ).
Настройка с помощью CSS
Вы можете настроить поле с помощью CSS. В методе initView
добавьте пользовательский класс к fieldGroup_
поля, а затем укажите этот класс в CSS.
Например, чтобы использовать другой курсор:
initView() {
...
// Add a custom CSS class.
if (this.fieldGroup_) {
Blockly.utils.dom.addClass(this.fieldGroup_, 'myCustomField');
}
}
.myCustomField {
cursor: cell;
}
Настройка курсора
По умолчанию классы, расширяющие FieldInput
, используют text
курсор при наведении курсора на поле, перетаскиваемые поля используют курсор grabbing
, а все остальные поля используют курсор default
. Если вы хотите использовать другой курсор, установите его с помощью CSS .