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

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

タグ:jquery

ウェブページ内の特定要素の見た目を JavaScript で動的に変更する方法を調べました。

一般的にウェブページ内の要素の見た目はスタイルシートで管理されます。例えばこのような感じ:
<head>
<style>
#container1{
  background-color: #fcc;
  height: 100px;
}
</style>
</head>
<body>

<div class="container" id="container1">
</div>

</body>

この時点では #container1 要素はピンク(#ffcccc)で塗りつぶされて表示されます:
2020082601


この部分の「背景色を動的に(JavaScript で)別の色に変えたい」場合、いくつかの方法が考えられます。一般的には背景色が指定された class を複数用意し、id ではなく class 指定で背景色を与えた上で、JavaScript で指定 class を切り替える(jQuery であれば removeClass + addClass)方法です。 ただこの方法は「事前に class を定義しておく」必要があります。切り替える瞬間の情報や属性値を参照して背景色を決めるとか、ランダム要素を使って背景色を決めるなど、事前に定義する内容が定まらないような場合では使えません。このような場合も含めて #container1 要素に定義された内容そのものを変更する(つまりスタイル定義内容そのものを動的に変更する)にはどのようにすればよいかを調べました。以下、jQuery を併用した場合で説明します。

例えば #container1 で定義されるエリアをクリックしたら背景色をランダムに変える、という場合であれば以下のような JavaScript で実現できます:
<script>
$(function(){
  $('#container1').on( 'click', function( e ){
    //. #container1 のスタイルシート定義を動的に変える。具体的には背景色を変更する
    var bgs = [ '#fcc', '#cfc', '#ccf', '#cff', '#fcf', '#ffc', '#ccc' ];
    var bg = bgs[Math.floor( Math.random() * bgs.length )];
    var t = $('#container1')[0];
    t.style['background-color'] = bg;
  });
});
</script>

スタイルシートの内容を変更したい要素をセレクターを指定して取り出し、その取得結果(配列)の最初の要素を取り出します。そして取り出した要素の style 属性がスタイルシートの内容なのでここを JavaScript で上書きするよう記述します(上例では t.style 内の 'background-color' 属性をランダムに上書きしています)。この方法であれば class を使うことなく、切り替えることもなく、スタイルシート定義を書き換えることで見た目の変更を実現できます。


実際に動くサンプルを用意しました:
https://dotnsf.github.io/dynamic-css/


上記 URL にアクセスすると以下のような画面が表示されます。画面上部に高さ 100px でピンク色の矩形が表示されています:
2020082602


この矩形部分をクリック(タップ)すると、7種類の中からランダムに選ばれた背景色に切り替わります:
2020082603


JavaScript でスタイルシートの内容を動的に書き換える方法のサンプルでした。class を事前に用意して指定し直す、というロジックが使えない場合でも実現可能な方法です。


HTML の <textarea> に行番号を付与する jQuery プラグイン jQuery LinedTextArea を使ってみました。

<textarea> フィールドを(コーディング目的などの)テキストエディタとして使いたい場合など、行番号を付与して使いたいケースがあります。CSS と JavaScript でがんばって作る方法もありますが、jQuery のプラグインを使うことで比較的簡単に実現できそうだったので紹介します。

目的のプラグインは jQuery LinedTextArea です。ざっと探した限りで CDN が存在していなさそうだったので、リンク先から git clone するかダウンロードしておきます(最低限必要になるのは jquery-linedtextarea.cssjquery-linedtextarea.js の2つです)。


準備段階として、これらを jQuery 本体の後にロードしておきます:
<script type="text/javascript" src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="./jquery-linedtextarea.js"></script>
<link href="./jquery-linedtextarea.css" rel="stylesheet"/>

  :

そして目的の <textarea> を指定して、以下のようなコードを実行します:
<textarea class="lined">
あいうえお
かきくけこ
さしすせそ
</textarea>

<script>
$(function(){
  $('.lined').linedtextarea({
    selectedLine: 1
  });
});
</script>

目的の <textarea> に "lined" というクラスを指定した上で、jQuery で $('.lined') に対して linedtextarea() を実行します。上記例では { selectedLine: 1 } というパラメータを付けて実行しています。これにより1行目が選択された状態で表示されます:
2020051200


こんな感じ。

