ラベル javascript の投稿を表示しています。 すべての投稿を表示
ラベル javascript の投稿を表示しています。 すべての投稿を表示

2015年11月19日

<iframe> を動的生成する際の注意点

javascript で src 無しの <iframe> とその中身を構築した際、はまったことがあったのでメモ。

  • 動的に追加した <iframe> の中身をいじる際は、
    iframe.contentWindow.documentdocument.write() を使う
    または
    <iframe> のロード完了を待つ。(<iframe> 自身に onload イベントがある)
  • <iframe> に onload イベントを設置する際は、DOM への追加前に行う。
    DOM に <iframe> を追加した瞬間、onload イベントが処理されるブラウザがあるため (Chrome)

[サンプルコード]

// 単純化のため、jquery 使用

// 良い例
// IE:OK / Firefox:OK / Chrome:OK
$(function(){
    $('<iframe>').load(function(){
        // ロード完了後、DOM を変更できる。
        var ibody = this.contentWindow.document.body;
        $(ibody).append('<div>ブロック要素</div>');
    }).appendTo('body');
    // イベント設置後に DOM へ追加
});


// 悪い例(1)
// IE:OK / Firefox:NG / Chrome:OK
// Chrome は iframe の DOM 追加後、即時ロード完了する模様
// IE は iframe のロード完了を待たなくても大丈夫っぽい??
$(function(){
    var iframe = $('<iframe>').appendTo('body');
    var ibody = iframe[0].contentWindow.document.body;
    $(ibody).append('<div>ブロック要素</div>');
});


// 悪い例(2)
// IE:OK / Firefox:OK / Chrome:NG
// Chrome は iframe が即時ロード完了するので、DOM 追加後に load イベントを付けても無意味
$(function(){
    var iframe = $('<iframe>').appendTo('body');
    iframe.load(function(){
        var ibody = this.contentWindow.document.body;
        $(ibody).append('<div>ブロック要素</div>');
    });
});
[環境]
  • Windows 7 64bit
  • Internet Explorer 11
  • Firefox 42.0
  • Chrome 48.0.2564.8

2015年10月31日

highlight.js で行番号を表示

ブログ内の SyntaxHighlighterhighlight.js 移行作業がようやく終わったので投稿。

highlight.js には行番号を表示する機能が付いてない。
とりあえず検索してみたところ、ちょうどいいプラグインがあったのでそれの紹介。(当ブログでも使用中)
wcoder/highlightjs-line-numbers.js

使用方法は、highlight.js を適用した要素(普通は <code>)に対して hljs.lineNumbersBlock(要素) を実行。
もしくは、hljs.initLineNumbersOnLoad() を仕掛けておく。
[使用例]

[].forEach.call(document.querySelectorAll('pre > code'), function(elem){
    // highlight.js は <code> 内の先頭と末尾の改行を無視してくれないので、ここで削除
    // ※ HTML を書く際、先頭と末尾に改行を入れない方法もある
    elem.textContent = elem.textContent.replace(/^[\r\n]+|[\r\n]+$/g, '');

    // highlight.js の適用
    hljs.highlightBlock(elem);

    // highlightjs-line-numbers.js の適用
    hljs.lineNumbersBlock(elem);
});

また、行番号ブロックのスタイル(.hljs-line-numbers)を自分で用意する必要がある。
[使用例(当ブログのスタイル・2015/10/31)]

