最終回
この連載も今回が最終回です。
単に季節物のPrototypeライブラリを解説する、
「あとで読む」
では、
Abstract.TimedObserver オブジェクト
2846: Abstract.TimedObserver = function() {}
2847: Abstract.TimedObserver.prototype = {
2848: initialize: function(element, frequency, callback) {
2849: this.frequency = frequency;
2850: this.element = $(element);
2851: this.callback = callback;
2852:
2853: this.lastValue = this.getValue();
2854: this.registerCallback();
2855: },
2856:
2846行目からはAbstract.
Form.
よって、
コンストラクタの中身は、
2857: registerCallback: function() {
2858: setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
2859: },
2860:
そのregisterCallback()です。前述したように、
2861: onTimerEvent: function() {
2862: var value = this.getValue();
2863: var changed = ('string' == typeof this.lastValue && 'string' == typeof value
2864: ? this.lastValue != value : String(this.lastValue) != String(value));
2865: if (changed) {
2866: this.callback(this.element, value);
2867: this.lastValue = value;
2868: }
2869: }
2870: }
2871:
2861行目からはタイマーイベントごとに呼ばれるonTimerEvent()メソッドです。
まずgetValue()で最新の値を取得します。Form.
あとは変化した場合にコンストラクタから渡されたコールバック関数を呼び、
Form.Element.Observer クラス
2872: Form.Element.Observer = Class.create();
2873: Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
2874: getValue: function() {
2875: return Form.Element.getValue(this.element);
2876: }
2877: });
2878:
2872行目からはForm.
フォームの入力要素を定期的に監視し、
実装はClass.
getValue()メソッドでは、
Form.Observer クラス
2879: Form.Observer = Class.create();
2880: Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
2881: getValue: function() {
2882: return Form.serialize(this.element);
2883: }
2884: });
2885:
2886: /*--------------------------------------------------------------------------*/
2887:
2879行目からはForm.
Form.
Abstract.EventObserver オブジェクト
2888: Abstract.EventObserver = function() {}
2889: Abstract.EventObserver.prototype = {
2890: initialize: function(element, callback) {
2891: this.element = $(element);
2892: this.callback = callback;
2893:
2894: this.lastValue = this.getValue();
2895: if (this.element.tagName.toLowerCase() == 'form')
2896: this.registerFormCallbacks();
2897: else
2898: this.registerCallback(this.element);
2899: },
2900:
2888行目からはAbstract.
コンストラクタでは、
個人的にはここはif文で分けるのではなく、
2901: onElementEvent: function() {
2902: var value = this.getValue();
2903: if (this.lastValue != value) {
2904: this.callback(this.element, value);
2905: this.lastValue = value;
2906: }
2907: },
2908:
2901行目からはonElementEvent()です。
これは各要素の'click'/'change'イベントハンドラとして実際に登録される関数で、
Abstract.
2909: registerFormCallbacks: function() {
2910: Form.getElements(this.element).each(this.registerCallback.bind(this));
2911: },
2912:
2909行目では、
Form.
2913: registerCallback: function(element) {
2914: if (element.type) {
2915: switch (element.type.toLowerCase()) {
2916: case 'checkbox':
2917: case 'radio':
2918: Event.observe(element, 'click', this.onElementEvent.bind(this));
2919: break;
2920: default:
2921: Event.observe(element, 'change', this.onElementEvent.bind(this));
2922: break;
2923: }
2924: }
2925: }
2926: }
2927:
2913行目からはregisterCallback()です。要素に対して必要なイベントハンドラを設定します。
type='checkbox', type='radio'の時だけonClickイベントを対象にし、
Form.Element.EventObserver クラス
2928: Form.Element.EventObserver = Class.create();
2929: Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
2930: getValue: function() {
2931: return Form.Element.getValue(this.element);
2932: }
2933: });
2934:
2928行目からはForm.
Form.
Form.EventObserver クラス
2935: Form.EventObserver = Class.create();
2936: Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
2937: getValue: function() {
2938: return Form.serialize(this.element);
2939: }
2940: });
Form.
Event オブジェクトへの拡張
2941: if (!window.Event) {
2942: var Event = new Object();
2943: }
2944:
2941行目からはEventオブジェクトです。IEではグローバルオブジェクトとしてEventが存在しますが、
2945: Object.extend(Event, {
2946: KEY_BACKSPACE: 8,
2947: KEY_TAB: 9,
2948: KEY_RETURN: 13,
2949: KEY_ESC: 27,
2950: KEY_LEFT: 37,
2951: KEY_UP: 38,
2952: KEY_RIGHT: 39,
2953: KEY_DOWN: 40,
2954: KEY_DELETE: 46,
2955: KEY_HOME: 36,
2956: KEY_END: 35,
2957: KEY_PAGEUP: 33,
2958: KEY_PAGEDOWN: 34,
2959:
2945行目からはObject.
まずはキーコードを名前で参照するために数値との対応を定義しています。
2960: element: function(event) {
2961: return $(event.target || event.srcElement);
2962: },
2963:
2960行目からはEvent.
イベントが発生した要素を返します。IE系はsrcElementに、
2964: isLeftClick: function(event) {
2965: return (((event.which) && (event.which == 1)) ||
2966: ((event.button) && (event.button == 1)));
2967: },
2968:
2964行目からはisLeftClick()です。
Mozilla系ではwhichプロパティに1が入っていれば左クリック、
したがって、
2969: pointerX: function(event) {
2970: return event.pageX || (event.clientX +
2971: (document.documentElement.scrollLeft || document.body.scrollLeft));
2972: },
2973:
2974: pointerY: function(event) {
2975: return event.pageY || (event.clientY +
2976: (document.documentElement.scrollTop || document.body.scrollTop));
2977: },
2978:
pointerX(), pointerY()関数です。イベントが発生した場所の、
Mozilla系ではevent.
IE系では、
2979: stop: function(event) {
2980: if (event.preventDefault) {
2981: event.preventDefault();
2982: event.stopPropagation();
2983: } else {
2984: event.returnValue = false;
2985: event.cancelBubble = true;
2986: }
2987: },
2988:
イベントキャンセル用のEvent.
Mozilla, Safari、
一方IEでは、
- DOM2 Events - 1.
4. Event interface - MDC - DOM:event.
preventDefault - DOM3 Events - 1.
2 DOM event flow - MSDN - event.
returnValue - MSDN - event.
cancelBubble
2989: // find the first node with the given tagName, starting from the
2990: // node the event was triggered on; traverses the DOM upwards
2991: findElement: function(event, tagName) {
2992: var element = Event.element(event);
2993: while (element.parentNode && (!element.tagName ||
2994: (element.tagName.toUpperCase() != tagName.toUpperCase())))
2995: element = element.parentNode;
2996: return element;
2997: },
2998:
2991行目からはEvent.
イベントターゲットの要素ではなく、
while文でparentNodeがnullの場合は、
2999: observers: false,
3000:
observersは初期値はfalseですが、
3001: _observeAndCache: function(element, name, observer, useCapture) {
3002: if (!this.observers) this.observers = [];
3003: if (element.addEventListener) {
3004: this.observers.push([element, name, observer, useCapture]);
3005: element.addEventListener(name, observer, useCapture);
3006: } else if (element.attachEvent) {
3007: this.observers.push([element, name, observer, useCapture]);
3008: element.attachEvent('on' + name, observer);
3009: }
3010: },
3011:
Event.
IEとそれ以外ではイベントの登録方法が違うので、
また、
3012: unloadCache: function() {
3013: if (!Event.observers) return;
3014: for (var i = 0, length = Event.observers.length; i < length; i++) {
3015: Event.stopObserving.apply(this, Event.observers[i]);
3016: Event.observers[i][0] = null;
3017: }
3018: Event.observers = false;
3019: },
3020:
Event.
IEで動的にイベントを追加すると、
3016行目では、
3021: observe: function(element, name, observer, useCapture) {
3022: element = $(element);
3023: useCapture = useCapture || false;
3024:
3025: if (name == 'keypress' &&
3026: (Prototype.Browser.WebKit || element.attachEvent))
3027: name = 'keydown';
3028:
3029: Event._observeAndCache(element, name, observer, useCapture);
3030: },
3031:
3021行目からはEvent.
引数useCaptureが省略された場合はundefinedになるので、
あとは受け取った引数をEvent._observeAndCache()に渡してイベント登録して終了です。
このobserve()と次のstopObserving()でのkeypress, keydownイベントの扱いは、
下記リンクに記載されている修正でいいかと思いますが、
- prototype.
jsでの、keypressイベントの扱いについて - Enjoy*Study 「Operaを使つてゐる場合、Prototype. jsは、keypressをkeydownに置換へてしまふ」 件をどうにかしてみた - 山岸めも
3032: stopObserving: function(element, name, observer, useCapture) {
3033: element = $(element);
3034: useCapture = useCapture || false;
3035:
3036: if (name == 'keypress' &&
3037: (Prototype.Browser.WebKit || element.attachEvent))
3038: name = 'keydown';
3039:
3040: if (element.removeEventListener) {
3041: element.removeEventListener(name, observer, useCapture);
3042: } else if (element.detachEvent) {
3043: try {
3044: element.detachEvent('on' + name, observer);
3045: } catch (e) {}
3046: }
3047: }
3048: });
3049:
3032行目からはstopObserving()です。
observe()と同様に引数のuseCapture, nameを整理して、
detachEvent()がtry {}で括られているのは、
3050: /* prevent memory leaks in IE */
3051: if (Prototype.Browser.IE)
3052: Event.observe(window, 'unload', Event.unloadCache, false);
ブラウザがIEの時のみ、
Position オブジェクト
3053: var Position = {
3054: // set to true if needed, warning: firefox performance problems
3055: // NOT neeeded for page scrolling, only if draggable contained in
3056: // scrollable elements
3057: includeScrollOffsets: false,
3058:
最後のオブジェクトPositionです。特にクラス化はしておらず、
また、
3057行目のincludeScrollOffsetsはbooleanで、
3059: // must be called before calling withinIncludingScrolloffset, every time the
3060: // page is scrolled
3061: prepare: function() {
3062: this.deltaX = window.pageXOffset
3063: || document.documentElement.scrollLeft
3064: || document.body.scrollLeft
3065: || 0;
3066: this.deltaY = window.pageYOffset
3067: || document.documentElement.scrollTop
3068: || document.body.scrollTop
3069: || 0;
3070: },
3071:
3061行目からはPosition.
Position.
求めたいのはページ自体のスクロール量なのですが、
deltaYの方も同様です。
3072: realOffset: function(element) {
3073: var valueT = 0, valueL = 0;
3074: do {
3075: valueT += element.scrollTop || 0;
3076: valueL += element.scrollLeft || 0;
3077: element = element.parentNode;
3078: } while (element);
3079: return [valueL, valueT];
3080: },
3081:
3072行目からはrealOffset()です。積み重なったスクロール可能なオブジェクトの階層を遡って、
対象となる要素から、
3082: cumulativeOffset: function(element) {
3083: var valueT = 0, valueL = 0;
3084: do {
3085: valueT += element.offsetTop || 0;
3086: valueL += element.offsetLeft || 0;
3087: element = element.offsetParent;
3088: } while (element);
3089: return [valueL, valueT];
3090: },
3091:
3082行目からはcumulativeOffset()です。ドキュメント基点からのオフセットを[X, Y]の配列で返します。
実装はrealOffset()と似ていますが、
Safariでは<body>の子供がposition:absoluteの時に正しい値を返さないため、
3092: positionedOffset: function(element) {
3093: var valueT = 0, valueL = 0;
3094: do {
3095: valueT += element.offsetTop || 0;
3096: valueL += element.offsetLeft || 0;
3097: element = element.offsetParent;
3098: if (element) {
3099: if(element.tagName=='BODY') break;
3100: var p = Element.getStyle(element, 'position');
3101: if (p == 'relative' || p == 'absolute') break;
3102: }
3103: } while (element);
3104: return [valueL, valueT];
3105: },
3106:
3092行目からはpositionedOffset()です。
cumulativeOffset()と似ていますが、
3107: offsetParent: function(element) {
3108: if (element.offsetParent) return element.offsetParent;
3109: if (element == document.body) return element;
3110:
3111: while ((element = element.parentNode) && element != document.body)
3112: if (Element.getStyle(element, 'position') != 'static')
3113: return element;
3114:
3115: return document.body;
3116: },
3117:
offsetParent プロパティをクロスブラウザ化したものです。CSSでのcontainer blockを求めるのに使います。
もし渡された要素がoffsetParentを持っていればそのまま返し、
あとはparentNodeを辿っていき、
それ以外の場合はdocument.
3118: // caches x/y coordinate pair to use with overlap
3119: within: function(element, x, y) {
3120: if (this.includeScrollOffsets)
3121: return this.withinIncludingScrolloffsets(element, x, y);
3122: this.xcomp = x;
3123: this.ycomp = y;
3124: this.offset = this.cumulativeOffset(element);
3125:
3126: return (y >= this.offset[1] &&
3127: y < this.offset[1] + element.offsetHeight &&
3128: x >= this.offset[0] &&
3129: x < this.offset[0] + element.offsetWidth);
3130: },
3131:
3118行目からはwithin()です。
Position.
そうでなければ、
this.
3132: withinIncludingScrolloffsets: function(element, x, y) {
3133: var offsetcache = this.realOffset(element);
3134:
3135: this.xcomp = x + offsetcache[0] - this.deltaX;
3136: this.ycomp = y + offsetcache[1] - this.deltaY;
3137: this.offset = this.cumulativeOffset(element);
3138:
3139: return (this.ycomp >= this.offset[1] &&
3140: this.ycomp < this.offset[1] + element.offsetHeight &&
3141: this.xcomp >= this.offset[0] &&
3142: this.xcomp < this.offset[0] + element.offsetWidth);
3143: },
3144:
3132行目からはwithinIncludingScrolloffsets()です。対象要素の上位にスクロールする要素がいる場合にwithin()の代わりに使います。
within()と異なるのは、
結果として、
これらの値と、
3145: // within must be called directly before
3146: overlap: function(mode, element) {
3147: if (!mode) return 0;
3148: if (mode == 'vertical')
3149: return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
3150: element.offsetHeight;
3151: if (mode == 'horizontal')
3152: return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
3153: element.offsetWidth;
3154: },
3155:
3146行目からはoverlap()関数です。
within()で設定されたthis.
modeがverticalかhorizontalかによって分岐していますが、
たとえばmodeが'vertical'の場合、
具体的には、
modeが'horizontal'の場合はX軸が対象となります。
3156: page: function(forElement) {
3157: var valueT = 0, valueL = 0;
3158:
3159: var element = forElement;
3160: do {
3161: valueT += element.offsetTop || 0;
3162: valueL += element.offsetLeft || 0;
3163:
3164: // Safari fix
3165: if (element.offsetParent == document.body)
3166: if (Element.getStyle(element,'position')=='absolute') break;
3167:
3168: } while (element = element.offsetParent);
3169:
3170: element = forElement;
3171: do {
3172: if (!window.opera || element.tagName=='BODY') {
3173: valueT -= element.scrollTop || 0;
3174: valueL -= element.scrollLeft || 0;
3175: }
3176: } while (element = element.parentNode);
3177:
3178: return [valueL, valueT];
3179: },
3180:
3156行目からはpage()です。指定された要素の、
まず、
途中、
次に、
3181: clone: function(source, target) {
3182: var options = Object.extend({
3183: setLeft: true,
3184: setTop: true,
3185: setWidth: true,
3186: setHeight: true,
3187: offsetTop: 0,
3188: offsetLeft: 0
3189: }, arguments[2] || {})
3190:
3191: // find page position of source
3192: source = $(source);
3193: var p = Position.page(source);
3194:
3195: // find coordinate system to use
3196: target = $(target);
3197: var delta = [0, 0];
3198: var parent = null;
3199: // delta [0,0] will do fine with position: fixed elements,
3200: // position:absolute needs offsetParent deltas
3201: if (Element.getStyle(target,'position') == 'absolute') {
3202: parent = Position.offsetParent(target);
3203: delta = Position.page(parent);
3204: }
3205:
3206: // correct by body offsets (fixes Safari)
3207: if (parent == document.body) {
3208: delta[0] -= document.body.offsetLeft;
3209: delta[1] -= document.body.offsetTop;
3210: }
3211:
3212: // set position
3213: if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
3214: if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
3215: if(options.setWidth) target.style.width = source.offsetWidth + 'px';
3216: if(options.setHeight) target.style.height = source.offsetHeight + 'px';
3217: },
3218:
3181行目からはclone()関数です。source要素の位置にtarget要素を移動します。
まずoptionsに、
target要素がposition: absoluteの場合、
この場合、
あとは、
3219: absolutize: function(element) {
3220: element = $(element);
3221: if (element.style.position == 'absolute') return;
3222: Position.prepare();
3223:
3224: var offsets = Position.positionedOffset(element);
3225: var top = offsets[1];
3226: var left = offsets[0];
3227: var width = element.clientWidth;
3228: var height = element.clientHeight;
3229:
3230: element._originalLeft = left - parseFloat(element.style.left || 0);
3231: element._originalTop = top - parseFloat(element.style.top || 0);
3232: element._originalWidth = element.style.width;
3233: element._originalHeight = element.style.height;
3234:
3235: element.style.position = 'absolute';
3236: element.style.top = top + 'px';
3237: element.style.left = left + 'px';
3238: element.style.width = width + 'px';
3239: element.style.height = height + 'px';
3240: },
3241:
3129行目からはabsolutize()です。ページレイアウトを変更せずに、
absoluteにすると直近のstatic 以外の親が基準になるので、
あとはaboslute 用に実際に設定する値を用意し、
3242: relativize: function(element) {
3243: element = $(element);
3244: if (element.style.position == 'relative') return;
3245: Position.prepare();
3246:
3247: element.style.position = 'relative';
3248: var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
3249: var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
3250:
3251: element.style.top = top + 'px';
3252: element.style.left = left + 'px';
3253: element.style.height = element._originalHeight;
3254: element.style.width = element._originalWidth;
3255: }
3256: }
3257:
3242行目からはrelativize()です。absolutize()でposition: absoluteに変更したものをrelativeに戻します。
absolutize()でも書いたように、
3258: // Safari returns margins on body which is incorrect if the child is absolutely
3259: // positioned. For performance reasons, redefine Position.cumulativeOffset for
3260: // KHTML/WebKit only.
3261: if (Prototype.Browser.WebKit) {
3262: Position.cumulativeOffset = function(element) {
3263: var valueT = 0, valueL = 0;
3264: do {
3265: valueT += element.offsetTop || 0;
3266: valueL += element.offsetLeft || 0;
3267: if (element.offsetParent == document.body)
3268: if (Element.getStyle(element, 'position') == 'absolute') break;
3269:
3270: element = element.offsetParent;
3271: } while (element);
3272:
3273: return [valueL, valueT];
3274: }
3275: }
3276:
3261行目からは、
Safariはbodyの子要素がpositon:absoluteの時に、
3277: Element.addMethods();
最後に、
Element, Element.