Monday, November 21, 2011

Scrolling on the iPad

Okay, this might be lame, but I decided I had a very specific scrolling requirement that none of the other options out there met.

I tried things like iScroll, but the refresh didn't always work (even after a timeout of several seconds) and I had to hack on another package to get the inputs to respond again.

I tried the new position:fixed options in IOS 5.x, but touch events seem to be broken if you have the page scrolled up. Click events work, but touch ones only work if you're scrolled back to the top!!

So, I dug through whatever I could find and decided to hack my own together. Maybe this will help you too.

Please note that I am no pro javascript developer; I know my way around Python pretty well, but I come by javascript by hook and by crook, as they might say.
Who they are I have no idea, but I'm just saying.

WHAT DOES IT DO?

This is ONLY a vertical scroller. I might adapt it later, but if all you want is a dynamically sizeable vertical scroller (with scrollbar!) with a fixed header (or footer) then this is for you.


Here's the javascript. Copy and paste this and save it as a file (ie. touch.js):



//
// IOS TOUCH EVENTS
//
function Scroller(element,container,maxheight) {
    if (document.getElementById(element))
        element =    document.getElementById(element)
    this.elementid    =    element.id
    this.containerid =    container
    this.maxheight = 0
    if (maxheight)
        this.maxheight = maxheight
    element.addEventListener('touchstart',this,false);
    element.addEventListener('touchmove',this,false);
    element.addEventListener('touchend',this,false);
    this.containerTop = parseInt(document.getElementById(container).style.top) || 0
    this.containerHeight = parseInt(document.getElementById(container).style.height) || 0
    this.ismove = 0
    this.t1 = 0
    this.t2 = 0
    this.starttime = 0
    this.elementHeight = 0
    this.lastY = 0
    this.lastYabsolute = 0
    this.startYabsolute = 0
    this.startY    =    0
    this.scrollTop = 0
    this.scrollHeight = 0
}
Scroller.prototype.handleEvent = function(e) {
    switch (e.type) {
        case 'touchstart': this.ScrollerOnTouchStart(e); break;
        case 'touchmove' : this.ScrollerOnTouchMove(e); break
        case 'touchend' : this.ScrollerOnTouchEnd(e); break
    }
}
Scroller.prototype.CreateScrollbar = function() {
    if (!document.getElementById('scrollbar')) {
        var newitem = document.createElement('div');
        newitem.setAttribute('id','scrollbar');
        document.getElementById(this.containerid).appendChild(newitem);
    }
    var obj = document.getElementById('scrollbar');
    obj.style.opacity = 0;
    obj.style['-webkit-transition-duration']    =    '0ms'
    var h = this.containerHeight/(this.elementHeight/this.containerHeight);
    obj.style.height=parseInt(h)+'px';
    var l = parseInt(document.getElementById(this.elementid).style.left);
    if (!l)
        var l = parseInt(window.getComputedStyle(document.getElementById(this.elementid)).left);
    if (!l)
        l = 0;
    l += parseInt(window.getComputedStyle(document.getElementById(this.elementid)).marginLeft);
    l += parseInt(window.getComputedStyle(document.getElementById(this.elementid)).paddingLeft);
    l += parseInt(window.getComputedStyle(document.getElementById(this.containerid)).marginLeft);
    l += parseInt(window.getComputedStyle(document.getElementById(this.containerid)).paddingLeft);
    l += parseInt(window.getComputedStyle(document.getElementById(this.elementid)).width)-5;
    if (!l)
        l = 900;
    obj.style.left = parseInt(l)+'px';
    this.scrollTop = parseInt(window.getComputedStyle(document.getElementById(this.elementid)).paddingTop)+parseInt(window.getComputedStyle(document.getElementById(this.elementid)).marginTop);
    this.scrollHeight = (parseInt(document.getElementById(this.containerid).style.height)-this.scrollTop)-parseInt(h);
    obj.style['-webkit-transition-duration']    =    '200ms'
    obj.style.opacity = 0.5;
}
Scroller.prototype.MoveScrollbar = function() {
    var t = (-this.lastY/this.elementHeight)
    x = parseInt((this.scrollHeight *t)+this.scrollTop)
    document.getElementById('scrollbar').style['-webkit-transition-duration']    =    '0ms'
    document.getElementById('scrollbar').style.webkitTransform = 'translate3d(0px,' + x + 'px,0px)';
}
Scroller.prototype.ScrollerOnTouchStart = function(e) {
    var d = new Date()
    this.ismove = 0
    this.starttime = d.getTime()
    document.getElementById(this.elementid).style['-webkit-transition-duration']    =    '0ms'
    this.startY = e.touches[0].pageY-this.lastY
    this.startYabsolute = e.touches[0].pageY
    this.elementHeight = parseInt(document.getElementById(this.elementid).style.height)-parseInt(document.getElementById(this.containerid).style.height)
    if (this.maxheight)
        this.elementHeight = this.maxheight
    //elementHeight = parseInt(document.getElementById(elementid).style.height)
    this.CreateScrollbar()
}
Scroller.prototype.ScrollerOnTouchMove = function(e) {
    this.ismove = 1
    var d = new Date()
    this.t2 = this.t1
    this.t1 = e.targetTouches[0].pageY
    this.lastY = e.targetTouches[0].pageY-this.startY
    this.lastYabsolute = e.targetTouches[0].pageY
    if (this.lastY >= (this.containerTop+100)) {
        this.lastY = this.containerTop+100
    }
    else if (this.lastY < -(this.elementHeight+100) && this.elementHeight>this.containerHeight) {
        this.lastY = -(this.elementHeight+100)
    }
    document.getElementById(this.elementid).style.webkitTransform = 'translate3d(0px,' + this.lastY + 'px,0px)';
    //console.log(this.lastY+'/'+this.elementHeight+'/'+this.containerHeight)
    this.MoveScrollbar()
}
Scroller.prototype.ScrollerOnTouchEnd = function(e) {
    // DO THE MOMENTUM
    if (this.ismove == 0) {
        var obj = document.getElementById('scrollbar')
        obj.style['-webkit-transition']    =    'all 200ms ease-out';
        obj.style.opacity = 0;
        return
    }
    var d = new Date()
    var endtime = d.getTime()
    var duration = (endtime-this.starttime)
    var distance = this.lastYabsolute - this.startYabsolute
    var b = 120*distance
    var distance = b/duration
    //console.log(this.t2+' / '+this.t1+ ' = '+(this.t2-this.t1))
    if (this.t2-this.t1 < 20 && this.t2-this.t1 > -20)
        distance    =    0

    if (duration > 600)
        distance = 0
    if (this.lastY > this.containerTop) {
        this.lastY = this.containerTop
        document.getElementById(this.elementid).style['-webkit-transition-duration']    =    '200ms'
        document.getElementById(this.elementid).style.webkitTransform = 'translate3d(0px,' + 0 + 'px,0px)';
        duration = 200
    }
    else if (this.lastY < -this.elementHeight) {
        this.lastY = -this.elementHeight
        var pos = this.elementHeight
        if (parseInt(document.getElementById(this.elementid).style.height) < this.containerHeight) {
            this.lastY = this.containerTop
            pos = 0
        }
        document.getElementById(this.elementid).style['-webkit-transition-duration']    =    '200ms'
        document.getElementById(this.elementid).style.webkitTransform = 'translate3d(0px,-' + pos + 'px,0px)';
        duration = 200
    }
    else {
        // USE MOMENTUM TO SLOW IT DOWN
        var t = this.t2-this.t1
        if (t < 0)
            t = -t
        t = t * 0.8
        distance = (distance * (t/20))*1.4
        duration = (duration * (t/20))*1.4
        this.lastY = this.lastY+distance
        if (this.lastY < -this.elementHeight)
            this.lastY = -this.elementHeight
        else if (this.lastY > this.containerTop)
            this.lastY = this.containerTop
        //document.getElementById(this.elementid).style['-webkit-transition-duration']    =    parseInt(duration)+'ms'
        document.getElementById(this.elementid).style['-webkit-transition']    =    'all '+parseInt(duration)+'ms cubic-bezier(0,0,0.1,1)'
        //document.getElementById(this.elementid).style['-webkit-transition-duration']    =    '400ms'
        document.getElementById(this.elementid).style.webkitTransform = 'translate3d(0px,' + this.lastY + 'px,0px)';

    }
    
    // MOVE THE SCROLLBAR
    var obj = document.getElementById('scrollbar')
    var t = (-this.lastY/this.elementHeight)
    x = (this.scrollHeight *t)+this.scrollTop
    obj.style['-webkit-transition']    =    'all '+parseInt(duration)+'ms ease-out';
    document.getElementById('scrollbar').style.webkitTransform = 'translate3d(0px,' + x + 'px,0px)';
    obj.style.opacity = 0;
}
Scroller.prototype.ScrollerReset = function() {
    //console.log('here')
    document.getElementById(this.elementid).style['-webkit-transition-duration']    =    '0ms'    
    document.getElementById(this.elementid).style.webkitTransform = 'translate3d(0px,0px,0px)';
    this.lastY = 0
}
Once you've got that, then you refer to it with something like this in the header:


