Merge pull request #12893 from andrewcoder002/main

Allow map target to be an external window
This commit is contained in:
MoonE
2021-11-03 21:20:24 +01:00
committed by GitHub
10 changed files with 347 additions and 156 deletions

View File

@@ -0,0 +1,13 @@
---
layout: example.html
title: External map
shortdesc: Move a map to a seperate window.
docs: >
Move a map to a seperate window.
tags: "external, window"
sources:
- path: resources/external-map-map.html
---
<div id="map" class="map"></div>
<input id="external-map-button" type="button" value="Open external map"></input>
<span id="blocker-notice" hidden>Could not open map in external window. If you are using a popup or ad blocker you may need to disable it for this example.</span>

112
examples/external-map.js Normal file
View File

@@ -0,0 +1,112 @@
import Map from '../src/ol/Map.js';
import OSM from '../src/ol/source/OSM.js';
import TileLayer from '../src/ol/layer/Tile.js';
import View from '../src/ol/View.js';
import {
Control,
FullScreen,
defaults as defaultControls,
} from '../src/ol/control.js';
import {fromLonLat} from '../src/ol/proj.js';
class UnusableMask extends Control {
constructor() {
super({
element: document.createElement('div'),
});
this.element.setAttribute('hidden', 'hidden');
this.element.className = 'ol-mask';
this.element.innerHTML = '<div>Map not usable</div>';
}
}
const localMapTarget = document.getElementById('map');
const map = new Map({
target: localMapTarget,
controls: defaultControls().extend([new FullScreen(), new UnusableMask()]),
layers: [
new TileLayer({
source: new OSM(),
}),
],
view: new View({
center: fromLonLat([37.41, 8.82]),
zoom: 4,
}),
});
let mapWindow;
function closeMapWindow() {
if (mapWindow) {
mapWindow.close();
mapWindow = undefined;
}
}
// Close external window in case the main page is closed or reloaded
window.addEventListener('pagehide', closeMapWindow);
const button = document.getElementById('external-map-button');
function resetMapTarget() {
localMapTarget.style.height = '';
map.setTarget(localMapTarget);
button.disabled = false;
}
function updateOverlay() {
if (!mapWindow) {
return;
}
const externalMapTarget = mapWindow.document.getElementById('map');
if (!externalMapTarget) {
return;
}
if (document.visibilityState === 'visible') {
// Show controls and enable keyboard input
externalMapTarget.classList.remove('unusable');
externalMapTarget.setAttribute('tabindex', '0');
externalMapTarget.focus();
} else {
// Hide all controls and disable keyboard input
externalMapTarget.removeAttribute('tabindex');
externalMapTarget.classList.add('unusable');
}
}
window.addEventListener('visibilitychange', updateOverlay);
button.addEventListener('click', function () {
const blockerNotice = document.getElementById('blocker-notice');
blockerNotice.setAttribute('hidden', 'hidden');
button.disabled = true;
// Reset button and map target in case window did not load or open
let timeoutKey = setTimeout(function () {
closeMapWindow();
resetMapTarget();
blockerNotice.removeAttribute('hidden');
timeoutKey = undefined;
}, 3000);
mapWindow = window.open(
'resources/external-map-map.html',
'MapWindow',
'toolbar=0,location=0,menubar=0,width=800,height=600'
);
mapWindow.addEventListener('DOMContentLoaded', function () {
const externalMapTarget = mapWindow.document.getElementById('map');
localMapTarget.style.height = '0px';
map.setTarget(externalMapTarget);
if (timeoutKey) {
timeoutKey = clearTimeout(timeoutKey);
}
mapWindow.addEventListener('pagehide', function () {
resetMapTarget();
// Close window in case user does a page reload
closeMapWindow();
});
updateOverlay();
});
});

View File

@@ -6,10 +6,12 @@ docs: >
The map in this example is rendered in a web worker, using `OffscreenCanvas`. **Note:** This is currently only supported in Chrome and Edge.
tags: "worker, offscreencanvas, vector-tiles"
experimental: true
sources:
- path: offscreen-canvas.worker.js
as: worker.js
cloak:
- key: get_your_own_D6rA4zTHduk6KOKTXzGB
value: Get your own API key at https://www.maptiler.com/cloud/
---
<div id="map" class="map">
<pre id="info" class="info"/>

View File

