diff --git a/ionic/components/app/structure.scss b/ionic/components/app/structure.scss index 960dc8f3a6..acc778ab8c 100644 --- a/ionic/components/app/structure.scss +++ b/ionic/components/app/structure.scss @@ -185,3 +185,9 @@ ion-toolbar[position=bottom] { bottom: 0; z-index: $z-index-toolbar; } + +.sticky { + position: -webkit-sticky; + position: sticky; + top: 0; +} diff --git a/ionic/components/content/content.ts b/ionic/components/content/content.ts index 2fca0a00b5..3a03905f37 100644 --- a/ionic/components/content/content.ts +++ b/ionic/components/content/content.ts @@ -6,7 +6,10 @@ import {Keyboard} from '../../util/keyboard'; import {ViewController} from '../nav/view-controller'; import {Animation} from '../../animations/animation'; import {ScrollTo} from '../../animations/scroll-to'; +import {FeatureDetect} from '../../util/feature-detect'; +import {StickyPoly} from './sticky-poly'; +console.log(StickyPoly); /** * The Content component provides an easy to use content area that can be configured to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser. @@ -35,7 +38,7 @@ export class Content extends Ion { * @param {ElementRef} elementRef A reference to the component's DOM element. * @param {Config} config The config object to change content's default settings. */ - constructor(elementRef: ElementRef, config: Config, keyboard: Keyboard, @Optional() viewCtrl: ViewController) { + constructor(elementRef: ElementRef, config: Config, keyboard: Keyboard, @Optional() viewCtrl: ViewController, private featureDetect: FeatureDetect) { super(elementRef, config); this.scrollPadding = 0; this.keyboard = keyboard; @@ -46,6 +49,10 @@ export class Content extends Ion { } } + getStickyPolyfill() { + return this._sticky; + } + /** * TODO * @private @@ -53,6 +60,14 @@ export class Content extends Ion { onInit() { super.onInit(); this.scrollElement = this.getNativeElement().children[0]; + + setTimeout(() => { + if(!this.featureDetect.has('sticky')) { + console.log('Enabling sticky polyfill'); + this._sticky = StickyPoly(this.scrollElement); + //this._sticky.init(); + } + }) } /** diff --git a/ionic/components/content/sticky-poly.ts b/ionic/components/content/sticky-poly.ts new file mode 100644 index 0000000000..77de72a50c --- /dev/null +++ b/ionic/components/content/sticky-poly.ts @@ -0,0 +1,474 @@ +/*! + * Stickyfill -- `position: sticky` polyfill + * v. 1.1.3 | https://github.com/wilddeer/stickyfill + * Copyright Oleg Korsunsky | http://wd.dizaina.net/ + * + * MIT License + */ + +export function StickyPoly(target) { + var watchArray = [], + scroll, + initialized = false, + html = document.documentElement, + noop = function() {}, + checkTimer, + + //visibility API strings + hiddenPropertyName = 'hidden', + visibilityChangeEventName = 'visibilitychange'; + + //fallback to prefixed names in old webkit browsers + if (document.webkitHidden !== undefined) { + hiddenPropertyName = 'webkitHidden'; + visibilityChangeEventName = 'webkitvisibilitychange'; + } + + //test getComputedStyle + if (!window.getComputedStyle) { + seppuku(); + } + + //test for native support + var prefixes = ['', '-webkit-', '-moz-', '-ms-'], + block = document.createElement('div'); + + for (var i = prefixes.length - 1; i >= 0; i--) { + try { + block.style.position = prefixes[i] + 'sticky'; + } + catch(e) {} + if (block.style.position != '') { + seppuku(); + } + } + + updateScrollPos(); + + //commit seppuku! + function seppuku() { + init = add = rebuild = pause = stop = kill = noop; + } + + function mergeObjects(targetObj, sourceObject) { + for (var key in sourceObject) { + if (sourceObject.hasOwnProperty(key)) { + targetObj[key] = sourceObject[key]; + } + } + } + + function parseNumeric(val) { + return parseFloat(val) || 0; + } + + function updateScrollPos() { + scroll = { + top: target.scrollTop, + left: target.scrollLeft}; + } + + function onScroll() { + /* + console.log(scroll.top); + if (target.scrollLeft != scroll.left) { + updateScrollPos(); + rebuild(); + return; + } + + if (target.scrollTop != scroll.top) { + updateScrollPos(); + recalcAllPos(); + } + */ + updateScrollPos(); + recalcAllPos(); + } + + //fixes flickering + function onWheel(event) { + setTimeout(function() { + if (target.scrollTop != scroll.top) { + scroll.top = target.scrollTop; + recalcAllPos(); + } + }, 0); + } + + function recalcAllPos() { + for (var i = watchArray.length - 1; i >= 0; i--) { + recalcElementPos(watchArray[i]); + } + } + + function recalcElementPos(el) { + if (!el.inited) return; + + var currentMode = (scroll.top <= el.limit.start? 0: scroll.top >= el.limit.end? 2: 1); + + if (el.mode != currentMode) { + switchElementMode(el, currentMode); + } + } + + //checks whether stickies start or stop positions have changed + function fastCheck() { + for (var i = watchArray.length - 1; i >= 0; i--) { + if (!watchArray[i].inited) continue; + + var deltaTop = Math.abs(getDocOffsetTop(watchArray[i].clone) - watchArray[i].docOffsetTop), + deltaHeight = Math.abs(watchArray[i].parent.node.offsetHeight - watchArray[i].parent.height); + + if (deltaTop >= 2 || deltaHeight >= 2) return false; + } + return true; + } + + function initElement(el) { + if (isNaN(parseFloat(el.computed.top)) || el.isCell || el.computed.display == 'none') return; + + el.inited = true; + + if (!el.clone) clone(el); + if (el.parent.computed.position != 'absolute' && + el.parent.computed.position != 'relative') el.parent.node.style.position = 'relative'; + + recalcElementPos(el); + + el.parent.height = el.parent.node.offsetHeight; + el.docOffsetTop = getDocOffsetTop(el.clone); + } + + function deinitElement(el) { + var deinitParent = true; + + el.clone && killClone(el); + mergeObjects(el.node.style, el.css); + + //check whether element's parent is used by other stickies + for (var i = watchArray.length - 1; i >= 0; i--) { + if (watchArray[i].node !== el.node && watchArray[i].parent.node === el.parent.node) { + deinitParent = false; + break; + } + }; + + if (deinitParent) el.parent.node.style.position = el.parent.css.position; + el.mode = -1; + } + + function initAll() { + for (var i = watchArray.length - 1; i >= 0; i--) { + initElement(watchArray[i]); + } + } + + function deinitAll() { + for (var i = watchArray.length - 1; i >= 0; i--) { + deinitElement(watchArray[i]); + } + } + + function switchElementMode(el, mode) { + var nodeStyle = el.node.style; + + switch (mode) { + case 0: + nodeStyle.position = 'absolute'; + nodeStyle.left = el.offset.left + 'px'; + nodeStyle.right = el.offset.right + 'px'; + nodeStyle.top = el.offset.top + 'px'; + nodeStyle.bottom = 'auto'; + nodeStyle.width = 'auto'; + nodeStyle.marginLeft = 0; + nodeStyle.marginRight = 0; + nodeStyle.marginTop = 0; + break; + + case 1: + nodeStyle.position = 'fixed'; + nodeStyle.left = el.box.left + 'px'; + nodeStyle.right = el.box.right + 'px'; + nodeStyle.top = el.css.top; + nodeStyle.bottom = 'auto'; + nodeStyle.width = 'auto'; + nodeStyle.marginLeft = 0; + nodeStyle.marginRight = 0; + nodeStyle.marginTop = 0; + break; + + case 2: + nodeStyle.position = 'absolute'; + nodeStyle.left = el.offset.left + 'px'; + nodeStyle.right = el.offset.right + 'px'; + nodeStyle.top = 'auto'; + nodeStyle.bottom = 0; + nodeStyle.width = 'auto'; + nodeStyle.marginLeft = 0; + nodeStyle.marginRight = 0; + break; + } + + el.mode = mode; + } + + function clone(el) { + el.clone = document.createElement('div'); + + var refElement = el.node.nextSibling || el.node, + cloneStyle = el.clone.style; + + cloneStyle.height = el.height + 'px'; + cloneStyle.width = el.width + 'px'; + cloneStyle.marginTop = el.computed.marginTop; + cloneStyle.marginBottom = el.computed.marginBottom; + cloneStyle.marginLeft = el.computed.marginLeft; + cloneStyle.marginRight = el.computed.marginRight; + cloneStyle.padding = cloneStyle.border = cloneStyle.borderSpacing = 0; + cloneStyle.fontSize = '1em'; + cloneStyle.position = 'static'; + cloneStyle.cssFloat = el.computed.cssFloat; + + el.node.parentNode.insertBefore(el.clone, refElement); + } + + function killClone(el) { + el.clone.parentNode.removeChild(el.clone); + el.clone = undefined; + } + + function getElementParams(node) { + var computedStyle = getComputedStyle(node), + parentNode = node.parentNode, + parentComputedStyle = getComputedStyle(parentNode), + cachedPosition = node.style.position; + + node.style.position = 'relative'; + + var computed = { + top: computedStyle.top, + marginTop: computedStyle.marginTop, + marginBottom: computedStyle.marginBottom, + marginLeft: computedStyle.marginLeft, + marginRight: computedStyle.marginRight, + cssFloat: computedStyle.cssFloat, + display: computedStyle.display + }, + numeric = { + top: parseNumeric(computedStyle.top), + marginBottom: parseNumeric(computedStyle.marginBottom), + paddingLeft: parseNumeric(computedStyle.paddingLeft), + paddingRight: parseNumeric(computedStyle.paddingRight), + borderLeftWidth: parseNumeric(computedStyle.borderLeftWidth), + borderRightWidth: parseNumeric(computedStyle.borderRightWidth) + }; + + node.style.position = cachedPosition; + + var css = { + position: node.style.position, + top: node.style.top, + bottom: node.style.bottom, + left: node.style.left, + right: node.style.right, + width: node.style.width, + marginTop: node.style.marginTop, + marginLeft: node.style.marginLeft, + marginRight: node.style.marginRight + }, + nodeOffset = getElementOffset(node), + parentOffset = getElementOffset(parentNode), + + parent = { + node: parentNode, + css: { + position: parentNode.style.position + }, + computed: { + position: parentComputedStyle.position + }, + numeric: { + borderLeftWidth: parseNumeric(parentComputedStyle.borderLeftWidth), + borderRightWidth: parseNumeric(parentComputedStyle.borderRightWidth), + borderTopWidth: parseNumeric(parentComputedStyle.borderTopWidth), + borderBottomWidth: parseNumeric(parentComputedStyle.borderBottomWidth) + } + }, + + el = { + node: node, + box: { + left: nodeOffset.win.left, + right: html.clientWidth - nodeOffset.win.right + }, + offset: { + top: nodeOffset.win.top - parentOffset.win.top - parent.numeric.borderTopWidth, + left: nodeOffset.win.left - parentOffset.win.left - parent.numeric.borderLeftWidth, + right: -nodeOffset.win.right + parentOffset.win.right - parent.numeric.borderRightWidth + }, + css: css, + isCell: computedStyle.display == 'table-cell', + computed: computed, + numeric: numeric, + width: nodeOffset.win.right - nodeOffset.win.left, + height: nodeOffset.win.bottom - nodeOffset.win.top, + mode: -1, + inited: false, + parent: parent, + limit: { + start: nodeOffset.doc.top - numeric.top, + end: parentOffset.doc.top + parentNode.offsetHeight - parent.numeric.borderBottomWidth - + node.offsetHeight - numeric.top - numeric.marginBottom + } + }; + + return el; + } + + function getDocOffsetTop(node) { + var docOffsetTop = 0; + + while (node) { + docOffsetTop += node.offsetTop; + node = node.offsetParent; + } + + return docOffsetTop; + } + + function getElementOffset(node) { + var box = node.getBoundingClientRect(); + + return { + doc: { + top: box.top + target.scrollTop, + left: box.left + target.scrollLeft}, + win: box + }; + } + + function startFastCheckTimer() { + checkTimer = setInterval(function() { + !fastCheck() && rebuild(); + }, 500); + } + + function stopFastCheckTimer() { + clearInterval(checkTimer); + } + + function handlePageVisibilityChange() { + if (!initialized) return; + + if (document[hiddenPropertyName]) { + stopFastCheckTimer(); + } + else { + startFastCheckTimer(); + } + } + + function init() { + if (initialized) return; + + // Store the left/top scroll positions + updateScrollPos(); + + // Initialize all elements added + initAll(); + + target.addEventListener('scroll', onScroll); + target.addEventListener('wheel', onWheel); + + //watch for width changes + target.addEventListener('resize', rebuild); + target.addEventListener('orientationchange', rebuild); + + //watch for page visibility + document.addEventListener(visibilityChangeEventName, handlePageVisibilityChange); + + // Start a timer to respond to changes in the layout/size + startFastCheckTimer(); + + initialized = true; + } + + function rebuild() { + if (!initialized) return; + + deinitAll(); + + for (var i = watchArray.length - 1; i >= 0; i--) { + watchArray[i] = getElementParams(watchArray[i].node); + } + + initAll(); + } + + function pause() { + target.removeEventListener('scroll', onScroll); + target.removeEventListener('wheel', onWheel); + target.removeEventListener('resize', rebuild); + target.removeEventListener('orientationchange', rebuild); + document.removeEventListener(visibilityChangeEventName, handlePageVisibilityChange); + + stopFastCheckTimer(); + + initialized = false; + } + + function stop() { + pause(); + deinitAll(); + } + + function kill() { + stop(); + + //empty the array without loosing the references, + //the most performant method according to http://jsperf.com/empty-javascript-array + while (watchArray.length) { + watchArray.pop(); + } + } + + function add(node) { + //check if Stickyfill is already applied to the node + for (var i = watchArray.length - 1; i >= 0; i--) { + if (watchArray[i].node === node) return; + }; + + var el = getElementParams(node); + + watchArray.push(el); + + if (!initialized) { + init(); + } + else { + initElement(el); + } + } + + function remove(node) { + for (var i = watchArray.length - 1; i >= 0; i--) { + if (watchArray[i].node === node) { + deinitElement(watchArray[i]); + watchArray.splice(i, 1); + } + }; + } + + //expose Stickyfill + return { + stickies: watchArray, + add: add, + remove: remove, + init: init, + rebuild: rebuild, + pause: pause, + stop: stop, + kill: kill + }; +} diff --git a/ionic/components/content/sticky.ts b/ionic/components/content/sticky.ts new file mode 100644 index 0000000000..359f53f93a --- /dev/null +++ b/ionic/components/content/sticky.ts @@ -0,0 +1,33 @@ +import {Content} from './content'; + +export class StickyPolyfill { + constructor(private content: Content) { + this._els = []; + } + + init() { + let handleScroll = (event) => { + let t = event.target; + let top = t.scrollTop; + console.log(top); + + } + + this._scrollListener = this.content.addScrollEventListener(handleScroll); + } + onDestroy() { + if(this._scrollListener) { + // Remove the old listener + this._scrollListener(); + } + } + add(element: Element) { + this._els.push(this.initElement(element)) + } + + remove(element: Element) { + } + + initElement(element) { + } +} diff --git a/ionic/components/item/item-group.ts b/ionic/components/item/item-group.ts index aa0b39dc90..d75aefe58c 100644 --- a/ionic/components/item/item-group.ts +++ b/ionic/components/item/item-group.ts @@ -2,7 +2,6 @@ import {Directive, ElementRef, Host, Optional} from 'angular2/angular2'; import {Content} from '../content/content'; import {throttle} from '../../util/util'; import {position, offset, CSS, raf} from '../../util/dom'; -import {FeatureDetect} from '../../util/feature-detect'; import {Config} from '../../config/config'; /** @@ -39,125 +38,13 @@ export class ItemGroupTitle { * TODO * @param {ElementRef} elementRef TODO */ - constructor(elementRef: ElementRef, config: Config, content: Content, featureDetect: FeatureDetect) { + constructor(private elementRef: ElementRef, config: Config, private content: Content) { + // make sure the sticky class gets set on the title this.isSticky = true; - this.content = content; - this.ele = elementRef.nativeElement; - this.parent = this.ele.parentNode; - this.isCssValid = true;//featureDetect.has('positionsticky') } - onInit() { - if (!this.content || this.isCssValid) { return; } - - 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 { - raf( 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; - } + setTimeout(() => { + this.content.getStickyPolyfill().add(this.elementRef.nativeElement); + }) } } diff --git a/ionic/util/feature-detect.ts b/ionic/util/feature-detect.ts index 74402bdc2d..882fb0677f 100644 --- a/ionic/util/feature-detect.ts +++ b/ionic/util/feature-detect.ts @@ -21,7 +21,7 @@ export class FeatureDetect { let featureDetects = {}; -FeatureDetect.add('positionsticky', function(window, document) { +FeatureDetect.add('sticky', function(window, document) { // css position sticky let ele = document.createElement('div'); ele.style.cssText = 'position:-webkit-sticky;position:sticky'; diff --git a/scripts/e2e/e2e.template.html b/scripts/e2e/e2e.template.html index 3e78fe667e..5d92e2d6b8 100644 --- a/scripts/e2e/e2e.template.html +++ b/scripts/e2e/e2e.template.html @@ -1,6 +1,7 @@ + Ionic E2E