まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2020/10

(注 このブログを書いた時点では 2021/02/12 だった更新期限は 2021/05/25 に変更されました)


IBM Watson サービスのエンドポイント URL が更新され、2021年2月12日5月26日に旧URLが廃止される予定です:
IBM Watsonサービスのネットワーク分離機能拡張のためのIAMの更新


IBM Watson の各種サービス API を(以前から)使っていて、そのエンドポイント URL のホスト部分が *.watsonplatform.net というパターンになっている場合にアプリケーションが正しく動作しなくなるなどの影響を受けます。その場合は月の旧URL廃止前に後述の作業を行って、新しい URL に更新する必要があります。

以下、Watson NLC(Natural Language Classifier) を例に対応手順を含めて個人でまとめたので紹介します。NLC 以外のサービスでも概ね同様ですので参考にしてください。また詳しくは後述のリンク先も参照ください。


【現在使っている Watson API のエンドポイント URL を確認】
まず今回の作業は例えば Watson Assistant の画面を使って作業しているだけなど、外部アプリケーションから API を使って呼び出したりしていない場合は関係ありません。apiKey を指定してプログラミングで Watson API を外部から呼び出す形で利用しているケースが対象となります。

現在 Watson API を使ってアプリケーションを動かしている場合、まずはその API のエンドポイントが旧 URL を使っているのか新 URL を使っているのかを確認します(新 URL であれば後述の作業は不要です)。

例えば Watson NLC を使ったアプリケーションであれば、IBM Cloud にログインし、リソース画面のサービス一覧から利用している該当サービス(Watson NLC サービス)を選択します:
2020103001


選択したサービスの概要が表示される画面内に API Key と URL が表示されています(※)。この URL という部分に着目してください:
2020103002


上図の例では
  https://gateway-tok.watsonplatform.net/natural-language-classifier/api
と表示されている部分です。ここがこのように watsonplatform.net という文字を含んでいる場合は旧 URL を利用しています。一方、ここが api.*****.*****.watson.cloud.ibm.com というパターンになっている場合は新 URL を使っています。

※API Key と URL は「サービス資格情報」メニューからも確認できます。

ここで新 URL を使っていることが確認できた場合は後述の作業は不要です。旧 URL を使っている場合は続けて対処が必要です。


【変更先の Watson API 新エンドポイント URL を確認】
上述の作業で旧 URL を使っているアプリケーションは利用エンドポイント URL を新 URL に変更する必要があります。

まず新しい URL を確認するために新しいサービス資格情報を作成する必要があります。上述の作業に続けて画面左のメニューから「サービス資格情報(Service credentials)」を選択し、新しく資格情報を追加して作成します(その際に現在使っている資格情報と同じロールを指定して作成します):
2020103003


追加された資格情報の名前の左側にある小さな矢印をクリックして展開します:
2020103004


Watson サービスの種類によっても異なりますが、概ね以下のような JSON フォーマットの情報が含まれた内容になっています(一部 ***** でマスクしています):
{
  "apikey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "iam_apikey_description": "Auto-generated for key *********************",
  "iam_apikey_name": "Service credentials-1",
  "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",
  "iam_serviceid_crn": "crn:v1:bluemix:public:iam-identity::********************::serviceid:ServiceId-*************************",
  "url": "https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/*************"
}

