新しいフィールドタイプを作成する前に、フィールドをカスタマイズするための他の方法のいずれかがニーズに合っているかどうかを検討してください。アプリケーションで新しい値の型を保存する必要がある場合や、既存の値の型に新しい UI を作成する場合は、新しいフィールド型を作成する必要があります。
新しいフィールドを作成するには、次の操作を行います。
- コンストラクタを実装する。
- JSON キーを登録して
fromJson
を実装する。 - ブロック上の UI とイベント リスナーの初期化を処理する。
- イベント リスナーの破棄を処理する(UI の破棄は自動的に処理されます)。
- 値の処理を実装する。
- ユーザー補助のため、フィールドの値のテキスト表現を追加します。
- 次のような追加機能を追加します。
- フィールドのその他の要素を構成します。たとえば、次のようなものがあります。
このセクションは、フィールドの構造の内容を読み、理解していることを前提としています。
カスタム フィールドの例については、カスタム フィールドのデモをご覧ください。
コンストラクタの実装
フィールドのコンストラクタは、フィールドの初期値を設定し、必要に応じてローカル バリデータを設定します。カスタム フィールドのコンストラクタは、ソースブロックが 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
を呼び出して、フィールド クラスを 2 番目の引数として渡し、このマップにフィールド タイプを追加します。
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
要素を作成します。フィールドにこれらの両方とその他の機能も追加する場合は、残りの DOM 要素を追加する前に、スーパークラスの initView
関数を呼び出します。フィールドにこれらの要素の一方だけを含める場合は、createBorderRect_
関数または createTextElement_
関数を使用します。
DOM の作成をカスタマイズする
フィールドが汎用テキスト フィールド(テキスト入力など)の場合、DOM の作成は自動的に処理されます。そうでない場合は、initView
関数をオーバーライドして、今後のフィールドのレンダリング時に必要な DOM 要素を作成する必要があります。
たとえば、プルダウン フィールドには画像とテキストの両方を含めることができます。initView
では、1 つの画像要素と 1 つのテキスト要素が作成されます。render_
では、選択したオプションのタイプに基づいて、アクティブな要素を表示し、他の要素を非表示にします。
DOM 要素の作成は、Blockly.utils.dom.createSvgElement
メソッドを使用するか、従来の DOM 作成メソッドを使用します。
フィールドのブロック内表示の要件は次のとおりです。
- すべての DOM 要素は、フィールドの
fieldGroup_
の子要素である必要があります。フィールド グループが自動的に作成されます。 - すべての DOM 要素は、フィールドの報告されたサイズ内に収める必要があります。
ブロック上のディスプレイのカスタマイズと更新の詳細については、レンダリングのセクションをご覧ください。
テキスト記号を追加する
フィールドのテキストに記号(角度フィールドの度記号など)を追加する場合は、記号要素(通常は <tspan>
に含まれる)をフィールドの textElement_
に直接追加します。
入力イベント
デフォルトでは、フィールドはツールチップ イベントと mousedown イベント(エディタの表示に使用)を登録します。他の種類のイベントをリッスンする場合(フィールドのドラッグを処理する場合など)は、フィールドの 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
関数を使用します。この方法でイベントをバインドすると、ドラッグ中の 2 番目のタップが除外されます。ドラッグの途中でもハンドラを実行する場合は、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_ をオーバーライドして、オンブロック ディスプレイが value_
ではなく displayValue_
に基づいて更新されるようにする必要があります。
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_
関数の最後で、個々のプロパティが無効な場合は、null
(無効)を返す前に、値が cacheValidatedValue_
プロパティにキャッシュに保存されます。個別に検証されたプロパティを持つオブジェクトをキャッシュに保存すると、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 をエディタに表示するには、Blockly の他の UI の上にフローティングする 2 つの特別な div(DropDownDiv と WidgetDiv)のいずれかにラップします。
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
関数は、DropDownDiv
の null
コンテキストで呼び出されます。WidgetDiv
では、WidgetDiv
のコンテキストで呼び出されます。どちらの場合も、上記の DropDownDiv
と WidgetDiv
の例に示すように、dispose 関数を渡すときに bind 関数を使用することをおすすめします。
→ エディタの破棄に固有ではない破棄については、破棄をご覧ください。
ブロック上のディスプレイの更新
render_
関数は、フィールドのブロック内表示を内部値と一致するように更新するために使用されます。
一般的な例:
- テキストを変更する(プルダウン)
- 色を変更する(color)
デフォルト
デフォルトの render_
関数は、表示テキストを getDisplayText_
関数の結果に設定します。getDisplayText_
関数は、テキストの最大長を考慮して切り捨てられた後、文字列にキャストされたフィールドの value_
プロパティを返します。
デフォルトのオンブロック表示を使用しており、デフォルトのテキスト動作がフィールドで機能する場合は、render_
をオーバーライドする必要はありません。
デフォルトのテキスト動作がフィールドで機能する場合でも、フィールドのブロック内表示に静的要素が追加されている場合は、デフォルトの render_
関数を呼び出すことができますが、フィールドのサイズを更新するには、オーバーライドする必要があります。
フィールドでデフォルトのテキスト動作が機能しない場合や、フィールドのブロック内表示に追加の動的要素がある場合は、render_
関数をカスタマイズする必要があります。
レンダリングのカスタマイズ
デフォルトのレンダリング動作がフィールドで機能しない場合は、カスタム レンダリング動作を定義する必要があります。カスタムの表示テキストの設定、画像要素の変更、背景色の更新など、さまざまなアクションが考えられます。
DOM 属性の変更はすべて有効です。ただし、次の 2 つの点に注意してください。
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;
}
}
シリアル化
シリアル化とは、フィールドの状態を保存して、後でワークスペースに再読み込みできるようにすることです。
ワークスペースの状態には常にフィールドの値が含まれますが、フィールドの UI の状態など、他の状態も含まれる場合があります。たとえば、フィールドがズーム可能な地図で、ユーザーが国を選択できる場合は、ズームレベルをシリアル化することもできます。
フィールドがシリアル化可能である場合は、SERIALIZABLE
プロパティを true
に設定する必要があります。
Blockly には、フィールド用に 2 つのシリアル化フックが用意されています。1 組のフックは新しい JSON シリアル化システムで動作し、もう 1 組のフックは古い 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
も受け取ります。これは、通常は別のシリアライザ(バッキング データモデルなど)によってシリアル化された状態を参照するフィールドで使用されます。このパラメータは、ブロックが逆シリアル化されたときに参照される状態が使用できなくなることを示します。そのため、フィールドはすべてのシリアル化を自身で行う必要があります。たとえば、個々のブロックがシリアル化される場合や、ブロックがコピー ペーストされる場合がこれに該当します。
一般的なユースケースは次のとおりです。
- 個々のブロックが、バッキング データモデルが存在しないワークスペースに読み込まれると、フィールドには新しいデータモデルを作成するために十分な情報が含まれます。
- ブロックをコピーして貼り付けると、フィールドは既存のデータモデルを参照するのではなく、常に新しいバッキング データモデルを作成します。
組み込み変数フィールドは、このフィールドの 1 つです。通常は、参照している変数の ID をシリアル化しますが、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
プロパティは、操作可能であることを示す UI をフィールドに表示するかどうかを決定します。デフォルトでは true
に設定されます。
SERIALIZABLE
プロパティは、フィールドをシリアル化するかどうかを決定します。デフォルトでは false
に設定されます。このプロパティが true
の場合、シリアル化関数と逆シリアル化関数を指定する必要があります(シリアル化を参照)。
カーソルのカスタマイズ
CURSOR
プロパティは、ユーザーがフィールドにカーソルを合わせたときに表示されるカーソルを決定します。有効な CSS カーソル文字列にする必要があります。デフォルトでは、.blocklyDraggable
で定義されたカーソル(グラブ カーソル)になります。