検索フォームにヘルプ表示するJavaScript

そこで作ったのが下記です。どうでしょうか?

http://techblog.yahoo.co.jp/cat207/how_to/javascript/

が汚いコードだというのと、表示のためだけにinputのvalueに入れるのもどうかと思ったので、CSSでその表示を後ろに出すようにし、操作するコードを書いてみました。

CSSでinputの後ろに文字列を表示

画像を使うのが手っ取り早いけど、CSSで重ねて表示する方法をとります。その方法もいろいろあるが、以下では、inline-blockとマイナスのmarginを利用する方法を使っています*1

<span style="display:inline-block;">
<div id="info"
  style='
  line-height: 25px;
  margin-bottom: -25px;
  text-indent: 0.2em;
  color: gray;
  z-index: -1;
  '
>ここで検索</div>
<div>
<input type="text" id="query" name="keywords"
  style='
  font-size: 100%;
  background: transparent;
  '
/>
</div>
</span>
<button type="submit">検索</button>
<button type="reset">x</button>

このHTMLは、以下のように表示されます

Firefox3.1
IE7
Opera9.6
Safari3.2
Chrome

label、position: absoluteを使う場合:

<label id="info" for="query"
  style='
  position: absolute;
  line-height: 25px;
  text-indent: 0.2em;
  color: gray;
  z-index: -1;
  '
>ここで検索</label>
<input type="text" id="query" name="keywords"
  style='
  font-size: 100%;
  background: transparent;
  '
/>
<button type="submit">検索</button>
<button type="reset">x</button>

おおむね同じ感じで表示されます。このHTMLでも以下のスクリプトで、blockの部分をinlineにすることで機能します。labelのforで指定することによってclick処理記述が不要になります。

文字入力に応じて、操作するスクリプト

上のHTMLに対して、例の動作条件を満たすようにするためのコードです。

var SearchInput = SearchInput || (function () {
  var add_listener = function (node, event, action) {
    if (!node) return;
    if (node.addEventListener) {
      node.addEventListener(event, action, false);
    } else {
      node.attachEvent("on" + event, action);
    }
  };

  return {
    connect_by_id: function (id_input, id_info) {
      this.connect(document.getElementById(id_input),
        document.getElementById(id_info));
    },
    connect: function (input, info) {
      if (!(input && info)) return;
      var check = function () {
        info.style.display = (input.value === "") ? "block" : "none";
      };
      var onreset = function () {
        setTimeout(check, 0);
        if (input.getAttribute("focus_get") === "true") input.focus();
      };
      var onblur = function () {
        setTimeout(check, 0);
      };
      var focus = function () {
        setTimeout(check, 0);
        input.focus();
      };
      var init = function () {
        onreset();
        add_listener(info, "click", focus);
        add_listener(input, "keydown", focus);
        add_listener(input, "blur", onblur);
        add_listener(input.form, "reset", onreset);
      };
      init();
    }
  };
})();

SearchInput.connect_by_id("query", "info");

適切なイベントで、input.valueを見て、info.style.displayを入れ替えてるだけの簡単なコードです。

元の仕様になるべく対応するようにしてますが、ただイベント名がわからず、テキストフィールドのドロップダウン入力には対応できてません(changeやinputではない模様)。

実のところ、仕様がfocus,blurだけでラベルをon/offするだけであれば、コードは劇的に短くできます。上のコードがonoff切り替えにしては多く、setTimeoutをかます等トリッキーなのは、入力テキストがあるかないかでラベルのon/offを切り替えるため、いろいろな入力操作イベントに対して処理をつなげていく必要があるからです。

一般的に、その対象問題と比べてコードが複雑になった場合それは根本の設計が良くない(or より良い設計が存在する)徴候とみなせ、問題に対して適切な構造をとることで、それを扱うプログラムも単純に記述できるようになるものです。

DOM構造を生成する

しかし、上記のspanやdivによるHTML構造をいちいち記述するのは面倒なので、これもJavaScriptで生成することにしましょう。

たとえば、以下のようなHTMLに対して処理します:

<input type="text" id="query" name="keywords"
  style="font-size: 100%;"
  info="ここで検索"
  focus_get="true"
/>
<button type="submit">検索</button>
<button type="reset">x</button>

このように記述しておけば、inputに埋め込んだ情報を使って、前述のDOM構造を作ります。

var SearchInput = SearchInput || (function () {
  
  var get_options = function (node, defaults) {
    var options = {};
    for (var key in defaults) {
      var value = node.getAttribute(key);
      options[key] = value !== null ? value : defaults[key];
    }
    return options;
  };

  var default_options = {
    info: "input words...",
    focus_get: "false",
    style_color: "gray",
    style_indent: "0.2em",
    style_z_index: "-1"
  };
  
  return {
    build: function (input) {
      if (!input) return;
      var options = get_options(input, default_options);
      var height = input.offsetHeight;

      var span = document.createElement("span");
      var info = document.createElement("div");
      var div = document.createElement("div");

      span.appendChild(info);
      info.appendChild(document.createTextNode(options.info));
      span.appendChild(div);
      input.parentNode.replaceChild(span, input);
      div.appendChild(input);

      input.style.background = "transparent";
      span.style.display = "inline-block";
      info.style.display = "block";
      info.style.lineHeight = height + "px";
      info.style.marginBottom = (-1 * height) + "px";
      info.style.zIndex = options.style_z_index;
      info.style.textIndent = options.style_indent;
      info.style.color = options.style_color;
    }
  };
})();