pre > code.hljs.hljs-line-numbers {
    background-color: #f5f5f5;
    border-right: 0 none;
    color: #707070;
    text-align: right;
    min-width: 14px;
    /* 以降は選択できないようにするためのスタイル */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

highlight.js に移行した理由

  • C# の新しいキーワードに対応 (素の SyntaxHighlighter は var 辺りから未対応)
  • 旧 Visual Studio 風ハイライトが選択可能
  • ハイライトする言語を選択してパッキングと Minify が公式サイトで可能
  • 外部ファイルの数が少ない (プラグインを除けば js と css の 2 ファイル)

2015年8月22日

document.querySelector で先頭が数字の ID を指定する方法

document.querySelectordocument.querySelectorAll で、数字から始まる ID をそのまま指定するとエラーになる。
document.getElementById はエラーにならず取得できる。
※ DOM じゃないけど、jquery セレクタもエラーにならず取得可能。

console.log(document.querySelector('#20150821abc'));
// Error: An invalid or illegal string was specified

console.log(document.getElementById('20150821abc'));
// <div id="20150821abc">

// 上記の div 要素は、このページ内に含まれているので、firebug・開発者ツールなどで確認可能

解決手段

querySelector は CSS セレクタを指定するものなので、CSS セレクタのエスケープ方法(コードポイント指定)で、先頭数字の ID を指定することができる。

// エスケープ方法は 2 通り
console.log(document.querySelector('#\\32 0150821abc'));    // '\3' + (digit) + ' '
console.log(document.querySelector('#\\0000320150821abc')); // '\00003' + (digit)

// <div id="20150821abc">

// もちろん、CSS の記述でも使用可能

自分用の GM スクリプトを作った際、先頭数字の ID を使っている web サイトがあったので・・・
普通は、先頭数字にはしないと思うのでレアケース?

参考URL

2015年2月28日

ASP.NET __doPostBack は jQuery.submit(handler) 未対応

jQuery.submit(handler) でイベントを登録しても、__doPostBack によるサブミット(ポストバック)では呼び出してくれない。
原因は、jQuery.submit(handler) は基本的にイベントリスナ(addEventListener, attachEvent)を使用し、__doPostBack はイベントリスナを考慮してないためである。

[一般的な __doPostBack の実装]

var theForm = document.forms['form1'];
if (!theForm) {
    theForm = document.form1;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}

DOM の onsubmit プロパティは考慮してくれているが、イベントリスナについてはノータッチである。
(ざっと調べた感じ、イベントリスナのみの呼び出しはかなり面倒・・・というかできるか怪しい)

__doPostBack を使わないようにすれば問題ないのだが、asp:LinkButton や各種コントロールのイベント(SelectedIndexChanged など)を使ってしまうと、__doPostBack の使用は回避できない。

解決手段① onsubmit プロパティを使用

__doPostBack の実装に合わせて、素直に onsubmit プロパティを使ってやれば問題ない。
ただし、複数イベントの登録が面倒になる。
(onsubmit は function を 1 つしか登録できない)

解決手段② __doPostBack 上書き

javascript は関数の上書きができるので、__doPostBack を jQuery.submit(handler) を考慮する関数で上書きする。

[__doPostBack 上書き]

if( typeof __doPostBack === 'function' && typeof theForm === 'object' ){
    var __doPostBackOriginal = __doPostBack;
    __doPostBack = function(eventTarget, eventArgument){
        if( $(theForm).triggerHandler('submit') !== false ){
            // イベントが登録されてない場合は undefined が返ってくるので厳密比較
            // イベントで false を返せば、サブミットは実行されない
            __doPostBackOriginal(eventTarget, eventArgument);
        }
    };
}

注意事項として、上記は event.preventDefault には対応してない。
event オブジェクトはクロスブラウザ対応が面倒なので・・・

また、aspx は <form runat=server> をページ内に 1 つしか定義できないため、theForm が複数あるなどの考慮は不要。

解決手段③ form.submit 上書き

上記と似ているが、今度は大元である form.submit を「サブミットボタン押下」の挙動に変更する。
form.submit では登録したイベントは発生しないが、HTMLElement.click を使ってボタン押下をシミュレートすると、イベントを発生させることができる。

[form.submit 上書き]

// 全 form 対象
[].forEach.call(document.forms, function(f){
    f.submitOriginal = f.submit;
    f.submit = function(){
        // 非表示のサブミットボタンを追加 -> 押下 -> 削除
        $('<input type="submit" name="_適当な重複しにくい名前_" style="display:none" />')
        .appendTo(f).click().remove();
    };
});

注意事項として、onsubmit プロパティを使用している場合は、__doPostBack を使うとイベントが 2 回呼ばれてしまう。
jQuery.submit() でサブミットした場合も、jQuery で登録したイベントが 2 回呼ばれてしまう。
なので、これらは使わないようにするしかない。

副作用は大きいが、form.submit でイベントが発生して欲しい場合は、便利かもしれない。
逆に、イベントが発生しないサブミットは、form.submitOriginal を使用する。

2014年7月28日

javascript 引数の無名関数内のみで使える関数

Jaml などのHTML ジェネレータを調べてる際に気付いたことだが、javascript では、引数に渡す無名関数内のみに関数を公開する方法がある。

[Jaml のサンプルコード]

Jaml.register('sample', function(){
    div(
        h1('サンプル'),
        p('Jaml です。'),
        br()
    );
});

上記サンプルの無名関数内で呼び出している div や h1 などの関数は、外部に公開されていない(グローバル関数ではない)。
どうやって実現しているのかさっぱり分からなかったので、Jaml のソースを拝見。
div や h1 は Jaml.Template の prototype 関数で、with と eval で実現できる模様。

[実現コード]

//[定義]
var Test = {
    console : function(callback){
        var logger = new Logger();
        //・callback を文字列展開して eval 実行することで、
        //  callback の変数スコープをこの関数内に変更
        //・with を使って prototype 関数に直接アクセス
        with( logger ) eval('(' + callback.toString() + ').call(logger)');
    }
};
var Logger = function(){ this.count = 0; };
Logger.prototype = {
    log: function(message){
        console.log(message);
        this.count++;
    },
};

//[実行]
Test.console(function(){
    log('one');
    log('two');
    log(count);
});

//[結果]
// one
// two
// 2

prototype 関数でなければ with で囲む必要はない。
上記例で、Test.console 内に定義した関数なら、外部に公開されず、引数の callback 内でのみ使える関数となる。

おまけ

本題とはあんまり関係ないけど、Jaml から eval と with を除いたもの。
Jaml.register に渡す無名関数の第一引数に Jaml.Template インスタンスが渡されるので、そこから div や h1 などのメソッドを使う。