mediawiki-extensions-Visual.../modules/ve/ui/elements/ve.ui.ClippableElement.js
Trevor Parscal 9303648406 The amazing mystery of scrollTop and onscroll
What I learned today:
* Window doesn't have a scrollTop property, body does (that's why animate
  doesn't work on window)
* jQuery.scrollTop() doesn't work on body (in firefox) but works on
  window everywhere
* jQuery.scrollTop() uses scroll offset, not the scrollTop property
* Body doesn't have an onscroll event, window does

What I really learned today:
* Browsers are very poorly designed

Objective:
* Make clippable elements properly resize in Firefox when scrolled

Diagnosis:
* Scroll events were not being emitted from the scrollable container
  after the merge of Ifec0dae598f7fd99270588bd8ca77777a07e9669 because
  such events are not emitted from body tags, only scrollable divs and
  windows
* jQuery.scrollTop was giving incorrect values when called on the body
  instead of the window, so also due to the aforementioned change, the
  clipping was being calculated incorrectly

Treatment:
* Add $clippableScroller property, which is either a scrollable div or
  the window (could this have side-effects if someone did something
  ridiculous like made the body absolutely positioned and overflow:auto?
  Yes, but I have no other option and that's a strange edge case don't
  you think?)
* Use $clippableScroller for listening to scroll events and getting the
  scrollTop value from jQuery

Bug: 55343
Change-Id: I819aba60b200059886b347115fda437b3dc9cb7a
2013-10-07 13:25:02 -07:00

134 lines
4.1 KiB
JavaScript

/*!
* VisualEditor UserInterface ClippableElement class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Clippable element.
*
* @class
* @abstract
*
* @constructor
* @param {jQuery} $clippable Group element
*/
ve.ui.ClippableElement = function VeUiClippableElement( $clippable ) {
// Properties
this.$clippable = $clippable;
this.clipping = false;
this.clipped = false;
this.$clippableContainer = null;
this.$clippableScroller = null;
this.$clippableWindow = null;
this.onClippableContainerScrollHandler = ve.bind( this.clip, this );
this.onClippableWindowResizeHandler = ve.bind( this.clip, this );
// Initialization
this.$clippable.addClass( 've-ui-clippableElement-clippable' );
};
/* Methods */
/**
* Set clipping.
*
* @method
* @param {boolean} value Enable clipping
* @chainable
*/
ve.ui.ClippableElement.prototype.setClipping = function ( value ) {
value = !!value;
if ( this.clipping !== value ) {
this.clipping = value;
if ( this.clipping ) {
this.$clippableContainer = this.$$( this.getClosestScrollableElementContainer() );
// If the clippable container is the body, we have to listen to scroll events and check
// jQuery.scrollTop on the window because of browser inconsistencies
this.$clippableScroller = this.$clippableContainer.is( 'body' ) ?
this.$$( ve.Element.getWindow( this.$clippableContainer ) ) :
this.$clippableContainer;
this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
this.$clippableWindow = this.$$( this.getElementWindow() )
.on( 'resize', this.onClippableWindowResizeHandler );
// Initial clip after visible
setTimeout( ve.bind( this.clip, this ) );
} else {
this.$clippableContainer = null;
this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
this.$clippableScroller = null;
this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
this.$clippableWindow = null;
}
}
return this;
};
/**
* Check if the element will be clipped to fit the visible area of the nearest scrollable container.
*
* @method
* @return {boolean} Element will be clipped to the visible area
*/
ve.ui.ClippableElement.prototype.isClipping = function () {
return this.clipping;
};
/**
* Check if the bottom or right of the element is being clipped by the nearest scrollable container.
*
* @method
* @return {boolean} Part of the element is being clipped
*/
ve.ui.ClippableElement.prototype.isClipped = function () {
return this.clipped;
};
/**
* Clip element to visible boundaries and allow scrolling when needed.
*
* Element will be clipped the bottom or right of the element is within 10px of the edge of, or
* overlapped by, the visible area of the nearest scrollable container.
*
* @method
* @chainable
*/
ve.ui.ClippableElement.prototype.clip = function () {
if ( !this.clipping ) {
// this.$clippableContainer and this.$clippableWindow are null, so the below will fail
return this;
}
var buffer = 10,
cOffset = this.$clippable.offset(),
ccOffset = this.$clippableContainer.offset() || { 'top': 0, 'left': 0 },
ccHeight = this.$clippableContainer.innerHeight() - buffer,
ccWidth = this.$clippableContainer.innerWidth() - buffer,
scrollTop = this.$clippableScroller.scrollTop(),
scrollLeft = this.$clippableScroller.scrollLeft(),
desiredWidth = ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
naturalWidth = this.$clippable.prop( 'scrollWidth' ),
naturalHeight = this.$clippable.prop( 'scrollHeight' ),
clipWidth = desiredWidth < naturalWidth,
clipHeight = desiredHeight < naturalHeight;
if ( clipWidth ) {
this.$clippable.css( { 'overflow-x': 'auto', 'width': desiredWidth } );
} else {
this.$clippable.css( { 'overflow-x': '', 'width': '' } );
}
if ( clipHeight ) {
this.$clippable.css( { 'overflow-y': 'auto', 'height': desiredHeight } );
} else {
this.$clippable.css( { 'overflow-y': '', 'height': '' } );
}
this.clipped = clipWidth || clipHeight;
return this;
};