IE6 向け position:fixed を right , bottom 指定にも対応させてみる

更新履歴

2010-01-21
スクロール時のガタつきをはじめとする諸問題等を解決した精度の高い fixed を使いたい場合はこちらをご覧ください。

前回のつづきです。

right , bottom 指定の対応

前回のロジックでは right , bottom 指定に対応してなかったので、こちらの対応方法についても考えてみます。
{position:absolute;bottom:0} とすると対象要素が画面表示枠の底辺に表示される事から、画面上方にスクロールアウトした高さ分を、scroll イベント内で bottom値から引けば、底辺に固定されるのでは?という予想ができます。
下記、ロジックで試してみます。

var win=$j(window).scroll(function(){
    var bottom=100-win.scrollTop();
    $j('div.bottom').css('bottom',bottom).html('bottom:'+bottom)
})

(サンプルでは scrollTop を $j(window) から取得してますが、$j(document) から取得しても同じ結果が得られるようです。)

サンプルページ

IE6 の場合のみ少しづつ下方にずれていってしまいます。
またサンプルでは、
var bottom=100-win.scrollTop()
とし 基準位置の 100px をリテラルで記述してますが、汎用性を考えると下記のように要素に適用されてる基準位置を取得したいところです。

var baseBottom = parseInt($j('div.bottom').css('bottom'))
var win=$j(window).scroll(function(){
    var bottom=baseBottom-win.scrollTop();
    $j('div.bottom').css('bottom',bottom).html(bottom)
})

サンプルページ


Firefox の場合のみ想定外の値が取得され表示位置がずれてしまいます。
例えば、Firefox で下記処理を実行すると…

<style>
body{
    margin:0;
    height:1000px
}
div.bottom{
    position:absolute;
    bottom:100px;
    height:100px;
}
</style>
<script>
jQuery(function($j){
    alert($j('div.bottom').css('top'))    //505px
    alert($j('div.bottom').height())      //100px
    alert($j('div.bottom').css('bottom')) //395px
});
</script>

という値が取得されます。
このことから、おそらく Firefox の場合のみ以下のような規則性をもって値が算出されていると思われます。

absolute 要素の bottom の値 =
[winodow要素(あるいは直近の relative な先祖要素)の height]
- [absolute 要素の top]
- [absolute 要素の height]

とりあえずここでは、IE6 向けの fixed を考えたいので、別の方法を考えてみます。

offset メソッドを使う

基本、jQuryのメソッドに頼りたいので、offset メソッドで代替できないか考えてみます。

最初の要素の、親要素からの相対的な表示位置を返します。
戻り値のオブジェクトはtopとleftの2つの数値を持ちます。この関数は、可視状態にある要素に対してのみ有効です。

offset メソッド - jQuery日本語リファレンス

offset メソッドの便利なところは、position :relative な親要素を持つ position : absolute 要素でも、document オブジェクトに対しての相対位置を返してくれることかと思います。
以下のような HTML でも、padding や border を考慮した値を返してくれます。

<style>
    body{
        margin:100px;
        border:solid 10px red;
        padding:100px;
    }
    div.relative{
        position:relative;
        left:50px;
        border:solid 10px blue;
        height:100px;
    }
    div.absolute{
        position:absolute;
        left:50px;
        background:#aaccff;
    }
</style>
<body>
    <div class="relative">
        <div>
            <div class="absolute">absolute</div>
        </div>
    </div>
</body>
<script>
jQuery(function($j){
    var abs=$j('div.absolute');
    abs.html('left : '+abs.offset().left) //left : 320
})
</script>

サンプルページ


window 表示枠からの絶対位置指定が可能なのが position:fixed の特徴で、これを position:absolute で実現させるためには下記ように親要素に relative を持たせない工夫が前提になります。

var s;target=$j('div.fixed'),relative = target.parents().filter(function(){
    var pos=$j(this).css('position');
    if(pos=='relative' || pos=='absolute')return this;
})
if(s=relative.size()>0)relative.eq(s-1).after(target)

2008.12.26 追記
上記のような処理を書かなくても offsetParent() メソッドで簡単に取得できるようです。詳しくはこちら


そういった意味では今回の実装では上記のような offset メソッドの高機能な部分はあまり活きてきません。
個人的には position:fixed も absolute と同じようにrelative要素からの相対位置に固定表示してくれた方が実用性が高いと思ってますので、別のエントリで上記の offset メソッドの機能を活かして試してみたいと思います。

話がそれましたが、要は親の relative 要素の存在を考慮した座標取得処理を考える必要が無いので、単純に以下のように記述することができます。

jQuery(function($j){
    var s,target=$j('div.fix').css('position','absolute');
    var relative = target.parents().filter(function(){
        var pos=$j(this).css('position');
        if(pos=='relative' || pos=='absolute')return this;
    })
    if(s=relative.size()>0)relative.eq(s-1).after(target)
    var base={
        top : target.offset().top,
        left : target.offset().left
    }   
    var win = $j(window).scroll(function(){
        target.css({
            'top':base.top + win.scrollTop(),
            'left':base.left + win.scrollLeft()
        })  
    })
})

