Skip to content
This repository has been archived by the owner on Jan 22, 2024. It is now read-only.

Hash context #20

Merged
merged 23 commits into from
Aug 13, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/modules/Map/Map/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import mapBoxGl from 'mapbox-gl';
import PropTypes from 'prop-types';
import 'mapbox-gl/dist/mapbox-gl.css';
import { connectState } from '../../State/context';

import { updateCluster } from '../services/cluster';

Expand All @@ -11,6 +12,7 @@ import CaptureControl from './components/CaptureControl';
import DrawControl from './components/DrawControl';
import PrintControl from './components/PrintControl';
import HomeControl from './components/HomeControl';
import PermalinkControl from './components/PermalinkControl';

import './Map.scss';

Expand All @@ -27,6 +29,7 @@ export const CONTROL_CAPTURE = 'CaptureControl';
export const CONTROL_DRAW = 'DrawControl';
export const CONTROL_PRINT = 'PrintControl';
export const CONTROL_HOME = 'HomeControl';
export const CONTROL_PERMALINK = 'PermalinkControl';

export const DEFAULT_CONTROLS = [{
control: CONTROL_ATTRIBUTION,
Expand Down Expand Up @@ -79,6 +82,7 @@ export class MapComponent extends React.Component {
CONTROL_DRAW,
CONTROL_PRINT,
CONTROL_HOME,
CONTROL_PERMALINK,
]),
PropTypes.shape({
onAdd: PropTypes.func,
Expand Down Expand Up @@ -386,6 +390,15 @@ export class MapComponent extends React.Component {
map.addControl(controlInstance, position);
break;
}
case CONTROL_PERMALINK: {
const controlInstance = new PermalinkControl({
...this.props,
...params,
});
this.controls.push(controlInstance);
map.addControl(controlInstance, position);
break;
}
default: {
const controlInstance = typeof control === 'string'
? new mapBoxGl[control]({ ...params })
Expand All @@ -403,4 +416,4 @@ export class MapComponent extends React.Component {
}
}

export default MapComponent;
export default connectState('initialState')(MapComponent);
3 changes: 2 additions & 1 deletion src/modules/Map/Map/Map.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
}

// Taken from .mapboxgl-ctrl-group > button
.print-button .mapboxgl-ctrl-icon {
.mapboxgl-ctrl-permalink .mapboxgl-ctrl-icon,
.mapboxgl-ctrl-print .mapboxgl-ctrl-icon {
width: 30px;
height: 30px;
display: block;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Icon, Popover, PopoverPosition, Button } from '@blueprintjs/core';
import PropTypes from 'prop-types';
import { stringify } from 'query-string';
import React from 'react';
import translateMock from '../../../../../utils/translate';
import { DEFAULT_OPTIONS } from '../../../../State/Hash/withHashState';

import AbstractMapControl from '../../../helpers/AbstractMapControl';


export class PermalinkControl extends AbstractMapControl {
static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-permalink';

static propTypes = {
/** Function used to translate wording. Takes key and object of options as parameters */
translate: PropTypes.func,
};

static defaultProps = {
translate: translateMock({
'terralego.map.permalink_control.button_label': 'Get permalink',
}),
};

state = {};

inputRef = React.createRef();

options = DEFAULT_OPTIONS;

generateHashString = () => {
const { initialState } = this.props;
const [currentUrl] = window.location.href.split('#');
const url = `${currentUrl}#${stringify(initialState, this.options)}`;
this.setState({ url });
};

render () {
const { translate } = this.props;
const { url, copySuccess } = this.state;
return (
<Popover position={PopoverPosition.LEFT}>
<button
className="mapboxgl-ctrl-icon"
type="button"
onClick={() => this.generateHashString()}
title={translate('terralego.map.capture_control.button_label')}
aria-label={translate('terralego.map.capture_control.button_label')}
>
<Icon icon="link" />
</button>
<>
<input
className="bp3-input"
ref={this.inputRef}
onClick={({ target: { value }, target }) => target.setSelectionRange(0, value.length)}
value={url}
readOnly
size={80}
/>
<Popover>
<Button
onClick={() => {
const { current: textInput } = this.inputRef;
textInput.setSelectionRange(0, textInput.value.length);
document.execCommand('copy');
this.setState({ copySuccess: true });
setTimeout(() => this.setState({ copySuccess: false }), 2000);
}}
intent={copySuccess ? 'success' : 'none'}
icon="clipboard"
/>
{copySuccess && 'Copied !'}
</Popover>
</>
</Popover>
);
}
}

export default PermalinkControl;
3 changes: 3 additions & 0 deletions src/modules/Map/Map/components/PermalinkControl/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PermalinkControl from './PermalinkControl';

export default PermalinkControl;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ORIENTATION_PORTRAIT = 'portrait';
const ORIENTATION_LANDSCAPE = 'landscape';

export class PrintControl extends AbstractMapControl {
static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group print-button';
static containerClassName = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-print';

static propTypes = {
translate: PropTypes.func,
Expand Down
3 changes: 1 addition & 2 deletions src/modules/Map/Map/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import debounce from 'lodash.debounce';
*/
class Hash {
constructor (hashName) {
this._onHashChange.bind(this);
this._hashName = hashName;

// Mobile Safari doesn't allow updating the hash more than 100 times per 30 seconds.
Expand Down Expand Up @@ -68,7 +67,7 @@ class Hash {
return hash;
}

_onHashChange () {
_onHashChange = () => {
let loc = '';
if (this._hashName) {
const params = new URLSearchParams(window.location.hash.slice(1));
Expand Down
4 changes: 4 additions & 0 deletions src/modules/State/Hash/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import StateProvider from '../StateProvider';
import { withHashState } from './withHashState';

export default withHashState()(StateProvider);
73 changes: 73 additions & 0 deletions src/modules/State/Hash/withHashState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as PropTypes from 'prop-types';
mabhub marked this conversation as resolved.
Show resolved Hide resolved
import { stringify, parse } from 'query-string';
import React from 'react';


export const DEFAULT_OPTIONS = {
encode: false,
arrayFormat: 'comma',
sort: false,
parseNumbers: true,
parseBooleans: true,
};

/**
* Decorator for getting an initialState from hash.
*/
export const withHashState = () => WrappedComponent =>
class WithHashState extends React.Component {
static propTypes = {
listenHash: PropTypes.bool,
updateHash: PropTypes.bool,
};

static defaultProps = {
listenHash: true,
updateHash: true,
};

options = DEFAULT_OPTIONS;

componentDidMount () {
const { listenHash } = this.props;
if (listenHash) {
window.addEventListener('hashchange', this.onHashChange, false);
}
}

componentWillUnmount () {
this.isUnmount = true;
window.removeEventListener('hashchange', this.onHashChange, false);
}

onHashChange = () => {
if (!this.isUnmount) {
this.forceUpdate();
}
};

getCurrentHashString = state => `#${stringify(state, this.options)}`;

updateHashString = state => {
const { updateHash } = this.props;
if (updateHash) {
window.history.replaceState(window.history.state, '', `#${stringify(state, this.options)}`);
}
};


render () {
const { listenHash, updateHash, ...props } = this.props;
const initialState = parse(window.location.hash, this.options);
return (
<WrappedComponent
initialState={initialState}
{...props}
getCurrentHashString={this.getCurrentHashString}
onStateChange={this.updateHashString}
/>
);
}
};

export default withHashState;
39 changes: 39 additions & 0 deletions src/modules/State/Hash/withHashState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import withHashState from './withHashState';

const Component = jest.fn(() => null);

it('should get correct parameters', () => {
const ComponentWithHash = withHashState('myparam')(Component);
const instance = new ComponentWithHash();

window.location.hash = '#myparam=1';
expect(instance.getInitialState()).toEqual({ myparam: 1 });

window.location.hash = '#myparam=1&foo=bar';
expect(instance.getInitialState()).toEqual({ myparam: 1 });
});

it('parameters should be versatile', () => {
const ComponentWithHash = withHashState(['myparam'])(Component);
const instance = new ComponentWithHash();

window.location.hash = '#myparam=1';
expect(instance.getInitialState()).toEqual({ myparam: 1 });
});

it('should set correct parameters', () => {
const ComponentWithHash = withHashState('myparam')(Component);
const instance = new ComponentWithHash();

window.location.hash = '';
instance.setCurrentState({ myparam: 1 });
expect(window.location.hash).toEqual('#myparam=1');

window.location.hash = '#foo=bar';
instance.setCurrentState({ myparam: 1 });
expect(window.location.hash).toEqual('#foo=bar&myparam=1');

window.location.hash = '#foo=bar';
instance.setCurrentState({ myparam: 0, baz: 1 });
expect(window.location.hash).toEqual('#foo=bar&myparam=0');
});
41 changes: 41 additions & 0 deletions src/modules/State/StateProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import React from 'react';
import context from './context';

const { Provider } = context;

export class StateProvider extends React.Component {
static propTypes = {
onStateChange: PropTypes.func,
};

static defaultProps = {
onStateChange () {},
};

componentWillMount () {
const { initialState } = this.props;
this.setState(initialState);
}

setCurrentState = state => {
const { onStateChange } = this.props;
this.setState(state, () => onStateChange(this.state));
}


render () {
const { children } = this.props;
return (
<Provider value={{
initialState: this.state,
setCurrentState: this.setCurrentState,
}}
>
{children}
</Provider>
);
}
}

export default StateProvider;
7 changes: 7 additions & 0 deletions src/modules/State/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';
import connect from 'react-ctx-connect';

export const context = React.createContext();
export const connectState = connect(context);

export default context;
Loading