Browse Source

added utilites for handling mouse input

Alexander Rose 7 years ago
parent
commit
69a0de707f

+ 194 - 0
src/mol-util/mouse-change.ts

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-change,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import * as mouse from './mouse-event'
+
+export type MouseModifiers = {
+    shift: boolean,
+    alt: boolean,
+    control: boolean,
+    meta: boolean
+}
+export type MouseChangeCallback = (buttonState: number, x: number, y: number, mods: MouseModifiers) => void
+
+export default function mouseListen (element: Element, callback: MouseChangeCallback) {
+    let buttonState = 0
+    let x = 0
+    let y = 0
+    const mods: MouseModifiers = {
+        shift: false,
+        alt: false,
+        control: false,
+        meta: false
+    }
+    let attached = false
+
+    function updateMods (event: MouseEvent | KeyboardEvent) {
+        let changed = false
+        if ('altKey' in event) {
+            changed = changed || event.altKey !== mods.alt
+            mods.alt = !!event.altKey
+        }
+        if ('shiftKey' in event) {
+            changed = changed || event.shiftKey !== mods.shift
+            mods.shift = !!event.shiftKey
+        }
+        if ('ctrlKey' in event) {
+            changed = changed || event.ctrlKey !== mods.control
+            mods.control = !!event.ctrlKey
+        }
+        if ('metaKey' in event) {
+            changed = changed || event.metaKey !== mods.meta
+            mods.meta = !!event.metaKey
+        }
+        return changed
+    }
+
+    function handleEvent (nextButtons: number, event: MouseEvent) {
+        const nextX = mouse.x(event)
+        const nextY = mouse.y(event)
+        if ('buttons' in event) {
+            nextButtons = event.buttons | 0
+        }
+        if (nextButtons !== buttonState || nextX !== x || nextY !== y || updateMods(event) ) {
+            buttonState = nextButtons | 0
+            x = nextX || 0
+            y = nextY || 0
+            callback && callback(buttonState, x, y, mods)
+        }
+    }
+
+    function clearState (event: MouseEvent) {
+        handleEvent(0, event)
+    }
+
+    function handleBlur () {
+        if (buttonState || x || y || mods.shift || mods.alt || mods.meta || mods.control) {
+            x = y = 0
+            buttonState = 0
+            mods.shift = mods.alt = mods.control = mods.meta = false
+            callback && callback(0, 0, 0, mods)
+        }
+    }
+
+    function handleMods (event: MouseEvent | KeyboardEvent) {
+        if (updateMods(event)) {
+            callback && callback(buttonState, x, y, mods)
+        }
+    }
+
+    function handleMouseMove (event: MouseEvent) {
+        if (mouse.buttons(event) === 0) {
+            handleEvent(0, event)
+        } else {
+            handleEvent(buttonState, event)
+        }
+    }
+
+    function handleMouseDown (event: MouseEvent) {
+        handleEvent(buttonState | mouse.buttons(event), event)
+    }
+
+    function handleMouseUp (event: MouseEvent) {
+        handleEvent(buttonState & ~mouse.buttons(event), event)
+    }
+
+    function attachListeners () {
+        if (attached) return
+        attached = true
+
+        element.addEventListener('mousemove', handleMouseMove as EventListener)
+        element.addEventListener('mousedown', handleMouseDown as EventListener)
+        element.addEventListener('mouseup', handleMouseUp as EventListener)
+
+        element.addEventListener('mouseleave', clearState as EventListener)
+        element.addEventListener('mouseenter', clearState as EventListener)
+        element.addEventListener('mouseout', clearState as EventListener)
+        element.addEventListener('mouseover', clearState as EventListener)
+
+        element.addEventListener('blur', handleBlur)
+        element.addEventListener('keyup', handleMods as EventListener)
+        element.addEventListener('keydown', handleMods as EventListener)
+        element.addEventListener('keypress', handleMods as EventListener)
+
+        if (!(element instanceof Window)) {
+            window.addEventListener('blur', handleBlur)
+            window.addEventListener('keyup', handleMods)
+            window.addEventListener('keydown', handleMods)
+            window.addEventListener('keypress', handleMods)
+        }
+    }
+
+    function detachListeners () {
+        if (!attached) return
+        attached = false
+
+        element.removeEventListener('mousemove', handleMouseMove as EventListener)
+        element.removeEventListener('mousedown', handleMouseDown as EventListener)
+        element.removeEventListener('mouseup', handleMouseUp as EventListener)
+
+        element.removeEventListener('mouseleave', clearState as EventListener)
+        element.removeEventListener('mouseenter', clearState as EventListener)
+        element.removeEventListener('mouseout', clearState as EventListener)
+        element.removeEventListener('mouseover', clearState as EventListener)
+
+        element.removeEventListener('blur', handleBlur)
+        element.removeEventListener('keyup', handleMods as EventListener)
+        element.removeEventListener('keydown', handleMods as EventListener)
+        element.removeEventListener('keypress', handleMods as EventListener)
+
+        if (!(element instanceof Window)) {
+            window.removeEventListener('blur', handleBlur)
+            window.removeEventListener('keyup', handleMods)
+            window.removeEventListener('keydown', handleMods)
+            window.removeEventListener('keypress', handleMods)
+        }
+    }
+
+    // Attach listeners
+    attachListeners()
+
+    const result = {
+        element: element
+    }
+
+    Object.defineProperties(result, {
+        enabled: {
+            get: function () { return attached },
+            set: function (f) {
+                if (f) {
+                    attachListeners()
+                } else {
+                    detachListeners()
+                }
+            },
+            enumerable: true
+        },
+        buttons: {
+            get: function () { return buttonState },
+            enumerable: true
+        },
+        x: {
+            get: function () { return x },
+            enumerable: true
+        },
+        y: {
+            get: function () { return y },
+            enumerable: true
+        },
+        mods: {
+            get: function () { return mods },
+            enumerable: true
+        }
+    })
+
+    return result
+}