<script src="touch.js" type="text/javascript"/>
In your body HTML, you might have some code like this:


<div id="contents" style="height: 400px; position: absolute; left: 0px; width: 100%; height: 400px;"></div>
    <div id="scrolleritems" style="position: absolute;">my html here</div>
</div>

The "contents" div is the parent div. It's what should have the dimensions you want. The "scrolleritems" is the window that will scroll.

Once you've got that, then you need to initialize it with this:


<script>myscroll = new Scroller('scrolleritems','contents')</script>

I like to put that bit of code at the end of the page, before the </body> tag, just because I seem to have an aversion to using the "onload" body event.

There's only one method for this routine, and that's "reset":

myscroll.ScrollerReset()

If you need to reset it for any reason, (say, an orientation change) that's what you do.

Now, this works in my environment; I haven't tested it as written here, so if you have problems just let me know and I'll get it fixed!

UPDATE:

I forgot to include the styling for the scrollbar! Somewhere in your css (or in a <style> tag) include this:


#scrollbar {
position: absolute;
border-radius: 3px;
width: 5px;
opacity 0.5;
border: 1px solid #000;
background-color: #000;
}

Wednesday, April 6, 2011

An Open Letter to Prime Minister Stephen Harper

Mr. Harper, I have been an ardent supporter of conservative politics as long as I have been able to vote. Whether that be for the Reform Party, the Progressive Conservative Party before that, or the current Conservative Party of Canada, it has always been my belief that a small, pro-business government was the best way to a healthy economy and a strong middle class.