サンプルページ


ブラウザをリサイズすると位置がずれますので、その辺も考慮するなら下記のように resize イベント内で表示位置の再調整をすれば良いかと思います。

jQuery(function($j){
    var s,target=$j('div.fix').css('position','absolute');
    var relative = target.parents().filter(function(){
        var pos=$j(this).css('position');
        if(pos=='relative' || pos=='absolute')return this;
    })
    if(s=relative.size()>0)relative.eq(s-1).after(target)
    var css = {
        top:target.css('top'),
        left:target.css('left'),
        right:target.css('right'),
        bottom:target.css('bottom')
    }
    var base,win;
    var setBasePos = function(){
        base={
            top : target.offset().top - (css.top=='auto'?win.scrollTop():0),
            left : target.offset().left - (css.left=='auto'?win.scrollLeft():0)
        }   
    }
    var setPos = function(){
        target.css({
            'top':base.top + win.scrollTop(),
            'left':base.left + win.scrollLeft()
        })  
    }
    win = $j(window).scroll(function(){
        setPos()
    })
    .resize(function(){
        setTimeout(function(){
            target.css(css)
            setBasePos();
            setPos()
        },1)
    })
    setBasePos();
    setPos()
})

サンプルページ


但し、上記処理だと FirefoxOpera の場合リサイズ時に表示位置がずれます。
理由は、他のブラウザでは、top 指定が省略されてる時、css('top')とすると 'auto' という文字列が取得されますが、FirefoxOpera の場合の具体的な値が取れてしまうためです。
そのため resize イベントでの表示位置の調整を正しく行うことができません。(Firefox の場合は取得される値も特殊なので大きくずれます。bottom 値の算出はできても適用スタイルが bottom ベースの指定かという判定ができない。)

FirefoxIE では現在適用されてる style の取得方法が異なりますが、Firefox 流の方法で取得してみても同様の結果になる事から、cssメソッドの処理結果もその辺の影響を受けての事なのかもしれません。

とりあえず今回やろうとしてることの趣旨は、IE6 向けの fixed なので影響はありませんが、適用されてる位置指定が top ベースなのか bottom ベースなのか(あるいは left なのか right なのか)が分からないということは、汎用ライブラリを作る際いろいろ不便がでるのではという気がします。
なにか良い方法は無いものでしょうか。

プラグイン

プラグイン化してみます。right , bottom 指定、ブラウザのリサイズによるズレ防止対応版です。

(function($j){
    $j.positionFixed = function(el){
        $j(el).each(function(){
            new fixed(this)
        })
        return el;                  
    }
    $j.fn.positionFixed = function(){
        return $j.positionFixed(this)
    }
    var fixed = $j.positionFixed.impl = function(el){
        var o=this;
        o.sts={
            target : $j(el).css('position','fixed'),
            container : $j(window)
        }
        o.sts.currentCss = {
            top : o.sts.target.css('top'),              
            right : o.sts.target.css('right'),              
            bottom : o.sts.target.css('bottom'),                
            left : o.sts.target.css('left')             
        }
        if(!o.ie6)return;
        o.bindEvent();
    }
    $j.extend(fixed.prototype,{
        ie6 : $.browser.msie && $.browser.version < 7.0,
        bindEvent : function(){
            var o=this;
            o.sts.target.css('position','absolute')
            o.overRelative().initBasePos();
            o.sts.target.css(o.sts.basePos)
            o.sts.container.scroll(o.scrollEvent()).resize(o.resizeEvent());
            o.setPos();
        },
        overRelative : function(){
            var o=this;
            var relative = o.sts.target.parents().filter(function(){
                var pos=$j(this).css('position');
                if(pos=='relative' || pos=='absolute')return this;
            })
            if(relative.size()>0)relative.after(o.sts.target)
            return o;
        },
        initBasePos : function(){
            var o=this;
            o.sts.basePos = {
                top: o.sts.target.offset().top - (o.sts.currentCss.top=='auto'?o.sts.container.scrollTop():0),
                left: o.sts.target.offset().left - (o.sts.currentCss.left=='auto'?o.sts.container.scrollLeft():0)
            }
            return o;
        },
        setPos : function(){
            var o=this;
            o.sts.target.css({
                top: o.sts.container.scrollTop() + o.sts.basePos.top,
                left: o.sts.container.scrollLeft() + o.sts.basePos.left
            })
        },
        scrollEvent : function(){
            var o=this;
            return function(){
                o.setPos();
            }
        },
        resizeEvent : function(){
            var o=this;
            return function(){
                setTimeout(function(){
                    o.sts.target.css(o.sts.currentCss)      
                    o.initBasePos();
                    o.setPos()
                },1)    
            }           
        }
    })
})(jQuery)

//実行
jQuery(function($j){
	$j('#fixed').positionFixed()
})

サンプルページ

animate メソッドによるスクロール追尾型の要素固定表示について

先にも書きましたが、window表示枠からの絶対位置指定な fixed 仕様だと利便性が悪いので、relateve 要素からの相対位置指定で実現する方法で、別エントリで考えてみたいと思います。