diff --git a/Gruntfile.js b/Gruntfile.js index abde193..df44d8f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,47 +3,60 @@ * http://jscrollpane.kelvinluck.com/ * * Copyright (c) 2013 Kelvin Luck + * Copyright (c) 2020 Tuukka Pasanen * Licensed under the MIT license. */ 'use strict'; -module.exports = function(grunt) { - - // Project configuration. - grunt.initConfig({ - - uglify: { - jsp: { - files: { - 'script/jquery.jscrollpane.min.js': 'script/jquery.jscrollpane.js' +module.exports = function (grunt) { + // Project configuration. + grunt.initConfig({ + uglify: { + jsp: { + files: { + 'script/jquery.jscrollpane.min.js': 'script/jquery.jscrollpane.js', + }, + options: { + preserveComments: 'some', + }, + }, }, - options: { - preserveComments: 'some' - } - } - }, - watch: { - content: { - files: ['script/jquery.jscrollpane.js'], - tasks: 'uglify' - } - }, - connect: { - site: { - options: { - base: '.' - } - } - } - - }); - - grunt.loadNpmTasks('grunt-contrib-connect'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-uglify'); + watch: { + content: { + files: ['script/jquery.jscrollpane.js'], + tasks: 'uglify', + }, + }, + connect: { + site: { + options: { + base: '.', + }, + }, + }, + prettier: { + pretty: { + options: { + printWidth: 140, + tabWidth: 4, + useTabs: false, + semi: true, + singleQuote: true, + trailingComma: 'all', + bracketSpacing: true, + progress: true, + }, + src: ['Gruntfile.js', 'script/jquery.jscrollpane.js'], + }, + }, + }); - grunt.registerTask('default', ['uglify']); - grunt.registerTask('serve', ['uglify', 'connect', 'watch']); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-prettier'); + grunt.registerTask('default', ['uglify', 'prettier']); + grunt.registerTask('serve', ['uglify', 'connect', 'watch']); }; diff --git a/package-lock.json b/package-lock.json index 9d1db03..be1c20d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jscrollpane", - "version": "2.2.3", + "version": "2.2.3-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1198,6 +1198,16 @@ "which": "~1.3.0" } }, + "grunt-prettier": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-prettier/-/grunt-prettier-2.1.0.tgz", + "integrity": "sha512-PKPz5Qa+xJsoN30DNvjsmozA3ej/YsRXJGnwGK5Rk3XwuuBUIzDkd6tpfmp2xlxQBW91FgDy1EdESclIkB0GPA==", + "dev": true, + "requires": { + "prettier": "^2.0.5", + "progress": "^2.0.0" + } + }, "gzip-size": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", @@ -2174,6 +2184,12 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "prettier": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "dev": true + }, "pretty-bytes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", @@ -2189,6 +2205,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", diff --git a/package.json b/package.json index bb6a433..9cd458e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "grunt-cli": "^1.3.2", "grunt-contrib-connect": "^2.1.0", "grunt-contrib-uglify": "^4.0.1", - "grunt-contrib-watch": "^1.1.0" + "grunt-contrib-watch": "^1.1.0", + "grunt-prettier": "^2.1.0" } } diff --git a/script/jquery.jscrollpane.js b/script/jquery.jscrollpane.js index 4da0a41..799c9df 100644 --- a/script/jquery.jscrollpane.js +++ b/script/jquery.jscrollpane.js @@ -64,94 +64,130 @@ // - #359 Use npm scripts and local dev dependencies to build the project (function (factory) { - if ( typeof define === 'function' && define.amd ) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof exports === 'object') { - // Node/CommonJS style for Browserify - module.exports = factory(jQuery || require('jquery')); - } else { - // Browser globals - factory(jQuery); - } -}(function($){ - - $.fn.jScrollPane = function(settings) - { - // JScrollPane "class" - public methods are available through $('selector').data('jsp') - function JScrollPane(elem, s) - { - var settings, jsp = this, pane, paneWidth, paneHeight, container, contentWidth, contentHeight, - percentInViewH, percentInViewV, isScrollableV, isScrollableH, verticalDrag, dragMaxY, - verticalDragPosition, horizontalDrag, dragMaxX, horizontalDragPosition, - verticalBar, verticalTrack, scrollbarWidth, verticalTrackHeight, verticalDragHeight, arrowUp, arrowDown, - horizontalBar, horizontalTrack, horizontalTrackWidth, horizontalDragWidth, arrowLeft, arrowRight, - reinitialiseInterval, originalPadding, originalPaddingTotalWidth, previousContentWidth, - wasAtTop = true, wasAtLeft = true, wasAtBottom = false, wasAtRight = false, - originalElement = elem.clone(false, false).empty(), resizeEventsAdded = false, - mwEvent = $.fn.mwheelIntent ? 'mwheelIntent.jsp' : 'mousewheel.jsp'; - - var reinitialiseFn = function() { - // if size has changed then reinitialise - if (settings.resizeSensorDelay > 0) { - setTimeout(function() { - initialise(settings); - }, settings.resizeSensorDelay); - } - else { - initialise(settings); - } - }; - - if (elem.css('box-sizing') === 'border-box') { - originalPadding = 0; - originalPaddingTotalWidth = 0; - } else { - originalPadding = elem.css('paddingTop') + ' ' + - elem.css('paddingRight') + ' ' + - elem.css('paddingBottom') + ' ' + - elem.css('paddingLeft'); - originalPaddingTotalWidth = (parseInt(elem.css('paddingLeft'), 10) || 0) + - (parseInt(elem.css('paddingRight'), 10) || 0); - } - - function initialise(s) - { - - var /*firstChild, lastChild, */isMaintainingPositon, lastContentX, lastContentY, - hasContainingSpaceChanged, originalScrollTop, originalScrollLeft, - newPaneWidth, newPaneHeight, maintainAtBottom = false, maintainAtRight = false; - - settings = s; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS style for Browserify + module.exports = factory(jQuery || require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +})(function ($) { + $.fn.jScrollPane = function (settings) { + // JScrollPane "class" - public methods are available through $('selector').data('jsp') + function JScrollPane(elem, s) { + var settings, + jsp = this, + pane, + paneWidth, + paneHeight, + container, + contentWidth, + contentHeight, + percentInViewH, + percentInViewV, + isScrollableV, + isScrollableH, + verticalDrag, + dragMaxY, + verticalDragPosition, + horizontalDrag, + dragMaxX, + horizontalDragPosition, + verticalBar, + verticalTrack, + scrollbarWidth, + verticalTrackHeight, + verticalDragHeight, + arrowUp, + arrowDown, + horizontalBar, + horizontalTrack, + horizontalTrackWidth, + horizontalDragWidth, + arrowLeft, + arrowRight, + reinitialiseInterval, + originalPadding, + originalPaddingTotalWidth, + previousContentWidth, + wasAtTop = true, + wasAtLeft = true, + wasAtBottom = false, + wasAtRight = false, + originalElement = elem.clone(false, false).empty(), + resizeEventsAdded = false, + mwEvent = $.fn.mwheelIntent ? 'mwheelIntent.jsp' : 'mousewheel.jsp'; + + var reinitialiseFn = function () { + // if size has changed then reinitialise + if (settings.resizeSensorDelay > 0) { + setTimeout(function () { + initialise(settings); + }, settings.resizeSensorDelay); + } else { + initialise(settings); + } + }; + + if (elem.css('box-sizing') === 'border-box') { + originalPadding = 0; + originalPaddingTotalWidth = 0; + } else { + originalPadding = + elem.css('paddingTop') + + ' ' + + elem.css('paddingRight') + + ' ' + + elem.css('paddingBottom') + + ' ' + + elem.css('paddingLeft'); + originalPaddingTotalWidth = (parseInt(elem.css('paddingLeft'), 10) || 0) + (parseInt(elem.css('paddingRight'), 10) || 0); + } + + function initialise(s) { + var /*firstChild, lastChild, */ isMaintainingPositon, + lastContentX, + lastContentY, + hasContainingSpaceChanged, + originalScrollTop, + originalScrollLeft, + newPaneWidth, + newPaneHeight, + maintainAtBottom = false, + maintainAtRight = false; + + settings = s; lastContentX = 0; lastContentY = 0; - if (pane === undefined) { - originalScrollTop = elem.scrollTop(); - originalScrollLeft = elem.scrollLeft(); - - elem.css( - { - overflow: 'hidden', - padding: '0' - } - ); - // TODO: Deal with where width/ height is 0 as it probably means the element is hidden and we should - // come back to it later and check once it is unhidden... - paneWidth = elem.innerWidth() + originalPaddingTotalWidth; - paneHeight = elem.innerHeight(); - - elem.width(paneWidth); - - pane = $('
').css('padding', originalPadding).append(elem.children()); - container = $('
') - .css({ - 'width': paneWidth + 'px', - 'height': paneHeight + 'px' - } - ).append(pane).appendTo(elem); - - /* + if (pane === undefined) { + originalScrollTop = elem.scrollTop(); + originalScrollLeft = elem.scrollLeft(); + + elem.css({ + overflow: 'hidden', + padding: '0', + }); + // TODO: Deal with where width/ height is 0 as it probably means the element is hidden and we should + // come back to it later and check once it is unhidden... + paneWidth = elem.innerWidth() + originalPaddingTotalWidth; + paneHeight = elem.innerHeight(); + + elem.width(paneWidth); + + pane = $('
').css('padding', originalPadding).append(elem.children()); + container = $('
') + .css({ + width: paneWidth + 'px', + height: paneHeight + 'px', + }) + .append(pane) + .appendTo(elem); + + /* // Move any margins from the first and last children up to the container so they can still // collapse with neighbouring elements as they would before jScrollPane firstChild = pane.find(':first-child'); @@ -165,1500 +201,1355 @@ firstChild.css('margin-top', 0); lastChild.css('margin-bottom', 0); */ - } else { - elem.css('width', ''); - - // To measure the required dimensions accurately, temporarily override the CSS positioning - // of the container and pane. - container.css({width: 'auto', height: 'auto'}); - pane.css('position', 'static'); - - newPaneWidth = elem.innerWidth() + originalPaddingTotalWidth; - newPaneHeight = elem.innerHeight(); - pane.css('position', 'absolute'); - - maintainAtBottom = settings.stickToBottom && isCloseToBottom(); - maintainAtRight = settings.stickToRight && isCloseToRight(); - - hasContainingSpaceChanged = newPaneWidth !== paneWidth || newPaneHeight !== paneHeight; - - paneWidth = newPaneWidth; - paneHeight = newPaneHeight; - container.css({width: paneWidth + "px", height: paneHeight + "px"}); - - // If nothing changed since last check... - if (!hasContainingSpaceChanged && previousContentWidth == contentWidth && pane.outerHeight() == contentHeight) { - elem.width(paneWidth); - return; - } - previousContentWidth = contentWidth; - - pane.css('width', ''); - elem.width(paneWidth); - - container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end(); - } - - pane.css('overflow', 'auto'); - if (s.contentWidth) { - contentWidth = s.contentWidth; - } else { - contentWidth = pane[0].scrollWidth; - } - contentHeight = pane[0].scrollHeight; - pane.css('overflow', ''); - - percentInViewH = contentWidth / paneWidth; - percentInViewV = contentHeight / paneHeight; - isScrollableV = percentInViewV > 1 || settings.alwaysShowVScroll; - isScrollableH = percentInViewH > 1 || settings.alwaysShowHScroll; - - if (!(isScrollableH || isScrollableV)) { - elem.removeClass('jspScrollable'); - pane.css({ - top: '0', - left: '0', - width: container.width() - originalPaddingTotalWidth - }); - removeMousewheel(); - removeFocusHandler(); - removeKeyboardNav(); - removeClickOnTrack(); - } else { - elem.addClass('jspScrollable'); - - isMaintainingPositon = settings.maintainPosition && (verticalDragPosition || horizontalDragPosition); - - if (isMaintainingPositon) { - lastContentX = contentPositionX(); - lastContentY = contentPositionY(); - } - - initialiseVerticalScroll(); - initialiseHorizontalScroll(); - resizeScrollbars(); - - if (settings.stickToBottom || settings.stickToRight) { - scrollToX(maintainAtRight ? (contentWidth - paneWidth ) : lastContentX, false); - scrollToY(maintainAtBottom ? (contentHeight - paneHeight) : lastContentY, false); - } - - initFocusHandler(); - initMousewheel(); - initTouch(); - - if (settings.enableKeyboardNavigation) { - initKeyboardNav(); - } - if (settings.clickOnTrack) { - initClickOnTrack(); - } - - observeHash(); - if (settings.hijackInternalLinks) { - hijackInternalLinks(); - } - } - - if (!settings.resizeSensor && settings.autoReinitialise && !reinitialiseInterval) { - reinitialiseInterval = setInterval( - function() - { - initialise(settings); - }, - settings.autoReinitialiseDelay - ); - } else if (!settings.resizeSensor && !settings.autoReinitialise && reinitialiseInterval) { - clearInterval(reinitialiseInterval); - } + } else { + elem.css('width', ''); - if(settings.resizeSensor && !resizeEventsAdded) { + // To measure the required dimensions accurately, temporarily override the CSS positioning + // of the container and pane. + container.css({ width: 'auto', height: 'auto' }); + pane.css('position', 'static'); - // detect size change in content - detectSizeChanges(pane, reinitialiseFn); + newPaneWidth = elem.innerWidth() + originalPaddingTotalWidth; + newPaneHeight = elem.innerHeight(); + pane.css('position', 'absolute'); - // detect size changes of scroll element - detectSizeChanges(elem, reinitialiseFn); + maintainAtBottom = settings.stickToBottom && isCloseToBottom(); + maintainAtRight = settings.stickToRight && isCloseToRight(); - // detect size changes of container - detectSizeChanges(elem.parent(), reinitialiseFn); + hasContainingSpaceChanged = newPaneWidth !== paneWidth || newPaneHeight !== paneHeight; - // add a reinit on window resize also for safety - window.addEventListener('resize', reinitialiseFn); + paneWidth = newPaneWidth; + paneHeight = newPaneHeight; + container.css({ width: paneWidth + 'px', height: paneHeight + 'px' }); - resizeEventsAdded = true; - } + // If nothing changed since last check... + if (!hasContainingSpaceChanged && previousContentWidth == contentWidth && pane.outerHeight() == contentHeight) { + elem.width(paneWidth); + return; + } + previousContentWidth = contentWidth; + + pane.css('width', ''); + elem.width(paneWidth); + + container.find('>.jspVerticalBar,>.jspHorizontalBar').remove().end(); + } + + pane.css('overflow', 'auto'); + if (s.contentWidth) { + contentWidth = s.contentWidth; + } else { + contentWidth = pane[0].scrollWidth; + } + contentHeight = pane[0].scrollHeight; + pane.css('overflow', ''); + + percentInViewH = contentWidth / paneWidth; + percentInViewV = contentHeight / paneHeight; + isScrollableV = percentInViewV > 1 || settings.alwaysShowVScroll; + isScrollableH = percentInViewH > 1 || settings.alwaysShowHScroll; + + if (!(isScrollableH || isScrollableV)) { + elem.removeClass('jspScrollable'); + pane.css({ + top: '0', + left: '0', + width: container.width() - originalPaddingTotalWidth, + }); + removeMousewheel(); + removeFocusHandler(); + removeKeyboardNav(); + removeClickOnTrack(); + } else { + elem.addClass('jspScrollable'); + + isMaintainingPositon = settings.maintainPosition && (verticalDragPosition || horizontalDragPosition); + + if (isMaintainingPositon) { + lastContentX = contentPositionX(); + lastContentY = contentPositionY(); + } - if(originalScrollTop && elem.scrollTop(0)) { - scrollToY(originalScrollTop, false); - } + initialiseVerticalScroll(); + initialiseHorizontalScroll(); + resizeScrollbars(); - if(originalScrollLeft && elem.scrollLeft(0)) { - scrollToX(originalScrollLeft, false); - } + if (settings.stickToBottom || settings.stickToRight) { + scrollToX(maintainAtRight ? contentWidth - paneWidth : lastContentX, false); + scrollToY(maintainAtBottom ? contentHeight - paneHeight : lastContentY, false); + } - elem.trigger('jsp-initialised', [isScrollableH || isScrollableV]); - } - - function detectSizeChanges(element, callback) { - - // create resize event elements - based on resize sensor: https://github.com/flowkey/resize-sensor/ - var resizeWidth, resizeHeight; - var resizeElement = document.createElement('div'); - var resizeGrowElement = document.createElement('div'); - var resizeGrowChildElement = document.createElement('div'); - var resizeShrinkElement = document.createElement('div'); - var resizeShrinkChildElement = document.createElement('div'); - - // add necessary styling - resizeElement.style.cssText = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; - resizeGrowElement.style.cssText = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; - resizeShrinkElement.style.cssText = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; - - resizeGrowChildElement.style.cssText = 'position: absolute; left: 0; top: 0;'; - resizeShrinkChildElement.style.cssText = 'position: absolute; left: 0; top: 0; width: 200%; height: 200%;'; - - // Create a function to programmatically update sizes - var updateSizes = function() { - - resizeGrowChildElement.style.width = resizeGrowElement.offsetWidth + 10 + 'px'; - resizeGrowChildElement.style.height = resizeGrowElement.offsetHeight + 10 + 'px'; - - resizeGrowElement.scrollLeft = resizeGrowElement.scrollWidth; - resizeGrowElement.scrollTop = resizeGrowElement.scrollHeight; - - resizeShrinkElement.scrollLeft = resizeShrinkElement.scrollWidth; - resizeShrinkElement.scrollTop = resizeShrinkElement.scrollHeight; - - resizeWidth = element.width(); - resizeHeight = element.height(); - }; - - // create functions to call when content grows - var onGrow = function() { - - // check to see if the content has change size - if (element.width() > resizeWidth || element.height() > resizeHeight) { - - // if size has changed then reinitialise - callback.apply(this, []); - } - // after reinitialising update sizes - updateSizes(); - }; - - // create functions to call when content shrinks - var onShrink = function() { - - // check to see if the content has change size - if (element.width() < resizeWidth || element.height() < resizeHeight) { - - // if size has changed then reinitialise - callback.apply(this, []); - } - // after reinitialising update sizes - updateSizes(); - }; - - // bind to scroll events - resizeGrowElement.addEventListener('scroll', onGrow.bind(this)); - resizeShrinkElement.addEventListener('scroll', onShrink.bind(this)); - - // nest elements before adding to pane - resizeGrowElement.appendChild(resizeGrowChildElement); - resizeShrinkElement.appendChild(resizeShrinkChildElement); - - resizeElement.appendChild(resizeGrowElement); - resizeElement.appendChild(resizeShrinkElement); - - element.append(resizeElement); - - // ensure parent element is not statically positioned - if(window.getComputedStyle(element[0], null).getPropertyValue('position') === 'static') { - element[0].style.position = 'relative'; - } - - // update sizes initially - updateSizes(); - } - - function initialiseVerticalScroll() - { - if (isScrollableV) { - - container.append( - $('
').append( - $('
'), - $('
').append( - $('
').append( - $('
'), - $('
') - ) - ), - $('
') - ) - ); + initFocusHandler(); + initMousewheel(); + initTouch(); - verticalBar = container.find('>.jspVerticalBar'); - verticalTrack = verticalBar.find('>.jspTrack'); - verticalDrag = verticalTrack.find('>.jspDrag'); - - if (settings.showArrows) { - arrowUp = $('').on( - 'mousedown.jsp', getArrowScroll(0, -1) - ).on('click.jsp', nil); - arrowDown = $('').on( - 'mousedown.jsp', getArrowScroll(0, 1) - ).on('click.jsp', nil); - if (settings.arrowScrollOnHover) { - arrowUp.on('mouseover.jsp', getArrowScroll(0, -1, arrowUp)); - arrowDown.on('mouseover.jsp', getArrowScroll(0, 1, arrowDown)); - } + if (settings.enableKeyboardNavigation) { + initKeyboardNav(); + } + if (settings.clickOnTrack) { + initClickOnTrack(); + } - appendArrows(verticalTrack, settings.verticalArrowPositions, arrowUp, arrowDown); - } + observeHash(); + if (settings.hijackInternalLinks) { + hijackInternalLinks(); + } + } - verticalTrackHeight = paneHeight; - container.find('>.jspVerticalBar>.jspCap:visible,>.jspVerticalBar>.jspArrow').each( - function() - { - verticalTrackHeight -= $(this).outerHeight(); - } - ); + if (!settings.resizeSensor && settings.autoReinitialise && !reinitialiseInterval) { + reinitialiseInterval = setInterval(function () { + initialise(settings); + }, settings.autoReinitialiseDelay); + } else if (!settings.resizeSensor && !settings.autoReinitialise && reinitialiseInterval) { + clearInterval(reinitialiseInterval); + } + if (settings.resizeSensor && !resizeEventsAdded) { + // detect size change in content + detectSizeChanges(pane, reinitialiseFn); - verticalDrag.on( - "mouseenter", - function() - { - verticalDrag.addClass('jspHover'); - } - ).on( - "mouseleave", - function() - { - verticalDrag.removeClass('jspHover'); - } - ).on( - 'mousedown.jsp', - function(e) - { - // Stop IE from allowing text selection - $('html').on('dragstart.jsp selectstart.jsp', nil); + // detect size changes of scroll element + detectSizeChanges(elem, reinitialiseFn); - verticalDrag.addClass('jspActive'); + // detect size changes of container + detectSizeChanges(elem.parent(), reinitialiseFn); - var startY = e.pageY - verticalDrag.position().top; + // add a reinit on window resize also for safety + window.addEventListener('resize', reinitialiseFn); - $('html').on( - 'mousemove.jsp', - function(e) - { - positionDragY(e.pageY - startY, false); - } - ).on('mouseup.jsp mouseleave.jsp', cancelDrag); - return false; - } - ); - sizeVerticalScrollbar(); - } - } - - function sizeVerticalScrollbar() - { - verticalTrack.height(verticalTrackHeight + 'px'); - verticalDragPosition = 0; - scrollbarWidth = settings.verticalGutter + verticalTrack.outerWidth(); - - // Make the pane thinner to allow for the vertical scrollbar - pane.width(paneWidth - scrollbarWidth - originalPaddingTotalWidth); - - // Add margin to the left of the pane if scrollbars are on that side (to position - // the scrollbar on the left or right set it's left or right property in CSS) - try { - if (verticalBar.position().left === 0) { - pane.css('margin-left', scrollbarWidth + 'px'); - } - } catch (err) { - } - } - - function initialiseHorizontalScroll() - { - if (isScrollableH) { - - container.append( - $('
').append( - $('
'), - $('
').append( - $('
').append( - $('
'), - $('
') - ) - ), - $('
') - ) - ); + resizeEventsAdded = true; + } - horizontalBar = container.find('>.jspHorizontalBar'); - horizontalTrack = horizontalBar.find('>.jspTrack'); - horizontalDrag = horizontalTrack.find('>.jspDrag'); - - if (settings.showArrows) { - arrowLeft = $('').on( - 'mousedown.jsp', getArrowScroll(-1, 0) - ).on('click.jsp', nil); - arrowRight = $('').on( - 'mousedown.jsp', getArrowScroll(1, 0) - ).on('click.jsp', nil); - if (settings.arrowScrollOnHover) { - arrowLeft.on('mouseover.jsp', getArrowScroll(-1, 0, arrowLeft)); - arrowRight.on('mouseover.jsp', getArrowScroll(1, 0, arrowRight)); - } - appendArrows(horizontalTrack, settings.horizontalArrowPositions, arrowLeft, arrowRight); - } + if (originalScrollTop && elem.scrollTop(0)) { + scrollToY(originalScrollTop, false); + } - horizontalDrag.on( - "mouseenter", - function() - { - horizontalDrag.addClass('jspHover'); - } - ).on( - "mouseleave", - function() - { - horizontalDrag.removeClass('jspHover'); - } - ).on( - 'mousedown.jsp', - function(e) - { - // Stop IE from allowing text selection - $('html').on('dragstart.jsp selectstart.jsp', nil); + if (originalScrollLeft && elem.scrollLeft(0)) { + scrollToX(originalScrollLeft, false); + } - horizontalDrag.addClass('jspActive'); + elem.trigger('jsp-initialised', [isScrollableH || isScrollableV]); + } - var startX = e.pageX - horizontalDrag.position().left; + function detectSizeChanges(element, callback) { + // create resize event elements - based on resize sensor: https://github.com/flowkey/resize-sensor/ + var resizeWidth, resizeHeight; + var resizeElement = document.createElement('div'); + var resizeGrowElement = document.createElement('div'); + var resizeGrowChildElement = document.createElement('div'); + var resizeShrinkElement = document.createElement('div'); + var resizeShrinkChildElement = document.createElement('div'); + + // add necessary styling + resizeElement.style.cssText = + 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; + resizeGrowElement.style.cssText = + 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; + resizeShrinkElement.style.cssText = + 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;'; + + resizeGrowChildElement.style.cssText = 'position: absolute; left: 0; top: 0;'; + resizeShrinkChildElement.style.cssText = 'position: absolute; left: 0; top: 0; width: 200%; height: 200%;'; + + // Create a function to programmatically update sizes + var updateSizes = function () { + resizeGrowChildElement.style.width = resizeGrowElement.offsetWidth + 10 + 'px'; + resizeGrowChildElement.style.height = resizeGrowElement.offsetHeight + 10 + 'px'; + + resizeGrowElement.scrollLeft = resizeGrowElement.scrollWidth; + resizeGrowElement.scrollTop = resizeGrowElement.scrollHeight; + + resizeShrinkElement.scrollLeft = resizeShrinkElement.scrollWidth; + resizeShrinkElement.scrollTop = resizeShrinkElement.scrollHeight; + + resizeWidth = element.width(); + resizeHeight = element.height(); + }; + + // create functions to call when content grows + var onGrow = function () { + // check to see if the content has change size + if (element.width() > resizeWidth || element.height() > resizeHeight) { + // if size has changed then reinitialise + callback.apply(this, []); + } + // after reinitialising update sizes + updateSizes(); + }; + + // create functions to call when content shrinks + var onShrink = function () { + // check to see if the content has change size + if (element.width() < resizeWidth || element.height() < resizeHeight) { + // if size has changed then reinitialise + callback.apply(this, []); + } + // after reinitialising update sizes + updateSizes(); + }; - $('html').on( - 'mousemove.jsp', - function(e) - { - positionDragX(e.pageX - startX, false); - } - ).on('mouseup.jsp mouseleave.jsp', cancelDrag); - return false; - } - ); - horizontalTrackWidth = container.innerWidth(); - sizeHorizontalScrollbar(); - } - } - - function sizeHorizontalScrollbar() - { - container.find('>.jspHorizontalBar>.jspCap:visible,>.jspHorizontalBar>.jspArrow').each( - function() - { - horizontalTrackWidth -= $(this).outerWidth(); - } - ); - - horizontalTrack.width(horizontalTrackWidth + 'px'); - horizontalDragPosition = 0; - } - - function resizeScrollbars() - { - if (isScrollableH && isScrollableV) { - var horizontalTrackHeight = horizontalTrack.outerHeight(), - verticalTrackWidth = verticalTrack.outerWidth(); - verticalTrackHeight -= horizontalTrackHeight; - $(horizontalBar).find('>.jspCap:visible,>.jspArrow').each( - function() - { - horizontalTrackWidth += $(this).outerWidth(); - } - ); - horizontalTrackWidth -= verticalTrackWidth; - paneHeight -= verticalTrackWidth; - paneWidth -= horizontalTrackHeight; - horizontalTrack.parent().append( - $('
').css('width', horizontalTrackHeight + 'px') - ); - sizeVerticalScrollbar(); - sizeHorizontalScrollbar(); - } - // reflow content - if (isScrollableH) { - pane.width((container.outerWidth() - originalPaddingTotalWidth) + 'px'); - } - contentHeight = pane.outerHeight(); - percentInViewV = contentHeight / paneHeight; - - if (isScrollableH) { - horizontalDragWidth = Math.ceil(1 / percentInViewH * horizontalTrackWidth); - if (horizontalDragWidth > settings.horizontalDragMaxWidth) { - horizontalDragWidth = settings.horizontalDragMaxWidth; - } else if (horizontalDragWidth < settings.horizontalDragMinWidth) { - horizontalDragWidth = settings.horizontalDragMinWidth; - } - horizontalDrag.css('width', horizontalDragWidth + 'px'); - dragMaxX = horizontalTrackWidth - horizontalDragWidth; - _positionDragX(horizontalDragPosition); // To update the state for the arrow buttons - } - if (isScrollableV) { - verticalDragHeight = Math.ceil(1 / percentInViewV * verticalTrackHeight); - if (verticalDragHeight > settings.verticalDragMaxHeight) { - verticalDragHeight = settings.verticalDragMaxHeight; - } else if (verticalDragHeight < settings.verticalDragMinHeight) { - verticalDragHeight = settings.verticalDragMinHeight; - } - verticalDrag.css('height', verticalDragHeight + 'px'); - dragMaxY = verticalTrackHeight - verticalDragHeight; - _positionDragY(verticalDragPosition); // To update the state for the arrow buttons - } - } - - function appendArrows(ele, p, a1, a2) - { - var p1 = "before", p2 = "after", aTemp; - - // Sniff for mac... Is there a better way to determine whether the arrows would naturally appear - // at the top or the bottom of the bar? - if (p == "os") { - p = /Mac/.test(navigator.platform) ? "after" : "split"; - } - if (p == p1) { - p2 = p; - } else if (p == p2) { - p1 = p; - aTemp = a1; - a1 = a2; - a2 = aTemp; - } - - ele[p1](a1)[p2](a2); - } - - function getArrowScroll(dirX, dirY, ele) - { - return function() - { - arrowScroll(dirX, dirY, this, ele); - this.blur(); - return false; - }; - } - - function arrowScroll(dirX, dirY, arrow, ele) - { - arrow = $(arrow).addClass('jspActive'); - - var eve, - scrollTimeout, - isFirst = true, - doScroll = function() - { - if (dirX !== 0) { - jsp.scrollByX(dirX * settings.arrowButtonSpeed); - } - if (dirY !== 0) { - jsp.scrollByY(dirY * settings.arrowButtonSpeed); - } - scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.arrowRepeatFreq); - isFirst = false; - }; - - doScroll(); - - eve = ele ? 'mouseout.jsp' : 'mouseup.jsp'; - ele = ele || $('html'); - ele.on( - eve, - function() - { - arrow.removeClass('jspActive'); - if(scrollTimeout) { - clearTimeout(scrollTimeout); + // bind to scroll events + resizeGrowElement.addEventListener('scroll', onGrow.bind(this)); + resizeShrinkElement.addEventListener('scroll', onShrink.bind(this)); + + // nest elements before adding to pane + resizeGrowElement.appendChild(resizeGrowChildElement); + resizeShrinkElement.appendChild(resizeShrinkChildElement); + + resizeElement.appendChild(resizeGrowElement); + resizeElement.appendChild(resizeShrinkElement); + + element.append(resizeElement); + + // ensure parent element is not statically positioned + if (window.getComputedStyle(element[0], null).getPropertyValue('position') === 'static') { + element[0].style.position = 'relative'; + } + + // update sizes initially + updateSizes(); } - scrollTimeout = null; - ele.off(eve); - } - ); - } - - function initClickOnTrack() - { - removeClickOnTrack(); - if (isScrollableV) { - verticalTrack.on( - 'mousedown.jsp', - function(e) - { - if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) { - var clickedTrack = $(this), - offset = clickedTrack.offset(), - direction = e.pageY - offset.top - verticalDragPosition, - scrollTimeout, - isFirst = true, - doScroll = function() - { - var offset = clickedTrack.offset(), - pos = e.pageY - offset.top - verticalDragHeight / 2, - contentDragY = paneHeight * settings.scrollPagePercent, - dragY = dragMaxY * contentDragY / (contentHeight - paneHeight); - if (direction < 0) { - if (verticalDragPosition - dragY > pos) { - jsp.scrollByY(-contentDragY); - } else { - positionDragY(pos); - } - } else if (direction > 0) { - if (verticalDragPosition + dragY < pos) { - jsp.scrollByY(contentDragY); - } else { - positionDragY(pos); - } - } else { - cancelClick(); - return; - } - scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq); - isFirst = false; - }, - cancelClick = function() - { - if(scrollTimeout) { - clearTimeout(scrollTimeout); + + function initialiseVerticalScroll() { + if (isScrollableV) { + container.append( + $('
').append( + $('
'), + $('
').append( + $('
').append( + $('
'), + $('
'), + ), + ), + $('
'), + ), + ); + + verticalBar = container.find('>.jspVerticalBar'); + verticalTrack = verticalBar.find('>.jspTrack'); + verticalDrag = verticalTrack.find('>.jspDrag'); + + if (settings.showArrows) { + arrowUp = $('').on('mousedown.jsp', getArrowScroll(0, -1)).on('click.jsp', nil); + arrowDown = $('') + .on('mousedown.jsp', getArrowScroll(0, 1)) + .on('click.jsp', nil); + if (settings.arrowScrollOnHover) { + arrowUp.on('mouseover.jsp', getArrowScroll(0, -1, arrowUp)); + arrowDown.on('mouseover.jsp', getArrowScroll(0, 1, arrowDown)); + } + + appendArrows(verticalTrack, settings.verticalArrowPositions, arrowUp, arrowDown); } - scrollTimeout = null; - $(document).off('mouseup.jsp', cancelClick); - }; - doScroll(); - $(document).on('mouseup.jsp', cancelClick); - return false; - } - } - ); - } - if (isScrollableH) { - horizontalTrack.on( - 'mousedown.jsp', - function(e) - { - if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) { - var clickedTrack = $(this), - offset = clickedTrack.offset(), - direction = e.pageX - offset.left - horizontalDragPosition, - scrollTimeout, - isFirst = true, - doScroll = function() - { - var offset = clickedTrack.offset(), - pos = e.pageX - offset.left - horizontalDragWidth / 2, - contentDragX = paneWidth * settings.scrollPagePercent, - dragX = dragMaxX * contentDragX / (contentWidth - paneWidth); - if (direction < 0) { - if (horizontalDragPosition - dragX > pos) { - jsp.scrollByX(-contentDragX); - } else { - positionDragX(pos); - } - } else if (direction > 0) { - if (horizontalDragPosition + dragX < pos) { - jsp.scrollByX(contentDragX); - } else { - positionDragX(pos); - } - } else { - cancelClick(); - return; - } - scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq); - isFirst = false; - }, - cancelClick = function() - { - if(scrollTimeout) { - clearTimeout(scrollTimeout); + verticalTrackHeight = paneHeight; + container.find('>.jspVerticalBar>.jspCap:visible,>.jspVerticalBar>.jspArrow').each(function () { + verticalTrackHeight -= $(this).outerHeight(); + }); + + verticalDrag + .on('mouseenter', function () { + verticalDrag.addClass('jspHover'); + }) + .on('mouseleave', function () { + verticalDrag.removeClass('jspHover'); + }) + .on('mousedown.jsp', function (e) { + // Stop IE from allowing text selection + $('html').on('dragstart.jsp selectstart.jsp', nil); + + verticalDrag.addClass('jspActive'); + + var startY = e.pageY - verticalDrag.position().top; + + $('html') + .on('mousemove.jsp', function (e) { + positionDragY(e.pageY - startY, false); + }) + .on('mouseup.jsp mouseleave.jsp', cancelDrag); + return false; + }); + sizeVerticalScrollbar(); + } + } + + function sizeVerticalScrollbar() { + verticalTrack.height(verticalTrackHeight + 'px'); + verticalDragPosition = 0; + scrollbarWidth = settings.verticalGutter + verticalTrack.outerWidth(); + + // Make the pane thinner to allow for the vertical scrollbar + pane.width(paneWidth - scrollbarWidth - originalPaddingTotalWidth); + + // Add margin to the left of the pane if scrollbars are on that side (to position + // the scrollbar on the left or right set it's left or right property in CSS) + try { + if (verticalBar.position().left === 0) { + pane.css('margin-left', scrollbarWidth + 'px'); } - scrollTimeout = null; - $(document).off('mouseup.jsp', cancelClick); - }; - doScroll(); - $(document).on('mouseup.jsp', cancelClick); - return false; - } - } - ); - } - } - - function removeClickOnTrack() - { - if (horizontalTrack) { - horizontalTrack.off('mousedown.jsp'); - } - if (verticalTrack) { - verticalTrack.off('mousedown.jsp'); - } - } - - function cancelDrag() - { - $('html').off('dragstart.jsp selectstart.jsp mousemove.jsp mouseup.jsp mouseleave.jsp'); - - if (verticalDrag) { - verticalDrag.removeClass('jspActive'); - } - if (horizontalDrag) { - horizontalDrag.removeClass('jspActive'); - } - } - - function positionDragY(destY, animate) - { - if (!isScrollableV) { - return; - } - if (destY < 0) { - destY = 0; - } else if (destY > dragMaxY) { - destY = dragMaxY; - } - - // allow for devs to prevent the JSP from being scrolled - var willScrollYEvent = new $.Event("jsp-will-scroll-y"); - elem.trigger(willScrollYEvent, [destY]); - - if (willScrollYEvent.isDefaultPrevented()) { - return; - } - - var tmpVerticalDragPosition = destY || 0; - - var isAtTop = tmpVerticalDragPosition === 0, - isAtBottom = tmpVerticalDragPosition == dragMaxY, - percentScrolled = destY/ dragMaxY, - destTop = -percentScrolled * (contentHeight - paneHeight); - - // can't just check if(animate) because false is a valid value that could be passed in... - if (animate === undefined) { - animate = settings.animateScroll; - } - if (animate) { - jsp.animate(verticalDrag, 'top', destY, _positionDragY, function() { - elem.trigger('jsp-user-scroll-y', [-destTop, isAtTop, isAtBottom]); - }); - } else { - verticalDrag.css('top', destY + 'px'); - _positionDragY(destY); - elem.trigger('jsp-user-scroll-y', [-destTop, isAtTop, isAtBottom]); - } - - } - - function _positionDragY(destY) - { - if (destY === undefined) { - destY = verticalDrag.position().top; - } - - container.scrollTop(0); - verticalDragPosition = destY || 0; - - var isAtTop = verticalDragPosition === 0, - isAtBottom = verticalDragPosition == dragMaxY, - percentScrolled = destY/ dragMaxY, - destTop = -percentScrolled * (contentHeight - paneHeight); - - if (wasAtTop != isAtTop || wasAtBottom != isAtBottom) { - wasAtTop = isAtTop; - wasAtBottom = isAtBottom; - elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]); - } - - updateVerticalArrows(isAtTop, isAtBottom); - pane.css('top', destTop + 'px'); - elem.trigger('jsp-scroll-y', [-destTop, isAtTop, isAtBottom]).trigger('scroll'); - } - - function positionDragX(destX, animate) - { - if (!isScrollableH) { - return; - } - if (destX < 0) { - destX = 0; - } else if (destX > dragMaxX) { - destX = dragMaxX; - } - - - // allow for devs to prevent the JSP from being scrolled - var willScrollXEvent = new $.Event("jsp-will-scroll-x"); - elem.trigger(willScrollXEvent, [destX]); - - if (willScrollXEvent.isDefaultPrevented()) { - return; - } - - var tmpHorizontalDragPosition = destX ||0; - - var isAtLeft = tmpHorizontalDragPosition === 0, - isAtRight = tmpHorizontalDragPosition == dragMaxX, - percentScrolled = destX / dragMaxX, - destLeft = -percentScrolled * (contentWidth - paneWidth); - - if (animate === undefined) { - animate = settings.animateScroll; - } - if (animate) { - jsp.animate(horizontalDrag, 'left', destX, _positionDragX, function() { - elem.trigger('jsp-user-scroll-x', [-destLeft, isAtLeft, isAtRight]); - }); - } else { - horizontalDrag.css('left', destX + 'px'); - _positionDragX(destX); - elem.trigger('jsp-user-scroll-x', [-destLeft, isAtLeft, isAtRight]); - } - } - - function _positionDragX(destX) - { - if (destX === undefined) { - destX = horizontalDrag.position().left; - } - - container.scrollTop(0); - horizontalDragPosition = destX ||0; - - var isAtLeft = horizontalDragPosition === 0, - isAtRight = horizontalDragPosition == dragMaxX, - percentScrolled = destX / dragMaxX, - destLeft = -percentScrolled * (contentWidth - paneWidth); - - if (wasAtLeft != isAtLeft || wasAtRight != isAtRight) { - wasAtLeft = isAtLeft; - wasAtRight = isAtRight; - elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]); - } - - updateHorizontalArrows(isAtLeft, isAtRight); - pane.css('left', destLeft + 'px'); - elem.trigger('jsp-scroll-x', [-destLeft, isAtLeft, isAtRight]).trigger('scroll'); - } - - function updateVerticalArrows(isAtTop, isAtBottom) - { - if (settings.showArrows) { - arrowUp[isAtTop ? 'addClass' : 'removeClass']('jspDisabled'); - arrowDown[isAtBottom ? 'addClass' : 'removeClass']('jspDisabled'); - } - } - - function updateHorizontalArrows(isAtLeft, isAtRight) - { - if (settings.showArrows) { - arrowLeft[isAtLeft ? 'addClass' : 'removeClass']('jspDisabled'); - arrowRight[isAtRight ? 'addClass' : 'removeClass']('jspDisabled'); - } - } - - function scrollToY(destY, animate) - { - var percentScrolled = destY / (contentHeight - paneHeight); - positionDragY(percentScrolled * dragMaxY, animate); - } - - function scrollToX(destX, animate) - { - var percentScrolled = destX / (contentWidth - paneWidth); - positionDragX(percentScrolled * dragMaxX, animate); - } - - function scrollToElement(ele, stickToTop, animate) - { - var e, eleHeight, eleWidth, eleTop = 0, eleLeft = 0, viewportTop, viewportLeft, maxVisibleEleTop, maxVisibleEleLeft, destY, destX; - - // Legal hash values aren't necessarily legal jQuery selectors so we need to catch any - // errors from the lookup... - try { - e = $(ele); - } catch (err) { - return; - } - eleHeight = e.outerHeight(); - eleWidth= e.outerWidth(); - - container.scrollTop(0); - container.scrollLeft(0); - - // loop through parents adding the offset top of any elements that are relatively positioned between - // the focused element and the jspPane so we can get the true distance from the top - // of the focused element to the top of the scrollpane... - while (!e.is('.jspPane')) { - eleTop += e.position().top; - eleLeft += e.position().left; - e = e.offsetParent(); - if (/^body|html$/i.test(e[0].nodeName)) { - // we ended up too high in the document structure. Quit! - return; - } - } - - viewportTop = contentPositionY(); - maxVisibleEleTop = viewportTop + paneHeight; - if (eleTop < viewportTop || stickToTop) { // element is above viewport - destY = eleTop - settings.horizontalGutter; - } else if (eleTop + eleHeight > maxVisibleEleTop) { // element is below viewport - destY = eleTop - paneHeight + eleHeight + settings.horizontalGutter; - } - if (!isNaN(destY)) { - scrollToY(destY, animate); - } - - viewportLeft = contentPositionX(); - maxVisibleEleLeft = viewportLeft + paneWidth; - if (eleLeft < viewportLeft || stickToTop) { // element is to the left of viewport - destX = eleLeft - settings.horizontalGutter; - } else if (eleLeft + eleWidth > maxVisibleEleLeft) { // element is to the right viewport - destX = eleLeft - paneWidth + eleWidth + settings.horizontalGutter; - } - if (!isNaN(destX)) { - scrollToX(destX, animate); - } - - } - - function contentPositionX() - { - return -pane.position().left; - } - - function contentPositionY() - { - return -pane.position().top; - } - - function isCloseToBottom() - { - var scrollableHeight = contentHeight - paneHeight; - - if(settings.maintainPosition == true) - { - return (scrollableHeight >= 20) && (scrollableHeight - contentPositionY() < 10); - } - - return true; - } - - function isCloseToRight() - { - var scrollableWidth = contentWidth - paneWidth; - - if(settings.maintainPosition == true) - { - return (scrollableWidth >= 20) && (scrollableWidth - contentPositionX() < 10); - } - - return true; - } - - function initMousewheel() - { - container.off(mwEvent).on( - mwEvent, - function (event, delta, deltaX, deltaY) { - - if (!horizontalDragPosition) horizontalDragPosition = 0; - if (!verticalDragPosition) verticalDragPosition = 0; - - var dX = horizontalDragPosition, dY = verticalDragPosition, factor = event.deltaFactor || settings.mouseWheelSpeed; - jsp.scrollBy(deltaX * factor, -deltaY * factor, false); - // return true if there was no movement so rest of screen can scroll - return dX == horizontalDragPosition && dY == verticalDragPosition; - } - ); - } - - function removeMousewheel() - { - container.off(mwEvent); - } - - function nil() - { - return false; - } - - function initFocusHandler() - { - pane.find(':input,a').off('focus.jsp').on( - 'focus.jsp', - function(e) - { - scrollToElement(e.target, false); - } - ); - } - - function removeFocusHandler() - { - pane.find(':input,a').off('focus.jsp'); - } - - function initKeyboardNav() - { - var keyDown, elementHasScrolled, validParents = []; - if(isScrollableH) { - validParents.push(horizontalBar[0]); - } + } catch (err) {} + } - if(isScrollableV) { - validParents.push(verticalBar[0]); - } + function initialiseHorizontalScroll() { + if (isScrollableH) { + container.append( + $('
').append( + $('
'), + $('
').append( + $('
').append( + $('
'), + $('
'), + ), + ), + $('
'), + ), + ); + + horizontalBar = container.find('>.jspHorizontalBar'); + horizontalTrack = horizontalBar.find('>.jspTrack'); + horizontalDrag = horizontalTrack.find('>.jspDrag'); + + if (settings.showArrows) { + arrowLeft = $('') + .on('mousedown.jsp', getArrowScroll(-1, 0)) + .on('click.jsp', nil); + arrowRight = $('') + .on('mousedown.jsp', getArrowScroll(1, 0)) + .on('click.jsp', nil); + if (settings.arrowScrollOnHover) { + arrowLeft.on('mouseover.jsp', getArrowScroll(-1, 0, arrowLeft)); + arrowRight.on('mouseover.jsp', getArrowScroll(1, 0, arrowRight)); + } + appendArrows(horizontalTrack, settings.horizontalArrowPositions, arrowLeft, arrowRight); + } - // IE also focuses elements that don't have tabindex set. - pane.on( - 'focus.jsp', - function() - { - elem.focus(); - } - ); - - elem.attr('tabindex', 0) - .off('keydown.jsp keypress.jsp') - .on( - 'keydown.jsp', - function(e) - { - if (e.target !== this && !(validParents.length && $(e.target).closest(validParents).length)){ - return; - } - var dX = horizontalDragPosition, dY = verticalDragPosition; - switch(e.keyCode) { - case 40: // down - case 38: // up - case 34: // page down - case 32: // space - case 33: // page up - case 39: // right - case 37: // left - keyDown = e.keyCode; - keyDownHandler(); - break; - case 35: // end - scrollToY(contentHeight - paneHeight); - keyDown = null; - break; - case 36: // home - scrollToY(0); - keyDown = null; - break; - } - - elementHasScrolled = e.keyCode == keyDown && dX != horizontalDragPosition || dY != verticalDragPosition; - return !elementHasScrolled; - } - ).on( - 'keypress.jsp', // For FF/ OSX so that we can cancel the repeat key presses if the JSP scrolls... - function(e) - { - if (e.keyCode == keyDown) { - keyDownHandler(); - } - // If the keypress is not related to the area, ignore it. Fixes problem with inputs inside scrolled area. Copied from line 955. - if (e.target !== this && !(validParents.length && $(e.target).closest(validParents).length)){ - return; - } - return !elementHasScrolled; - } - ); + horizontalDrag + .on('mouseenter', function () { + horizontalDrag.addClass('jspHover'); + }) + .on('mouseleave', function () { + horizontalDrag.removeClass('jspHover'); + }) + .on('mousedown.jsp', function (e) { + // Stop IE from allowing text selection + $('html').on('dragstart.jsp selectstart.jsp', nil); + + horizontalDrag.addClass('jspActive'); + + var startX = e.pageX - horizontalDrag.position().left; + + $('html') + .on('mousemove.jsp', function (e) { + positionDragX(e.pageX - startX, false); + }) + .on('mouseup.jsp mouseleave.jsp', cancelDrag); + return false; + }); + horizontalTrackWidth = container.innerWidth(); + sizeHorizontalScrollbar(); + } + } - if (settings.hideFocus) { - elem.css('outline', 'none'); - if ('hideFocus' in container[0]){ - elem.attr('hideFocus', true); - } - } else { - elem.css('outline', ''); - if ('hideFocus' in container[0]){ - elem.attr('hideFocus', false); - } - } - - function keyDownHandler() - { - var dX = horizontalDragPosition, dY = verticalDragPosition; - switch(keyDown) { - case 40: // down - jsp.scrollByY(settings.keyboardSpeed, false); - break; - case 38: // up - jsp.scrollByY(-settings.keyboardSpeed, false); - break; - case 34: // page down - case 32: // space - jsp.scrollByY(paneHeight * settings.scrollPagePercent, false); - break; - case 33: // page up - jsp.scrollByY(-paneHeight * settings.scrollPagePercent, false); - break; - case 39: // right - jsp.scrollByX(settings.keyboardSpeed, false); - break; - case 37: // left - jsp.scrollByX(-settings.keyboardSpeed, false); - break; - } - - elementHasScrolled = dX != horizontalDragPosition || dY != verticalDragPosition; - return elementHasScrolled; - } - } - - function removeKeyboardNav() - { - elem.attr('tabindex', '-1') - .removeAttr('tabindex') - .off('keydown.jsp keypress.jsp'); - - pane.off('.jsp'); - } - - function observeHash() - { - if (location.hash && location.hash.length > 1) { - var e, - retryInt, - hash = escape(location.hash.substr(1)) // hash must be escaped to prevent XSS - ; - try { - e = $('#' + hash + ', a[name="' + hash + '"]'); - } catch (err) { - return; - } - - if (e.length && pane.find(hash)) { - // nasty workaround but it appears to take a little while before the hash has done its thing - // to the rendered page so we just wait until the container's scrollTop has been messed up. - if (container.scrollTop() === 0) { - retryInt = setInterval( - function() - { - if (container.scrollTop() > 0) { - scrollToElement(e, true); - $(document).scrollTop(container.position().top); - clearInterval(retryInt); - } - }, - 50 - ); - } else { - scrollToElement(e, true); - $(document).scrollTop(container.position().top); - } - } - } - } - - function hijackInternalLinks() - { - // only register the link handler once - if ($(document.body).data('jspHijack')) { - return; - } - - // remember that the handler was bound - $(document.body).data('jspHijack', true); - - // use live handler to also capture newly created links - $(document.body).delegate('a[href*="#"]', 'click', function(event) { - // does the link point to the same page? - // this also takes care of cases with a -Tag or Links not starting with the hash # - // e.g. when the current url already is index.html - var href = this.href.substr(0, this.href.indexOf('#')), - locationHref = location.href, - hash, - element, - container, - jsp, - scrollTop, - elementTop; - if (location.href.indexOf('#') !== -1) { - locationHref = location.href.substr(0, location.href.indexOf('#')); - } - if (href !== locationHref) { - // the link points to another page - return; - } - - // check if jScrollPane should handle this click event - hash = escape(this.href.substr(this.href.indexOf('#') + 1)); - - // find the element on the page - try { - element = $('#' + hash + ', a[name="' + hash + '"]'); - } catch (e) { - // hash is not a valid jQuery identifier - return; - } - - if (!element.length) { - // this link does not point to an element on this page - return; - } - - container = element.closest('.jspScrollable'); - jsp = container.data('jsp'); - - // jsp might be another jsp instance than the one, that bound this event - // remember: this event is only bound once for all instances. - jsp.scrollToElement(element, true); - - if (container[0].scrollIntoView) { - // also scroll to the top of the container (if it is not visible) - scrollTop = $(window).scrollTop(); - elementTop = element.offset().top; - if (elementTop < scrollTop || elementTop > scrollTop + $(window).height()) { - container[0].scrollIntoView(); - } - } - - // jsp handled this event, prevent the browser default (scrolling :P) - event.preventDefault(); - }); - } - - // Init touch on iPad, iPhone, iPod, Android - function initTouch() - { - var startX, - startY, - touchStartX, - touchStartY, - moved, - moving = false; - - container.off('touchstart.jsp touchmove.jsp touchend.jsp click.jsp-touchclick').on( - 'touchstart.jsp', - function(e) - { - var touch = e.originalEvent.touches[0]; - startX = contentPositionX(); - startY = contentPositionY(); - touchStartX = touch.pageX; - touchStartY = touch.pageY; - moved = false; - moving = true; - } - ).on( - 'touchmove.jsp', - function(ev) - { - if(!moving) { - return; - } + function sizeHorizontalScrollbar() { + container.find('>.jspHorizontalBar>.jspCap:visible,>.jspHorizontalBar>.jspArrow').each(function () { + horizontalTrackWidth -= $(this).outerWidth(); + }); - var touchPos = ev.originalEvent.touches[0], - dX = horizontalDragPosition, dY = verticalDragPosition; + horizontalTrack.width(horizontalTrackWidth + 'px'); + horizontalDragPosition = 0; + } - jsp.scrollTo(startX + touchStartX - touchPos.pageX, startY + touchStartY - touchPos.pageY); + function resizeScrollbars() { + if (isScrollableH && isScrollableV) { + var horizontalTrackHeight = horizontalTrack.outerHeight(), + verticalTrackWidth = verticalTrack.outerWidth(); + verticalTrackHeight -= horizontalTrackHeight; + $(horizontalBar) + .find('>.jspCap:visible,>.jspArrow') + .each(function () { + horizontalTrackWidth += $(this).outerWidth(); + }); + horizontalTrackWidth -= verticalTrackWidth; + paneHeight -= verticalTrackWidth; + paneWidth -= horizontalTrackHeight; + horizontalTrack.parent().append($('
').css('width', horizontalTrackHeight + 'px')); + sizeVerticalScrollbar(); + sizeHorizontalScrollbar(); + } + // reflow content + if (isScrollableH) { + pane.width(container.outerWidth() - originalPaddingTotalWidth + 'px'); + } + contentHeight = pane.outerHeight(); + percentInViewV = contentHeight / paneHeight; + + if (isScrollableH) { + horizontalDragWidth = Math.ceil((1 / percentInViewH) * horizontalTrackWidth); + if (horizontalDragWidth > settings.horizontalDragMaxWidth) { + horizontalDragWidth = settings.horizontalDragMaxWidth; + } else if (horizontalDragWidth < settings.horizontalDragMinWidth) { + horizontalDragWidth = settings.horizontalDragMinWidth; + } + horizontalDrag.css('width', horizontalDragWidth + 'px'); + dragMaxX = horizontalTrackWidth - horizontalDragWidth; + _positionDragX(horizontalDragPosition); // To update the state for the arrow buttons + } + if (isScrollableV) { + verticalDragHeight = Math.ceil((1 / percentInViewV) * verticalTrackHeight); + if (verticalDragHeight > settings.verticalDragMaxHeight) { + verticalDragHeight = settings.verticalDragMaxHeight; + } else if (verticalDragHeight < settings.verticalDragMinHeight) { + verticalDragHeight = settings.verticalDragMinHeight; + } + verticalDrag.css('height', verticalDragHeight + 'px'); + dragMaxY = verticalTrackHeight - verticalDragHeight; + _positionDragY(verticalDragPosition); // To update the state for the arrow buttons + } + } - moved = moved || Math.abs(touchStartX - touchPos.pageX) > 5 || Math.abs(touchStartY - touchPos.pageY) > 5; + function appendArrows(ele, p, a1, a2) { + var p1 = 'before', + p2 = 'after', + aTemp; + + // Sniff for mac... Is there a better way to determine whether the arrows would naturally appear + // at the top or the bottom of the bar? + if (p == 'os') { + p = /Mac/.test(navigator.platform) ? 'after' : 'split'; + } + if (p == p1) { + p2 = p; + } else if (p == p2) { + p1 = p; + aTemp = a1; + a1 = a2; + a2 = aTemp; + } + + ele[p1](a1)[p2](a2); + } + + function getArrowScroll(dirX, dirY, ele) { + return function () { + arrowScroll(dirX, dirY, this, ele); + this.blur(); + return false; + }; + } + + function arrowScroll(dirX, dirY, arrow, ele) { + arrow = $(arrow).addClass('jspActive'); + + var eve, + scrollTimeout, + isFirst = true, + doScroll = function () { + if (dirX !== 0) { + jsp.scrollByX(dirX * settings.arrowButtonSpeed); + } + if (dirY !== 0) { + jsp.scrollByY(dirY * settings.arrowButtonSpeed); + } + scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.arrowRepeatFreq); + isFirst = false; + }; + + doScroll(); + + eve = ele ? 'mouseout.jsp' : 'mouseup.jsp'; + ele = ele || $('html'); + ele.on(eve, function () { + arrow.removeClass('jspActive'); + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + scrollTimeout = null; + ele.off(eve); + }); + } + + function initClickOnTrack() { + removeClickOnTrack(); + if (isScrollableV) { + verticalTrack.on('mousedown.jsp', function (e) { + if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) { + var clickedTrack = $(this), + offset = clickedTrack.offset(), + direction = e.pageY - offset.top - verticalDragPosition, + scrollTimeout, + isFirst = true, + doScroll = function () { + var offset = clickedTrack.offset(), + pos = e.pageY - offset.top - verticalDragHeight / 2, + contentDragY = paneHeight * settings.scrollPagePercent, + dragY = (dragMaxY * contentDragY) / (contentHeight - paneHeight); + if (direction < 0) { + if (verticalDragPosition - dragY > pos) { + jsp.scrollByY(-contentDragY); + } else { + positionDragY(pos); + } + } else if (direction > 0) { + if (verticalDragPosition + dragY < pos) { + jsp.scrollByY(contentDragY); + } else { + positionDragY(pos); + } + } else { + cancelClick(); + return; + } + scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq); + isFirst = false; + }, + cancelClick = function () { + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + scrollTimeout = null; + $(document).off('mouseup.jsp', cancelClick); + }; + doScroll(); + $(document).on('mouseup.jsp', cancelClick); + return false; + } + }); + } + + if (isScrollableH) { + horizontalTrack.on('mousedown.jsp', function (e) { + if (e.originalTarget === undefined || e.originalTarget == e.currentTarget) { + var clickedTrack = $(this), + offset = clickedTrack.offset(), + direction = e.pageX - offset.left - horizontalDragPosition, + scrollTimeout, + isFirst = true, + doScroll = function () { + var offset = clickedTrack.offset(), + pos = e.pageX - offset.left - horizontalDragWidth / 2, + contentDragX = paneWidth * settings.scrollPagePercent, + dragX = (dragMaxX * contentDragX) / (contentWidth - paneWidth); + if (direction < 0) { + if (horizontalDragPosition - dragX > pos) { + jsp.scrollByX(-contentDragX); + } else { + positionDragX(pos); + } + } else if (direction > 0) { + if (horizontalDragPosition + dragX < pos) { + jsp.scrollByX(contentDragX); + } else { + positionDragX(pos); + } + } else { + cancelClick(); + return; + } + scrollTimeout = setTimeout(doScroll, isFirst ? settings.initialDelay : settings.trackClickRepeatFreq); + isFirst = false; + }, + cancelClick = function () { + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + scrollTimeout = null; + $(document).off('mouseup.jsp', cancelClick); + }; + doScroll(); + $(document).on('mouseup.jsp', cancelClick); + return false; + } + }); + } + } - // return true if there was no movement so rest of screen can scroll - return dX == horizontalDragPosition && dY == verticalDragPosition; - } - ).on( - 'touchend.jsp', - function(e) - { - moving = false; - /*if(moved) { + function removeClickOnTrack() { + if (horizontalTrack) { + horizontalTrack.off('mousedown.jsp'); + } + if (verticalTrack) { + verticalTrack.off('mousedown.jsp'); + } + } + + function cancelDrag() { + $('html').off('dragstart.jsp selectstart.jsp mousemove.jsp mouseup.jsp mouseleave.jsp'); + + if (verticalDrag) { + verticalDrag.removeClass('jspActive'); + } + if (horizontalDrag) { + horizontalDrag.removeClass('jspActive'); + } + } + + function positionDragY(destY, animate) { + if (!isScrollableV) { + return; + } + if (destY < 0) { + destY = 0; + } else if (destY > dragMaxY) { + destY = dragMaxY; + } + + // allow for devs to prevent the JSP from being scrolled + var willScrollYEvent = new $.Event('jsp-will-scroll-y'); + elem.trigger(willScrollYEvent, [destY]); + + if (willScrollYEvent.isDefaultPrevented()) { + return; + } + + var tmpVerticalDragPosition = destY || 0; + + var isAtTop = tmpVerticalDragPosition === 0, + isAtBottom = tmpVerticalDragPosition == dragMaxY, + percentScrolled = destY / dragMaxY, + destTop = -percentScrolled * (contentHeight - paneHeight); + + // can't just check if(animate) because false is a valid value that could be passed in... + if (animate === undefined) { + animate = settings.animateScroll; + } + if (animate) { + jsp.animate(verticalDrag, 'top', destY, _positionDragY, function () { + elem.trigger('jsp-user-scroll-y', [-destTop, isAtTop, isAtBottom]); + }); + } else { + verticalDrag.css('top', destY + 'px'); + _positionDragY(destY); + elem.trigger('jsp-user-scroll-y', [-destTop, isAtTop, isAtBottom]); + } + } + + function _positionDragY(destY) { + if (destY === undefined) { + destY = verticalDrag.position().top; + } + + container.scrollTop(0); + verticalDragPosition = destY || 0; + + var isAtTop = verticalDragPosition === 0, + isAtBottom = verticalDragPosition == dragMaxY, + percentScrolled = destY / dragMaxY, + destTop = -percentScrolled * (contentHeight - paneHeight); + + if (wasAtTop != isAtTop || wasAtBottom != isAtBottom) { + wasAtTop = isAtTop; + wasAtBottom = isAtBottom; + elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]); + } + + updateVerticalArrows(isAtTop, isAtBottom); + pane.css('top', destTop + 'px'); + elem.trigger('jsp-scroll-y', [-destTop, isAtTop, isAtBottom]).trigger('scroll'); + } + + function positionDragX(destX, animate) { + if (!isScrollableH) { + return; + } + if (destX < 0) { + destX = 0; + } else if (destX > dragMaxX) { + destX = dragMaxX; + } + + // allow for devs to prevent the JSP from being scrolled + var willScrollXEvent = new $.Event('jsp-will-scroll-x'); + elem.trigger(willScrollXEvent, [destX]); + + if (willScrollXEvent.isDefaultPrevented()) { + return; + } + + var tmpHorizontalDragPosition = destX || 0; + + var isAtLeft = tmpHorizontalDragPosition === 0, + isAtRight = tmpHorizontalDragPosition == dragMaxX, + percentScrolled = destX / dragMaxX, + destLeft = -percentScrolled * (contentWidth - paneWidth); + + if (animate === undefined) { + animate = settings.animateScroll; + } + if (animate) { + jsp.animate(horizontalDrag, 'left', destX, _positionDragX, function () { + elem.trigger('jsp-user-scroll-x', [-destLeft, isAtLeft, isAtRight]); + }); + } else { + horizontalDrag.css('left', destX + 'px'); + _positionDragX(destX); + elem.trigger('jsp-user-scroll-x', [-destLeft, isAtLeft, isAtRight]); + } + } + + function _positionDragX(destX) { + if (destX === undefined) { + destX = horizontalDrag.position().left; + } + + container.scrollTop(0); + horizontalDragPosition = destX || 0; + + var isAtLeft = horizontalDragPosition === 0, + isAtRight = horizontalDragPosition == dragMaxX, + percentScrolled = destX / dragMaxX, + destLeft = -percentScrolled * (contentWidth - paneWidth); + + if (wasAtLeft != isAtLeft || wasAtRight != isAtRight) { + wasAtLeft = isAtLeft; + wasAtRight = isAtRight; + elem.trigger('jsp-arrow-change', [wasAtTop, wasAtBottom, wasAtLeft, wasAtRight]); + } + + updateHorizontalArrows(isAtLeft, isAtRight); + pane.css('left', destLeft + 'px'); + elem.trigger('jsp-scroll-x', [-destLeft, isAtLeft, isAtRight]).trigger('scroll'); + } + + function updateVerticalArrows(isAtTop, isAtBottom) { + if (settings.showArrows) { + arrowUp[isAtTop ? 'addClass' : 'removeClass']('jspDisabled'); + arrowDown[isAtBottom ? 'addClass' : 'removeClass']('jspDisabled'); + } + } + + function updateHorizontalArrows(isAtLeft, isAtRight) { + if (settings.showArrows) { + arrowLeft[isAtLeft ? 'addClass' : 'removeClass']('jspDisabled'); + arrowRight[isAtRight ? 'addClass' : 'removeClass']('jspDisabled'); + } + } + + function scrollToY(destY, animate) { + var percentScrolled = destY / (contentHeight - paneHeight); + positionDragY(percentScrolled * dragMaxY, animate); + } + + function scrollToX(destX, animate) { + var percentScrolled = destX / (contentWidth - paneWidth); + positionDragX(percentScrolled * dragMaxX, animate); + } + + function scrollToElement(ele, stickToTop, animate) { + var e, + eleHeight, + eleWidth, + eleTop = 0, + eleLeft = 0, + viewportTop, + viewportLeft, + maxVisibleEleTop, + maxVisibleEleLeft, + destY, + destX; + + // Legal hash values aren't necessarily legal jQuery selectors so we need to catch any + // errors from the lookup... + try { + e = $(ele); + } catch (err) { + return; + } + eleHeight = e.outerHeight(); + eleWidth = e.outerWidth(); + + container.scrollTop(0); + container.scrollLeft(0); + + // loop through parents adding the offset top of any elements that are relatively positioned between + // the focused element and the jspPane so we can get the true distance from the top + // of the focused element to the top of the scrollpane... + while (!e.is('.jspPane')) { + eleTop += e.position().top; + eleLeft += e.position().left; + e = e.offsetParent(); + if (/^body|html$/i.test(e[0].nodeName)) { + // we ended up too high in the document structure. Quit! + return; + } + } + + viewportTop = contentPositionY(); + maxVisibleEleTop = viewportTop + paneHeight; + if (eleTop < viewportTop || stickToTop) { + // element is above viewport + destY = eleTop - settings.horizontalGutter; + } else if (eleTop + eleHeight > maxVisibleEleTop) { + // element is below viewport + destY = eleTop - paneHeight + eleHeight + settings.horizontalGutter; + } + if (!isNaN(destY)) { + scrollToY(destY, animate); + } + + viewportLeft = contentPositionX(); + maxVisibleEleLeft = viewportLeft + paneWidth; + if (eleLeft < viewportLeft || stickToTop) { + // element is to the left of viewport + destX = eleLeft - settings.horizontalGutter; + } else if (eleLeft + eleWidth > maxVisibleEleLeft) { + // element is to the right viewport + destX = eleLeft - paneWidth + eleWidth + settings.horizontalGutter; + } + if (!isNaN(destX)) { + scrollToX(destX, animate); + } + } + + function contentPositionX() { + return -pane.position().left; + } + + function contentPositionY() { + return -pane.position().top; + } + + function isCloseToBottom() { + var scrollableHeight = contentHeight - paneHeight; + + if (settings.maintainPosition == true) { + return scrollableHeight >= 20 && scrollableHeight - contentPositionY() < 10; + } + + return true; + } + + function isCloseToRight() { + var scrollableWidth = contentWidth - paneWidth; + + if (settings.maintainPosition == true) { + return scrollableWidth >= 20 && scrollableWidth - contentPositionX() < 10; + } + + return true; + } + + function initMousewheel() { + container.off(mwEvent).on(mwEvent, function (event, delta, deltaX, deltaY) { + if (!horizontalDragPosition) horizontalDragPosition = 0; + if (!verticalDragPosition) verticalDragPosition = 0; + + var dX = horizontalDragPosition, + dY = verticalDragPosition, + factor = event.deltaFactor || settings.mouseWheelSpeed; + jsp.scrollBy(deltaX * factor, -deltaY * factor, false); + // return true if there was no movement so rest of screen can scroll + return dX == horizontalDragPosition && dY == verticalDragPosition; + }); + } + + function removeMousewheel() { + container.off(mwEvent); + } + + function nil() { + return false; + } + + function initFocusHandler() { + pane.find(':input,a') + .off('focus.jsp') + .on('focus.jsp', function (e) { + scrollToElement(e.target, false); + }); + } + + function removeFocusHandler() { + pane.find(':input,a').off('focus.jsp'); + } + + function initKeyboardNav() { + var keyDown, + elementHasScrolled, + validParents = []; + if (isScrollableH) { + validParents.push(horizontalBar[0]); + } + + if (isScrollableV) { + validParents.push(verticalBar[0]); + } + + // IE also focuses elements that don't have tabindex set. + pane.on('focus.jsp', function () { + elem.focus(); + }); + + elem.attr('tabindex', 0) + .off('keydown.jsp keypress.jsp') + .on('keydown.jsp', function (e) { + if (e.target !== this && !(validParents.length && $(e.target).closest(validParents).length)) { + return; + } + var dX = horizontalDragPosition, + dY = verticalDragPosition; + switch (e.keyCode) { + case 40: // down + case 38: // up + case 34: // page down + case 32: // space + case 33: // page up + case 39: // right + case 37: // left + keyDown = e.keyCode; + keyDownHandler(); + break; + case 35: // end + scrollToY(contentHeight - paneHeight); + keyDown = null; + break; + case 36: // home + scrollToY(0); + keyDown = null; + break; + } + + elementHasScrolled = (e.keyCode == keyDown && dX != horizontalDragPosition) || dY != verticalDragPosition; + return !elementHasScrolled; + }) + .on( + 'keypress.jsp', // For FF/ OSX so that we can cancel the repeat key presses if the JSP scrolls... + function (e) { + if (e.keyCode == keyDown) { + keyDownHandler(); + } + // If the keypress is not related to the area, ignore it. Fixes problem with inputs inside scrolled area. Copied from line 955. + if (e.target !== this && !(validParents.length && $(e.target).closest(validParents).length)) { + return; + } + return !elementHasScrolled; + }, + ); + + if (settings.hideFocus) { + elem.css('outline', 'none'); + if ('hideFocus' in container[0]) { + elem.attr('hideFocus', true); + } + } else { + elem.css('outline', ''); + if ('hideFocus' in container[0]) { + elem.attr('hideFocus', false); + } + } + + function keyDownHandler() { + var dX = horizontalDragPosition, + dY = verticalDragPosition; + switch (keyDown) { + case 40: // down + jsp.scrollByY(settings.keyboardSpeed, false); + break; + case 38: // up + jsp.scrollByY(-settings.keyboardSpeed, false); + break; + case 34: // page down + case 32: // space + jsp.scrollByY(paneHeight * settings.scrollPagePercent, false); + break; + case 33: // page up + jsp.scrollByY(-paneHeight * settings.scrollPagePercent, false); + break; + case 39: // right + jsp.scrollByX(settings.keyboardSpeed, false); + break; + case 37: // left + jsp.scrollByX(-settings.keyboardSpeed, false); + break; + } + + elementHasScrolled = dX != horizontalDragPosition || dY != verticalDragPosition; + return elementHasScrolled; + } + } + + function removeKeyboardNav() { + elem.attr('tabindex', '-1').removeAttr('tabindex').off('keydown.jsp keypress.jsp'); + + pane.off('.jsp'); + } + + function observeHash() { + if (location.hash && location.hash.length > 1) { + var e, + retryInt, + hash = escape(location.hash.substr(1)); // hash must be escaped to prevent XSS + try { + e = $('#' + hash + ', a[name="' + hash + '"]'); + } catch (err) { + return; + } + + if (e.length && pane.find(hash)) { + // nasty workaround but it appears to take a little while before the hash has done its thing + // to the rendered page so we just wait until the container's scrollTop has been messed up. + if (container.scrollTop() === 0) { + retryInt = setInterval(function () { + if (container.scrollTop() > 0) { + scrollToElement(e, true); + $(document).scrollTop(container.position().top); + clearInterval(retryInt); + } + }, 50); + } else { + scrollToElement(e, true); + $(document).scrollTop(container.position().top); + } + } + } + } + + function hijackInternalLinks() { + // only register the link handler once + if ($(document.body).data('jspHijack')) { + return; + } + + // remember that the handler was bound + $(document.body).data('jspHijack', true); + + // use live handler to also capture newly created links + $(document.body).delegate('a[href*="#"]', 'click', function (event) { + // does the link point to the same page? + // this also takes care of cases with a -Tag or Links not starting with the hash # + // e.g.
when the current url already is index.html + var href = this.href.substr(0, this.href.indexOf('#')), + locationHref = location.href, + hash, + element, + container, + jsp, + scrollTop, + elementTop; + if (location.href.indexOf('#') !== -1) { + locationHref = location.href.substr(0, location.href.indexOf('#')); + } + if (href !== locationHref) { + // the link points to another page + return; + } + + // check if jScrollPane should handle this click event + hash = escape(this.href.substr(this.href.indexOf('#') + 1)); + + // find the element on the page + try { + element = $('#' + hash + ', a[name="' + hash + '"]'); + } catch (e) { + // hash is not a valid jQuery identifier + return; + } + + if (!element.length) { + // this link does not point to an element on this page + return; + } + + container = element.closest('.jspScrollable'); + jsp = container.data('jsp'); + + // jsp might be another jsp instance than the one, that bound this event + // remember: this event is only bound once for all instances. + jsp.scrollToElement(element, true); + + if (container[0].scrollIntoView) { + // also scroll to the top of the container (if it is not visible) + scrollTop = $(window).scrollTop(); + elementTop = element.offset().top; + if (elementTop < scrollTop || elementTop > scrollTop + $(window).height()) { + container[0].scrollIntoView(); + } + } + + // jsp handled this event, prevent the browser default (scrolling :P) + event.preventDefault(); + }); + } + + // Init touch on iPad, iPhone, iPod, Android + function initTouch() { + var startX, + startY, + touchStartX, + touchStartY, + moved, + moving = false; + + container + .off('touchstart.jsp touchmove.jsp touchend.jsp click.jsp-touchclick') + .on('touchstart.jsp', function (e) { + var touch = e.originalEvent.touches[0]; + startX = contentPositionX(); + startY = contentPositionY(); + touchStartX = touch.pageX; + touchStartY = touch.pageY; + moved = false; + moving = true; + }) + .on('touchmove.jsp', function (ev) { + if (!moving) { + return; + } + + var touchPos = ev.originalEvent.touches[0], + dX = horizontalDragPosition, + dY = verticalDragPosition; + + jsp.scrollTo(startX + touchStartX - touchPos.pageX, startY + touchStartY - touchPos.pageY); + + moved = moved || Math.abs(touchStartX - touchPos.pageX) > 5 || Math.abs(touchStartY - touchPos.pageY) > 5; + + // return true if there was no movement so rest of screen can scroll + return dX == horizontalDragPosition && dY == verticalDragPosition; + }) + .on('touchend.jsp', function (e) { + moving = false; + /*if(moved) { return false; }*/ - } - ).on( - 'click.jsp-touchclick', - function(e) - { - if(moved) { - moved = false; - return false; - } - } - ); - } - - function destroy(){ - var currentY = contentPositionY(), - currentX = contentPositionX(); - elem.removeClass('jspScrollable').off('.jsp'); - pane.off('.jsp'); - elem.replaceWith(originalElement.append(pane.children())); - originalElement.scrollTop(currentY); - originalElement.scrollLeft(currentX); - - // clear reinitialize timer if active - if (reinitialiseInterval) { - clearInterval(reinitialiseInterval); - } - } - - // Public API - $.extend( - jsp, - { - // Reinitialises the scroll pane (if it's internal dimensions have changed since the last time it - // was initialised). The settings object which is passed in will override any settings from the - // previous time it was initialised - if you don't pass any settings then the ones from the previous - // initialisation will be used. - reinitialise: function(s) - { - s = $.extend({}, settings, s); - initialise(s); - }, - // Scrolls the specified element (a jQuery object, DOM node or jQuery selector string) into view so - // that it can be seen within the viewport. If stickToTop is true then the element will appear at - // the top of the viewport, if it is false then the viewport will scroll as little as possible to - // show the element. You can also specify if you want animation to occur. If you don't provide this - // argument then the animateScroll value from the settings object is used instead. - scrollToElement: function(ele, stickToTop, animate) - { - scrollToElement(ele, stickToTop, animate); - }, - // Scrolls the pane so that the specified co-ordinates within the content are at the top left - // of the viewport. animate is optional and if not passed then the value of animateScroll from - // the settings object this jScrollPane was initialised with is used. - scrollTo: function(destX, destY, animate) - { - scrollToX(destX, animate); - scrollToY(destY, animate); - }, - // Scrolls the pane so that the specified co-ordinate within the content is at the left of the - // viewport. animate is optional and if not passed then the value of animateScroll from the settings - // object this jScrollPane was initialised with is used. - scrollToX: function(destX, animate) - { - scrollToX(destX, animate); - }, - // Scrolls the pane so that the specified co-ordinate within the content is at the top of the - // viewport. animate is optional and if not passed then the value of animateScroll from the settings - // object this jScrollPane was initialised with is used. - scrollToY: function(destY, animate) - { - scrollToY(destY, animate); - }, - // Scrolls the pane to the specified percentage of its maximum horizontal scroll position. animate - // is optional and if not passed then the value of animateScroll from the settings object this - // jScrollPane was initialised with is used. - scrollToPercentX: function(destPercentX, animate) - { - scrollToX(destPercentX * (contentWidth - paneWidth), animate); - }, - // Scrolls the pane to the specified percentage of its maximum vertical scroll position. animate - // is optional and if not passed then the value of animateScroll from the settings object this - // jScrollPane was initialised with is used. - scrollToPercentY: function(destPercentY, animate) - { - scrollToY(destPercentY * (contentHeight - paneHeight), animate); - }, - // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then - // the value of animateScroll from the settings object this jScrollPane was initialised with is used. - scrollBy: function(deltaX, deltaY, animate) - { - jsp.scrollByX(deltaX, animate); - jsp.scrollByY(deltaY, animate); - }, - // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then - // the value of animateScroll from the settings object this jScrollPane was initialised with is used. - scrollByX: function(deltaX, animate) - { - var destX = contentPositionX() + Math[deltaX<0 ? 'floor' : 'ceil'](deltaX), - percentScrolled = destX / (contentWidth - paneWidth); - positionDragX(percentScrolled * dragMaxX, animate); - }, - // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then - // the value of animateScroll from the settings object this jScrollPane was initialised with is used. - scrollByY: function(deltaY, animate) - { - var destY = contentPositionY() + Math[deltaY<0 ? 'floor' : 'ceil'](deltaY), - percentScrolled = destY / (contentHeight - paneHeight); - positionDragY(percentScrolled * dragMaxY, animate); - }, - // Positions the horizontal drag at the specified x position (and updates the viewport to reflect - // this). animate is optional and if not passed then the value of animateScroll from the settings - // object this jScrollPane was initialised with is used. - positionDragX: function(x, animate) - { - positionDragX(x, animate); - }, - // Positions the vertical drag at the specified y position (and updates the viewport to reflect - // this). animate is optional and if not passed then the value of animateScroll from the settings - // object this jScrollPane was initialised with is used. - positionDragY: function(y, animate) - { - positionDragY(y, animate); - }, - // This method is called when jScrollPane is trying to animate to a new position. You can override - // it if you want to provide advanced animation functionality. It is passed the following arguments: - // * ele - the element whose position is being animated - // * prop - the property that is being animated - // * value - the value it's being animated to - // * stepCallback - a function that you must execute each time you update the value of the property - // * completeCallback - a function that will be executed after the animation had finished - // You can use the default implementation (below) as a starting point for your own implementation. - animate: function(ele, prop, value, stepCallback, completeCallback) - { - var params = {}; - params[prop] = value; - ele.animate( - params, - { - 'duration' : settings.animateDuration, - 'easing' : settings.animateEase, - 'queue' : false, - 'step' : stepCallback, - 'complete' : completeCallback - } - ); - }, - // Returns the current x position of the viewport with regards to the content pane. - getContentPositionX: function() - { - return contentPositionX(); - }, - // Returns the current y position of the viewport with regards to the content pane. - getContentPositionY: function() - { - return contentPositionY(); - }, - // Returns the width of the content within the scroll pane. - getContentWidth: function() - { - return contentWidth; - }, - // Returns the height of the content within the scroll pane. - getContentHeight: function() - { - return contentHeight; - }, - // Returns the horizontal position of the viewport within the pane content. - getPercentScrolledX: function() - { - return contentPositionX() / (contentWidth - paneWidth); - }, - // Returns the vertical position of the viewport within the pane content. - getPercentScrolledY: function() - { - return contentPositionY() / (contentHeight - paneHeight); - }, - // Returns whether or not this scrollpane has a horizontal scrollbar. - getIsScrollableH: function() - { - return isScrollableH; - }, - // Returns whether or not this scrollpane has a vertical scrollbar. - getIsScrollableV: function() - { - return isScrollableV; - }, - // Gets a reference to the content pane. It is important that you use this method if you want to - // edit the content of your jScrollPane as if you access the element directly then you may have some - // problems (as your original element has had additional elements for the scrollbars etc added into - // it). - getContentPane: function() - { - return pane; - }, - // Scrolls this jScrollPane down as far as it can currently scroll. If animate isn't passed then the - // animateScroll value from settings is used instead. - scrollToBottom: function(animate) - { - positionDragY(dragMaxY, animate); - }, - // Hijacks the links on the page which link to content inside the scrollpane. If you have changed - // the content of your page (e.g. via AJAX) and want to make sure any new anchor links to the - // contents of your scroll pane will work then call this function. - hijackInternalLinks: $.noop, - // Removes the jScrollPane and returns the page to the state it was in before jScrollPane was - // initialised. - destroy: function() - { - destroy(); - } - } - ); - - initialise(s); - } - - // Pluginifying code... - settings = $.extend({}, $.fn.jScrollPane.defaults, settings); - - // Apply default speed - $.each(['arrowButtonSpeed', 'trackClickSpeed', 'keyboardSpeed'], function() { - settings[this] = settings[this] || settings.speed; - }); - - return this.each( - function() - { - var elem = $(this), jspApi = elem.data('jsp'); - if (jspApi) { - jspApi.reinitialise(settings); - } else { - $("script",elem).filter('[type="text/javascript"],:not([type])').remove(); - jspApi = new JScrollPane(elem, settings); - elem.data('jsp', jspApi); - } - } - ); - }; - - $.fn.jScrollPane.defaults = { - showArrows : false, - maintainPosition : true, - stickToBottom : false, - stickToRight : false, - clickOnTrack : true, - autoReinitialise : false, - autoReinitialiseDelay : 500, - verticalDragMinHeight : 0, - verticalDragMaxHeight : 99999, - horizontalDragMinWidth : 0, - horizontalDragMaxWidth : 99999, - contentWidth : undefined, - animateScroll : false, - animateDuration : 300, - animateEase : 'linear', - hijackInternalLinks : false, - verticalGutter : 4, - horizontalGutter : 4, - mouseWheelSpeed : 3, - arrowButtonSpeed : 0, - arrowRepeatFreq : 50, - arrowScrollOnHover : false, - trackClickSpeed : 0, - trackClickRepeatFreq : 70, - verticalArrowPositions : 'split', - horizontalArrowPositions : 'split', - enableKeyboardNavigation : true, - hideFocus : false, - keyboardSpeed : 0, - initialDelay : 300, // Delay before starting repeating - speed : 30, // Default speed when others falsey - scrollPagePercent : 0.8, // Percent of visible area scrolled when pageUp/Down or track area pressed - alwaysShowVScroll : false, - alwaysShowHScroll : false, - resizeSensor : false, - resizeSensorDelay : 0, - }; - -})); + }) + .on('click.jsp-touchclick', function (e) { + if (moved) { + moved = false; + return false; + } + }); + } + + function destroy() { + var currentY = contentPositionY(), + currentX = contentPositionX(); + elem.removeClass('jspScrollable').off('.jsp'); + pane.off('.jsp'); + elem.replaceWith(originalElement.append(pane.children())); + originalElement.scrollTop(currentY); + originalElement.scrollLeft(currentX); + + // clear reinitialize timer if active + if (reinitialiseInterval) { + clearInterval(reinitialiseInterval); + } + } + + // Public API + $.extend(jsp, { + // Reinitialises the scroll pane (if it's internal dimensions have changed since the last time it + // was initialised). The settings object which is passed in will override any settings from the + // previous time it was initialised - if you don't pass any settings then the ones from the previous + // initialisation will be used. + reinitialise: function (s) { + s = $.extend({}, settings, s); + initialise(s); + }, + // Scrolls the specified element (a jQuery object, DOM node or jQuery selector string) into view so + // that it can be seen within the viewport. If stickToTop is true then the element will appear at + // the top of the viewport, if it is false then the viewport will scroll as little as possible to + // show the element. You can also specify if you want animation to occur. If you don't provide this + // argument then the animateScroll value from the settings object is used instead. + scrollToElement: function (ele, stickToTop, animate) { + scrollToElement(ele, stickToTop, animate); + }, + // Scrolls the pane so that the specified co-ordinates within the content are at the top left + // of the viewport. animate is optional and if not passed then the value of animateScroll from + // the settings object this jScrollPane was initialised with is used. + scrollTo: function (destX, destY, animate) { + scrollToX(destX, animate); + scrollToY(destY, animate); + }, + // Scrolls the pane so that the specified co-ordinate within the content is at the left of the + // viewport. animate is optional and if not passed then the value of animateScroll from the settings + // object this jScrollPane was initialised with is used. + scrollToX: function (destX, animate) { + scrollToX(destX, animate); + }, + // Scrolls the pane so that the specified co-ordinate within the content is at the top of the + // viewport. animate is optional and if not passed then the value of animateScroll from the settings + // object this jScrollPane was initialised with is used. + scrollToY: function (destY, animate) { + scrollToY(destY, animate); + }, + // Scrolls the pane to the specified percentage of its maximum horizontal scroll position. animate + // is optional and if not passed then the value of animateScroll from the settings object this + // jScrollPane was initialised with is used. + scrollToPercentX: function (destPercentX, animate) { + scrollToX(destPercentX * (contentWidth - paneWidth), animate); + }, + // Scrolls the pane to the specified percentage of its maximum vertical scroll position. animate + // is optional and if not passed then the value of animateScroll from the settings object this + // jScrollPane was initialised with is used. + scrollToPercentY: function (destPercentY, animate) { + scrollToY(destPercentY * (contentHeight - paneHeight), animate); + }, + // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then + // the value of animateScroll from the settings object this jScrollPane was initialised with is used. + scrollBy: function (deltaX, deltaY, animate) { + jsp.scrollByX(deltaX, animate); + jsp.scrollByY(deltaY, animate); + }, + // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then + // the value of animateScroll from the settings object this jScrollPane was initialised with is used. + scrollByX: function (deltaX, animate) { + var destX = contentPositionX() + Math[deltaX < 0 ? 'floor' : 'ceil'](deltaX), + percentScrolled = destX / (contentWidth - paneWidth); + positionDragX(percentScrolled * dragMaxX, animate); + }, + // Scrolls the pane by the specified amount of pixels. animate is optional and if not passed then + // the value of animateScroll from the settings object this jScrollPane was initialised with is used. + scrollByY: function (deltaY, animate) { + var destY = contentPositionY() + Math[deltaY < 0 ? 'floor' : 'ceil'](deltaY), + percentScrolled = destY / (contentHeight - paneHeight); + positionDragY(percentScrolled * dragMaxY, animate); + }, + // Positions the horizontal drag at the specified x position (and updates the viewport to reflect + // this). animate is optional and if not passed then the value of animateScroll from the settings + // object this jScrollPane was initialised with is used. + positionDragX: function (x, animate) { + positionDragX(x, animate); + }, + // Positions the vertical drag at the specified y position (and updates the viewport to reflect + // this). animate is optional and if not passed then the value of animateScroll from the settings + // object this jScrollPane was initialised with is used. + positionDragY: function (y, animate) { + positionDragY(y, animate); + }, + // This method is called when jScrollPane is trying to animate to a new position. You can override + // it if you want to provide advanced animation functionality. It is passed the following arguments: + // * ele - the element whose position is being animated + // * prop - the property that is being animated + // * value - the value it's being animated to + // * stepCallback - a function that you must execute each time you update the value of the property + // * completeCallback - a function that will be executed after the animation had finished + // You can use the default implementation (below) as a starting point for your own implementation. + animate: function (ele, prop, value, stepCallback, completeCallback) { + var params = {}; + params[prop] = value; + ele.animate(params, { + duration: settings.animateDuration, + easing: settings.animateEase, + queue: false, + step: stepCallback, + complete: completeCallback, + }); + }, + // Returns the current x position of the viewport with regards to the content pane. + getContentPositionX: function () { + return contentPositionX(); + }, + // Returns the current y position of the viewport with regards to the content pane. + getContentPositionY: function () { + return contentPositionY(); + }, + // Returns the width of the content within the scroll pane. + getContentWidth: function () { + return contentWidth; + }, + // Returns the height of the content within the scroll pane. + getContentHeight: function () { + return contentHeight; + }, + // Returns the horizontal position of the viewport within the pane content. + getPercentScrolledX: function () { + return contentPositionX() / (contentWidth - paneWidth); + }, + // Returns the vertical position of the viewport within the pane content. + getPercentScrolledY: function () { + return contentPositionY() / (contentHeight - paneHeight); + }, + // Returns whether or not this scrollpane has a horizontal scrollbar. + getIsScrollableH: function () { + return isScrollableH; + }, + // Returns whether or not this scrollpane has a vertical scrollbar. + getIsScrollableV: function () { + return isScrollableV; + }, + // Gets a reference to the content pane. It is important that you use this method if you want to + // edit the content of your jScrollPane as if you access the element directly then you may have some + // problems (as your original element has had additional elements for the scrollbars etc added into + // it). + getContentPane: function () { + return pane; + }, + // Scrolls this jScrollPane down as far as it can currently scroll. If animate isn't passed then the + // animateScroll value from settings is used instead. + scrollToBottom: function (animate) { + positionDragY(dragMaxY, animate); + }, + // Hijacks the links on the page which link to content inside the scrollpane. If you have changed + // the content of your page (e.g. via AJAX) and want to make sure any new anchor links to the + // contents of your scroll pane will work then call this function. + hijackInternalLinks: $.noop, + // Removes the jScrollPane and returns the page to the state it was in before jScrollPane was + // initialised. + destroy: function () { + destroy(); + }, + }); + + initialise(s); + } + + // Pluginifying code... + settings = $.extend({}, $.fn.jScrollPane.defaults, settings); + + // Apply default speed + $.each(['arrowButtonSpeed', 'trackClickSpeed', 'keyboardSpeed'], function () { + settings[this] = settings[this] || settings.speed; + }); + + return this.each(function () { + var elem = $(this), + jspApi = elem.data('jsp'); + if (jspApi) { + jspApi.reinitialise(settings); + } else { + $('script', elem).filter('[type="text/javascript"],:not([type])').remove(); + jspApi = new JScrollPane(elem, settings); + elem.data('jsp', jspApi); + } + }); + }; + + $.fn.jScrollPane.defaults = { + showArrows: false, + maintainPosition: true, + stickToBottom: false, + stickToRight: false, + clickOnTrack: true, + autoReinitialise: false, + autoReinitialiseDelay: 500, + verticalDragMinHeight: 0, + verticalDragMaxHeight: 99999, + horizontalDragMinWidth: 0, + horizontalDragMaxWidth: 99999, + contentWidth: undefined, + animateScroll: false, + animateDuration: 300, + animateEase: 'linear', + hijackInternalLinks: false, + verticalGutter: 4, + horizontalGutter: 4, + mouseWheelSpeed: 3, + arrowButtonSpeed: 0, + arrowRepeatFreq: 50, + arrowScrollOnHover: false, + trackClickSpeed: 0, + trackClickRepeatFreq: 70, + verticalArrowPositions: 'split', + horizontalArrowPositions: 'split', + enableKeyboardNavigation: true, + hideFocus: false, + keyboardSpeed: 0, + initialDelay: 300, // Delay before starting repeating + speed: 30, // Default speed when others falsey + scrollPagePercent: 0.8, // Percent of visible area scrolled when pageUp/Down or track area pressed + alwaysShowVScroll: false, + alwaysShowHScroll: false, + resizeSensor: false, + resizeSensorDelay: 0, + }; +});