diff --git a/src/interaction/RSelect.tsx b/src/interaction/RSelect.tsx new file mode 100644 index 00000000..068a8ce1 --- /dev/null +++ b/src/interaction/RSelect.tsx @@ -0,0 +1,139 @@ +import React from 'react'; +import {Collection, Feature, MapBrowserEvent} from 'ol'; +import {default as Select, SelectEvent} from 'ol/interaction/Select'; +import {StyleLike} from 'ol/style/Style'; +import Geometry from 'ol/geom/Geometry'; +import Layer from 'ol/layer/Layer'; +import RenderFeature from 'ol/render/Feature'; + +import {default as RBaseInteraction} from './RBaseInteraction'; + +/** + * @propsfor RSelect + */ +export interface RSelectProps { + /** + * An optional OpenLayers condition to allow selection of the feature. + * Use this if you want to use different events for add and remove instead + * of toggle. + * @default never + */ + addCondition?: (e: MapBrowserEvent) => boolean; + + /** + * An optional OpenLayers condition. + * Clicking on a feature selects that feature and removes any that were in + * the selection. + * Clicking outside any feature removes all from the selection. + * See toggle, add, remove options for adding/removing extra features + * to/from the selection. + * @default singleClick + */ + condition?: (e: MapBrowserEvent) => boolean; + + /** + * If placed inside a vector layer, RSelect will only select features + * from that layer. + * + * If placed directly inside a map: + * Features from this list of layers may be selected. + * Alternatively, a filter function can be provided. + * The function will be called for each layer in the map and should + * return true for layers that you want to be selectable. + * If the option is absent, all visible layers will be considered + * selectable. + */ + layers?: Layer[] | ((f: Layer) => boolean); + + /** + * Style for rendering while selected, supports only Openlayers styles. + * Once the interaction is finished, the resulting feature will adopt + * the style of its layer. + */ + style?: StyleLike; + + /** + * An optional OpenLayers condition to allow deselection of the feature. + * Use this if you want to use different events for add and remove instead + * of toggle. + * @default never + */ + removeCondition?: (e: MapBrowserEvent) => boolean; + + /** + * An optional OpenLayers condition to allow toggling the selection. + * This is in addition to the condition event. See add and remove if + * you want to use different events instead of a toggle. + * @default shiftKeyOnly */ + toggleCondition?: (e: MapBrowserEvent) => boolean; + + /** + * A boolean that determines if the default behaviour should select + * only single features or all (overlapping) features at the clicked + * map position. + * The default of false means single select. + * @default false + */ + multi?: boolean; + + /** + * Collection where the interaction will place selected features. + * If not set the interaction will create a collection. + */ + features?: Collection> | Feature; + + /** + * A function that takes a Feature and a Layer and returns true if the + * feature may be selected or false otherwise. + */ + filter?: (Feature: Feature | RenderFeature, layer: Layer) => boolean; + + /** + * Hit-detection tolerance. + * Pixels inside the radius around the given position will be checked for + * features. + * @default 0 + */ + hitTolerance?: number; + + /** + * Triggered when feature(s) has been (de)selected. + */ + onSelect?: (this: RSelect, e: SelectEvent) => void; +} + +/** + * Interaction for selecting vector features. + * When placed in a vector layer, the interaction will only select features + * from that layer. When placed directly inside a map, features from all + * visible layers may be selected, unless a filter array/function is provided. + */ +export default class RSelect extends RBaseInteraction { + protected static classProps = [ + 'addCondition', + 'condition', + 'layers', + 'style', + 'removeCondition', + 'toggleCondition', + 'multi', + 'features', + 'filter', + 'hitTolerance' + ]; + ol: Select; + + createOL(props: RSelectProps): Select { + this.classProps = RSelect.classProps; + let layers: typeof props.layers; + if (this.context?.vectorlayer) { + layers = [this.context.vectorlayer]; + } + return new Select({ + layers, + ...Object.keys(props) + .filter((p) => this.classProps.includes(p)) + .reduce((ac, p) => ({...ac, [p]: props[p]}), {}) + }); + } +} diff --git a/src/interaction/index.ts b/src/interaction/index.ts index b5faff73..07c63f6b 100644 --- a/src/interaction/index.ts +++ b/src/interaction/index.ts @@ -13,3 +13,4 @@ export {default as RPinchRotate} from './RPinchRotate'; export {default as RPinchZoom} from './RPinchZoom'; export {default as RKeyboardPan} from './RKeyboardPan'; export {default as RKeyboardZoom} from './RKeyboardZoom'; +export {default as RSelect} from './RSelect'; diff --git a/test/RInteractions.test.tsx b/test/RInteractions.test.tsx index aedf2e41..aefdcd02 100644 --- a/test/RInteractions.test.tsx +++ b/test/RInteractions.test.tsx @@ -267,6 +267,79 @@ describe('', () => { }); }); +describe('', () => { + it('should create a Select interaction', async () => { + const ref = React.createRef() as React.RefObject; + const {container, unmount} = render( + + + + + + ); + expect(container.innerHTML).toMatchSnapshot(); + expect(ref.current).toBeInstanceOf(RInteraction.RSelect); + unmount(); + }); + it('should support styles', async () => { + const ref = React.createRef() as React.RefObject; + const style = new Style({ + stroke: new Stroke({ + color: 'red', + width: 3 + }) + }); + const {container, unmount} = render( + + + + + + ); + expect(container.innerHTML).toMatchSnapshot(); + expect(ref.current).toBeInstanceOf(RInteraction.RSelect); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const styleResult = (ref.current?.ol as any).style_ as Style; + expect(styleResult.getStroke()?.getWidth?.()).toBe(3); + unmount(); + }); + it('can be used directly inside a map', async () => { + const ref = React.createRef() as React.RefObject; + const {container, unmount} = render( + + + + + ); + expect(container.innerHTML).toMatchSnapshot(); + expect(ref.current).toBeInstanceOf(RInteraction.RSelect); + // Should have no layer filter + expect((ref.current?.ol as any).layerFilter_?.()).toBe(true); + unmount(); + }); + it('can be used inside a layer', async () => { + const ref = React.createRef() as React.RefObject; + const layerRef = React.createRef() as React.RefObject; + const otherLayerRef = React.createRef() as React.RefObject; + const {container, unmount} = render( + + + + + + + ); + expect(container.innerHTML).toMatchSnapshot(); + expect(ref.current).toBeInstanceOf(RInteraction.RSelect); + // Should include a filter for the containing layer + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((ref.current?.ol as any).layerFilter_?.(layerRef.current?.ol)).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((ref.current?.ol as any).layerFilter_?.(otherLayerRef.current?.ol)).toBe(false); + unmount(); + }); +}); + describe('', () => { it('should throw', async () => { // eslint-disable-next-line no-console diff --git a/test/__snapshots__/RInteractions.test.tsx.snap b/test/__snapshots__/RInteractions.test.tsx.snap index 0b91197c..c2626222 100644 --- a/test/__snapshots__/RInteractions.test.tsx.snap +++ b/test/__snapshots__/RInteractions.test.tsx.snap @@ -10,6 +10,14 @@ exports[` should create a Modify interaction 1`] = `"
    "`; +exports[` can be used directly inside a map 1`] = `"
      "`; + +exports[` can be used inside a layer 1`] = `"
        "`; + +exports[` should create a Select interaction 1`] = `"
          "`; + +exports[` should support styles 1`] = `"
            "`; + exports[` should create a Translate interaction 1`] = `"
              "`; exports[`Default interactions should support manually adding all the default interactions 1`] = `"
                "`;