スタイルシートの書き換え

CSSの設定方法

おもなCSS(Cascading Style Sheets)の設定方法をまとめてみます。

  1. HTML要素のstyle属性を設定する(インラインスタイル)
  2. スタイルシートを使う
    • CSSファイル(外部スタイルシート)
    • <style>タグ(ページ内スタイルシート)
  3. 動的に設定・変更する
    • DOMメソッド
    • style.cssTextプロパティ
    • styleプロパティ

1. HTML要素のstyle属性を設定する(インラインスタイル)

HTMLのstyle属性で設定したスタイルは、スタイルシートで指定されたスタイルを上書きします。

HTML

<div style="color: red;"></div>

2. スタイルシートを使う

セレクタ(要素や要素の組み合わせ)でスタイルを指定します。要素はIDやタグ名、クラスを使って表すことができます。
また、<style>タグは<script>タグと同様にHTMLとしては扱われません。

CSSファイル(外部スタイルシート)

HTML

<!DOCTYPE html>
<html>
<head>
<link href="css/style.css" rel="stylesheet" type="text/css">
</head>
<body>
  <div id="sample"></div>
...

CSS

#sample {
  color: red;
}

<style>タグ(ページ内スタイルシート)

HTML

<!DOCTYPE html>
<html>
<head>
<style>
  #sample {
    color: red;
  }	
</style>
</head>
<body>
  <div id="sample"></div>
...

3. 動的に設定・変更する

JavaScriptによりスタイルを動的に設定・変更することができます。

DOMメソッド

ElementオブジェクトのsetAttribute()メソッドを使ってスタイルを設定することができます。第一引数の属性名に”style”、第二引数にスタイル文字列を指定します。

Script

var elem = document.getElementById("sample");
elem.setAttribute("style", "color: red;");

style.cssTextプロパティ

上記DOMメソッドと同様のスタイル文字列を扱います。

Script

var elem = document.getElementById("sample");
elem.style.cssText = "color: red;";

styleプロパティ

styleプロパティは文字列ではなくCSSStyleDeclarationオブジェクトになります。このオブジェクトのCSSプロパティにスタイルの値として文字列を設定します。文字列には単位が必要になります。また、セミコロンは含めません。一般にプロパティアクセスのほうがDOMメソッドより高速といわれています。

Script

var elem = document.getElementById("sample");
elem.style.color = "red";

スタイルシートの書き換え

通常は要素のクラス属性やstyleプロパティを書き換えることで容易にスタイルを設定することができるので、スタイルシート自体を書き換えることはないと思います。スタイルシートの書き換えは、スクリプトで要素ひとつひとつにアクセスしながらスタイルを設定するという方式とは異なり、ページ全体のスタイルを変更することができます。

スタイルシートオブジェクトの構造

document.styleSheetsプロパティには、配列のようなオブジェクトであるCSSStyleSheetオブジェクトが格納されています。cssRules配列には、スタイルシートのルールが格納されています。disabledプロパティは、false/trueを設定することでスタイルシートを有効化/無効化することができます。

document
    .styleSheets[0]    // 一枚目のスタイルシート
        .cssRules          : null
        .disabled          : false
        .(略)
    .styleSheets[1]    // 二枚目のスタイルシート
        .cssRules[0]       // 一個目のルール
            .cssText           : "#sample { color: red; }"
            .selectorText      : "#sample"
            .style
                .color             : "red"
                .fontSize          : "20px"
                .(略)
        .cssRules[1]       // 二個目のルール
            .(略)
        .cssRules[2]       // 三個目のルール
            .(略)
        .disabled          : false
        .(略)

なお、<link>タグで指定したcssファイルのルール(cssRules)はnullになるようです。また、cssRulesのcssTextは読み取り専用で直接書き換えることはできないようです。

ルールの追加/削除

ルールの追加や削除はスタイルシートのメソッドであるinsertRule()やdeleteRule()で行うことができます。このときの引数はスタイル文字列とルールのインデックスになります。

Script

document.styleSheets[1].insertRule("#sample { color: red; }", 0);
document.styleSheets[1].deleteRule(0);

また、cssRules[].styleプロパティにはCSSプロパティ一式が格納されており、これを直接読み書きすることができます。