@@ -0,0 +1,30 @@
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../css/ol.css" type="text/css">
<style>
body {
margin: 0;
}
.map {
height: 100%;
}
.map.unusable .ol-mask {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
background-color: rgba(0, 0, 0, .7);
color: white;
font: bold 3rem 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.map.unusable .ol-control {
display: none;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
</body>
</html>

View File

@@ -211,13 +211,12 @@
&lt;/html&gt;</code></pre>
</div>
{{#if worker.source}}
<div class="row-fluid">
<h5 class="source-heading">worker.js</h5>
<pre><code id="example-worker-source" class="language-js">{{ worker.source }}</code></pre>
{{#each extraSources}}
<div class="row-fluid extra-source">
<h5 class="source-heading">{{./name}}</h5>
<pre><code class="language-{{./type}}">{{ ./source }}</code></pre>
</div>
{{/if}}
{{/each}}
<div class="row-fluid">
<h5 class="source-heading">package.json</h5>
<pre><code id="example-pkg-source" class="language-json">{{ pkgJson }}</code></pre>

View File

@@ -13,7 +13,7 @@ const baseDir = dirname(fileURLToPath(import.meta.url));
const isCssRegEx = /\.css(\?.*)?$/;
const isJsRegEx = /\.js(\?.*)?$/;
const importRegEx = /^import .* from '(.*)';$/;
const importRegEx = /(?:^|\n)import .* from '(.*)';(?:\n|$)/g;
const isTemplateJs =
/\/(jquery(-\d+\.\d+\.\d+)?|(bootstrap(\.bundle)?))(\.min)?\.js(\?.*)?$/;
const isTemplateCss = /\/bootstrap(\.min)?\.css(\?.*)?$/;
@@ -134,29 +134,21 @@ function createWordIndex(exampleData) {
* @return {Object<string, string>} dependencies
*/
function getDependencies(jsSource, pkg) {
const lines = jsSource.split('\n');
const dependencies = {
ol: pkg.version,
};
for (let i = 0, ii = lines.length; i < ii; ++i) {
const line = lines[i];
const importMatch = line.match(importRegEx);
if (importMatch) {
let importMatch;
while ((importMatch = importRegEx.exec(jsSource))) {
const imp = importMatch[1];
if (!imp.startsWith('ol/') && imp != 'ol') {
const parts = imp.split('/');
let dep;
if (imp.startsWith('@')) {
dep = parts.slice(0, 2).join('/');
} else {
dep = parts[0];
}
const dep = imp.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
if (dep in pkg.devDependencies) {
dependencies[dep] = pkg.devDependencies[dep];
}
}
}
}
return dependencies;
}
@@ -279,80 +271,79 @@ export default class ExampleBuilder {
data.jsSource = jsSource;
// process tags
if (data.tags) {
data.tags = data.tags.replace(/[\s"]+/g, '').split(',');
} else {
data.tags = [];
}
data.tags = data.tags ? data.tags.replace(/[\s"]+/g, '').split(',') : [];
return data;
}
transformJsSource(source) {
return (
source
// remove "../src/" prefix and ".js" to have the same import syntax as the documentation
.replace(/'\.\.\/src\//g, "'")
.replace(/\.js';/g, "';")
// Remove worker loader import and modify `new Worker()` to add source
.replace(/import Worker from 'worker-loader![^\n]*\n/g, '')
.replace('new Worker()', "new Worker('./worker.js', {type: 'module'})")
);
}
cloakSource(source, cloak) {
if (cloak) {
for (const entry of cloak) {
source = source.replace(new RegExp(entry.key, 'g'), entry.value);
}
}
return source;
}
async render(data) {
const assets = {};
const readOptions = {encoding: 'utf8'};
// add in script tag
const jsName = `${data.name}.js`;
// remove "../src/" prefix and ".js" to have the same import syntax as the documentation
let jsSource = data.jsSource.replace(/'\.\.\/src\//g, "'");
jsSource = jsSource.replace(/\.js';/g, "';");
if (data.cloak) {
for (const entry of data.cloak) {
jsSource = jsSource.replace(new RegExp(entry.key, 'g'), entry.value);
}
}
// Remove worker loader import and modify `new Worker()` to add source
jsSource = jsSource.replace(
/import Worker from 'worker-loader![^\n]*\n/g,
''
const jsSource = this.transformJsSource(
this.cloakSource(data.jsSource, data.cloak)
);
jsSource = jsSource.replace('new Worker()', "new Worker('./worker.js')");
data.js = {
tag: `<script src="${this.common}.js"></script>
<script src="${jsName}"></script>`,
source: jsSource,
};
// check for worker js
const workerName = `${data.name}.worker.js`;
const workerPath = path.join(data.dir, workerName);
let workerSource;
try {
workerSource = await fse.readFile(workerPath, readOptions);
} catch (err) {
// pass
let jsSources = jsSource;
if (data.sources) {
data.extraSources = await Promise.all(
data.sources.map(async (sourceConfig) => {
const fileName = sourceConfig.path;
const extraSourcePath = path.join(data.dir, fileName);
let source = await fse.readFile(extraSourcePath, readOptions);
let ext = fileName.match(/\.(\w+)$/)[1];
if (ext === 'mjs') {
ext = 'js';
}
if (workerSource) {
// remove "../src/" prefix and ".js" to have the same import syntax as the documentation
workerSource = workerSource.replace(/'\.\.\/src\//g, "'");
workerSource = workerSource.replace(/\.js';/g, "';");
if (data.cloak) {
for (const entry of data.cloak) {
workerSource = workerSource.replace(
new RegExp(entry.key, 'g'),
entry.value
);
if (ext === 'js') {
source = this.transformJsSource(source);
jsSources += '\n' + source;
}
}
data.worker = {
source: workerSource,
source = this.cloakSource(source, data.cloak);
assets[fileName] = source;
return {
name: sourceConfig.as ?? fileName,
source: source,
type: ext,
};
assets[workerName] = workerSource;
})
);
}
const pkg = await getPackageInfo();
data.pkgJson = JSON.stringify(
{
name: data.name,
dependencies: getDependencies(
jsSource + (workerSource ? `\n${workerSource}` : ''),
pkg
),
dependencies: getDependencies(jsSources, pkg),
devDependencies: {
parcel: '^2.0.0-beta.1',
parcel: '^2.0.0',
},
scripts: {
start: 'parcel index.html',

View File

@@ -323,7 +323,7 @@ class PluggableMap extends BaseObject {
* @private
* @type {?Array<import("./events.js").EventsKey>}
*/
this.keyHandlerKeys_ = null;
this.targetChangeHandlerKeys_ = null;
/**
* @type {Collection<import("./control/Control.js").default>}
@@ -356,12 +356,6 @@ class PluggableMap extends BaseObject {
*/
this.renderer_ = null;
/**
* @type {undefined|function(Event): void}
* @private
*/
this.handleResize_;
/**
* @private
* @type {!Array<PostRenderFunction>}
@@ -1146,21 +1140,11 @@ class PluggableMap extends BaseObject {
* @private
*/
handleTargetChanged_() {
// target may be undefined, null, a string or an Element.
// If it's a string we convert it to an Element before proceeding.
// If it's not now an Element we remove the viewport from the DOM.
// If it's an Element we append the viewport element to it.
let targetElement;
if (this.getTarget()) {
targetElement = this.getTargetElement();
}
if (this.mapBrowserEventHandler_) {
for (let i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) {
unlistenByKey(this.keyHandlerKeys_[i]);
for (let i = 0, ii = this.targetChangeHandlerKeys_.length; i < ii; ++i) {
unlistenByKey(this.targetChangeHandlerKeys_[i]);
}
this.keyHandlerKeys_ = null;
this.targetChangeHandlerKeys_ = null;
this.viewport_.removeEventListener(
EventType.CONTEXTMENU,
this.boundHandleBrowserEvent_
@@ -1169,15 +1153,17 @@ class PluggableMap extends BaseObject {
EventType.WHEEL,
this.boundHandleBrowserEvent_
);
if (this.handleResize_ !== undefined) {
removeEventListener(EventType.RESIZE, this.handleResize_, false);
this.handleResize_ = undefined;
}
this.mapBrowserEventHandler_.dispose();
this.mapBrowserEventHandler_ = null;
removeNode(this.viewport_);
}
// target may be undefined, null, a string or an Element.
// If it's a string we convert it to an Element before proceeding.
// If it's not now an Element we remove the viewport from the DOM.
// If it's an Element we append the viewport element to it.
const targetElement = this.getTargetElement();
if (!targetElement) {
if (this.renderer_) {
clearTimeout(this.postRenderTimeoutHandle_);
@@ -1217,10 +1203,11 @@ class PluggableMap extends BaseObject {
PASSIVE_EVENT_LISTENERS ? {passive: false} : false
);
const defaultView = this.getOwnerDocument().defaultView;
const keyboardEventTarget = !this.keyboardEventTarget_
? targetElement
: this.keyboardEventTarget_;
this.keyHandlerKeys_ = [
this.targetChangeHandlerKeys_ = [
listen(
keyboardEventTarget,
EventType.KEYDOWN,
@@ -1233,12 +1220,8 @@ class PluggableMap extends BaseObject {
this.handleBrowserEvent,
this
),
listen(defaultView, EventType.RESIZE, this.updateSize, this),
];
if (!this.handleResize_) {
this.handleResize_ = this.updateSize.bind(this);
window.addEventListener(EventType.RESIZE, this.handleResize_, false);
}
}
this.updateSize();

View File

@@ -3,8 +3,9 @@
*/
import Control from './Control.js';
import EventType from '../events/EventType.js';
import MapProperty from '../MapProperty.js';
import {CLASS_CONTROL, CLASS_UNSELECTABLE, CLASS_UNSUPPORTED} from '../css.js';
import {listen} from '../events.js';
import {listen, unlistenByKey} from '../events.js';
import {replaceNode} from '../dom.js';
const events = [
@@ -111,6 +112,12 @@ class FullScreen extends Control {
this.cssClassName_ =
options.className !== undefined ? options.className : 'ol-full-screen';
/**
* @private
* @type {Array<import("../events.js").EventsKey>}
*/
this.documentListeners_ = [];
/**
* @private
* @type {Array<string>}
@@ -157,7 +164,6 @@ class FullScreen extends Control {
this.button_ = document.createElement('button');
const tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
this.setClassName_(this.button_, isFullScreen());
this.button_.setAttribute('type', 'button');
this.button_.title = tipLabel;
this.button_.appendChild(this.labelNode_);
@@ -168,17 +174,8 @@ class FullScreen extends Control {
false
);
const cssClasses =
this.cssClassName_ +
' ' +
CLASS_UNSELECTABLE +
' ' +
CLASS_CONTROL +
' ' +
(!isFullScreenSupported() ? CLASS_UNSUPPORTED : '');
const element = this.element;
element.className = cssClasses;
element.appendChild(this.button_);
this.element.className = `${this.cssClassName_} ${CLASS_UNSELECTABLE} ${CLASS_CONTROL}`;
this.element.appendChild(this.button_);
/**
* @private
@@ -191,6 +188,17 @@ class FullScreen extends Control {
* @type {HTMLElement|string|undefined}
*/
this.source_ = options.source;
/**
* @type {boolean}
* @private
*/
this.isInFullscreen_ = false;
/**
* @private
*/
this.boundHandleMapTargetChange_ = this.handleMapTargetChange_.bind(this);
}
/**
@@ -206,21 +214,22 @@ class FullScreen extends Control {
* @private
*/
handleFullScreen_() {
if (!isFullScreenSupported()) {
return;
}
const map = this.getMap();
if (!map) {
return;
}
if (isFullScreen()) {
exitFullScreen();
const doc = map.getOwnerDocument();
if (!isFullScreenSupported(doc)) {
return;
}
if (isFullScreen(doc)) {
exitFullScreen(doc);
} else {
let element;
if (this.source_) {
element =
typeof this.source_ === 'string'
? document.getElementById(this.source_)
? doc.getElementById(this.source_)
: this.source_;
} else {
element = map.getTargetElement();
@@ -238,16 +247,20 @@ class FullScreen extends Control {
*/
handleFullScreenChange_() {
const map = this.getMap();
if (isFullScreen()) {
this.setClassName_(this.button_, true);
if (!map) {
return;
}
const wasInFullscreen = this.isInFullscreen_;
this.isInFullscreen_ = isFullScreen(map.getOwnerDocument());
if (wasInFullscreen !== this.isInFullscreen_) {
this.setClassName_(this.button_, this.isInFullscreen_);
if (this.isInFullscreen_) {
replaceNode(this.labelActiveNode_, this.labelNode_);
this.dispatchEvent(FullScreenEventType.ENTERFULLSCREEN);
} else {
this.setClassName_(this.button_, false);
replaceNode(this.labelNode_, this.labelActiveNode_);
this.dispatchEvent(FullScreenEventType.LEAVEFULLSCREEN);
}
if (map) {
map.updateSize();
}
}
@@ -274,37 +287,76 @@ class FullScreen extends Control {
* @api
*/
setMap(map) {
super.setMap(map);
if (map) {
for (let i = 0, ii = events.length; i < ii; ++i) {
this.listenerKeys.push(
listen(document, events[i], this.handleFullScreenChange_, this)
const oldMap = this.getMap();
if (oldMap) {
oldMap.removeChangeListener(
MapProperty.TARGET,
this.boundHandleMapTargetChange_
);
}
super.setMap(map);
this.handleMapTargetChange_();
if (map) {
map.addChangeListener(
MapProperty.TARGET,
this.boundHandleMapTargetChange_
);
}
}
/**
* @private
*/
handleMapTargetChange_() {
const listeners = this.documentListeners_;
for (let i = 0, ii = listeners.length; i < ii; ++i) {
unlistenByKey(listeners[i]);
}
listeners.length = 0;
const map = this.getMap();
if (map) {
const doc = map.getOwnerDocument();
if (isFullScreenSupported(doc)) {
this.element.classList.remove(CLASS_UNSUPPORTED);
} else {
this.element.classList.add(CLASS_UNSUPPORTED);
}
for (let i = 0, ii = events.length; i < ii; ++i) {
listeners.push(
listen(doc, events[i], this.handleFullScreenChange_, this)
);
}
this.handleFullScreenChange_();
}
}
}
/**
* @param {Document} doc The root document to check.
* @return {boolean} Fullscreen is supported by the current platform.
*/
function isFullScreenSupported() {
const body = document.body;
function isFullScreenSupported(doc) {
const body = doc.body;
return !!(
body['webkitRequestFullscreen'] ||
(body['msRequestFullscreen'] && document['msFullscreenEnabled']) ||
(body.requestFullscreen && document.fullscreenEnabled)
(body['msRequestFullscreen'] && doc['msFullscreenEnabled']) ||
(body.requestFullscreen && doc.fullscreenEnabled)
);
}
/**
* @param {Document} doc The root document to check.
* @return {boolean} Element is currently in fullscreen.
*/
function isFullScreen() {
function isFullScreen(doc) {
return !!(
document['webkitIsFullScreen'] ||
document['msFullscreenElement'] ||
document.fullscreenElement
doc['webkitIsFullScreen'] ||
doc['msFullscreenElement'] ||
doc.fullscreenElement
);
}
@@ -336,14 +388,15 @@ function requestFullScreenWithKeys(element) {
/**
* Exit fullscreen.
* @param {Document} doc The document to exit fullscren from
*/
function exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document['msExitFullscreen']) {
document['msExitFullscreen']();
} else if (document['webkitExitFullscreen']) {
document['webkitExitFullscreen']();
function exitFullScreen(doc) {
if (doc.exitFullscreen) {
doc.exitFullscreen();
} else if (doc['msExitFullscreen']) {
doc['msExitFullscreen']();
} else if (doc['webkitExitFullscreen']) {
doc['webkitExitFullscreen']();
}
}

View File

@@ -83,7 +83,9 @@ export const altShiftKeysOnly = function (mapBrowserEvent) {
* @api
*/
export const focus = function (event) {
return event.target.getTargetElement().contains(document.activeElement);
const targetElement = event.map.getTargetElement();
const activeElement = event.map.getOwnerDocument().activeElement;
return targetElement.contains(activeElement);
};
/**

View File

@@ -816,7 +816,7 @@ describe('ol/Map', function () {
it('removes window listeners', function () {
map.dispose();
expect(map.handleResize_).to.be(undefined);
expect(map.targetChangeHandlerKeys_).to.be(null);
});
});
@@ -828,7 +828,7 @@ describe('ol/Map', function () {
map = new Map({
target: document.createElement('div'),
});
expect(map.handleResize_).to.be.ok();
expect(map.targetChangeHandlerKeys_).to.be.ok();
});
describe('map with target not attached to dom', function () {
@@ -840,7 +840,7 @@ describe('ol/Map', function () {
describe('call setTarget with null', function () {
it('unregisters the viewport resize listener', function () {
map.setTarget(null);
expect(map.handleResize_).to.be(undefined);
expect(map.targetChangeHandlerKeys_).to.be(null);
});
});
@@ -848,7 +848,7 @@ describe('ol/Map', function () {
it('registers a viewport resize listener', function () {
map.setTarget(null);
map.setTarget(document.createElement('div'));
expect(map.handleResize_).to.be.ok();
expect(map.targetChangeHandlerKeys_).to.be.ok();
});
});
});
@@ -877,8 +877,14 @@ describe('ol/Map', function () {
hasAttribute: function (attribute) {
return hasTabIndex;
},
contains: function () {
return hasFocus;
},
};
},
getOwnerDocument: function () {
return {};
},
},
originalEvent: {
isPrimary: isPrimary,