Skip to content

Commit

Permalink
Continue scrolling in custom scrollbars even when the cursor enters o…
Browse files Browse the repository at this point in the history
…r goes past the boundary of an iframe (#41)

This commit adds `SciMouseDispatcher` to dispatch 'mousemove' and 'mouseup' events between the application window and another window. Communication is based on `postMessage` and `onmessage` to safely propagate events cross-origin.

fixes #41
  • Loading branch information
danielwiehl committed Nov 15, 2018
1 parent 3e91321 commit 4527d2a
Show file tree
Hide file tree
Showing 16 changed files with 413 additions and 15 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ before_install:
install:
- npm install
script:
- ng lint @scion/workbench
- ng test @scion/workbench --watch=false
- ng build @scion/workbench --prod
- npm run lint
- npm run test
- npm run build
39 changes: 39 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,45 @@
}
}
}
},
"@scion/mouse-dispatcher": {
"root": "projects/scion/mouse-dispatcher",
"sourceRoot": "projects/scion/mouse-dispatcher/src",
"projectType": "library",
"prefix": "sci",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/scion/mouse-dispatcher/tsconfig.lib.json",
"project": "projects/scion/mouse-dispatcher/ng-package.json"
},
"configurations": {
"production": {
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/scion/mouse-dispatcher/src/test.ts",
"tsConfig": "projects/scion/mouse-dispatcher/tsconfig.spec.json",
"karmaConfig": "projects/scion/mouse-dispatcher/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/scion/mouse-dispatcher/tsconfig.lib.json",
"projects/scion/mouse-dispatcher/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "@scion/workbench"
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@
},
"scripts": {
"ng": "ng",
"build": "ng build --prod @scion/workbench && cp -r projects/scion/workbench/src/theme/** dist/scion/workbench",
"test": "ng test",
"lint": "ng lint",
"build": "npm run build-mouse-dispatcher && npm run build-workbench",
"build-workbench": "ng build --prod @scion/workbench && cp -r projects/scion/workbench/src/theme/** dist/scion/workbench",
"build-mouse-dispatcher": "ng build --prod @scion/mouse-dispatcher",
"test": "npm run test-workbench",
"test-workbench": "ng test @scion/workbench --watch=false",
"lint": "npm run lint-workbench & npm run lint-mouse-dispatcher",
"lint-workbench": "ng lint @scion/workbench",
"lint-mouse-dispatcher": "ng lint @scion/mouse-dispatcher",
"e2e": "ng e2e"
},
"private": true,
Expand Down
41 changes: 41 additions & 0 deletions projects/scion/mouse-dispatcher/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
8 changes: 8 additions & 0 deletions projects/scion/mouse-dispatcher/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/scion/mouse-dispatcher",
"lib": {
"entryFile": "src/public_api.ts"
},
"whitelistedNonPeerDependencies": ["@scion"]
}
25 changes: 25 additions & 0 deletions projects/scion/mouse-dispatcher/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@scion/mouse-dispatcher",
"version": "0.0.0-beta.11",
"description": "Dispatches mouse events between the application window and another cross-origin window.",
"license": "EPL-2.0",
"homepage": "https://github.com/SchweizerischeBundesbahnen/scion-workbench",
"bugs": {
"url": "https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues"
},
"author": {
"name": "SCION Workbench contributors",
"url": "https://github.com/SchweizerischeBundesbahnen/scion-workbench"
},
"dependencies": {
},
"peerDependencies": {
"rxjs": "^6.0.0"
},
"keywords": [
],
"repository": {
"type": "git",
"url": "git+https://github.com/SchweizerischeBundesbahnen/scion-workbench.git"
}
}
139 changes: 139 additions & 0 deletions projects/scion/mouse-dispatcher/src/lib/mouse-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { fromEvent, merge, Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';

/**
* Indicates that the primary mouse button is pressed (usually left).
*/
const PRIMARY_MOUSE_BUTTON = 1;

/**
* Installs dispatching of the mouse events 'mousemove' (if pressed the primary mouse button) and 'mouseup'
* between the application window and the given target window.
*
* Communication is based on `postMessage` and `onmessage` to safely propagate events cross-origin.
*
* Events are dispatched as synthetic events via document event dispatcher of the target window.
* A 'mousemove' event with the primary mouse button pressed is dispatched as 'sci-mousemove' event
* and a 'mouseup' event as 'sci-mouseup' event. Properties dispatched are `screenX` and `screenY`.
*
* Mouse event dispatching is fundamental if using custom scrollbars in combination with iframes. It provides
* continued delivery of mouse events even when the cursor goes past the boundary of the iframe boundary.
*
* @param targetWindow
* A reference to the window to dispatch mouse events
* @param targetOrigin
* Specifies what the origin of `targetWindow` must be for the events to be,
* either as the literal string "*" (indicating no preference) or as a URI.
* @return handle to uninstall mouse dispatching
*/
export function installMouseDispatcher(targetWindow: Window, targetOrigin: string): SciMouseDispatcher {
const destroy$ = new Subject<void>();

// Dispatch native mouse events to the target window
merge(
fromEvent<MouseEvent>(document, 'mousemove').pipe(filter(event => event.buttons === PRIMARY_MOUSE_BUTTON)),
fromEvent<MouseEvent>(document, 'mouseup'),
)
.pipe(takeUntil(destroy$))
.subscribe((event: MouseEvent) => {
targetWindow.postMessage({
type: `sci-${event.type}`,
screenX: event.screenX,
screenY: event.screenY,
}, targetOrigin);
});

// Dispatch synthetic mouse events to the target window (unless emitted itself)
merge(
fromEvent<SciMouseEvent>(document, 'sci-mousemove'),
fromEvent<SciMouseEvent>(document, 'sci-mouseup')
)
.pipe(
filter((event: SciMouseEvent) => event.source !== targetWindow),
takeUntil(destroy$),
)
.subscribe((event: SciMouseEvent) => {
targetWindow.postMessage({
type: event.type,
screenX: event.screenX,
screenY: event.screenY,
}, targetOrigin);
});


// Dispatch synthetic mouse events received from the target window to this document's event bus
fromEvent<MessageEvent>(window, 'message')
.pipe(takeUntil(destroy$))
.subscribe((messageEvent: MessageEvent) => {
if (messageEvent.source !== targetWindow) {
return;
}

if (targetOrigin !== '*' && messageEvent.origin !== targetOrigin) {
throw Error(`[OriginError] Message of illegal origin received [expected=${targetOrigin}, actual=${messageEvent.origin}]`);
}

const mouseEvent = parseSyntheticMouseEvent(messageEvent);
mouseEvent && document.dispatchEvent(mouseEvent);
});

return {
dispose: (): void => destroy$.next()
};
}

function parseSyntheticMouseEvent(messageEvent: MessageEvent): Event & SciMouseEvent | null {
const event: SciMouseEvent = messageEvent.data;
if (isNullOrUndefined(event) || typeof event !== 'object') {
return null;
}
if (isNullOrUndefined(event.type) || !event.type.startsWith('sci-mouse')) {
return null;
}
if (isNullOrUndefined(event.screenX)) {
return null;
}
if (isNullOrUndefined(event.screenY)) {
return null;
}

const syntheticMouseEvent: any = new Event(event.type);
syntheticMouseEvent.screenX = event.screenX;
syntheticMouseEvent.screenY = event.screenY;
syntheticMouseEvent.source = messageEvent.source;
return syntheticMouseEvent;
}

function isNullOrUndefined(value: any): boolean {
return value === null || value === undefined;
}

/**
* Synthetic mouse event dispatched from another window.
*/
export interface SciMouseEvent {
type: 'sci-mousemove' | 'sci-mouseup';
screenX: number;
screenY: number;
source?: Window;
}

/**
* Dispatches mouse events between the application window and another cross-origin window.
*/
export interface SciMouseDispatcher {
/**
* Invoke to uninstall mouse dispatching.
*/
dispose(): void;
}
14 changes: 14 additions & 0 deletions projects/scion/mouse-dispatcher/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

/**
* Entry point for all public APIs of this package.
*/
export * from './lib/mouse-dispatcher';
32 changes: 32 additions & 0 deletions projects/scion/mouse-dispatcher/src/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
32 changes: 32 additions & 0 deletions projects/scion/mouse-dispatcher/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"module": "es2015",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"enableResourceInlining": true
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}
17 changes: 17 additions & 0 deletions projects/scion/mouse-dispatcher/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}
Loading

0 comments on commit 4527d2a

Please sign in to comment.