Script

document.styleSheets[1].cssRules[0].style.color = "red";

以下に、必要に迫られて作ったものの後になって必要がないことに気づき愕然としたスタイルシート書き換えのサンプルを掲載します。

See the Pen stylesheet overwrite test by senmyou (@senmyou) on CodePen.

私がスタイルシートの書き換えが必要だと思ったのは、contenteditable属性を使ったときリターンキーの入力で自動的に生成されるdiv要素に動的にスタイルを設定したかったからです。div要素を生成するコード(createElement()など)は隠蔽されているため、div要素にアクセスできそうにありませんでした。そこで、セレクタ “#parent > div” のスタイルを変更できればdiv要素にアクセスしなくてもスタイルが変更できるのではと調べていたところ、スタイルシートの書き換えにたどり着きました。
ところが、スタイルシートの操作関数を作成し動作確認しているとき、このdiv要素は兄弟要素のスタイルを引き継いで生成されるということに気づきました。つまり、スタイルシートを書き換えなくても兄弟要素のstyleプロパティを変更するだけでよかったのです。。。
いつか使う日が来るといいなと思ってます。

contenteditable 属性

HTML5 の Content Editable 属性を使用したノートを作成してみました。

See the Pen note (contenteditable) by senmyou (@senmyou) on CodePen.

現在 Android 用の軽量ノート、というかメモの切れ端の代わりになるようなアプリを作っているところです。
最初 textarea 要素で作り始めたのですが罫線がうまく引けませんでした。テキストエリアの背景の高さ(background-size)と行の高さ(line-height)を合わせ linear-gradient で背景に罫線を引くといい感じになるのですが、縦スクロールが発生したとき罫線は固定されたままでスクロールについてきません。試行錯誤を繰り返しているうち、背景(background-size)の高さがテキストエリアより小さいのでスクロールが発生しないのではと思い至りこの方法をあきらめました。
ほかに方法がないか探していたところ contenteditable 属性というものを発見。聞いたこともなかったのですが使ってみました。そのさいに気づいたことをまとめてみます。

contenteditable 属性の挙動について

HTML 要素の contenteditable 属性に true を設定すると、それだけで編集可能状態になります。入力した文字はその要素のコンテンツとなり、要素の幅に達すると overflow プロパティに従い自動的にスクロールバーを表示したり折り返したりします。改行すると新しい子要素が作成され、行の先頭でバックスペースを入力するとその行にあたる子要素が削除されカーソルが上の行へ移動します。
実際の html とその表示は以下のようになります。CSS で装飾していますが html は一行のみです。

  <div id="notearea" contenteditable="true"></div>

See the Pen RgPEry by senmyou (@senmyou) on CodePen.

white-space プロパティの設定

HTML5.jp で以下の解説を見つけました。

ウェブ制作者は、編集ホスト、および、これら編集メカニズムを通して生成されたマークアップ上で、’white-space’ プロパティを値 ‘pre-wrap’ にセットすることが推奨されます。デフォルトの HTML ホワイトスペースのハンドリングは、WYSIWYG 編集に良く適しているわけではありません。’white-space’ がデフォルト値のままだと、行の折り返しが正しく機能しないでしょう。

行の折り返しに連続した半角スペースがあたる場合について警告しています。また、デフォルト(normal)のままだと半角スペースを何個入力しても一個分のスペースになってしまいますが、これはエディタとしてはありがたくないですね。そこで white-space を設定しました。

white-space: pre-wrap;

ちなみに pre-wrap と pre の挙動の違いはわかりませんでした。

罫線を引く

ノートを作るにあたり、最初から罫線を引きたいと思っていました。そこで、あらかじめ子要素として div 要素を10個配置し、CSS で各要素に線を引きました。textarea 要素に線を引くと前述のとおり縦スクロールについてこないのですが、div 要素に線を引くことで div 要素のコンテンツと罫線が一緒にスクロールするようになります。

#notearea > div {
  margin: 0;
  padding: 0 4px;
  min-height: 2rem;
  background-image:
    linear-gradient(to bottom, transparent, transparent calc(2rem - 1px), rgba(0,150,136,0.4) calc(2rem - 1px));
  background-size: auto 2rem;
}

