mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
fix(list): Fix for sticky titles in Chrome/Android. Currently broken
This commit is contained in:
@ -1,4 +1,8 @@
|
||||
import {Directive, ElementRef} from 'angular2/angular2';
|
||||
import {Directive, ElementRef, Host, Optional} from 'angular2/angular2';
|
||||
import {Content} from '../content/content';
|
||||
import {throttle} from '../../util/util';
|
||||
import {position, offset, CSS} from '../../util/dom';
|
||||
import {IonicConfig} from '../../config/config';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
@ -34,8 +38,126 @@ export class ItemGroupTitle {
|
||||
* TODO
|
||||
* @param {ElementRef} elementRef TODO
|
||||
*/
|
||||
constructor(elementRef: ElementRef) {
|
||||
constructor(elementRef: ElementRef, config: IonicConfig, @Host() content: Content) {
|
||||
this.isSticky = true;
|
||||
this.content = content;
|
||||
this.ele = elementRef.nativeElement;
|
||||
this.parent = this.ele.parentNode;
|
||||
}
|
||||
|
||||
onInit() {
|
||||
|
||||
this.scrollContent = this.content.elementRef.nativeElement.children[0];
|
||||
|
||||
this.scrollMin = 0;
|
||||
this.scrollMax = 0;
|
||||
this.scrollTransition = 0;
|
||||
this.isSticking = false;
|
||||
|
||||
|
||||
this.scrollContent.addEventListener('scroll', event => this.scrollEvent(event));
|
||||
|
||||
this.calculateScrollLimits = scrollTop => {
|
||||
var containerPosition = position(this.parent);
|
||||
var elementOffset = offset(this.ele);
|
||||
|
||||
var containerTop = containerPosition.top;
|
||||
var containerHeight = containerPosition.height;
|
||||
|
||||
var affixHeight = elementOffset.height;
|
||||
|
||||
this.scrollMin = containerTop;
|
||||
this.scrollMax = this.scrollMin + containerHeight;
|
||||
this.scrollTransition = this.scrollMax - affixHeight;
|
||||
};
|
||||
|
||||
// throttled version of the same calculation
|
||||
let CALCULATION_THROTTLE_MS = 500;
|
||||
this.throttledCalculateScrollLimits = throttle(
|
||||
this.calculateScrollLimits,
|
||||
CALCULATION_THROTTLE_MS,
|
||||
{trailing: false}
|
||||
);
|
||||
}
|
||||
|
||||
applyTransform(element, transformString) {
|
||||
// do not apply the transformation if it is already applied
|
||||
if (element.style[CSS.transform] == transformString) {
|
||||
}
|
||||
else {
|
||||
element.style[CSS.transform] = transformString;
|
||||
}
|
||||
}
|
||||
|
||||
translateUp(element, dy, executeImmediately) {
|
||||
var translateDyPixelsUp = dy == 0 ? 'translate3d(0px, 0px, 0px)' : 'translate3d(0px, -' + dy + 'px, 0px)';
|
||||
// if immediate execution is requested, then just execute immediately
|
||||
// if not, execute in the animation frame.
|
||||
if (executeImmediately) {
|
||||
this.applyTransform(element, translateDyPixelsUp);
|
||||
}
|
||||
else {
|
||||
// see http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
|
||||
// see http://ionicframework.com/docs/api/utility/ionic.DomUtil/
|
||||
requestAnimationFrame( a => this.applyTransform(element, translateDyPixelsUp) );
|
||||
}
|
||||
}
|
||||
|
||||
createAffixClone() {
|
||||
var clone = this.ele.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.top = 0;
|
||||
clone.style.left = 0;
|
||||
clone.style.right = 0;
|
||||
|
||||
this.scrollContent.parentNode.appendChild(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
scrollEvent(event) {
|
||||
var scrollTop = event.target.scrollTop;
|
||||
|
||||
// when scroll to top, we should always execute the immediate calculation.
|
||||
// this is because of some weird problem which is hard to describe.
|
||||
// if you want to experiment, always use the throttled one and just click on the page
|
||||
// you will see all affix elements stacked on top
|
||||
if (scrollTop == 0) {
|
||||
this.calculateScrollLimits(scrollTop);
|
||||
}
|
||||
else {
|
||||
this.throttledCalculateScrollLimits(scrollTop);
|
||||
}
|
||||
|
||||
// when we scrolled to the container, create the clone of element and place it on top
|
||||
if (scrollTop >= this.scrollMin && scrollTop <= this.scrollMax) {
|
||||
// we need to track if we created the clone just now
|
||||
// that is important since normally we apply the transforms in the animation frame
|
||||
// but, we need to apply the transform immediately when we add the element for the first time. otherwise it is too late!
|
||||
var cloneCreatedJustNow = false;
|
||||
|
||||
if (!this.affixClone) {
|
||||
this.affixClone = this.createAffixClone();
|
||||
cloneCreatedJustNow = true;
|
||||
this.isSticking = true;
|
||||
}
|
||||
|
||||
// if we're reaching towards the end of the container, apply some nice translation to move up/down the clone
|
||||
// but if we're reached already to the container and we're far away than the end, move clone to top
|
||||
if (scrollTop > this.scrollTransition) {
|
||||
this.translateUp(this.affixClone, Math.floor(scrollTop - this.scrollTransition), cloneCreatedJustNow);
|
||||
} else {
|
||||
this.translateUp(this.affixClone, 0, cloneCreatedJustNow);
|
||||
}
|
||||
} else {
|
||||
this.removeAffixClone();
|
||||
this.isSticking = false;
|
||||
}
|
||||
}
|
||||
|
||||
removeAffixClone() {
|
||||
if (this.affixClone) {
|
||||
this.scrollContent.parentNode.removeChild(this.affixClone);
|
||||
this.affixClone = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,14 +26,16 @@ $item-ios-divider-bg: #f5f5f5 !default;
|
||||
$item-ios-divider-color: #222 !default;
|
||||
$item-ios-divider-padding: 5px 15px !default;
|
||||
|
||||
.list {
|
||||
|
||||
|
||||
.item-group-title {
|
||||
padding: $item-ios-padding-top $item-ios-padding-right $item-ios-padding-bottom $item-ios-padding-left;
|
||||
background-color: $item-ios-divider-bg;
|
||||
color: $item-ios-divider-color;
|
||||
}
|
||||
|
||||
.list {
|
||||
|
||||
|
||||
|
||||
.item-group {
|
||||
// Make sure the first and last items don't have borders
|
||||
> .item:first-of-type:before {
|
||||
|
@ -268,3 +268,70 @@ export function flushDimensionCache() {
|
||||
|
||||
let dimensionCache = {};
|
||||
let dimensionIds = 0;
|
||||
|
||||
function getStyle(el, cssprop) {
|
||||
if (el.currentStyle) { //IE
|
||||
return el.currentStyle[cssprop];
|
||||
} else if (window.getComputedStyle) {
|
||||
return window.getComputedStyle(el)[cssprop];
|
||||
}
|
||||
// finally try and get inline style
|
||||
return el.style[cssprop];
|
||||
}
|
||||
|
||||
function isStaticPositioned(element) {
|
||||
return (getStyle(element, 'position') || 'static') === 'static';
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the closest, non-statically positioned parentOffset of a given element
|
||||
* @param element
|
||||
*/
|
||||
export function parentOffsetEl(element) {
|
||||
var offsetParent = element.offsetParent || document;
|
||||
while (offsetParent && offsetParent !== document && isStaticPositioned(offsetParent)) {
|
||||
offsetParent = offsetParent.offsetParent;
|
||||
}
|
||||
return offsetParent || document;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current coordinates of the element, relative to the offset parent.
|
||||
* Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
|
||||
* @param {element} element The element to get the position of.
|
||||
* @returns {object} Returns an object containing the properties top, left, width and height.
|
||||
*/
|
||||
export function position(element) {
|
||||
var elBCR = offset(element);
|
||||
var offsetParentBCR = { top: 0, left: 0 };
|
||||
var offsetParentEl = parentOffsetEl(element);
|
||||
if (offsetParentEl != document) {
|
||||
offsetParentBCR = offset(offsetParentEl);
|
||||
offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
|
||||
offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
|
||||
}
|
||||
|
||||
var boundingClientRect = element.getBoundingClientRect();
|
||||
return {
|
||||
width: boundingClientRect.width || element.prop('offsetWidth'),
|
||||
height: boundingClientRect.height || element.prop('offsetHeight'),
|
||||
top: elBCR.top - offsetParentBCR.top,
|
||||
left: elBCR.left - offsetParentBCR.left
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current coordinates of the element, relative to the document.
|
||||
* Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
|
||||
* @param {element} element The element to get the offset of.
|
||||
* @returns {object} Returns an object containing the properties top, left, width and height.
|
||||
*/
|
||||
export function offset(element) {
|
||||
var boundingClientRect = element.getBoundingClientRect();
|
||||
return {
|
||||
width: boundingClientRect.width || element.prop('offsetWidth'),
|
||||
height: boundingClientRect.height || element.prop('offsetHeight'),
|
||||
top: boundingClientRect.top + (window.pageYOffset || document.documentElement.scrollTop),
|
||||
left: boundingClientRect.left + (window.pageXOffset || document.documentElement.scrollLeft)
|
||||
};
|
||||
}
|
||||
|
@ -180,3 +180,35 @@ export function getQuerystring(url, key) {
|
||||
}
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle the given fun, only allowing it to be
|
||||
* called at most every `wait` ms.
|
||||
*/
|
||||
export function throttle(func, wait, options) {
|
||||
var context, args, result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
options || (options = {});
|
||||
var later = function() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
};
|
||||
return function() {
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user