汎用的に使える表示位置補正メソッドを定義してみる


前回のエントリのつづきです。

見た目上の原点位置を取得するメソッドを定義する

スクロールコンテナの表示原点位置を取得する処理のみを抽出し、汎用的に活用できるメソッドを定義できないか考えてみたいと思います。

と言って終わったので、jQuery でこれを実現する方法を考えてみます。
前回の最終的なロジックの位置調整処理をみると

top:Math.max(
    scroller.scrollTop()
        -(containerPos.top-scrollerPos.top),
    c.top),

となっている事から、表示領域コンテナの見た目上の原点位置が以下の計算で求まることが分かります。

表示領域コンテナの見た目上の原点位置 = 
   表示領域コンテナのスクロール量
        - (absolute 要素にとっての コンテナの内寸位置 - 表示領域コンテナの内寸位置)

この部分の処理を抽出しメソッド化してみます。

$j.fn.startingPoint = function(viewContainer,cfg){
    var o=this;

    //キャッシュを取得
    var c=$j.extend({
        cache:true
    },o.data('startingPointCfg')||{},cfg);

    //キャッシュなし時
    if(!c.cache||!c.viewContainer){
        //擬似フレームの指定が無い場合は$j(window)を表示領域コンテナにする
        c.viewContainer=viewContainer||$j(window)

        //absolute 要素の位置を取得
        c.targetPos=o.position();

        //コンテナ要素の位置を取得
        c.container=o.exOffsetParent();
        c.containerPos=c.container.exOffsetPosition();

        //表示領域コンテナの位置を取得
        c.viewContainerPos=c.viewContainer.exOffsetPosition();

        //コンテナ要素の位置と表示領域コンテナの位置を内寸位置に変換
        c.containerPos.top+=c.container.cssInt('border-top-width');
        c.containerPos.left+=c.container.cssInt('border-left-width');
        c.viewContainerPos.top+=c.viewContainer.cssInt('border-top-width');
        c.viewContainerPos.left+=c.viewContainer.cssInt('border-left-width');

        //調整量を算出
        c.adjustPos={
            top:c.containerPos.top-c.viewContainerPos.top,
            left:c.containerPos.left-c.viewContainerPos.left
        }

        //キャッシュ
        o.data('startingPointCfg',c);
    }

    //表示領域コンテナの見た目上の原点位置を算出
    return {
        top : c.viewContainer.exScrollTop()-c.adjustPos.top,
        left : c.viewContainer.exScrollLeft()-c.adjustPos.left
    }

前回のサンプルのように scroll や hover のような連続的に発生するイベントでの使用が想定されるので、重くならないようスクロール量以外の位置情報はキャッシュするようにします。

これを前回のサンプルのスクロール追従メソッドに適用してみます。

$j.fn.syncroll_f = function(scroller){
	return this.each(function(){
		var o=$j(this);
		var c=o.position();
		(scroller=$j(scroller||window)).scroll(function(){
			o.delayEfect(function(){

				//見た目上の原点位置を求めて
				var vPos=o.startingPoint(scroller)

				//初期位置と比較して大きい方を採用
				return {
					top:Math.max(vPos.top,c.top),
					left:Math.max(vPos.left,c.left)
				}	
			})
		})
	})
}
//実行(擬似フレームのスクロールバーに追従)
$j('div.absolute').syncroll_f('div.scroller')

//実行(ブラウザのスクロールバーに追従)
$j('div.absolute').syncroll_f()

ロジックがだいぶスッキリしました。
動作の方も問題なさそうです。

サンプルページ(擬似フレームのスクロールバーに追従)
サンプルページ(ブラウザのスクロールバーに追従)

動的なDOM構成の変更などて、キャッシュを無効化したくなった場合は以下のようにします。

var vPos=o.startingPoint(scroller,{cache:false})

表示位置補正メソッドを定義してみる

ポップアップウィンドウやドロップダウンメニューの実装の際に表示位置の補正処理が必要になりますが、これらの処理に使える汎用的な表示位置補正メソッドを考えてみたいと思います。

実装としては、パラメータで指定した表示位置が表示領域コンテナからはみ出す場合、表示領域内に収まる位置に補正した値を返すというものにしてみます。

absolute 要素の右・下部分の判定

absolute 要素の右・下部分が表示領域からはみ出していないかを以下の式で判定してみます。

(absolute 要素の位置 + absolute 要素の外寸)
    - (表示領域コンテナの原点位置 + 表示領域コンテナの内寸)

値が正になる場合は absolute 要素が表示領域をはみ出してる事になるので、以下のように補正します。

absolute 要素の位置
  - Math.max(
        (absolute 要素の位置 + absolute 要素の外寸)
            - (表示領域コンテナの原点位置 + 表示領域コンテナの内寸),
    0)
absolute 要素の左・上部分の判定

補正した結果 absolute 要素の、左・上部分が隠れてしまう可能性がありますが、右・下部分より左・上部分の方を優先して表示したいケースが多いと思うので、さらに以下のように補正します。

Math.max(
    表示領域コンテナの原点位置,
        absolute 要素の位置
          - Math.max(
                (absolute 要素の位置 + absolute 要素の外寸)
                    - (表示領域コンテナの原点位置 + 表示領域コンテナの外寸),
            0)
)

メソッドを定義してみます。

$j.fn.adjustPosition = function(cfg){
	var o=this;
	var c=$j.extend({
		top:null,
		left:null,
		viewContainer:window,
		cache:true
	},cfg);
	c.viewContainer=$j(c.viewContainer);
	vPos=o.startingPoint(c.viewContainer);
	var ret={}
	if(c.top!=null){
		ret.top = Math.max(
			vPos.top,
			c.top-Math.max(
				(c.top+o.outerHeight())
					-(vPos.top+c.viewContainer.exClientHeight()),0)
		)
	}
	if(c.left!=null){
		ret.left = Math.max(
			vPos.left,
			c.left-Math.max(
				(c.left+o.outerWidth())
					-(vPos.left+c.viewContainer.exClientWidth()),0)
		)
	}
	return ret
}

以下のように補正したい位置情報を引数で渡します。

$j('div.absolute').adjustPosition({
    top:150,left:700
})

画面の表示領域に収まる位置に補正された値がハッシュ値で返ってきます。

{top:200,left:500}

表示領域要素の指定、キャッシュの無効化が指定できます。

$j('div.absolute').adjustPosition({
    top:150,left:700
    viewContainer:$j('div.scroller'),
    cache:false
})

ドロップダウンメニューの実装で使ってみる

簡単なドロップダウンメニューのプラグインを作って、補正メソッドを試してみます。

こんな html を定義して、

<div class="links">
	<a>link</a>
	<a>link</a>
	<a>link</a>
</div>
<div class="menu">
	<a>menu</a>
	<a>menu</a>
	・・・
</div>
<div class="menu">
	<a>menu</a>
	<a>menu</a>
	・・・
</div>
・・・

こうすると、

$j('div.links a').dropDownMenu('div.menu')

こうなるプラグイン

サンプルページ

ソース

$j.fn.dropDownMenu=function(menu){
	var link=this;
	(menu=$j(menu)).hide().hover(
		function(){$j(this).show();},
		function(){$j(this).hide();}
	);
	return link.hover(
		function(){
			var idx=link.index(this);
			var cLink=link.eq(idx)
			var pos=link.eq(idx).position();

			//位置調整・・・(1)
			menu.eq(idx).css({
				left:pos.left,
				top:pos.top+cLink.outerHeight()
			})		
			.show()
		},
		function(){
			menu.eq(link.index(this)).hide();
		}
	)
}

(1)の箇所で位置調整をしてますが、表示領域に対する補正をしてないので、画面右端のメニューが隠れてしまいます。

(1)の処理を、表示位置補正メソッドで位置補正するようにしてみます。

menu.eq(idx).css(
	menu.eq(idx).adjustPosition({
		left:pos.left,
		top:pos.top+cLink.outerHeight()
	})
)		

サンプルページ

IEOpera の場合のみ隠れずに表示されるようになりましたが、

FirefoxSafari がうまく表示されません。

startingPoint メソッド内の position メソッドで実行時エラーになっています。

//absolute 要素の位置を取得
c.targetPos=o.position();

対象要素が非表示の場合エラーになってしまうようですので、measur メソッドで一時的に表示状態にした後、位置を求めるような拡張メソッドで代替します。

$j.fn.exPosition = function(){
	var o=this;
	return o.measur(function(){
		if(o.attr('tagName')=='HTML'||o[0]==window||o[0]==document)
			return {top:0,left:0};
		else{
			return o.position();
		}
	})
}

サンプルページ

FirefoxSafari でも正しく表示されます。

擬似フレームの対応

以下 html のような擬似フレーム内での表示位置調整も試してみます。

<body>
    <div class="frame relative"> ←擬似フレーム
        <div class="links"></div></div>
</body>

位置補正メソッドで、表示領域コンテナに擬似フレーム要素を指定します。

menu.eq(idx).css(
	menu.eq(idx).adjustPosition({
		viewContainer:$j('div.frame'),//擬似フレームを指定
		left:pos.left,
		top:pos.top+cLink.outerHeight()
	})
)		

サンプルページ

FirefoxSafari で以下のようなズレが生じます。

startingPoint メソッド内の offsetParent メソッドで document オブジェクトを取得してしまってるようです。
(div.frame が取得されないとまずい)

//コンテナ要素の位置を取得
c.container=o.exOffsetParent(); // document がとれてしまう

原因は、対象要素(上記の o 、ここではdiv.menu)が非表示状態だと document オブジェクトをひろってしまうようです。
先程と同様に measur メソッド経由で取得するようにします。(その他の位置、サイズ取得メソッドも measur メソッド経由にした方が良さそうです)

$j.fn.exOffsetParent = function(){
	var o=this;
	return o.measur(function(){
	    var op=o.offsetParent();
	    return op.attr('tagName')=='BODY'&& op.css('position')=='static'?$j('html'):op;
	})
}

サンプルページ

これで一見うまくいってるように見えますが、

スクロール状態で hover すると表示位置がずれます。

dropDownMenu メソッド内の position メソッドの取得結果が、擬似フレームのスクロール量分減算された値になってるようです。(擬似フレームがない場合、ブラウザのスクロール量については減算されない)

var pos=link.eq(idx).position(); //擬似フレームのスクロール量分減算されてる

exPosition メソッドを以下のよう修正し、これで代替します。

$j.fn.exPosition = function(){
	var o=this;
	return o.measur(function(){
		if(o.attr('tagName')=='HTML'||o[0]==window||o[0]==document)
			return {top:0,left:0};
		else{
			var pos=o.position();
			var container = o.exOffsetParent();
			if(container.attr('tagName')=='HTML')return pos;
			return {
				top:pos.top+container.exScrollTop(),
				left:pos.left+container.exScrollLeft()
			}
		}
	})
}

サンプルページ

正しく表示されるようになりました。

relative 要素の入れ子

以下のように relative 要素を入れ子にしてみます。

<body>
    <div class="frame relative"> ←擬似フレーム & relative
        <div class="relative"> ←relative 入れ子

            relative top:50px;left:50px;

            <div class="links"></div></div>
    </div>
</body>

サンプルページ(擬似フレーム + div.relative)

IE の場合のみ微妙に位置がずれます。

relative 要素の下にある文字列(relative top:50px;left:50px;)を消すと位置が合うので、これが悪さしてるようです。
zoom:1 な div で囲ってみます。

<div style="zoom:1">
    relative top:50px;left:50px;
</div>

サンプルページ

直りました。

いろいろいじったので、最初の擬似フレームなし版も再度試してみます。

サンプルページ

問題なさそうです。

次回

ポップアップウィンドウの位置補正についても試してみたいと思います。