pre-wrap の影響

Monaca デバッガを使用し android 上で確認しながら作っていたのですが、試しに APK を構築して動かしてみることにしました。すると以下のように、一見すると line-height が3倍になったかのような表示になってしまいました。

See the Pen NgqerV by senmyou (@senmyou) on CodePen.

この現象は PC 上のシミュレータ(プレビュー機能)でも発生していました。よくみると空白文字がいくつか挿入されています。また、罫線が引かれておらずインスペクタで DOM をみても一つの div 要素になっています。つまり通常の改行文字の入力や折り返しによるものではなさそうです。このときの html のコードは以下のようになっていました。

<div id="notearea" contenteditable="true">
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
</div>

ここで white-space プロパティの pre-wrap をデフォルト(normal)にすると空白文字が詰められるせいか問題がなくなります。ではなぜ上記コードで空白文字が挿入されるのか。これは私見ですが、contenteditable を指定した要素のなかでは html の記述がそのままコンテンツの対象になっていると思われます。つまり、上記 html では「div 開始タグの前にあるタブ文字」と「div 終了タグの後ろにある改行」が空白文字に変換され、それぞれ div 要素の外に挿入される感じです。
html の記述から以下のようにタブ文字と改行を削除してみるとうまくいきました。

<div id="notearea" contenteditable="true"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>

ふと「inline 要素を横に並べると要素間に不要な隙間が生じるけれど html の記述から改行を削除して詰めると隙間がなくなる」という現象を思い出しました。また、なぜ Monaca デバッガでは大丈夫だったのかは謎です。

backspace 入力

行の先頭でバックスペースを入力すると、その行にあたる div 要素が削除され、カーソルが上の行へ移動します。このとき、上の行でコンテンツが空(””)の div 要素はまとめて削除され、文字のある行までカーソルが一気に移動しました。この現象は各 div 要素のコンテンツに <br> を入力しておくことで防ぐことができました。これは子要素を持たない contenteditable 属性の要素で改行を行うと div 要素のコンテンツに <br> が入力されるというデフォルト動作になります。<br> なんて必要なさそうに思えますが、実は必要だったわけです。
以下の html が最終形態となります。うーん、見づらいですねw

<div id="notearea" contenteditable="true"><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div><div><br></div></div>

Android のキーボード出現時のスクロール

実機上で contenteditable 属性を指定した要素をタップすると編集可能状態になりキーボードが表示されます。このとき、要素のスクロールが行われないためカーソルがキーボードに隠れてしまうことがあります。これは contenteditable の影響か onsen ui の影響か、はたまた android の影響か私の作り方が原因か定かではありませんが、以下の対応でカーソルを見える位置にスクロールできました。
なお、以下のコードでは cordova のプラグイン ionic-plugin-keyboard を使用してキーボード表示イベントの監視を行っています。ちなみにこのリスナ内での window.innerHeight は画面の高さからキーボードの高さを引いた値になります。

var clickedDiv = {
  elem: null,
  y: 0
};
document.getElementById("notearea").addEventListener('click', function(e) {
  clickedDiv.elem = e.target;
  clickedDiv.y = e.y;
});
window.addEventListener('native.keyboardshow', function(e) {
  if (clickedDiv.y + clickedDiv.elem.clientHeight > window.innerHeight) {
    clickedDiv.elem.scrollIntoView(false);
  }
});

読み出しと書き込み

最後に上記 html(div の子要素10個配置)で使用している読み書き関数を記載して本記事を終了します。

function read() {
  var divs = document.getElementById("notearea").getElementsByTagName("div");
  var num = divs.length;
  var i, note = [];

  for (i=0; i<num; i++ ) {
    note.push(divs[i].textContent);
  }
  return note;
}
function write(note) {
  var num = note.length;
  var i, text = "";

  for (i=0; i<num; i++) {
    text = note[i] ? note[i] : "<br>";
    text += ("<div>" + text + "</div>");
  }
  document.getElementById("notearea").innerHTML = text;
}

Monaca メモ

Monacaで久しぶりにプロジェクトを作成し Monaca デバッガを立ち上げたところ、以下のエラーが発生しました。