+ 65 - 0
src/mol-util/mouse-event.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-event,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+export function buttons(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('buttons' in event) {
+            return event.buttons
+        } else if ('which' in event) {
+            const b = (event as any).which  // 'any' to support older browsers
+            if (b === 2) {
+                return 4
+            } else if (b === 3) {
+                return 2
+            } else if (b > 0) {
+                return 1<<(b-1)
+            }
+        } else if ('button' in event) {
+            const b = (event as any).button  // 'any' to support older browsers
+            if (b === 1) {
+                return 4
+            } else if (b === 2) {
+                return 2
+            } else if (b >= 0) {
+                return 1<<b
+            }
+        }
+    }
+    return 0
+}
+
+export function element(event: MouseEvent) {
+    return event.target as Element || event.srcElement || window
+}
+
+export function x(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('offsetX' in event) {
+            return event.offsetX
+        }
+        const target = element(event)
+        const bounds = target.getBoundingClientRect()
+        return (event as any).clientX - bounds.left  // 'any' to support older browsers
+    }
+    return 0
+}
+
+export function y(event: MouseEvent) {
+    if (typeof event === 'object') {
+        if ('offsetY' in event) {
+            return event.offsetY
+        }
+        const target = element(event)
+        const bounds = target.getBoundingClientRect()
+        return (event as any).clientY - bounds.top  // 'any' to support older browsers
+    }
+    return 0
+}

+ 40 - 0
src/mol-util/mouse-wheel.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/mouse-wheel,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import toPixels from './to-pixels'
+
+export type MouseWheelCallback = (dx: number, dy: number, dz: number, event: MouseWheelEvent) => void
+
+export default function mouseWheelListen(element: Element, callback: MouseWheelCallback, noScroll = false) {
+    const lineHeight = toPixels('ex', element)
+    const listener = function (event: MouseWheelEvent) {
+        if (noScroll) {
+            event.preventDefault()
+        }
+        const mode = event.deltaMode
+        let dx = event.deltaX || 0
+        let dy = event.deltaY || 0
+        let dz = event.deltaZ || 0
+        let scale = 1
+        switch (mode) {
+            case 1: scale = lineHeight; break
+            case 2: scale = window.innerHeight; break
+        }
+        dx *= scale
+        dy *= scale
+        dz *= scale
+        if (dx || dy || dz) {
+            return callback(dx, dy, dz, event)
+        }
+    }
+    element.addEventListener('wheel', listener)
+    return listener
+}

+ 21 - 0
src/mol-util/parse-unit.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mattdesl/parse-unit,
+ * copyright (c) 2014 Matt DesLauriers. MIT License
+ */
+
+const reUnit = /[\d.\-\+]*\s*(.*)/
+
+export default function parseUnit(str: string, out: [number, string] = [ 0, '' ]) {
+    str = String(str)
+    const num = parseFloat(str)
+    out[0] = num
+    const m = str.match(reUnit)
+    if (m) out[1] = m[1] || ''
+    return out
+}

+ 63 - 0
src/mol-util/to-pixels.ts

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/*
+ * This code has been modified from https://github.com/mikolalysenko/to-px,
+ * copyright (c) 2015 Mikola Lysenko. MIT License
+ */
+
+import parseUnit from './parse-unit'
+
+const PIXELS_PER_INCH = 96
+
+function getPropertyInPX(element: Element, prop: string) {
+    const parts = parseUnit(getComputedStyle(element).getPropertyValue(prop))
+    return parts[0] * toPixels(parts[1], element)
+}
+
+// This brutal hack is needed
+function getSizeBrutal(unit: string, element: Element) {
+    const testDIV = document.createElement('div')
+    testDIV.style.setProperty('font-size', '128' + unit)
+    element.appendChild(testDIV)
+    const size = getPropertyInPX(testDIV, 'font-size') / 128
+    element.removeChild(testDIV)
+    return size
+}
+
+export default function toPixels(str: string, element: Element = document.body): number {
+    str = (str || 'px').trim().toLowerCase()
+    switch (str) {
+        case '%':  // Ambiguous, not sure if we should use width or height
+            return element.clientHeight / 100.0
+        case 'ch':
+        case 'ex':
+            return getSizeBrutal(str, element)
+        case 'em':
+            return getPropertyInPX(element, 'font-size')
+        case 'rem':
+            return getPropertyInPX(document.body, 'font-size')
+        case 'vw':
+            return window.innerWidth/100
+        case 'vh':
+            return window.innerHeight/100
+        case 'vmin':
+            return Math.min(window.innerWidth, window.innerHeight) / 100
+        case 'vmax':
+            return Math.max(window.innerWidth, window.innerHeight) / 100
+        case 'in':
+            return PIXELS_PER_INCH
+        case 'cm':
+            return PIXELS_PER_INCH / 2.54
+        case 'mm':
+            return PIXELS_PER_INCH / 25.4
+        case 'pt':
+            return PIXELS_PER_INCH / 72
+        case 'pc':
+            return PIXELS_PER_INCH / 6
+  }
+  return 1
+}