オープンな地図データである OSM(OpenStreetMap) と、オープンな地図操作 JavaScript ライブラリである Leaflet.js を使うことで、ライセンスフリーな地図アプリを作ることができます。このブログでも過去に何度か扱ったことがあります:
http://dotnsf.blog.jp/tag/leaflet

例えば、単に地図を全画面表示するのであればこんな感じのコードを書くと実現できちゃいます:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', {}).setView( [ lat, lng ], zoomlevel );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
  </div>
</body>
</html>

(東京あたりを中心に、ズームレベル9で地図を表示)
2020041501


さて、上図の左上にズームコントロールがあります(上記コードでは特に指定していませんが、明示的に非表示にしない限り、デフォルトでズームコントロールが表示されます)。スマホであればピンチイン・アウトの操作でズームイン・アウトができるのですが、タッチスクリーンに対応していない PC のブラウザで地図を見ている場合は、このコントールを使ってズームイン・アウトを行うことになります。

ただ例えば画面上部に常に表示されるメッセージがある場合など、メッセージとズームコントロールが重なってしまい、操作しにくくなってしまうことがあります:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', {}).setView( [ lat, lng ], zoomlevel );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
#message{
  position: absolute;
  top: 10px;
  font-size: 20px;
  color: red;
  font-weight: bold;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
    <div id="message">このメッセージが邪魔でズーム制御ができない?</div>
  </div>
</body>
</html>

(画面上のメッセージとズームコントロールが重なってしまう例)
2020041502


この状態をなんとか回避できないか、というのが今回のブログエントリのテーマです。メッセージ側の位置を変えるのは簡単ですが、当然意図があって画面上部にメッセージを表示しているわけですから、できればズームコントロールの位置を変えたい。一方、なまじデフォルトで表示されてしまっているズームコントロールだけにどうすると位置を変更できるのか、がわかりにくいのでした。

正解の一例を先にお見せすると、こんな感じになります:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', { dragging: true, zoomControl: false }).setView( [ lat, lng ], zoomlevel );
  L.control.zoom( { position: 'bottomright' } ).addTo( map );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
#message{
  position: absolute;
  top: 10px;
  font-size: 20px;
  color: red;
  font-weight: bold;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
    <div id="message">このメッセージがあってもズーム制御できる</div>
  </div>
</body>
</html>

青字部分が肝になります。初期化時(L.map() 実行時)のオプションで zoomControl: false をつけることでズームコントロールを(一旦)非表示にします。なおもう一つ指定しているオプション dragging: true は地図のスクロール(ドラッグ)ができるようにするオプションで、ここを false にするとスクロールできない地図になります。

このままだとズームコントロールは表示されないのですが、直後の1行でズームコントロールを再び表示しています。そしてこの行では position: 'bottomright' というオプションをつけることで「右下」にズームコントロールを表示させています(デフォルトは 'topleft'):

2020041503

↑こんな感じになって、メッセージと重ならない位置にズームコントロールを移動できました。


(参考)
https://www.wrld3d.com/wrld.js/latest/docs/leaflet/L.Control.zoom/



HTML の <select> 要素は複数の選択肢から1つを選択させるためのパーツです。特にスマホなどでは入力の負担が大きいため、入力内容が長いケースでは選択肢の中から選ぶことができると利用者の負担を軽くすることができて便利です。

ただ必ずしも全てのケースで「選択肢から選ぶ」ことが可能とは限りません。めぼしい選択肢を用意した上で、それでも選択肢以外の答を入力したい/させたい場合、多くのケースでは「その他」という選択肢を用意した上で、「その他」を選んだ場合のみ、詳しい内容を別のテキストフィールドに入力させる、という方法が取られているように感じます。 これはこれで(最低限の目的を達成することができるという意味で)いいのですが、UI/UX の観点では例外的なパーツ処理となり、見た目にもスマートではありません。

HTML を使わないネイティブアプリケーションなどでは「任意文字列も入力可能なセレクトボックス」のようなパーツも用意されていて、これを使うことで選択肢の中から選ぶことも(ダブルクリックするなどして編集状態に切り替えた上で)任意文字列を入力することもできます。これだと見た目もスマートでいいですよね。というわけで、このようなパーツを HTML でも用意できないか挑戦してみました。目標はこのブログエントリのタイトルでもある『ダブルクリックすると <input> になって任意入力可能な <select> 』の実現です。