ググッてもヒットしなかったので私の使い方や環境が悪いだけかもしれませんが、プロジェクトの作成から解決するまでをメモっておきます。

開発環境

  • localkit v2.3.0 / Monaca IDE
  • Monaca Debugger v6.1.1
  • PC: Windows10

プロジェクト作成

以下の設定でプロジェクトを作成しました。

カテゴリ Onsen UI
テンプレート Onsen UI V2 JS Minimum

Monaca デバッガ立ち上げ

Monaca デバッガを立ち上げると、以下の2つのエラーがログに出力されました。

Uncaught ReferenceError: Set is not defined
www/lib/onsenui/js/onsenui.min.js: 3
Uncaught ReferenceError: ons is not defined
www/index.html: 16

なにが悪いのか想像すらつきません。
ちなみに localkit/IDEの Preview ではエラーはでていません。

ファイル構成とバージョン確認

index.html のソースをみてみます。

<script src="components/loader.js"></script>
<script src="lib/onsenui/js/onsenui.min.js"></script>

<link rel="stylesheet" href="components/loader.css">
<link rel="stylesheet" href="lib/onsenui/css/onsenui.css">
<link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css">

lib フォルダ内のファイルを開いてバージョンを確認すると、

onsenui.min.js v2.2.4
onsenui.css v2.2.4
onsen-css-components.css ?不明

次に loader ファイルを開いて中身を確認すると、

components/loader.js monaca-cordova-loader のコード
monaca-core-utils のコード
components/loader.css ヘッダのみ

Onsen UI のリリース情報を調べてみると最新版は v2.3.1。内容をざっと見ても本件と関連するような箇所はわからなかったのですが、v2.2.4 からけっこう更新されてました。

最新版 Onsen UI インストール

ということで、v2.2.4 以降のどこかで修正されていることを祈りつつ Onsen UI の最新版をインストールしてみることにしました。JS/CSSコンポーネントの追加画面で Onsen UI (Monaca Version)を選択し、選択可能なバージョンのなかで最新となる v2.3.0 をインストール。そして、Monaca デバッガで再実行。

エラーが消えました!!!

… が、よくみると 謎メッセージが2個?

ググったところ Shadow DOM を使っていなければ気にしなくていい的なことが書いているように見えた(英語わかりません)ので無視することに。

でも、なんで2個?

index.html 見直し

index.html を見てみると loader ファイルを読み込んでから lib フォルダ内のファイルを読み込んでいます。

そこで、もう一度 loader ファイルを確認してみます。

components/loader.js monaca-cordova-loader のコード
monaca-core-utils のコード
monaca-onsenui のコード追加
components/loader.css onsenui.css と onsen-css-components.css のインポート文追加

loader ファイルに Onsen UI の記述が追加されています。バージョンは v2.3.0。

次に lib フォルダ内のファイルを開いてバージョンを確認すると、js ファイル/css ファイル ともに v2.2.4 のまま変わりありません。

ということは loader ファイルで v2.3.0 を読み込んだ後に lib フォルダ内の v2.2.4 を読み込んでる!
だから同じ内容のメッセージが2個表示されたんですね。

index.html 修正

ということで index.html の lib フォルダ内のファイル読み込みをコメントアウトしました。

  <script src="components/loader.js"></script>
<!--
  <script src="lib/onsenui/js/onsenui.min.js"></script>
-->
  <link rel="stylesheet" href="components/loader.css">
<!--
  <link rel="stylesheet" href="lib/onsenui/css/onsenui.css">
  <link rel="stylesheet" href="lib/onsenui/css/onsen-css-components.css">
-->

実行した結果は以下のようになりました。

まとめ

JS/CSSコンポーネントの追加画面から追加したファイルは loader ファイルに書き込まれます。Onsen UI を追加(更新)した場合は、index.html に前バージョンのファイルの読み込みを行う script 文が残っているので、これを手動で削除する必要があります。

以前は Onsen UI は最初から loader ファイルに記述されていて、 index.html には Onsen UI を読み込む script 文はなかったと思うんです。今後また仕様が変わるかもしれませんが現状報告ということで。