# スキンUIを拡張する

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

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

## 原則 {#principles}

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

## 1. 準備のタイミングをつかむ {#ready}

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

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

## 2. メニューボタンを追加する {#menu}

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

```js
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. ダイアログで入力を受け取る {#dialog}

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

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

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

## 4. 領域を設置する {#mount}

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

```js
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](/ja/doc/3.4)を参照してください。

## 5. 解除関数で後片付けする {#cleanup}

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

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

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

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

## コールバックは同期関数で {#sync-callbacks}

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

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

## コード全体 {#full-code}

```html
<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` を呼んでください。→ [リアルタイムデータの送受信](/ja/doc/2.4)