説明の前に、まずは実際に動くサンプルを使ってみてください。下の <select> から値を選び、"Value" ボタンをクリックすると、選択されている値が alert() で表示されます。<select> 部分をダブルクリックすると編集可能になって任意文字列が入力可能になり、その上で "Value" ボタンをクリックすると、入力されている値が alert() で表示される、というものです:




そんなに特別なことをしているわけではないのですが、以下解説です。まず HTML での該当箇所は以下のようになっています:
<select id="mySelect">
  <option value="12345">12345</option>
  <option value="23456">23456</option>
  <option value="34567">34567</option>
  <option value="45678">45678</option>
  <option value="56789">56789</option>
</select>
<input type="text" id="myInput" style="display:none;" value=""/>

<select>(id = "mySelect")と <input>(id = "myInput")を1つずつ配置しています。ただし <input> には style="display:none;" を指定して非表示にしています。つまりこの時点では <select> のみが表示されています。先にコツを言っておくと、<select> の値は常に <input> に引き継がれるように(この後で)設定しておき、"Value" ボタンクリック時には <select> ではなく <input> の値を取得するようにします。

次に JavaScript の解説です:
$(function(){
  //. select の値が変わった時のハンドリングを定義
  $('#mySelect').change( onSelectChange );
  onSelectChange();

  //. select がダブルクリックされたら select を非表示にした上で、同じ値が入っている input を表示する
  $('#mySelect').dblclick( function( e ){
    $('#mySelect').css( 'display', 'none' );
    $('#myInput').css( 'display', 'block' );
  });
});

//. select の値が変わったら、その値を隠しフィールドに代入しておく
function onSelectChange(){
  var v = $('#mySelect').val();
  $('#myInput').val( v );
}

まず画面ロード時に <select> の値が変わった時の処理を定義しています。ここでは <select> の値が変わるたびに onSelectChange() が実行され、<select> で選択された値が常に <input> の値として反映されるようにしています(またロード直後に1回実行することで <input> を初期化しています)。

また <select> がダブルクリックされた時の処理も記述しています。ダブルクリックされたら <select> を非表示に、<input> を表示状態に切り替えます。<input> には上述の処理によって常に <select> で選択されていた値が入っているので、その値が引き継がれた状態で編集可能なフィールドとして表示されることになります。

加えて、以下のスタイルシートを適用することで <select> と <input> が同じサイズになるように設定し、ダブルクリック時に <select> が自然に <input> に切り替わったように見えるよう調整しています:
#mySelect{
  height: 40px;
  width: 200px;
}
#myInput{
  height: 40px;
  width: 200px;
}

これでダブルクリックすると <input> に切り替わる <select> を作ることができました。 あとは "Value" ボタンですが、これは onClick 属性に以下のような関数を定義して、<input> に設定された値を取り出すようにしています:
<input type="button" class="btn btn-primary" value="Value" onClick="getValue();"/>
//. 現在の値を取り出す
function getValue(){
  //. select ではなく input に入っている値を取り出す
  var v = $('#myInput').val();
  alert( v );
}

実際のサンプルコードは github で公開しているので、こちらも参照ください:
https://github.com/dotnsf/select2input

 

むかーしから存在している技術なのですが、イメージマップという便利な機能があります(最近あまり使われなくなったという印象もあります)。これは HTML ページ内の画像内にクリッカブルな領域を複数定義し、いずれかの領域がクリックされたら何らかの処理を行う、というものです。クリックされる領域によって処理内容(このページにジャンプするとか、この JavaScript 関数を実行するとか、・・)を変えることができる、というものです。

例えばこんな感じで実現します。いらすとやさんの『ホワイト企業~ブラック企業のイラスト』を例に紹介します:



このイラスト画像は横650ピクセル、縦137ピクセルです。その中にホワイト企業~ブラック企業が5段階のイラストで表示されています。

画像の左上の座標を ( 0, 0 ) とすると、このうち一番左の企業は、そのビルの矩形部分は左上が ( 37, 13 ) から右下 ( 100, 127 ) という矩形でできています。いま、このビルの矩形部分がクリックされたら "white" というメッセージを表示するようにしてみます:
2019100701