この JSON の中の url の値(上図では https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/************* )が新 URL です(実際の文字列パターンは使っている IBM Watson サービスの種類やロケーションによって異なります。また最後の ***** でマスクされている部分はインスタンス ID という個別の文字列となります)。アプリケーション内の旧 URL が使われている部分をこの新 URL に変更する必要があります。 また同時に旧アプリケーションで使われている apiKey も新しく作成したもの(上図の apikey で表示されている値 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX )に書き換える必要があります。


【変更作業】 
ここまでの作業で変更が必要な箇所と、変更後の値がわかりました。アプリケーションのソースコードを編集し、旧 URL (上述例では https://gateway-tok.watsonplatform.net/natural-language-classifier/api)が使われている部分を新 URL (上述例では https://api.jp-tok.natural-language-classifier.watson.cloud.ibm.com/instances/*************)に、また古い apiKey が使われている部分を新しい apiKey の値(上述例では XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX)に、それぞれ書き直して保存し、新しいコードで動作確認をしてください。

なお、NLC のように学習が必要な API も再学習の必要はありません。学習済みのデータへそのまま問い合わせが利用できるはずです。


以上、個人でまとめたものですが、背景や詳細な情報はソリューションブログや IBM Cloud Document に記載されています。以下情報も参考にしてください:
IBM Watsonサービスのネットワーク分離機能拡張のためのIAMの更新
Updating endpoint URLs from watsonplatform.net(英語)

Twitter や facebook ではそれなりの頻度で触れている話題なのですが、9月末にキングジムのポメラ DM200 を購入しました:
1 開封


小型 PC(といっていいのか?)の中では抜群のキーボード操作性を持ち、テキスト入力作業中心に使う人からの人気が高い機種です。ただ自分の場合は購入当初から普通にテキスト入力機として使うつもりはなく、Linux(Debian) 化できることを理解して、Linux 化して使うつもりで購入しました。DM200 の Linux 化手順や Linux 化直後の各種ツールの導入については以下の2つのサイトが有名で、実際に自分も大変お世話になりました。先人たちの努力で Linux 化は非常に簡単でした。感謝を意を表すと同時に、DM200 の Linux 化についてはこちらを参照いただけると一連の手順が非常にわかりやすくまとまっています:
pomera DM200 の Linux 化のメモ
KING JIM ポメラDM200でEmacs、Vim、Ruby、Pythonが動くなんて素敵すぎる!


実際に自分も日本語環境を含めて X Window 対応を行い、普段使いの PC でも使っている開発者エディタである VSCode を導入したり、オフィススイートである LibreOffice を導入したりしてみました。DM200 はメモリが 0.5GB しかないこともあり、正直なところ VSCode や LibreOffice はちと重すぎで、軽快な作業というわけにはいかないアプリでした:
3 vscode


一方で X Window を使わなければ充分に軽快なテキスト編集マシンとして使えそうだという感触は持っています。ただ X Window 自体が悪というわけではなく、「重さを感じるかどうかは X Window 上で動かすアプリケーション次第」だと思っています。画像編集アプリとか、FireFox のような重めのアプリだと厳しそうですが、軽いアプリなら X Window でもまあ苦にはならないな、という印象です:
6 scratch


この DM200 の Linux 化環境、なんといっても(アーキテクチャが同じなので)「ラズベリーパイ向けの資産の多くが使える」のです。重い X Window アプリだから使わない、というのはあまりにもったいないと思いました。今の所の自分の感覚では
・テキスト入力
・コーディングや動作確認を含めたプログラミング
・SSH などによるリモート操作端末
としての利用であれば、最高に使いやすい物理キーボードと合わせて快適に使える環境だと思っています。

そんな自分が現時点で DM200 に導入してよく使っているアプリケーションをいくつか紹介させていただきます。なお特にリンクをつけていないものは上述のページの手順内で紹介されているものか、標準作業で導入されるものです。


【日本語テキスト入力用】
- uim-fep
- IBus
- vi/vim

日本語FEPとテキストエディタという、本来のポメラが得意とする使い方を Linux でも使う、というものです。この使い方に限っては Linux 環境ではなく、普通のポメラとして使ったほうが(便利な各種辞書なども併用できるなど)便利であるとは思います。 ただ入力したテキストをインターネットを使って外部とやりとりする段になった時に Linux 環境であったほうが断然便利で、自分はそのように使うことが多いので、困らない限りは Linux 環境で入力も行っています。

また当初は screen や tmux といったターミナルマルチプレクサーも使っていましたが、ALT + Fn キーで端末を切り替えたり、X Window であればターミナルを複数起動したりすることで同様の操作ができるのであまり使わなくなりました。ちなみにターミナルが複数必要な理由ですが、DM200 の Linux で無線LANやBluetooth(外部マウス)の有効化/無効化時に root 権限での作業が必要※になり、切り替えが面倒だったのでターミナルごとわけて使っている、という背景があります。

※普通の PC のように両方ともはじめから有効にしておけばいい、という声もあると思いますが、この2つはどうも互いに干渉するようで、なるべくなら両方有効にはしたくないという事情と、無線 LAN については使う環境で接続先を切り替えて使うことになるのですが、その切替にコマンドライン操作が必要という事情があるのでした。


【プログラミング用】
- Node.js
- Uzbl
- ngrok
- cf cli

ある意味「このためにポメラ DM200 を購入した」という使いみちでもあります。まず自分の普段のメイン開発言語が Node.js なため、Node.js & npm を導入しています。またウェブアプリケーション開発時の動作確認用にウェブブラウザが必要なのですが、FireFox が重いので Uzbl という軽量ブラウザを使っています。軽量で便利な一方、JavaScript の互換性が充分ではないので、多少苦労することもあります。

ngrok はローカルで開発したアプリケーションを一時的にインターネットに公開する IP フォワードツールです。一時的とはいえ、外部の人にも使ってもらえるようになるのがとても便利。

で、ある程度動くようになったらサービスとして公開するのですが、その際に安価な IBM Cloud の Cloud Foundry 環境で公開することが多く、その時に cf cli ツールを使います。

その他 git なども使ってますが、ほぼ最初から導入済みの環境なので割愛します。

おまけでプログラミング環境として Node-RED や Scratch も導入できました($ sudo npm install -g nodered や $ sudo apt-get install scratch で導入できます)。ただ自分自身がこの環境をあまり使うわけではないのと、Node-RED を localhost で動かす際のブラウザが Uzbl だとちゃんと動かない箇所があったりしてイマイチな感じ(FireFox とか使えばいいんでしょうけど重くて・・):
5 nodered


【リモート操作端末用】
- SSH
- OpenVPN クライアント
- VNC Viewer
- x3270

DM200 購入時点ではあまり想定してなかった使いみちでしたが、使い勝手のいいキーボードや、DM200 の(Linux としての)非力さを補う使い方という相性の良さもあって、使いみちの中心がこのリモート操作端末に移行しつつあります。

リモート操作のほとんどは SSH で済ませています。ただその際に VPN 接続が必要な場合もあり、その場合は OpenVPN クライアントで接続しています(普通に $ sudo apt-get install openvpn で導入できます):

(↓ 右のターミナルで OpenVPN して、左のターミナルで SSH 接続してます)
8 ssh


また VNC Viewer を使ってデスクトップ環境へリモート接続も可能です。このあたりはラズベリーパイと同じアーキテクチャであることで、多くのツールが DM200 でも使えるメリットを生かしています:
s4QNvdc


ほとんどの人は不要だと思いますが、3270 と呼ばれるホスト端末のエミュレーターを使うことがあります。自分は上述の x3270 のフリーソフトを使っています。ソースコードからビルドする必要がありますが、普通に $ ./configure && make して、 $ sudo make install で導入できます。



といった具合で使っています。もともとは出先でのプログラミングマシンとして購入したのですが、クラウドなどの各種サーバーにログインして操作するための環境としての便利さにも気づき、今は利用用途が半々くらいになっています。なんといってもキーボードで不便さを感じずに使えることがストレス無く利用できて、素晴らしく便利です。


サブジェクトが少しわかりにくいと思ったので最初にやりたいことを補足しておきます。

ウェブサービスを公開する際に Basic 認証と呼ばれる認証機能を有効にすることがあります。アクセス時にユーザーIDとパスワードが聞かれ、正しい組み合わせを入力しないと先に進めなくなる、というものです。会員制サービスや、正式公開前のサービスを限られた人だけで使いたい場合、グーグル等の検索エンジンクローラーに見つからない状態で運用したい場合などによく使われます:
thumb_basic


今回やりたかったのは、この Basic 認証を例えば以下の条件で実現するような Node.js アプリケーションを作ることです:
・パス /hello 以下にアクセスした際に Basic 認証が必要
・/hello にアクセスするには URL パラメータ id が必要(つまり GET /hello だけではエラーとなり、GET /hello?id=XXX というフォーマットでアクセスする必要がある)
/hello?id=XXX の時と /hello?id=YYY の時とでは Basic 認証のユーザーIDやパスワードが異なる


最後のが今回の肝となる条件です。パラメータ id の値ごとに Basic 認証のユーザーIDやパスワードが変わり(データベース等に格納されているものを id をキーに取り出して比較するイメージ)、これを Node.js + Express 環境でどのように実現するか、というのが挑戦内容です。


やりたいことが明確になったところで、改めて Node.js + Express 環境で Basic 認証をかける方法をググってみると、basic-auth-connect モジュールを使う方法がメジャーな方法の1つとして見つかります。これは簡単にいうと以下のような感じで Basic 認証をかけるものことができるものです:
var express = require( 'express' ),
    basicAuth = require( 'basic-auth-connect' ),
    app = express();

app.all( '/hello*', basicAuth( function( user, pass ){
  return ( 'username' === user && 'password' === pass );
}));

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :

GET /hello リクエストに対しては単に { status: true } という JSON を返すだけの定義がされていますが、その前に Basic 認証を有効にする部分が記述されています。この例では(/hello に何らかの URL パラメータが付属する場合も含めた) /hello* というパスに GET リクエストが行われた場合に Basic 認証が必要になり、ユーザーID 'username' 、パスワード 'password' が入力された場合のみ true(認証成功)で実際の GET /hello の処理が行われ、それ以外の場合は false(認証失敗)という扱いとなって再度入力が求められたり、何度か間違えると認証エラー扱いとなる、というものです。とても便利で、よく使っています。


さて、今回は上述の条件で Basic 認証を有効にする必要があり、少し異なる処理が必要です。正しいユーザーIDとパスワードは URL パラメータ id によって変わるのですが、この URL パラメータは req オブジェクから取り出す必要があり、今の形のままでは(認証判断時に req オブジェクトが取得できないので)取得が難しそうです。自分もこの basic-auth-connect モジュールを使う前提で実装を考えていたので詰まってしまいました。。

結論としては basic-auth-connect モジュールを使うことを諦め、自分で認証判断してエラー時にエラーコードを返す、という地味な処理に切り替えて実装できました:
var express = require( 'express' ),
    //basicAuth = require( 'basic-auth-connect' ),  //. basic-auth-connect は使わない
    app = express();

//. パラメータ id 毎に必要なユーザーIDとパスワード(本当はデータベース等から取得するイメージ)
var db = {
  "000" : { user: 'a', pass: 'x' },
  "001" : { user: 'b', pass: 'y' },
  "002" : { user: 'c', pass: 'z' }
};

/* URL パラメータ毎に認証情報を変えたい */
app.use( function( req, res, next ){
  //. hello* へのリクエスト時かどうかを判断
  var originalUrl = req.originalUrl;
  if( originalUrl.startsWith( '/hello' ) ){
    //. URL パラメータ ID を取り出す
    var id = req.query.id;
    //. 指定された ID のユーザー ID とパスワードが存在しているかどうかを調べる
    if( db[id] ){
      //. ヘッダから入力されたユーザーIDとパスワードを取り出す
      var b64auth = ( req.headers.authorization || '' ).split( ' ' )[1] || '';
      var [ user, pass ] = Buffer.from( b64auth, 'base64' ).toString().split( ':' );

      //. 入力内容が正しい場合のみ next() を返して本来の処理へ
      if( db[id].user == user && db[id].pass == pass ){
        return next();
      }else{
        //. 入力内容が間違っていた場合は認証エラー扱いとする
        res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
        res.status(401).send( 'Authentication required.' );
      }
    }else{
      //. 指定された ID が存在していなかった場合も認証エラー扱いとする
      res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
      res.status(401).send( 'Authentication required.' );
    }
  }else{
    return next();
  }
});

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :


赤字部分が今回作成した処理です。req オブジェクトからリクエスト先のパスや Basic 認証で指定された情報を取り出して正しい情報かどうかを判断し、正しい場合は本来の処理へ、間違っていた場合は HTTP の認証エラー結果を返すような内容を記述しています。basic-auth-connect モジュールを使うとこのあたりの細かな記述をする必要がなかったのですが、自分で判断する場合はこのあたりも自分の責任範囲で用意する必要があります。

上述の例では URL パラメータ id は "000", "001", "002" のいずれかである必要があり、それぞれの場合の Basic 認証情報(ユーザーID : パスワード)はそれぞれ "a":"x", "b":"y", "c":"z" としています。この正しい組み合わせが指定された場合のみ GET /hello が実行されて結果が返される、という処理が実行されるようになります。


(参照)
https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4


ウェブ画面でタイル状のブロックを縦方向にいくつか並べて表示する際に「1つ上のタイルにぶらさがる」感じを出したいことがままあります。

例えば並べるブロックの数が不定で、画面を一度表示した後からブロックを追加したくなったりする際に「1つ前のブロックの続きである」ことを明確にするため、1つ前のブロックにぶら下がる感じで表現したい、というケースです。更に具体的にはブロックチェーンのブロックを並べるようなケースなどで、こんな風に:
2020101300


この「ぶらさがってる」感じはブロックの左右に紐状(上図ではチェーン)の画像を表示することで目的に近いものが表現できます。これを CSS で実現するにはどうすればよいか、というのが今回のテーマです。なお今回はブロック部分を Bootstrap4 の card クラスを使って実現していますが、他の多くの環境でも同様に実現できる、はず。

まず紐となる画像を用意しました。今回用意したのは↓のチェーン状の画像で、サイズは 23x50 ピクセルです(画像の高さである 50 を前提に以下の CSS を記述します):
chain


また HTML 的には以下のようにしたいと思っています。今回は Bootstrap4 の card クラスが付与された <div> を複数並べるのですが、2つ目以降の <div> には mycard クラスも付与し、この mycard クラスの CSS で2つ目以降のブロックは1つ前のブロックにぶら下がっているように表現できることを目指します:
<div class="container" style="padding:80px 0;">
  <div class="card text-white bg-primary border-danger">
    <div class="card-body">
      <h4 class="card-title">Title #0</h4>
      <p class="card-text">Text for #0</p>
    </div>
  </div>

  <div class="card text-white bg-primary border-danger mycard">
    <div class="card-body">
      <h4 class="card-title">Title #1</h4>
      <p class="card-text">Text for #1</p>
    </div>
  </div>

  <div class="card text-white bg-primary border-danger mycard">
    <div class="card-body">
      <h4 class="card-title">Title #2</h4>
      <p class="card-text">Text for #2</p>
    </div>
  </div>


    :
</div>

で、この mycard クラスをどのように定義すればよいか? が今回の課題なのですが、こんな感じで実現してみました:
<style type="text/css">
.mycard{
  margin: 55px 0;
}
.mycard::before{
  content: url(./chain.png);
  margin: 0px;
  position: absolute;
  top: -52px;
  left: 10px;
}
.mycard::after{
  content: url(./chain.png);
  margin: 0px;
  position: absolute;
  top: -52px;
  right: 10px;
}
</style>

まず mycard クラスを付与するブロックは画像(./chain.png)の高さぶんだけの余幅を持って表示される必要があります(その余幅に画像が入ります)。今回、画像の高さが 50px なので、少し余裕をもたせて margin: 55px 0; というスタイルを指定しています(つまり実際には 55px ぶん下に表示されることになります)。

この空いた余幅に画像を含めたいのですが、今回は上述のように左端と右端の両方に画像を入れ、2箇所で引っかかっているようにぶら下げようとしています。そのため ::before 要素::after 要素両方を使って mycard クラスの左端と右端に画像を差し込みます。

まず ::before 要素ですが、content に目的の画像を含めます(content: url( './chain.png' );)。更に画像位置をブロックの左端に設定するため、position: absolute; top: -52px; left:10px; を指定しています。これによって 50px の高さをもつ画像を 52px ぶん上から表示させて空間の隙間に収まるようにしています(また左端ギリギリではなく、10px ぶんのマージンを取っています)。

::after 要素にも同様の設定を行いますが、こちらは右端に表示したいため right: 10px; と、ブロックの右側に 10px ぶんのマージンができるような位置を指定しています。これによってブロックの右側に画像を表示させるよう指定しています。

こうして出来上がったサンプルがこちらです:
https://dotnsf.github.io/hanging_css/

2020101301


一番上の #0 には mycard クラスがついていないので、パネルのブロックがそのまま表示されてます。#1 以下には全て mycard クラスを指定しているのでチェーンでぶらさがる効果が付与されています。

この方法ならいくつでも足せるし、JavaScript で後から動的に足すこともできるし、ブロックの要素に mycard クラスを1つ追加するだけで実現できるので簡単です。

上記ページのソースコードをまるごと参照していただき、chain.png を同じフォルダに置いてブラウザで開けばローカルでも見れると思います。


贅沢をいうと、現在はチェーンの画像サイズに合わせて CSS を書いて調整しているのですが、この部分をもう少しスマートにできたらいいなあ、と。


Bootstrap を使うと、簡単にタブを実装することができます:

タブA

タブB

タブC




この中の特定のタブの選択したり、有効化/無効化を JavaScript(jQuery) で切り替える方法を調べ、シンプルな実装方法を見つけたので共有します。また上のタブとボタンで実際の挙動を確認できるようにしています。

まず、特定のタブを選択するにはタブのアンカー(<a id="XXX">)をセレクトして、
$('.tav-tabs a[href="#XXX"]').show( 'tab' );
というメソッドを実行することで選択できます。

またタブのアンカーにつける data-toggle="tab" という属性を取り除くことでタブとしての挙動を無効化することができます。逆に data-toggle="tab" という属性を付けてあげることで、無効化されていたタブを再び有効化することができるようになります:
//. 無効化
$('.tav-tabs a[href="#XXX"]').removeAttr( 'data-toggle' );

//. 有効化
$('.tav-tabs a[href="#XXX"]').attr( 'data-toggle', 'tab' );

あとはこれらを組み合わせることで特定のタブを選択したり、その際に他のタブを有効化/無効化したりすることで実現できるようになります。

上述の画面では以下のようなコードを実装しています。サンプルとしてどうぞ:
(HTML)
<div  class="container">
  <ul  class="nav nav-tabs">
    <li  class="nav-item"><a  href="#tab-a" data-toggle="tab" class="nav-link active">A</a></li>
    <li  class="nav-item"><a  href="#tab-b" data-toggle="tab" class="nav-link">B</a></li>
    <li  class="nav-item"><a  href="#tab-c" data-toggle="tab" class="nav-link">C</a></li>
  </ul>
  <div  class="tab-content">
    <div  id="tab-a" class="tab-pane active">
      <p  class="text-left">タブA</p>
    </div>
    <div  id="tab-b" class="tab-pane">
      <p  class="text-center">タブB</p>
    </div>
    <div  id="tab-c" class="tab-pane">
      <p  class="text-right">タブC</p>
    </div>
  </div>
</div>

<div  class="container">
<button  onclick="selectX();" class="btn btn-xs btn-primary" id="btnX">タブAを選択&タブAのみ有効</button>
<button  onclick="selectY();" class="btn btn-xs btn-success" id="btnY">タブBを選択&タブBとCのみ有効</button>
<button  onclick="selectZ();" class="btn btn-xs btn-warning" id="btnZ">タブCを選択&タブA, B, Cすべて有効</button>
</div>
(JavaScript)
functon activeTab( tabid ){
  $('.nav-tabs a[href="#' + tabid + '"]').tab( 'show' );
}
function enableTab( tabid ){
  $('.nav-tabs a[href="#' + tabid + '"]').attr( 'data-toggle', 'tab' );
}
function disableTab( tabid ){
  $('.nav-tabs a[href="#' + tabid + '"]').removeAttr( 'data-toggle' );
}

function selectX(){
  //. A を選択して A のみを有効にする
  activeTab( 'tab-a' );
  enableTab( 'tab-a' );
  disableTab( 'tab-b' );
  disableTab( 'tab-c' );
}
function selectY(){
  //. B を選択して B, C のみを有効にする
  activeTab( 'tab-b' );
  disableTab( 'tab-a' );
  enableTab( 'tab-b' );
  enableTab( 'tab-c' );
}
function selectZ(){
  //. C を選択して A, B, C すべてを有効にする
  activeTab( 'tab-c' );
  enableTab( 'tab-a' );
  enableTab( 'tab-b' );
  enableTab( 'tab-c' );
}



このページのトップヘ