はじめに
現在制作中のエディタアプリではMonaco Editorを使っているが、小規模な機能をうまくUIに表現したいときに、わざわざMonaco Editor外のHTML要素にダイアログを定義してイベント設定して・・・がなかなか煩わしいと思う時がある。
できればMonaco Editorに近い場所で定義して管理できればいいのに、と思っていたら、面白い機能を発見したので紹介したい。
それが IStandaloneCodeEditorインターフェースにある addOverlayWidget だ。
オーバーレイウィジェットとは?
VSCodeやAntigravitiyのエディタの親である Monaco Editor。自作アプリで使えばそれらの有名エディタとほぼ同等の超高性能なエディタが実現できる。
ただそれだけに機能は豊富で、意外と細かく知られていないものもあるようだ。
オーバーレイウィジェットもその一つだろう。
画面に表示するとこうなる。
エディタ部の上部中央・右上・右下にある青いポップアップがそれだ。
こういうの、VSCodeやAntigravityでご覧になったことあるだろうか?
あれらもそれ以外の機能が豊富すぎて、UI的にMonaco Editorをフル活用できていないのではないかと思うフシもある。
このオーバーレイウィジェット、中は素のHTML要素だ。普通にDOM操作をして定義したものを表示する。
それが意外と簡単だったので、方法を紹介しよう。
オーバーレイウィジェットクラスを作る
メインのエディタは、IStandaloneCodeEditorだ。
それにいきなりHTML要素を追加する前に、オーバーレイウィジェットクラスを定義する必要がある。
// シンプルなオーバーレイウィジェットクラス
class SimpleOverlayWidget {
constructor(id, content, position) {
this.id = id;
this.content = content;
this.position = position;
this.domNode = null;
}
getId() {
return this.id;
}
getDomNode() {
if (!this.domNode) {
this.domNode = document.createElement('div');
this.domNode.style.background = 'rgba(33, 150, 243, 0.9)';
this.domNode.style.color = 'white';
this.domNode.style.padding = '10px 15px';
this.domNode.style.borderRadius = '5px';
this.domNode.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
this.domNode.style.fontSize = '14px';
this.domNode.innerHTML = this.content;
}
return this.domNode;
}
getPosition() {
return {
preference: this.position
};
}
}
Typescriptの定義上では IOverlayWidgetと呼ばれるインターフェースだ。
IOverlayWidget - Monaco Editor
オーバーライドして(Javascriptでは普通に定義)定義するには次のメソッドは必須だ。
- getId()
- getDomNode()
- gertPosition()
IStandaloneCodeEditorから諸々参照されるのに this.id や preferenceは欠かせない。
ウィジェットの位置を決める
preferenceにセットしている this.position は、公式ドキュメントでは IOverlayWidgetPositionインターフェースまたはOverlayWidgetPositionPreference列挙型となっている。
IOverlayWidgetPosition
IOverlayWidgetPositionCoordinates
OverlayWidgetPositionPreference
実際のコードでは getPosition()メソッドで正式な形式にて返している。
getPosition() {
return {
preference: this.position
};
}
OverlayWidgetPositionPreferenceの場合、monaco.editor から次のように取得して渡すこともできるが、実際の値で渡すことも可能だ。
//monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER or 0
//monaco.editor.OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER or 1
//monaco.editor.OverlayWidgetPositionPreference.TOP_CENTER or 2
TOP_RIGHT_CORNERはエディタの右上に、TOP_CENTERはエディタの上部中央に、BOTTOM_RIGHT_CORNERは右下にそれぞれ表示する意味となる。
自由な位置に設置することも可能だ。その場合、次のようにする。
constructor(){
...
this.position = {top:50, left:250};
...
}
getPosition() {
return {
preference: this.position
};
}
つまり、IOverlayWidgetPositionCoordinatesである、{top: Number, left: Number} の形式で渡せば良い。
すると、上のスクリーンショットのように、「自由位置」というウィジェットが設置される。
内容を設定する
では次に、ウィジェットに表示するHTML要素を定義する。
これは getDomNode() メソッド内で定義していく。
DOM操作の方法はなんでもよい。 が、一つ大事なポイントがある。
それは、 Monaco Editorはどうやら内部的にはデフォルトでマウス操作を受け付けないようになっているらしく、ウィジェット内でボタンを設置してそれを押したらなにか実行する、というのがそのままイベントを定義しても、 押せない のだ。
そのため、CSSの定義で、次のものを必ず定義する必要がある。
ボタンを設置するウィジェットのコードサンプルはこちらだ。
class ActionWidget {
constructor(editor) {
this.id = 'action.widget';
this.domNode = null;
this.editor = editor;
}
getId() {
return this.id;
}
getDomNode() {
if (!this.domNode) {
this.domNode = document.createElement('div');
this.domNode.style.cssText = `
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
this.domNode.innerHTML = `
<div style="margin-bottom: 10px; font-weight: bold;">
コード整形
</div>
<button class="format-btn" style="
background: white;
color: #667eea;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
margin-right: 8px;
">整形する</button>
<button class="close-btn" style="
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">閉じる</button>
`;
// イベントリスナーをDOMに直接追加
const formatBtn = this.domNode.querySelector('.format-btn');
const closeBtn = this.domNode.querySelector('.close-btn');
formatBtn.addEventListener('click', (e) => {
e.stopPropagation(); // イベントの伝播を止める
console.log('整形ボタンがクリックされました');
this.editor.getAction('editor.action.formatDocument').run();
});
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
console.log('閉じるボタンがクリックされました');
this.editor.removeOverlayWidget(this);
});
}
return this.domNode;
}
getPosition() {
return {
preference: monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
};
}
}
ボタンがあるのでイベントを定義した。これをこのままの形で流用して改良すれば、すぐに面白いウィジェットが作れるようになるはずだ。
オーバーレイウィジェットを呼び出す
オーバーレイウィジェットクラスの定義が終わったら、実際にエディタから呼び出してみよう。
なお、以下の例では IStandaloneCodeEditorを editor で定義済みとする。
// 右上隅のウィジェット
const topRightWidget = new SimpleOverlayWidget(
'widget.top.right',
'📍 右上',
this.monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER // 0
);
editor.addOverlayWidget(topRightWidget);
// 右下隅のウィジェット
const bottomRightWidget = new SimpleOverlayWidget(
'widget.bottom.right',
'📍 右下',
this.monaco.editor.OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER // 1
);
editor.addOverlayWidget(bottomRightWidget);
// 上部中央のウィジェット
const topCenterWidget = new SimpleOverlayWidget(
'widget.top.center',
'📍 上部中央',
this.monaco.editor.OverlayWidgetPositionPreference.TOP_CENTER // 2
);
editor.addOverlayWidget(topCenterWidget);
// カスタム位置のウィジェット
const customPosWidget = new SimpleOverlayWidget2(
'widget.custompos.center',
'📍 自由位置',
{top: 50, left: 250}
);
editor.addOverlayWidget(customPosWidget);
const actionWidget = new ActionWidget(editor);
editor.addOverlayWidget(actionWidget);
使用上は必ずクラス化するので、自然と管理はしやすくなるだろう。使いまわしも楽だ。
オーバーレイウィジェットを削除する
使い終わったら削除したい。そういうときには removeOverlayWidget()メソッドを使う。
上記例では、オーバーレイウィジェット内のボタンを押したら閉じるようにしたかったので、クラスには IStandaloneCodeEditorを受け渡して使っている。
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
console.log('閉じるボタンがクリックされました');
this.editor.removeOverlayWidget(this);
});
そのため、上記のように親のエディタを呼び出して削除させれば、Monaco Editor外のUIで削除ボタンを用意する手間も省ける。
終わりに
以上、Monaco Editorのオーバーレイウィジェットを見てきた。
使い道としては色々考えられるだろう。
- 通知用のポップアップとして
- 細かい機能を呼び出すボタンのウィンドウとして
実際のアプリ作りではエディタ部の外に色々なUIや機能を定義しなければいけない。管理は煩雑になるだろう。
そこで、エディタに関する機能のUIだけはせめてMonaco Editor付近にまとめたい、というときに、このオーバーレイウィジェットが活用できるだろう。
オーバレイつまりテキストの上に表示されるので、実際のテキスト処理の邪魔になることもない。
(実際の見た目的には邪魔になるかもしれないが)
Monaco Editorをお使いの皆様にはぜひこのかゆいところに手が届くオーバーレイウィジェットを活用していただければ幸いだ。