同様にして、5つのビルそれぞれにクリッカブルな矩形部分を定義し、それぞれがクリックされた時に以下のようなメッセージが表示されるようにしてみます:
ビル番号(左から何番目のビル)矩形範囲クリックされた時のメッセージ
1 ( 37, 13 ) - ( 100, 127 ) white
2 ( 165, 13 ) - ( 227, 127 ) lightgray
3 ( 291, 13 ) - ( 355, 127 ) gray
4 ( 481, 13 ) - ( 483, 127 ) darkgray
5 ( 546, 13 ) - ( 610, 127 ) black


これをイメージマップで実現すると、 HTML の該当箇所は以下のようになります:
<body>
  <div class="container">
    <img src="https://1.bp.blogspot.com/-BmJohMucVBI/XYhOV_VMQmI/AAAAAAABVHA/sZF8lMjPedUWSkxBwUGZXmVri2OFKEZ4gCNcBGAsYHQ/s650/company_white_black_kigyou_5dankai.png" border="0" usemap="#image_map"/><br/>
    <map name="image_map">
      <area shape="rect" coords="37,13,100,127" href="javascript:alert( 'white' )"/>
      <area shape="rect" coords="165,13,227,127" href="javascript:alert( 'rightgray' )"/>
      <area shape="rect" coords="291,13,355,127" href="javascript:alert( 'gray' )"/>
      <area shape="rect" coords="418,13,483,127" href="javascript:alert( 'darkgray' )"/>
      <area shape="rect" coords="546,13,610,127" href="javascript:alert( 'black' )"/>
    </map>
  </div>
</body>

<img> タグの usemap 属性でイメージマップの名称を定義します。そして <map> タグを使って、対象(name 属性で指定)の <img> のクリッカブル領域と、クリックされた時のハンドラを href 属性に定義します。上記例ではハンドラを javascript 関数の実行にしていますが、普通に URL を記述すればリンクを作成することもできます。

これで各ビルをクリックした時に、(左から順に)"white", "lightgray", "gray", "darkgray", "black" というメッセージが表示されるようになります。元は1枚の画像でしたが、クリッカブルな領域を5箇所定義することができ、それぞれのクリックハンドラを個別に設定することもできました:

(左から2番めのビルをクリックした時の様子)
2019100703


ここまでが普通のイメージマップです。このイメージマップ、非常に便利な反面で「レスポンシブ対応が難しい」という難点がありました。要はクリッカブルな矩形領域をピクセル絶対値で指定しているため、画像がそのまま表示された場合はいいのですが、レスポンシブ対応などで画面サイズに合わせて拡大縮小されて画面いっぱいに表示された場合、縮尺が変更になってしまい、絶対値で指定していたイメージマップの定義がおかしくなってしまうのでした。

具体的に同じコードをスマホのシミュレーターで確認した結果、下の赤点線部が "lightgray" のクリッカブル矩形部分となっていて、本来の位置からズレていました。これをどうにかしたい、というのが今回のテーマです:
2019100702


色々と調べましたが、答としては jQuery RWD Image Maps という jQuery プラグインを使うことで解決できました:
2019100700



以下に具体的な対応手順を紹介します。

まず HTML 内でレスポンシブのための宣言をしておきます。例えば以下のようなコードが既に含まれているものと仮定します:
  :
<meta name="viewport" content="width=device-width,initial-scale=1"/>
  :


次に jQuery と jQuery RWD Image Maps をロードします。以下の例では CDN からそれぞれをロードしています(jQuery RWD Image Maps の方を後にロードする必要があります):
  :
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery-rwdImageMaps/1.6/jquery.rwdImageMaps.min.js"></script>
  :

CSS で画像サイズの調整を行います。この例ではイメージマップを用いる画像の横幅を 100% に、高さは自動調整するように指定しています:
  :
<style>
img[usemap]{
  max-width: 100%;
  height: auto;
}
</style>
  :

そして最後に JavaScript で該当部分に RWD Image Maps を適用します:
  :
<script>
$(function(){
  $('img[usemap]').rwdImageMaps();
});
</script>
  :

これでスマホでおなじページにアクセスした時でもイメージマップの矩形部分がズレることなく利用できるようになりました:
2019100704




このページのトップヘ