Recent events around the world, as well as in Canada, have called that belief into question.

It seems clear to me now that conservative governments around the world are truly in the pockets of big business, bought and paid for without any democratic process. Certainly an argument could be made that all governments of all creeds are in fact owned by business, but conservative regimes certainly seem to be the most aggressive with this policy.

A wealthy businessman I greatly respect said to me once that "a business that treats it's employees with respect has no need of unions". I believe that sentiment, however the last few years have seen such rampant runaway greed, especially after the global economic collapse, that we are seeing the swift erosion of the middle class, the swelling ranks of those who's standard of life is receding, and the ever more rapid increase of wealth for the wealthiest of our societies.

How much is too much? What is fair? I have no love of communism; it was clearly a failed ideology and I very strongly believe that individuals should be able to benefit from the fruits of their labour. A strong democratic society based on capitalism has traditionally been the means of accomplishing this, but greed and dishonesty have taken the viability out of Ayn Rand's dream of a pure, fair capitalistic society. It is my belief that an unfettered, self-righting economy with little or no strings attached is no longer possible, and it seems I am far from alone.

We have situations where multinational companies have the right to sue small family farms, who have no means to defend themselves. In the United States we are seeing the intentional destruction of worker rights by Republican governments. Around the world common men and women not of the elite are now being expected to pay for the excesses of the extremely wealthy, whether that be wealthy individuals or organizations, or more often both. Our systems meant to care for us, the proverbial unwashed masses, are being eroded so that the rich may continue to benefit.

One of the arguments frequently used is that by offering incentives to large businesses in the form of tax breaks, we improve the economy and wealth of the citizens because that large business is now encouraged to do business with us. I will tell you in no uncertain terms right now: That is a lie, and governments around the world are selling out their people because of it.

In any situation where tax breaks must be made to encourage a company to do business, we should instead rely on the conservative policy of non-interference and permit the market to dictate a solution. If one business is unwilling to do business due to a lack of graft, then another business will happily take it's place.

I fear, Mr. Harper, that we may now be seeing the seeds of revolution. Certainly this is what is happening in the middle east right now, and anyone who believes this cannot happen in western society is, to be blunt, a fool. It has happened before, and can happen again, and tools like the Internet have become a powerful means for society to mobilize against their perceived oppressors.

It is time for Canada to get out of the pockets of big business before our society collapses around us. It is time for the citizens of Canada to get back some of the powers they have lost over the years. It may be too late already for the United States, and it's possible they are already on an unstoppable track to revolution in one form or another. I do not believe it's too late for us. Should big business have rights? Yes, however not at the expense of the rights of individuals, and certainly not at the expense of taxpayers.

This is a simple request Mr. Prime Minister. I do not want to suddenly make millions of dollars at the expense of others. I would like to work knowing that my future, and the future of my family, is secured. I believe that hard work builds character, and I am nigh afraid of it. And I believe that most Canadians would gladly contribute their fair share, but there's that word again: fair.

What is fair? I don't pretend to have the answer, but I encourage you and your government to give it some serious thought before Canadians decide that the Conservative Party has a definition of that word that is at odds with society at large.

Thank you.

Aaron Fransen

April 2, 2011