diff --git a/src/mol-util/mouse-change.ts b/src/mol-util/mouse-change.ts new file mode 100644 index 0000000000000000000000000000000000000000..96d7f87a1f359afd9feecb06f934583debe81b8c --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-util/mouse-event.ts b/src/mol-util/mouse-event.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f38bc7cb98c6de03d8474ed11154d9e70f1f2ba --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-util/mouse-wheel.ts b/src/mol-util/mouse-wheel.ts new file mode 100644 index 0000000000000000000000000000000000000000..51a11f7853825370729bdd5d0aba76f015d3b310 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-util/parse-unit.ts b/src/mol-util/parse-unit.ts new file mode 100644 index 0000000000000000000000000000000000000000..4071eec017d1810712f2a0e77438908337f97a36 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-util/to-pixels.ts b/src/mol-util/to-pixels.ts new file mode 100644 index 0000000000000000000000000000000000000000..870404025b91e58164aee11dc0ddd561d2da557e --- /dev/null +++ b/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 +} \ No newline at end of file