新しいフィールド タイプを作成する前に、フィールドをカスタマイズする他の方法のいずれかがニーズに合っているかどうかを検討してください。アプリで新しい値の型を保存する必要がある場合や、既存の値の型に対して新しい UI を作成したい場合は、新しいフィールドの型を作成する必要があるでしょう。
新しいフィールドを作成するには、次の操作を行います。
- コンストラクタを実装する。
- JSON キーを登録して
fromJson
を実装します。 - オンブロック UI とイベント リスナーの初期化を処理します。
- イベント リスナーの破棄を処理します(UI の破棄は自動的に処理されます)。
- 値の処理を実装する。
- アクセシビリティのために、フィールドの値のテキスト表現を追加します。
- 次のような追加機能を追加します。
- フィールドのその他の側面を構成します。たとえば、次のものがあります。
このセクションは、フィールドの構造の内容を読み、理解していることを前提としています。
カスタム フィールドの例については、カスタム フィールドのデモをご覧ください。
コンストラクタの実装
フィールドのコンストラクタは、フィールドの初期値を設定し、必要に応じてローカル バリデータを設定します。カスタム フィールドのコンストラクタは、ソースブロックが JSON で定義されているか JavaScript で定義されているかにかかわらず、ソースブロックの初期化中に呼び出されます。そのため、カスタム フィールドは構築中にソースブロックにアクセスできません。
次のコードサンプルでは、GenericField
という名前のカスタム フィールドを作成します。
class GenericField extends Blockly.Field {
constructor(value, validator) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
メソッド シグネチャ
フィールド コンストラクタは通常、値とローカル バリデータを受け取ります。値は省略可能です。値を渡さない場合(またはクラス検証に失敗する値を渡した場合)は、スーパークラスのデフォルト値が使用されます。デフォルトの Field
クラスの場合、この値は null
です。デフォルト値が望ましくない場合は、適切な値を渡してください。validator パラメータは編集可能なフィールドにのみ存在し、通常は省略可能としてマークされます。バリデーターの詳細については、バリデーターのドキュメントをご覧ください。
構造
コンストラクタ内のロジックは、次のフローに従う必要があります。
- 継承されたスーパー コンストラクタ(すべてのカスタム フィールドは
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
関数を使用します。このイベント バインディング方法では、ドラッグ中にセカンダリ タッチが除外されます。進行中のドラッグの途中でもハンドラを実行する場合は、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
に設定し、on-block 表示の 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 を表示するには、DropDownDiv と WidgetDiv という 2 つの特別な div のいずれかで HTML をラップします。これらの div は、Blockly の他の UI の上にフローティング表示されます。
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
の例に示すように、破棄関数を渡すときは bind 関数を使用することをおすすめします。
→ エディタの破棄に固有ではない破棄については、破棄をご覧ください。
オンブロック ディスプレイの更新
render_
関数は、フィールドのブロック内表示を内部値と一致するように更新するために使用されます。
一般的な例:
- テキストを変更する(プルダウン)
- 色を変更する(色)
デフォルト
デフォルトの 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
の場合は、シリアル化関数と逆シリアル化関数を指定する必要がある場合があります(シリアル化を参照)。
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 を使用して設定します。