jQuery exFixed を動的な位置移動やリサイズ処理に対応させてみた

更新履歴

2010-11-14
exFixed Ver 1.3.0 ベースの記述に変更しました。
2009-10-16
本ページのサンプルソースjQuery.exFixed.js の Ver 1.2.0 に対応した記述に差し替えました。詳細は以下をご覧下さい。

一ヶ月以上間のあいた久しぶりの更新ですが、今回もしつこく IE6 の position : fixed ネタです。

最近では HTML でリファレンスマニュアルなどを作る機会もあり、position : fixed で目次一覧を固定表示させたりしてます。そんな時 exFixed の出番となるわけですが、固定表示させた目次エリアに対し、さらにリサイズや位置調整機能などを付加しようとするといろいろと問題が生じます。
今回はそれらの問題を解決し、exFixed で動的な位置移動やリサイズ処理に対応させる方法について考えてみました。(以下の Demo のようなことができるようになります)

Demo

exFixed で位置移動やリサイズを行なおうとした場合の問題点


exFixed により固定表示された要素に対し、位置移動やリサイズを行なおうとした場合、以下のような問題が発生します。

top と left を指定した position : fixed の場合

  • a) fixed 要素の位置移動後に window をスクロールさせると配置位置が元に戻ってしまう
  • b) fixed 要素のサイズ変更後に window のサイズを変更するとサイズが元に戻ってしまう


bottom と right を指定した position : fixed の場合

  • c) fixed 要素の位置移動ができない
  • d) fixed 要素のサイズ変更をすると、top / left 位置を軸にサイズ変更されしまう


サンプルページ

原因と対策


原因と対策を考えてみます。

a) fixed 要素の位置移動後に window をスクロールさせると配置位置が元に戻ってしまう


原因は以下のように expression ベースで固定配置を実現してるため。

element.style.setExpression('top', 'this.exFixedTop+(document.body.scrollTop||document.documentElement.scrollTop) + "px"');

対策としては、移動後の位置座標で再度、expression の指定をし直す必要がある。

b) fixed 要素のサイズ変更後に window のサイズを変更するとサイズが元に戻ってしまう


fixed 要素のサイズ・位置指定が(%)単位の場合、window の width / height に対して実寸を求める必要がある。そのため expression で位置変更される前の初期位置の値を保持しておき、window リサイズにはこの値を基準に算出した実寸値を fixed 要素に適用するという実装になっているので、サイズ変更をしてたとしても、元サイズに戻ってしまう。
対策としては、位置移動やリサイズがあった場合は、初期設定値の値を更新する必要がある。

c) fixed 要素の位置移動ができない

上記エントリで触れてるとおり、スクロール時のガタつきを無くすには top / left ベースで配置する必要があるので、exFixed の実装では bottom / right で指定された場合は、top / left ベースに置換して配置するようにしている。固定位置の配置処理は expression ベースで行なっているため、後から css メソッド等で bottom / right ベースで位置移動しようとしても、expression による top / left ベースの位置指定の方が強く作用し、位置移動ができない。

対策としては以下の2つが考えられる。

top / leftベースに置換しない方法


top / left 方式のように expression による位置調整は行わず、下記のような filter 指定で固定表示を実現する。

element.style.setExpression('', 'this.style.filter=""');


たったこれだけで bottom / right ベースな position:fixed が実現できてしまう!ガタつきの方も多少ちらつく程度!

サンプルページ

注意点としては、上記のような記述だと $(element).css('opacity',0.5) 等の透過指定が無効になってしまうということ。透過指定を使用したい場合は下記のように filter で指定する必要がある。

element.setExpression('', 'this.style.filter="alpha(opacity=50 ,finishopacity=50 ,style=1)"');

リサイズや位置移動の際にのみ right / bottom 配置に戻す方法


前述の方法だと多少とはいえスクロール時にちらつきが発生するので、リサイズや位置移動をさせないケースなどでは性能劣化に等しい。
なので基本 top / left 置換方式でガタつきを抑止しておき、リサイズや位置移動の開始時に bottom / right 配置に変換し、処置完了後に top / left 置換方式に戻すという方法がより良い。
ネックなのは「処置開始前後に配置変換処理の呼び出しを忘れず記述しなければならない」というコーディングルールをプラグイン利用者に強要してしまうこと。

d) fixed 要素のサイズ変更をすると、top / left 位置を軸にサイズ変更されしまう


これは c) で述べてる通り top / left 置換方式を採用してるため。c) が解決すれば d) も解決する。

exFixed を動的な位置移動やリサイズ処理に対応させてみた

exFixed で動的な位置移動やリサイズ処理を行なうには、以下の前提があります。

  • プラグイン実行時に、動的な位置移動やリサイズ処理を行なうことを宣言する
  • プラグイン実行時に、最初の固定位置を指定する
  • 位置移動やリサイズは、API オブジェクト経由で行う

具体的には以下のように記述します。

var api = $('div.fixed').exFixed({
    api : true,
    dynamicFixed : true,
    right : 100,
    bottom : 100
});

api パラメータに true を指定すると exFixed() メソッドの返却値が API オブジェクトになります。また、dynamicFixed パラメータを指定することで、動的な位置移動やリサイズ処理が行なえるようになります。
ちなに API オブジェクトは jQuery exAPI という jQuery プラグインをベースに生成してます。API オブジェクトの基本的な使い方については、以下を参照ください。

