スキンUIを拡張する

基本スキンにメニューボタンを追加し、ダイアログで入力を受け取り、会話の上に領域を設置し、解除関数で後片付けする流れを1つの例で追っていきます。

最終更新 2026年10月17日

このページ

スキンUI API(room.skin)を使うと、基本スキン(basic-v2)の中にボタン・ダイアログ・領域を追加できます。このページでは「管理者がボタンを押して会話の上にバナーを表示する」機能を作りながら流れを説明します。関数とオプションの全体はスキンUI APIにあります。

原則

  • 位置・順序・重なりはスキンが決め、自分のコードはスキンから渡された空の領域(host)の中だけを扱います。スキン内部のDOMを探して書き換えないでください。
  • 登録時に渡す id は、種類ごとに重複してはいけません。重複するとエラーになります。
  • 登録関数は解除関数を返します。解除関数は何度呼んでも安全です。
  • このAPIは基本スキンにだけあります。レガシースキン(basic)はレガシースキンAPIを使います。

1. 準備のタイミングをつかむ

スキンUI APIはチャットルームの画面の準備ができてから使えるので、after.create の中で登録します。事前登録コードは非同期読み込みと複数のチャットルームを参照してください。

U.chat('my-room').on('after.create', function (room) {
	var ui = room.skin;
	if (!ui.menu) return; // 基本スキンでなければ中止
	// ここで登録
});

ui.menu.add は入力欄のツールバーにボタンを追加します。condition が false を返すと、ボタンは非表示になります。

var removeMenu = ui.menu.add({
	id: 'my-banner',
	label: 'バナー',
	title: '会話の上にバナーを表示',
	condition: function (ctx) { return ctx.room.my.auth >= 2; }, // モデレーター以上のみ
	onSelect: function (ctx) { askBanner(); }
});

ニックネームを押したときに出るメニューには、ui.userMenu.add で同じ形の項目を追加します。このとき ctx.target が押した人のユーザーオブジェクトです。メニューを非表示にしても権限の検査はサーバーが行うので、非表示は画面の整理のためと考えてください。

3. ダイアログで入力を受け取る

ui.dialog.* は open を除いてすべてPromiseを返します。prompt は確定すると入力した文字列、キャンセルすると null を返します。

function askBanner() {
	ui.dialog.prompt('バナー', '表示する文言を入力してください。').then(function (text) {
		if (text) showBanner(text);
	});
}

alert、confirm(キャンセルすると false)、password、select も同じ方式です。途中で閉じる必要がある場合は、ui.dialog.open で close を持つハンドルを受け取ります。→ スキンUI API

4. 領域を設置する

ui.mount.* に mount(host, ctx) 関数を渡すと、スキンがその場所に空の領域を作り、host として渡します。mount が関数を返すと、領域がなくなるときに1回呼ばれます。

var removeBanner = null;

function showBanner(text) {
	if (removeBanner) removeBanner(); // 前のバナーを後片付け
	removeBanner = ui.mount.messageList({
		id: 'my-banner-area',
		position: 'before', // 会話領域の上
		mount: function (host, ctx) {
			var doc = host.ownerDocument;
			var box = doc.createElement('div');
			box.style.padding = '8px 12px';
			box.textContent = text;
			var close = doc.createElement('button');
			close.type = 'button';
			close.textContent = '閉じる';
			close.addEventListener('click', function () { ctx.close(); });
			box.appendChild(close);
			host.appendChild(box);
			return function () { host.replaceChildren(); };
		}
	});
}
  • host はチャットルームのiframe内の要素です。新しい要素は host.ownerDocument で作ります。
  • 文字は textContent で入れます。ユーザーの入力を innerHTML で入れないでください。
  • ctx.close() はこの登録を解除します。ctx.signal は後片付けの直前にabortされるので、fetch やイベントリスナーの後片付けに使えます。

messageList のほかに、header、middle、userList、footer、wrap、composer、headerTitle、toolbar の場所と、レイアウトを押し広げずに重ねて表示する overlay があります。場所ごとの position の値と overlay の layer・scope はスキンUI APIを参照してください。

5. 解除関数で後片付けする

登録したものは、受け取った解除関数で削除します。チャットルームが閉じたり作り直されたりすると、スキンがすべての登録を後片付けするので、自分で後片付けするのは機能をオフにしたり変更したりするときです。

function disableBanner() {
	if (removeBanner) removeBanner();
	removeMenu();
}

すでに解除したものをもう一度解除してもエラーにはなりません。同じ id でもう一度登録するには、先に解除する必要があります。

room.reload() やタグの再挿入でチャットルームが作り直されると、チャットルームIDで登録したイベントも一緒に削除され、after.create が再び来なくなります。作り直されたときにも登録されるようにするには、U.chat('*') で登録して room.id を確認してください。→ 非同期読み込みと複数のチャットルーム

コールバックは同期関数で

メニューの condition、describe、resolverの resolve、message.extend の match は、スキンが画面を描画している途中で呼ばれます。これらのコールバックはすぐに値を返す同期関数でなければなりません。

  • condition、describe、resolve、match がPromiseを返すとエラーとして扱われ、その結果は無視されます(メニュー項目であれば表示されません)。async function で書かないでください。
  • サーバーから値を受け取る必要がある場合は、あらかじめ受け取って変数に入れておき、値が変わったら ui.menu.resolver.refresh() のように再計算を要求します。
  • onSelect の戻り値は使いません。ボタンを押したあとの非同期処理は、onSelect の中で開始するだけで構いません(上の askBanner のように)。

コード全体

<script>U=window.U=window.U||{},U.events=U.events||[],U.chat=function(n){return{on:function(e,t){U.events.push([n,e,t])},off:function(e){for(var t=U.events.length;t>0;)U.events[--t][0]==n&&U.events[t][1]==e&&U.events.splice(t,1)}}};</script>
<script async src="https://client.uchat.io/uchat.js"></script>
<u-chat room="my-room" style="display:inline-block; width:100%; max-width:500px; height:500px;"></u-chat>
<script>
U.chat('my-room').on('after.create', function (room) {
	var ui = room.skin;
	if (!ui.menu) return;
	var removeBanner = null;

	function showBanner(text) {
		if (removeBanner) removeBanner();
		removeBanner = ui.mount.messageList({
			id: 'my-banner-area',
			position: 'before',
			mount: function (host, ctx) {
				var doc = host.ownerDocument;
				var box = doc.createElement('div');
				box.style.padding = '8px 12px';
				box.textContent = text;
				var close = doc.createElement('button');
				close.type = 'button';
				close.textContent = '閉じる';
				close.addEventListener('click', function () { ctx.close(); });
				box.appendChild(close);
				host.appendChild(box);
				return function () { host.replaceChildren(); };
			}
		});
	}

	ui.menu.add({
		id: 'my-banner',
		label: 'バナー',
		title: '会話の上にバナーを表示',
		condition: function (ctx) { return ctx.room.my.auth >= 2; },
		onSelect: function () {
			ui.dialog.prompt('バナー', '表示する文言を入力してください。').then(function (text) {
				if (text) showBanner(text);
			});
		}
	});
});
</script>

バナーはボタンを押した人の画面にだけ表示されます。全員に表示するには、チャンネルで文言を送り、受け取る側で showBanner を呼んでください。→ リアルタイムデータの送受信

商号 UCHAT · 代表者 김경욱 · 事業者登録番号 141-05-32832
所在地 경기도 파주시 와석순환로 61 · 通信販売業届出番号 2013-경기파주-4485호
代表電話 010-8677-8633 · メール support@uchat.io

ⓒ 2013–2026 UCHAT. All rights reserved.