├── .gitignore ├── LICENSE ├── README.md ├── lib └── virtualscroll.js ├── package.json ├── src ├── index.js ├── keycodes.js └── support.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Florian Morel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | virtual-scroll 2 | ===== 3 | 4 | A **2kb gzipped** low-level library to create custom scrollers with touch and keyboard support. 5 | This is heavily inspired by Bartek Drozdz VirtualScroll util. See his [article](http://www.everyday3d.com/blog/index.php/2014/08/18/smooth-scrolling-with-virtualscroll/) for reference. 6 | 7 | ### Features 8 | - Can create multiple instances with different elements as targets 9 | - Let you do the actual scrolling logic: use CSS Transforms, WebGL animation or anything you like 10 | - Native arrow keys support and shift/space support mimicking default browser behaviour 11 | 12 | For high-level libraries based off **virtual-scroll**, check [locomotive-scroll](https://github.com/locomotivemtl/locomotive-scroll) or [smooth-scrolling](https://github.com/baptistebriel/smooth-scrolling). 13 | 14 | ### Installation 15 | ``` 16 | npm i virtual-scroll -S 17 | ``` 18 | 19 | ### Usage & API 20 | #### Constructor 21 | - `new VirtualScroll(options)` 22 | - `el`: the target element for mobile touch events. *Defaults to window.* 23 | - `mouseMultiplier`: General multiplier for all mousewheel (including Firefox). *Default to 1.* 24 | - `touchMultiplier`: Mutiply the touch action by this modifier to make scroll faster than finger movement. *Defaults to 2.* 25 | - `firefoxMultiplier`: Firefox on Windows needs a boost, since scrolling is very slow. *Defaults to 15.* 26 | - `keyStep`: How many pixels to move with each key press. *Defaults to 120.* 27 | - `preventTouch`: If true, automatically call `e.preventDefault` on touchMove. *Defaults to false.* 28 | - `unpreventTouchClass`: Elements with this class won't `preventDefault` on touchMove. For instance, useful for a scrolling text inside a VirtualScroll-controled element. *Defaults to `vs-touchmove-allowed`*. 29 | - `passive`: if used, will use [passive events declaration](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners) for the wheel and touch listeners. Can be true or false. *Defaults to undefined.* 30 | - `useKeyboard`: if true, allows to use arrows to navigate, and space to jump from one screen. *Defaults to true* 31 | - `useTouch`: if true, uses touch events to simulate scrolling. *Defaults to true* 32 | 33 | #### Methods 34 | - `instance.on(callback, context)` 35 | Listen to the scroll event using the specified callback and optional context. 36 | 37 | - `instance.off(callback, context)` 38 | Remove the listener. 39 | 40 | - `instance.destroy()` 41 | Remove all events and unbind the DOM listeners. 42 | 43 | Events note: 44 | Each instance will listen only once to any DOM listener. These listener are enabled/disabled automatically. However, it's a good practice to always call `destroy()` on your VirtualScroll instance, especially if you are working with a SPA. 45 | 46 | #### Event 47 | When a scroll event happens, all the listeners attached with *instance.on(callback, context)* will get triggered with the following event: 48 | ```js 49 | { 50 | x, // total distance scrolled on the x axis 51 | y, // total distance scrolled on the y axis 52 | deltaX, // distance scrolled since the last event on the x axis 53 | deltaY, // distance scrolled since the last event on the y axis 54 | originalEvent // the native event triggered by the pointer device or keyboard 55 | } 56 | ``` 57 | 58 | ### Example 59 | ```js 60 | import VirtualScroll from 'virtual-scroll' 61 | 62 | const scroller = new VirtualScroll() 63 | scroller.on(event => { 64 | wrapper.style.transform = `translateY(${event.y}px)` 65 | }) 66 | 67 | ``` 68 | 69 | ### License 70 | MIT. 71 | -------------------------------------------------------------------------------- /lib/virtualscroll.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e||self).virtualScroll=t()}(this,function(){var e=0;function t(t){return"__private_"+e+++"_"+t}function i(e,t){if(!Object.prototype.hasOwnProperty.call(e,t))throw new TypeError("attempted to use private field on non-instance");return e}function n(){}n.prototype={on:function(e,t,i){var n=this.e||(this.e={});return(n[e]||(n[e]=[])).push({fn:t,ctx:i}),this},once:function(e,t,i){var n=this;function o(){n.off(e,o),t.apply(i,arguments)}return o._=t,this.on(e,o,i)},emit:function(e){for(var t=[].slice.call(arguments,1),i=((this.e||(this.e={}))[e]||[]).slice(),n=0,o=i.length;n1,hasPointer:!!window.navigator.msPointerEnabled,hasKeyDown:"onkeydown"in document,isFirefox:navigator.userAgent.indexOf("Firefox")>-1}),i(this,r)[r]=Object.assign({mouseMultiplier:1,touchMultiplier:2,firefoxMultiplier:15,keyStep:120,preventTouch:!1,unpreventTouchClass:"vs-touchmove-allowed",useKeyboard:!0,useTouch:!0},e),i(this,l)[l]=new o,i(this,u)[u]={y:0,x:0,deltaX:0,deltaY:0},i(this,c)[c]={x:null,y:null},i(this,d)[d]=null,void 0!==i(this,r)[r].passive&&(this.listenerOptions={passive:i(this,r)[r].passive})}var t=e.prototype;return t._notify=function(e){var t=i(this,u)[u];t.x+=t.deltaX,t.y+=t.deltaY,i(this,l)[l].emit(h,{x:t.x,y:t.y,deltaX:t.deltaX,deltaY:t.deltaY,originalEvent:e})},t._bind=function(){s.hasWheelEvent&&i(this,a)[a].addEventListener("wheel",this._onWheel,this.listenerOptions),s.hasMouseWheelEvent&&i(this,a)[a].addEventListener("mousewheel",this._onMouseWheel,this.listenerOptions),s.hasTouch&&i(this,r)[r].useTouch&&(i(this,a)[a].addEventListener("touchstart",this._onTouchStart,this.listenerOptions),i(this,a)[a].addEventListener("touchmove",this._onTouchMove,this.listenerOptions)),s.hasPointer&&s.hasTouchWin&&(i(this,d)[d]=document.body.style.msTouchAction,document.body.style.msTouchAction="none",i(this,a)[a].addEventListener("MSPointerDown",this._onTouchStart,!0),i(this,a)[a].addEventListener("MSPointerMove",this._onTouchMove,!0)),s.hasKeyDown&&i(this,r)[r].useKeyboard&&document.addEventListener("keydown",this._onKeyDown)},t._unbind=function(){s.hasWheelEvent&&i(this,a)[a].removeEventListener("wheel",this._onWheel),s.hasMouseWheelEvent&&i(this,a)[a].removeEventListener("mousewheel",this._onMouseWheel),s.hasTouch&&(i(this,a)[a].removeEventListener("touchstart",this._onTouchStart),i(this,a)[a].removeEventListener("touchmove",this._onTouchMove)),s.hasPointer&&s.hasTouchWin&&(document.body.style.msTouchAction=i(this,d)[d],i(this,a)[a].removeEventListener("MSPointerDown",this._onTouchStart,!0),i(this,a)[a].removeEventListener("MSPointerMove",this._onTouchMove,!0)),s.hasKeyDown&&i(this,r)[r].useKeyboard&&document.removeEventListener("keydown",this._onKeyDown)},t.on=function(e,t){i(this,l)[l].on(h,e,t);var n=i(this,l)[l].e;n&&n[h]&&1===n[h].length&&this._bind()},t.off=function(e,t){i(this,l)[l].off(h,e,t);var n=i(this,l)[l].e;(!n[h]||n[h].length<=0)&&this._unbind()},t.destroy=function(){i(this,l)[l].off(),this._unbind()},e}()}); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-scroll", 3 | "version": "2.2.1", 4 | "description": "Custom scroll events for smooth, fake scroll", 5 | "main": "lib/virtualscroll.js", 6 | "source": "src/index.js", 7 | "scripts": { 8 | "build": "microbundle build -i src/index.js --format umd --compress --no-sourcemap --no-pkg-main --external none", 9 | "test": "browserify test/index.js | smokestack | faucet", 10 | "test-debug": "budo test/index.js --live" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ayamflow/virtual-scroll" 15 | }, 16 | "keywords": [ 17 | "virtual", 18 | "scroll", 19 | "smooth" 20 | ], 21 | "author": "Florian Morel", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/ayamflow/virtual-scroll/issues" 25 | }, 26 | "homepage": "https://github.com/ayamflow/virtual-scroll", 27 | "dependencies": { 28 | "tiny-emitter": "^2.1.0" 29 | }, 30 | "devDependencies": { 31 | "browserify": "^14.3.0", 32 | "budo": "^9.4.7", 33 | "faucet": "0.0.1", 34 | "microbundle": "^0.13.0", 35 | "smokestack": "^3.4.1", 36 | "tape": "^4.6.3", 37 | "tiny-trigger": "scottcorgan/tiny-trigger" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Emitter from 'tiny-emitter' 2 | import { getSupport } from './support' 3 | import { keyCodes } from './keycodes' 4 | 5 | const EVT_ID = 'virtualscroll' 6 | var support 7 | 8 | export default class VirtualScroll { 9 | #options 10 | #el 11 | #emitter 12 | #event 13 | #touchStart 14 | #bodyTouchAction 15 | 16 | constructor(options) { 17 | this.#el = window 18 | if (options && options.el) { 19 | this.#el = options.el 20 | delete options.el 21 | } 22 | 23 | if (!support) support = getSupport() 24 | 25 | this.#options = Object.assign( 26 | { 27 | mouseMultiplier: 1, 28 | touchMultiplier: 2, 29 | firefoxMultiplier: 15, 30 | keyStep: 120, 31 | preventTouch: false, 32 | unpreventTouchClass: 'vs-touchmove-allowed', 33 | useKeyboard: true, 34 | useTouch: true 35 | }, 36 | options 37 | ) 38 | 39 | this.#emitter = new Emitter() 40 | this.#event = { 41 | y: 0, 42 | x: 0, 43 | deltaX: 0, 44 | deltaY: 0 45 | } 46 | this.#touchStart = { 47 | x: null, 48 | y: null 49 | } 50 | this.#bodyTouchAction = null 51 | 52 | if (this.#options.passive !== undefined) { 53 | this.listenerOptions = { passive: this.#options.passive } 54 | } 55 | } 56 | 57 | _notify(e) { 58 | var evt = this.#event 59 | evt.x += evt.deltaX 60 | evt.y += evt.deltaY 61 | 62 | this.#emitter.emit(EVT_ID, { 63 | x: evt.x, 64 | y: evt.y, 65 | deltaX: evt.deltaX, 66 | deltaY: evt.deltaY, 67 | originalEvent: e 68 | }) 69 | } 70 | 71 | _onWheel = (e) => { 72 | var options = this.#options 73 | var evt = this.#event 74 | 75 | // In Chrome and in Firefox (at least the new one) 76 | evt.deltaX = e.wheelDeltaX || e.deltaX * -1 77 | evt.deltaY = e.wheelDeltaY || e.deltaY * -1 78 | 79 | // for our purpose deltamode = 1 means user is on a wheel mouse, not touch pad 80 | // real meaning: https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#Delta_modes 81 | if (support.isFirefox && e.deltaMode === 1) { 82 | evt.deltaX *= options.firefoxMultiplier 83 | evt.deltaY *= options.firefoxMultiplier 84 | } 85 | 86 | evt.deltaX *= options.mouseMultiplier 87 | evt.deltaY *= options.mouseMultiplier 88 | 89 | this._notify(e) 90 | } 91 | 92 | _onMouseWheel = (e) => { 93 | var evt = this.#event 94 | 95 | // In Safari, IE and in Chrome if 'wheel' isn't defined 96 | evt.deltaX = e.wheelDeltaX ? e.wheelDeltaX : 0 97 | evt.deltaY = e.wheelDeltaY ? e.wheelDeltaY : e.wheelDelta 98 | 99 | this._notify(e) 100 | } 101 | 102 | _onTouchStart = (e) => { 103 | var t = e.targetTouches ? e.targetTouches[0] : e 104 | this.#touchStart.x = t.pageX 105 | this.#touchStart.y = t.pageY 106 | } 107 | 108 | _onTouchMove = (e) => { 109 | var options = this.#options 110 | if ( 111 | options.preventTouch && 112 | !e.target.classList.contains(options.unpreventTouchClass) 113 | ) { 114 | e.preventDefault() 115 | } 116 | 117 | var evt = this.#event 118 | 119 | var t = e.targetTouches ? e.targetTouches[0] : e 120 | 121 | evt.deltaX = (t.pageX - this.#touchStart.x) * options.touchMultiplier 122 | evt.deltaY = (t.pageY - this.#touchStart.y) * options.touchMultiplier 123 | 124 | this.#touchStart.x = t.pageX 125 | this.#touchStart.y = t.pageY 126 | 127 | this._notify(e) 128 | } 129 | 130 | _onKeyDown = (e) => { 131 | var evt = this.#event 132 | evt.deltaX = evt.deltaY = 0 133 | var windowHeight = window.innerHeight - 40 134 | 135 | switch (e.keyCode) { 136 | case keyCodes.LEFT: 137 | case keyCodes.UP: 138 | evt.deltaY = this.#options.keyStep 139 | break 140 | 141 | case keyCodes.RIGHT: 142 | case keyCodes.DOWN: 143 | evt.deltaY = -this.#options.keyStep 144 | break 145 | case keyCodes.SPACE: 146 | evt.deltaY = windowHeight * (e.shiftKey ? 1 : -1) 147 | break 148 | default: 149 | return 150 | } 151 | 152 | this._notify(e) 153 | } 154 | 155 | _bind() { 156 | if (support.hasWheelEvent) { 157 | this.#el.addEventListener( 158 | 'wheel', 159 | this._onWheel, 160 | this.listenerOptions 161 | ) 162 | } 163 | 164 | if (support.hasMouseWheelEvent) { 165 | this.#el.addEventListener( 166 | 'mousewheel', 167 | this._onMouseWheel, 168 | this.listenerOptions 169 | ) 170 | } 171 | 172 | if (support.hasTouch && this.#options.useTouch) { 173 | this.#el.addEventListener( 174 | 'touchstart', 175 | this._onTouchStart, 176 | this.listenerOptions 177 | ) 178 | this.#el.addEventListener( 179 | 'touchmove', 180 | this._onTouchMove, 181 | this.listenerOptions 182 | ) 183 | } 184 | 185 | if (support.hasPointer && support.hasTouchWin) { 186 | this.#bodyTouchAction = document.body.style.msTouchAction 187 | document.body.style.msTouchAction = 'none' 188 | this.#el.addEventListener('MSPointerDown', this._onTouchStart, true) 189 | this.#el.addEventListener('MSPointerMove', this._onTouchMove, true) 190 | } 191 | 192 | if (support.hasKeyDown && this.#options.useKeyboard) { 193 | document.addEventListener('keydown', this._onKeyDown) 194 | } 195 | } 196 | 197 | _unbind() { 198 | if (support.hasWheelEvent) { 199 | this.#el.removeEventListener('wheel', this._onWheel) 200 | } 201 | 202 | if (support.hasMouseWheelEvent) { 203 | this.#el.removeEventListener('mousewheel', this._onMouseWheel) 204 | } 205 | 206 | if (support.hasTouch) { 207 | this.#el.removeEventListener('touchstart', this._onTouchStart) 208 | this.#el.removeEventListener('touchmove', this._onTouchMove) 209 | } 210 | 211 | if (support.hasPointer && support.hasTouchWin) { 212 | document.body.style.msTouchAction = this.#bodyTouchAction 213 | this.#el.removeEventListener( 214 | 'MSPointerDown', 215 | this._onTouchStart, 216 | true 217 | ) 218 | this.#el.removeEventListener( 219 | 'MSPointerMove', 220 | this._onTouchMove, 221 | true 222 | ) 223 | } 224 | 225 | if (support.hasKeyDown && this.#options.useKeyboard) { 226 | document.removeEventListener('keydown', this._onKeyDown) 227 | } 228 | } 229 | 230 | on(cb, ctx) { 231 | this.#emitter.on(EVT_ID, cb, ctx) 232 | 233 | var events = this.#emitter.e 234 | if (events && events[EVT_ID] && events[EVT_ID].length === 1) 235 | this._bind() 236 | } 237 | 238 | off(cb, ctx) { 239 | this.#emitter.off(EVT_ID, cb, ctx) 240 | 241 | var events = this.#emitter.e 242 | if (!events[EVT_ID] || events[EVT_ID].length <= 0) this._unbind() 243 | } 244 | 245 | destroy() { 246 | this.#emitter.off() 247 | this._unbind() 248 | } 249 | } -------------------------------------------------------------------------------- /src/keycodes.js: -------------------------------------------------------------------------------- 1 | export const keyCodes = { 2 | LEFT: 37, 3 | UP: 38, 4 | RIGHT: 39, 5 | DOWN: 40, 6 | SPACE: 32 7 | } 8 | -------------------------------------------------------------------------------- /src/support.js: -------------------------------------------------------------------------------- 1 | export function getSupport() { 2 | return { 3 | hasWheelEvent: 'onwheel' in document, 4 | hasMouseWheelEvent: 'onmousewheel' in document, 5 | hasTouch: 'ontouchstart' in document, 6 | hasTouchWin: navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 1, 7 | hasPointer: !!window.navigator.msPointerEnabled, 8 | hasKeyDown: 'onkeydown' in document, 9 | isFirefox: navigator.userAgent.indexOf('Firefox') > -1, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var VirtualScroll = require('../'); 5 | var trigger = require('tiny-trigger'); 6 | 7 | var KEY_CODE = { 8 | DOWN: 40, 9 | SPACE: 32 10 | } 11 | var el = document.createElement('div'); 12 | el.style.position = 'absolute'; 13 | el.style.width = '400px'; 14 | el.style.height = '400px'; 15 | el.style.backgroundColor = 'transparent'; 16 | document.body.appendChild(el); 17 | 18 | test('Scroll with target test', function(assert) { 19 | var v = new VirtualScroll({ 20 | el: el 21 | }); 22 | 23 | v.on(function(event) { 24 | if (event.originalEvent.type == 'wheel') { 25 | if (event.originalEvent.currentTarget == el) { 26 | assert.pass('Wheel events should only fire when hovering the target element.'); 27 | } else { 28 | assert.fail('A wheel event fired on a different element.'); 29 | } 30 | v.destroy(); 31 | assert.end(); 32 | } 33 | }); 34 | 35 | trigger(el, 'wheel'); 36 | }); 37 | 38 | test('Arrow scroll test', function(assert) { 39 | var v = new VirtualScroll(); 40 | 41 | v.on(function(event) { 42 | if (event.originalEvent.type == 'keydown') { 43 | assert.pass('Event triggered by keydown.'); 44 | v.destroy(); 45 | assert.end(); 46 | } 47 | }); 48 | 49 | triggerKeyboard(KEY_CODE.DOWN); 50 | }); 51 | 52 | test('Space keypress', function(assert) { 53 | var v = new VirtualScroll(); 54 | 55 | v.on(function(event) { 56 | if (!event.originalEvent.shiftKey && event.originalEvent.keyCode == KEY_CODE.SPACE) { 57 | assert.pass('Event triggered by space key.'); 58 | v.destroy(); 59 | assert.end(); 60 | } 61 | }); 62 | 63 | triggerKeyboard(KEY_CODE.SPACE); 64 | }); 65 | 66 | test('Shift and space keypress', function(assert) { 67 | var v = new VirtualScroll(); 68 | 69 | v.on(function(event) { 70 | if (event.originalEvent.shiftKey && event.originalEvent.keyCode == KEY_CODE.SPACE) { 71 | assert.pass('Event triggered by space and shift key.'); 72 | v.destroy(); 73 | assert.end(); 74 | } 75 | }); 76 | triggerKeyboardWithShift(KEY_CODE.SPACE); 77 | }); 78 | 79 | test('Passive listener test', function(assert) { 80 | var vNone = new VirtualScroll(); 81 | var vPassive = new VirtualScroll({ 82 | passive: true 83 | }); 84 | var vActive = new VirtualScroll({ 85 | passive: false 86 | }); 87 | 88 | assert.ok(vNone.listenerOptions === undefined, 'No passive option'); 89 | assert.ok(vPassive.listenerOptions.passive, 'Passive event listener'); 90 | assert.notOk(vActive.listenerOptions.passive, 'Active event listener'); 91 | vPassive.destroy(); 92 | vActive.destroy(); 93 | vNone.destroy(); 94 | assert.end(); 95 | }); 96 | 97 | test('Off test', function(assert) { 98 | var v = new VirtualScroll(); 99 | var scrollCount = 0; 100 | 101 | var onScroll = function(event) { 102 | scrollCount++; 103 | v.off(onScroll); 104 | }; 105 | 106 | v.on(onScroll); 107 | trigger(el, 'wheel'); 108 | trigger(el, 'wheel'); 109 | 110 | assert.ok(scrollCount === 1, 'Scroll handler should have fired only once.'); 111 | v.destroy(); 112 | assert.end(); 113 | }); 114 | 115 | test('Destroy test', function(assert) { 116 | var v = new VirtualScroll(); 117 | var scrollCount = 0; 118 | 119 | var onScroll = function(event) { 120 | scrollCount++; 121 | v.destroy(); 122 | }; 123 | 124 | v.on(onScroll); 125 | trigger(el, 'wheel'); 126 | trigger(el, 'wheel'); 127 | 128 | assert.ok(scrollCount === 1, 'Scroll handler should have fired only once.'); 129 | assert.end(); 130 | }); 131 | 132 | function triggerKeyboard(keyCode, shiftKeyPressed) { 133 | var event = document.createEvent('KeyboardEvent'); 134 | Object.defineProperty(event, 'keyCode', { 135 | get: function() { 136 | return keyCode; 137 | } 138 | }); 139 | Object.defineProperty(event, 'which', { 140 | get: function() { 141 | return keyCode; 142 | } 143 | }); 144 | if (event.initKeyboardEvent) { 145 | event.initKeyboardEvent("keydown", true, true, document.defaultView, keyCode, keyCode, "", "", shiftKeyPressed ? "shift" : "", ""); 146 | } else { 147 | event.initKeyEvent("keydown", true, true, document.defaultView, false, false, shiftKeyPressed, false, keyCode, 0); 148 | } 149 | document.dispatchEvent(event); 150 | } 151 | 152 | function triggerKeyboardWithShift(keyCode) { 153 | return triggerKeyboard(keyCode, true) 154 | } 155 | 156 | // Just used to close the testing window 157 | test('Wrap up testing', function(assert) { 158 | assert.end() 159 | window.close() 160 | }) --------------------------------------------------------------------------------