jQuery ライクなプラグイン API の定義方法を考えてみる - Cyokodog::Diary

単純な リサイズ / 位置移動

api.getTarget().click(function(){
    api.fixedOpen(function(){
        api.fixedClose({
            right : '25%',
            bottom : '25%',
            width : '50%',
            height : '50%'
        });
    }) 
})

getTarget() メソッドで exFixed の適用対象となった要素が取得できます。例では対象要素をクリック時に、リサイズと位置移動を行なっています。fixedOpen() メソッドで fixed 要素を変更可能な状態にし、fixedClose() メソッドで fixed 要素の位置、サイズを確定し、変更可能状態を閉じます。

Demo

animte メソッドを併用する

api.getTarget().click(function(){
    api.fixedOpen(function(){
        var size={
            right : '25%',
            bottom : '25%',
            width : '50%',
            height : '50%'
        };
        api.getTarget().animate(api.getFixedSize(size),function(){
            api.fixedClose(size);
        });
    }) 
})

getFixedSize() メソッドを使用し animate メソッド用に実寸値を求めます。fixed 要素の変更処理が終わるタイミングで fixedClose() メソッドを実行する必要があるので、animate のコールバック関数内で fixedClose() メソッドを実行してます。fixedClose() メソッドには論理値サイズを渡します。

Demo

複数要素に対して処理する(CSS でサイズ指定)
#a{
    width:250px;
    height:250px;
}
#b{
    width:200px;
    height:200px;
}
#c{
    width:150px;
    height:150px;
}
#d{
    width:100px;
    height:100px;
}
$('#a,#b,#c,#d').exFixed({
    api : true,
    dynamicFixed : true,
    right : 100,
    bottom : 100
})
.each(function(idx,api){
    api.getTarget()
        .css('z-index',idx)
        .click(function(){
            api.fixedOpen(function(){
            var size = {
                right : '25%',
                bottom : (idx * 25) + '%',
                width : '50%',
                height : '24%'
            };
            api.getTarget().animate(api.getFixedSize(size),function(){
                api.fixedClose(size);
            });
        })
    })
})

each() メソッドで 各要素が個別に保有する API オブジェクトを順次読み込みしています。

Demo

複数要素に対して処理する(パラメータ でサイズ指定)

$('#a,#b,#c,#d').each(function(idx){
    var api = $(this).css( 'z-index' , idx ).exFixed({
        api : true,
        dynamicFixed : true,
        width : 250 - ( idx * 50 ),
        height : 250 - ( idx * 50 ),
        right : 100 + ( idx * 25 ),
        bottom : 100 + ( idx * 25 )
    });
    api.getTarget().click( function(){
        api.fixedOpen( function(){
            var size = {
                right : '25%',
                bottom : ( idx * 25 ) + '%',
                width : '50%',
                height : '24%'
            }
            api.getTarget().animate( api.getFixedSize(size) , function(){
                api.fixedClose( size );
            });
        })
    });
});


Demo

再クリックで元の位置に戻るようにする

$('#a,#b,#c,#d').each(function(idx){
    var orgSize;
    var size = orgSize = {
        width : 250 - (idx * 50),
        height : 250 - (idx * 50),
        right : 100 + (idx * 25),
        bottom : 100 + (idx * 25)
    }
    var api = $(this).css('z-index', idx).exFixed($.extend({
        api : true,
        dynamicFixed : true
    },size));
    api.getTarget().click(function(){
        api.fixedOpen(function(){
            size = (size == orgSize ? {
                right : '25%',
                bottom : (idx * 25) + '%',
                width : '50%',
                height : '24%'
            } : orgSize);
            api.getTarget().animate(api.getFixedSize(size), function(){
                api.fixedClose(size);
            });
        })
    });
});

初期値をキャッシュしておきクリックの都度、配置位置を切り替えてます。

Demo

四隅に配置したパターン

$('#a,#b,#c,#d').each(function(idx){
    var orgCss = {}, newCss = {};
    var hPos = (idx % 2) ? 'right' : 'left';
    var vPos = (idx < 2) ? 'top' : 'bottom';

    orgCss[hPos] = orgCss[vPos] = 0;
    newCss[hPos] = newCss[vPos] = ((idx + 1) * 10 ) + '%';
    orgCss['width'] = orgCss['height'] = 100;
    newCss['width'] = newCss['height'] = (80-idx*20)+'%';

    var css = orgCss;
    var api = $(this).exFixed( $.extend({
        api : true,
        dynamicFixed : true
    }, css ));

    api.getTarget().click(function(){
        css = ( css == orgCss ? newCss : orgCss );
        api.fixedOpen(function(){
            api.getTarget().animate(api.getFixedSize(css),function(){
                api.fixedClose(css);
            });
        });
    });
});


Demo

最後に

fixed 要素の変更の際には fixedOpen() や fixedClose() 、値の指定には fixedSize()メソッドをとコーディングに制約が付きますが、クロスブラウザな記述を考慮する必要がなくなるので、動的に position:fixed を扱いたい場合はそれなりに便利かと思います。