Создание нового типа поля

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

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

  1. Реализовать конструктор .
  2. Зарегистрируйте ключ JSON и реализуйте fromJson .
  3. Обработка инициализации блочного пользовательского интерфейса и прослушивателей событий .
  4. Обработка удаления прослушивателей событий (утилизация пользовательского интерфейса выполняется за вас).
  5. Реализуйте обработку значений .
  6. Добавьте текстовое представление значения вашего поля для доступности .
  7. Добавьте дополнительные функции, такие как:
  8. Настройте дополнительные аспекты вашего поля, такие как:

В этом разделе предполагается, что вы прочитали и знакомы с содержанием «Анатомии поля» .

Пример настраиваемого поля см. в демо «Настраиваемые поля» .

Реализация конструктора

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

В следующем примере кода создается настраиваемое поле с именем GenericField :

class GenericField extends Blockly.Field {
  constructor(value, validator) {
    super(value, validator);

    this.SERIALIZABLE = true;
  }
}

Сигнатура метода

Конструкторы полей обычно принимают значение и локальный валидатор. Значение является необязательным, и если вы не передаете значение (или передаете значение, которое не проходит проверку класса), то будет использоваться значение по умолчанию суперкласса. Для класса Field по умолчанию это значение равно null . Если вам не нужно это значение по умолчанию, обязательно передайте подходящее значение. Параметр валидатора присутствует только для редактируемых полей и обычно помечен как необязательный. Узнайте больше о валидаторах в документации по валидаторам .

Состав

Логика внутри вашего конструктора должна следовать следующему порядку:

  1. Вызовите унаследованный суперконструктор (все настраиваемые поля должны наследовать от Blockly.Field или одного из его подклассов), чтобы правильно инициализировать значение и установить локальный валидатор для вашего поля.
  2. Если ваше поле сериализуемо, установите соответствующее свойство в конструкторе. Редактируемые поля должны быть сериализуемыми, а поля доступны для редактирования по умолчанию, поэтому вам, вероятно, следует установить для этого свойства значение true, если только вы не уверены, что оно не должно быть сериализуемым.
  3. Необязательно: примените дополнительную настройку (например, поля метки позволяют передавать класс 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

Если ваше поле является общим текстовым полем (например, «Ввод текста» ), построение 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_ — это флаг, используемый в функции 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 используется для предоставления редакторов, которые находятся внутри блока, подключенного к полю. Он автоматически позиционируется рядом с полем, оставаясь в видимых границах. Выбор угла и выбор цвета являются хорошими примерами DropDownDiv .

Изображение средства выбора угла

WidgetDiv используется для предоставления редакторов, которые не находятся внутри коробки. Числовые поля используют WidgetDiv для покрытия поля полем ввода текста HTML. В то время как DropDownDiv обрабатывает позиционирование за вас, WidgetDiv этого не делает. Элементы необходимо будет расположить вручную. Система координат находится в пиксельных координатах относительно верхнего левого угла окна. Редактор текстового ввода — хороший пример WidgetDiv .

Изображение редактора текстового ввода

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));
}

Пример кода виджетдива

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 . В любом случае лучше всего использовать функцию привязки при передаче функции удаления, как показано в приведенных выше примерах DropDownDiv и WidgetDiv .

→ Информацию об удалении, не связанную с удалением редакторов, см. в разделе «Утилизация» .

Обновление экранного дисплея

Функция render_ используется для обновления отображения поля в блоке в соответствии с его внутренним значением.

Общие примеры включают в себя:

  • Изменить текст (выпадающий список)
  • Изменить цвет (цвет)

По умолчанию

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

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

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

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

Блок-схема, описывающая, как принять решение о переопределении render_

Настройка рендеринга

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

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

  1. Создание DOM должно выполняться во время инициализации , поскольку это более эффективно.
  2. Всегда следует обновлять свойство 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 . Доступ к цвету вам понадобится через свойство стиля блока.

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 , вам может потребоваться предоставить функции сериализации и десериализации (см. Сериализация ).

Настройка курсора

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