Put it together

<!DOCTYPE html>
<html>
<body>

<form method="GET">
<input type="text" id="query" name="keywords"
  style="font-size: 100%;"
  info="ここで検索"
  focus_get="true"
/>
<button type="submit">検索</button>
<button type="reset">x</button>
</form>

<script>//<!--

var SearchInput = SearchInput || (function () {
  var add_listener = function (node, event, action) {
    if (!node) return;
    if (node.addEventListener) {
      node.addEventListener(event, action, false);
    } else {
      node.attachEvent("on" + event, action);
    }
  };
  var get_options = function (node, defaults) {
    var options = {};
    for (var key in defaults) {
      var value = node.getAttribute(key);
      options[key] = value !== null ? value : defaults[key];
    }
    return options;
  };

  var default_options = {
    info: "input words...",
    focus_get: "false",
    style_color: "gray",
    style_indent: "0.2em",
    style_z_index: "-1"
  };

  return {
    build_by_id: function (id_input) {
      return this.build(document.getElementById(id_input));
    },
    build: function (input) {
      if (!input) return;
      var options = get_options(input, default_options);
      var height = input.offsetHeight;

      var span = document.createElement("span");
      var info = document.createElement("div");
      var div = document.createElement("div");

      span.appendChild(info);
      info.appendChild(document.createTextNode(options.info));
      span.appendChild(div);
      input.parentNode.replaceChild(span, input);
      div.appendChild(input);

      input.style.background = "transparent";
      span.style.display = "inline-block";
      info.style.display = "block";
      info.style.lineHeight = height + "px";
      info.style.marginBottom = (-1 * height) + "px";
      info.style.zIndex = options.style_z_index;
      info.style.textIndent = options.style_indent;
      info.style.color = options.style_color;

      return this.connect(input, info);
    },
    connect_by_id: function (id_input, id_info) {
      return this.connect(document.getElementById(id_input),
        document.getElementById(id_info));
    },
    connect: function (input, info) {
      if (!(input && info)) return;
      var options = get_options(input, default_options);
      var check = function () {
        info.style.display = (input.value === "") ? "block" : "none";
      };
      var onreset = function () {
        setTimeout(check, 0);
        if (options.focus_get === "true") input.focus();
      };
      var onblur = function () {
        setTimeout(check, 0);
      };
      var focus = function () {
        setTimeout(check, 0);
        input.focus();
      };
      var init = function () {
        onreset();
        add_listener(info, "click", focus);
        add_listener(input, "keydown", focus);
        add_listener(input, "blur", onblur);
        add_listener(input.form, "reset", onreset);
      };
      init();
    },
    options: default_options
  };
})();

SearchInput.build_by_id("query");
//--></script>
</body>
</html>

分割版

<!DOCTYPE html>
<html>
<body>

<form method="GET">
<input type="text" id="query" name="keywords"
  style="font-size: 100%;"
  info="ここで検索"
  focus_get="true"
/>
<button type="submit">検索</button>
<button type="reset">x</button>
</form>

<script src="searchinput.js"></script>
<script>
SearchInput.build_by_id("query");
</script>
</body>
</html>
// searchinput.js
var SearchInput = SearchInput || (function () {
  var add_listener = function (node, event, action) {
    if (!node) return;
    if (node.addEventListener) {
      node.addEventListener(event, action, false);
    } else {
      node.attachEvent("on" + event, action);
    }
  };
  var get_options = function (node, defaults) {
    var options = {};
    for (var key in defaults) {
      var value = node.getAttribute(key);
      options[key] = value !== null ? value : defaults[key];
    }
    return options;
  };

  var default_options = {
    info: "input words...",
    focus_get: "false",
    style_color: "gray",
    style_indent: "0.2em",
    style_z_index: "-1"
  };

  return {
    build_by_id: function (id_input) {
      return this.build(document.getElementById(id_input));
    },
    build: function (input) {
      if (!input) return;
      var options = get_options(input, default_options);
      var height = input.offsetHeight;

      var span = document.createElement("span");
      var info = document.createElement("div");
      var div = document.createElement("div");

      span.appendChild(info);
      info.appendChild(document.createTextNode(options.info));
      span.appendChild(div);
      input.parentNode.replaceChild(span, input);
      div.appendChild(input);

      input.style.background = "transparent";
      span.style.display = "inline-block";
      info.style.display = "block";
      info.style.lineHeight = height + "px";
      info.style.marginBottom = (-1 * height) + "px";
      info.style.zIndex = options.style_z_index;
      info.style.textIndent = options.style_indent;
      info.style.color = options.style_color;

      return this.connect(input, info);
    },
    connect_by_id: function (id_input, id_info) {
      return this.connect(document.getElementById(id_input),
        document.getElementById(id_info));
    },
    connect: function (input, info) {
      if (!(input && info)) return;
      var options = get_options(input, default_options);
      var check = function () {
        info.style.display = (input.value === "") ? "block" : "none";
      };
      var onreset = function () {
        setTimeout(check, 0);
        if (options.focus_get === "true") input.focus();
      };
      var onblur = function () {
        setTimeout(check, 0);
      };
      var focus = function () {
        setTimeout(check, 0);
        input.focus();
      };
      var init = function () {
        onreset();
        add_listener(info, "click", focus);
        add_listener(input, "keydown", focus);
        add_listener(input, "blur", onblur);
        add_listener(input.form, "reset", onreset);
      };
      init();
    },
    options: default_options
  };
})();

gistに張っておく

*1:positonで位置を操作したり