diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6bceab96..1cddb434 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -43,7 +43,7 @@ mykomap's repository: Install the prerequisites apt install php php-curl rsync # On Debian - npm run install + npm install Run the development web server in the background (do this in a separate console): diff --git a/src/map-app/app/map-ui.ts b/src/map-app/app/map-ui.ts index ebf5b756..d2c79208 100644 --- a/src/map-app/app/map-ui.ts +++ b/src/map-app/app/map-ui.ts @@ -8,7 +8,7 @@ import { EventBus } from "../eventbus"; import "./map"; // Seems to be needed to prod the leaflet CSS into loading. import { SidebarPresenter } from "./presenter/sidebar"; import { PhraseBook } from "../localisations"; -import { toString as _toString } from '../utils'; +import { toString as _toString, canDisplayExpandedSidebar } from '../utils'; import { Action, AppState, PropEquality, StateManager, TextSearch } from "./state-manager"; import { StateChange } from "../undo-stack"; import { Dictionary } from "../common-types"; @@ -48,7 +48,8 @@ export class MapUI { }); }; - EventBus.Directory.initiativeClicked.sub(initiative => this.onInitiativeClickedInSidebar(initiative)); + EventBus.Map.initiativeClicked.sub(initiative => this.onInitiativeClicked(initiative)); + EventBus.Map.clearFiltersAndSearch.sub(() => this.clearFiltersAndSearch()); } // This inspects the config and constructs an appropriate set of @@ -113,8 +114,8 @@ export class MapUI { this.stateManager.clearPropFilter(filterName); } - resetSearch(): void { - this.stateManager.reset(); + clearFiltersAndSearch(): void { + this.stateManager.clearFiltersAndSearch(); } /// Returns a list of property values matching the given filter @@ -131,36 +132,38 @@ export class MapUI { throw new Error(`an attempt to add a filter on an unknown propery name '${propName}'`); const isMulti = propDef.type === 'multi'; - if (value === undefined) + if (value === undefined) { this.stateManager.clearPropFilter(propName); - else + } else { this.stateManager.propFilter(new PropEquality(propName, value, isMulti)); + } + + if (canDisplayExpandedSidebar()) // on smaller screens, wait until user clicks Apply Filters + EventBus.Sidebar.showInitiativeList.pub(); } isSelected(initiative: Initiative): boolean { - // Currently unimplemented - I can see an sea-initiative-active class - // being applied if a test was true, but it makes no sense in the current - // context AFAICT. The test was: - // initiative === this.contextStack.current()?.initiatives[0] - - // The main thing is seemed to do is highlight an initiative (or - // initiatives) in the directory pop-out list of initiatives in a - // category. - - // For now, just return false always. We may re-implement this later. - return false; + const selectedInitiatives = this.mapPresenter?.getSelectedInitiatives() ?? []; + return selectedInitiatives.includes(initiative); } toggleSelectInitiative(initiative: Initiative) { - // Currently unimplemented. + const selectedInitiatives = this.mapPresenter?.getSelectedInitiatives() ?? []; - // EventBus.Markers.needToShowLatestSelection.pub(lastContent.initiatives); + if (selectedInitiatives.includes(initiative)) { + EventBus.Markers.needToShowLatestSelection.pub(selectedInitiatives.filter(i => i !== initiative)); + } else { + EventBus.Markers.needToShowLatestSelection.pub([...selectedInitiatives, initiative]); + } } - performSearch(text: string) { console.log("Search submitted: [" + text + "]"); this.stateManager.textSearch(new TextSearch(text)); + + if (canDisplayExpandedSidebar()) {// on smaller screens, wait until user clicks Apply Filters + EventBus.Sidebar.showInitiativeList.pub(); + } } // Text to show in the search box @@ -182,26 +185,40 @@ export class MapUI { } private refreshSidebar() { - this.getSidebarPresenter(this).then((presenter) => presenter.changeSidebar()); + this.getSidebarPresenter(this).then((presenter) => { + presenter.changeSidebar(); + }); } - - - private notifyMapNeedsToNeedsToBeZoomedAndPannedOneInitiative(initiative: Initiative) { - const data = EventBus.Map.mkSelectAndZoomData([initiative]); - EventBus.Map.needsToBeZoomedAndPanned.pub(data); + private notifyMapNeedsToNeedsToSelectAndZoomOnInitiative(initiative: Initiative) { + const maxZoom = this.config.getMaxZoomOnOne(); + const defaultPos = this.config.getDefaultLatLng(); + + const data = EventBus.Map.mkSelectAndZoomData([initiative], { maxZoom, defaultPos }); + EventBus.Map.selectAndZoomOnInitiative.pub(data); } - private onInitiativeClickedInSidebar(initiative?: Initiative) { - if (!initiative) - return; + private onInitiativeClicked(initiative?: Initiative) { + console.log('Clicked', initiative); - //this.parent.mapui.stateManager.append(new SearchResults([initiative])); - //console.log(this.parent.mapui.stateManager.current()); + if (initiative) { + // Move the window to the right position first + this.notifyMapNeedsToNeedsToSelectAndZoomOnInitiative(initiative); + this.refreshSidebar(); + // Populate the sidebar and highlight the intiative in the directory + this.getSidebarPresenter(this).then((presenter) => { + presenter.populateInitiativeSidebar( + initiative, + this.markers.getInitiativeContent(initiative) ?? '' + ); + }); + } + else { + // User has deselected + EventBus.Markers.needToShowLatestSelection.pub([]); + } - this.notifyMapNeedsToNeedsToBeZoomedAndPannedOneInitiative(initiative); this.refreshSidebar(); - EventBus.Initiative.searchedInitiativeClicked.pub(initiative); } currentItem() { diff --git a/src/map-app/app/presenter/map.ts b/src/map-app/app/presenter/map.ts index 96e057a9..67e650f0 100644 --- a/src/map-app/app/presenter/map.ts +++ b/src/map-app/app/presenter/map.ts @@ -8,7 +8,7 @@ import { MapUI } from '../map-ui'; export class MapPresenter extends BasePresenter { readonly view: MapView; - private previouslySelected: Initiative[] = []; + private currentlySelected: Initiative[] = []; constructor(readonly mapUI: MapUI) { super(); @@ -35,7 +35,7 @@ export class MapPresenter extends BasePresenter { EventBus.Initiatives.loadStarted.sub(() => this.onInitiativeLoadMessage()); EventBus.Initiatives.loadFailed.sub(error => this.onInitiativeLoadMessage(error)); - EventBus.Markers.needToShowLatestSelection.sub(initiative => this.onMarkersNeedToShowLatestSelection(initiative)); + EventBus.Markers.needToShowLatestSelection.sub(initiatives => this.onMarkersNeedToShowLatestSelection(initiatives)); EventBus.Map.needsToBeZoomedAndPanned.sub(data => this.onMapNeedsToBeZoomedAndPanned(data)); EventBus.Map.needToShowInitiativeTooltip.sub(initiative => this.onNeedToShowInitiativeTooltip(initiative)); EventBus.Map.needToHideInitiativeTooltip.sub(initiative => this.onNeedToHideInitiativeTooltip(initiative)); @@ -49,7 +49,7 @@ export class MapPresenter extends BasePresenter { } onInitiativeClicked() { - EventBus.Directory.initiativeClicked.pub(undefined); + EventBus.Map.initiativeClicked.pub(undefined); } onLoad() { @@ -96,11 +96,11 @@ export class MapPresenter extends BasePresenter { } onMarkersNeedToShowLatestSelection(selected: Initiative[]) { - this.previouslySelected.forEach((initiative) => { + this.currentlySelected.forEach((initiative) => { this.mapUI.markers.setUnselected(initiative); }); - this.previouslySelected = selected; + this.currentlySelected = selected; //zoom in and then select selected.forEach((initiative) => { @@ -144,4 +144,7 @@ export class MapPresenter extends BasePresenter { this.view.selectAndZoomOnInitiative(data); } + getSelectedInitiatives(): Initiative[] { + return this.currentlySelected; + } } diff --git a/src/map-app/app/presenter/sidebar.ts b/src/map-app/app/presenter/sidebar.ts index 461b8792..09891574 100644 --- a/src/map-app/app/presenter/sidebar.ts +++ b/src/map-app/app/presenter/sidebar.ts @@ -1,6 +1,7 @@ import { Dictionary } from '../../common-types'; import { EventBus } from '../../eventbus'; import { MapUI } from '../map-ui'; +import { Initiative } from '../model/initiative'; import { SidebarView } from '../view/sidebar'; import { BasePresenter } from './base'; import { AboutSidebarPresenter } from './sidebar/about'; @@ -63,7 +64,7 @@ export class SidebarPresenter extends BasePresenter { if (name !== undefined) { // If name is set, change the current sidebar and then refresh this.sidebarName = name; - this.children[this.sidebarName]?.refreshView(); + this.children[this.sidebarName]?.refreshView(true); } else { // Just refresh the currently showing sidebar. @@ -76,11 +77,13 @@ export class SidebarPresenter extends BasePresenter { return; // No sidebars? Can't do anything. } - this.children[this.sidebarName]?.refreshView(); + this.children[this.sidebarName]?.refreshView(false); } } showSidebar() { + // Refresh the view before showing + this.children[this.sidebarName ?? 'undefined']?.refreshView(true); this.view.showSidebar(); } @@ -97,6 +100,14 @@ export class SidebarPresenter extends BasePresenter { this.view.hideInitiativeSidebar(); } + populateInitiativeSidebar(initiative: Initiative, initiativeContent: string) { + this.view.populateInitiativeSidebar(initiative, initiativeContent); + } + + showInitiativeList() { + this.view.showInitiativeList(); + } + hideInitiativeList() { this.view.hideInitiativeList(); } @@ -112,7 +123,7 @@ export class SidebarPresenter extends BasePresenter { }); EventBus.Sidebar.showDirectory.sub(() => { this.changeSidebar("directory"); - this.view.showInitiativeList(); + this.showSidebar(); }); EventBus.Sidebar.showDatasets.sub(() => { this.changeSidebar("datasets"); @@ -121,8 +132,12 @@ export class SidebarPresenter extends BasePresenter { EventBus.Sidebar.showSidebar.sub(() => this.showSidebar()); EventBus.Sidebar.hideSidebar.sub(() => this.hideSidebar()); EventBus.Sidebar.hideInitiativeSidebar.sub(() => this.hideInitiativeSidebar()); + EventBus.Sidebar.showInitiativeList.sub(() => this.showInitiativeList()); EventBus.Sidebar.hideInitiativeList.sub(() => this.hideInitiativeList()); - EventBus.Initiatives.reset.sub(() => this.changeSidebar()); + EventBus.Initiatives.reset.sub(() => { + this.changeSidebar(); + this.hideInitiativeList(); + }); EventBus.Initiatives.loadComplete.sub(() => this.changeSidebar()); } diff --git a/src/map-app/app/presenter/sidebar/base.ts b/src/map-app/app/presenter/sidebar/base.ts index 1519d357..8bbdc4e4 100644 --- a/src/map-app/app/presenter/sidebar/base.ts +++ b/src/map-app/app/presenter/sidebar/base.ts @@ -2,6 +2,8 @@ import { EventBus } from '../../../eventbus'; import { BasePresenter }from '../base'; import { BaseSidebarView } from '../../view/sidebar/base'; import { SidebarPresenter } from '../sidebar'; +import { Initiative } from '../../model/initiative'; +import { canDisplayExpandedSidebar } from '../../../utils'; export interface NavigationCallback { disabled: boolean; @@ -16,10 +18,16 @@ export abstract class BaseSidebarPresenter extends BasePresenter { super(); } - // If the sidebar wants to do something more than to get its view to refresh when the history buttons have been used, then - // it should override this definition with its own: - historyButtonsUsed(): void { - this.view.refresh(); + /** + * If the sidebar wants to do something more than to get its view to refresh when the history + * buttons have been used, then it should override this definition with its own. + */ + historyButtonsUsed(): void { + this.view.refresh(false); + + // Show the results pane again, since there have been changes + if (canDisplayExpandedSidebar()) // on smaller screens, wait until user clicks Apply Filters + EventBus.Sidebar.showInitiativeList.pub(); } deselectInitiatives(): void { @@ -48,9 +56,26 @@ export abstract class BaseSidebarPresenter extends BasePresenter { this.historyButtonsUsed(); } - /// Refreshes the view - refreshView() { - this.view.refresh(); + /** + * Refreshes the sidebar view + * + * @param changed true if we changed to this sidebar, false if it was already showing and we're + * just refreshing it. + */ + refreshView(changed: boolean) { + this.view.refresh(changed); + } + + onInitiativeClicked(initiative: Initiative): void { + EventBus.Map.initiativeClicked.pub(initiative); + } + + onInitiativeMouseoverInSidebar(initiative: Initiative): void { + EventBus.Map.needToShowInitiativeTooltip.pub(initiative); + } + + onInitiativeMouseoutInSidebar(initiative: Initiative): void { + EventBus.Map.needToHideInitiativeTooltip.pub(initiative); } } diff --git a/src/map-app/app/presenter/sidebar/directory.ts b/src/map-app/app/presenter/sidebar/directory.ts index bf689bfc..293ea173 100644 --- a/src/map-app/app/presenter/sidebar/directory.ts +++ b/src/map-app/app/presenter/sidebar/directory.ts @@ -1,7 +1,6 @@ import { EventBus } from '../../../eventbus'; import { DirectorySidebarView } from '../../view/sidebar/directory'; import { BaseSidebarPresenter } from './base'; -import { Initiative } from '../../model/initiative'; import { SidebarPresenter } from '../sidebar'; export class DirectorySidebarPresenter extends BaseSidebarPresenter { @@ -18,89 +17,6 @@ export class DirectorySidebarPresenter extends BaseSidebarPresenter { //todo reload new ones inside instead (without closing) EventBus.Sidebar.hideInitiativeList.pub(); }); - EventBus.Directory.initiativeClicked.sub(initiative => this.initiativeClicked(initiative)); - } - - notifyViewToBuildDirectory(): void { - this.view.refresh(); - } - - // Gets the initiatives with a selection key, or if absent, gets all the initiatives - getInitiativesForFieldAndSelectionKey(propName: string, key?: string): Initiative[] { - if (key == null) - return this.parent.mapui.dataServices.getAggregatedData().loadedInitiatives; - else - return this.parent.mapui.dataServices.getAggregatedData().registeredValues[propName]?.[key] ?? []; - } - - notifyMapNeedsToNeedsToBeZoomedAndPanned(initiatives: Initiative[]): void { - if (initiatives.length <= 0) - return; - const data = EventBus.Map.mkSelectAndZoomData(initiatives, { maxZoom: this.parent.mapui.config.getMaxZoomOnGroup() }); - EventBus.Map.needsToBeZoomedAndPanned.pub(data); - } - - notifyMapNeedsToNeedsToSelectInitiative(initiatives: Initiative[]): void { - if (initiatives.length == 0) - return; - - const maxZoom = initiatives.length === 1? this.parent.mapui.config.getMaxZoomOnGroup() : undefined; - const defaultPos = this.parent.mapui.config.getDefaultLatLng(); - - const data = EventBus.Map.mkSelectAndZoomData(initiatives, { maxZoom, defaultPos }); - EventBus.Map.selectAndZoomOnInitiative.pub(data); - } - - onInitiativeMouseoverInSidebar(initiative: Initiative): void { - EventBus.Map.needToShowInitiativeTooltip.pub(initiative); - } - onInitiativeMouseoutInSidebar(initiative: Initiative): void { - EventBus.Map.needToHideInitiativeTooltip.pub(initiative); - } - - clearLatestSelection() { - EventBus.Markers.needToShowLatestSelection.pub([]); - } - - removeFilters(filterName?: string) { - //remove specific filter - if (filterName) { - this.parent.mapui.removeFilter(filterName); - } - else { - //remove all filters - this.parent.mapui.removeFilters(); - } - this.view.d3selectAndClear( - "#sea-initiatives-list-sidebar-content" - ); - //clear the window - EventBus.Sidebar.hideInitiativeList.pub(); - this.clearLatestSelection(); - } - - initiativeClicked(initiative?: Initiative): void { - if (initiative) { - //this.parent.contentStack.append(new SearchResults([initiative])); - // Move the window to the right position first - this.notifyMapNeedsToNeedsToSelectInitiative([initiative]); - - // Populate the sidebar and hoghlight the iitiative in the directory - this.view.populateInitiativeSidebar( - initiative, - this.parent.mapui.markers.getInitiativeContent(initiative) ?? '' - ); - - } - else { - // User has deselected - // TODO: This probably shouldn\t be here - EventBus.Markers.needToShowLatestSelection.pub([]); - // Deselect the sidebar and hoghlight the iitiative in the directory - this.view.deselectInitiativeSidebar(); - - //doesn't do much? - } } // This gets the localised 'allEntries' label in all cases. diff --git a/src/map-app/app/presenter/sidebar/initiatives.ts b/src/map-app/app/presenter/sidebar/initiatives.ts index 9ad9c902..011899d8 100644 --- a/src/map-app/app/presenter/sidebar/initiatives.ts +++ b/src/map-app/app/presenter/sidebar/initiatives.ts @@ -1,9 +1,8 @@ import { EventBus } from '../../../eventbus'; import { InitiativesSidebarView } from '../../view/sidebar/initiatives'; import { BaseSidebarPresenter } from './base'; -import { SearchResults } from '../../../search-results'; import { Initiative } from '../../model/initiative'; -import { compactArray, toString as _toString } from '../../../utils'; +import { toString as _toString } from '../../../utils'; import { SidebarPresenter } from '../sidebar'; export class InitiativesSidebarPresenter extends BaseSidebarPresenter { @@ -11,7 +10,6 @@ export class InitiativesSidebarPresenter extends BaseSidebarPresenter { _eventbusRegister(): void { EventBus.Marker.selectionToggled.sub(initiative => this.onMarkerSelectionToggled(initiative)); - EventBus.Initiative.searchedInitiativeClicked.sub(initiative => this.searchedInitiativeClicked(_toString(initiative.uri, undefined))); } constructor(readonly parent: SidebarPresenter) { @@ -27,54 +25,16 @@ export class InitiativesSidebarPresenter extends BaseSidebarPresenter { changeFilters(propName: string, value?: string) { this.parent.mapui.changeFilters(propName, value); } - - notifyShowInitiativeTooltip(initiative: Initiative) { - EventBus.Map.needToShowInitiativeTooltip.pub(initiative); - } - - notifyHideInitiativeTooltip(initiative: Initiative) { - EventBus.Map.needToHideInitiativeTooltip.pub(initiative); - } - - historyButtonsUsed() { - //console.log("sidebar/initiatives historyButtonsUsed"); - //console.log(lastContent); - //this.notifyMarkersNeedToShowNewSelection(lastContent); - this.view.refresh(); - } - - initClicked(initiative: Initiative) { - EventBus.Directory.initiativeClicked.pub(initiative); - if (window.outerWidth <= 800) { - EventBus.Directory.initiativeClickedHideSidebar.pub(initiative); - } - } - - onInitiativeMouseoverInSidebar(initiative: Initiative) { - this.notifyShowInitiativeTooltip(initiative); - } - onInitiativeMouseoutInSidebar(initiative: Initiative) { - this.notifyHideInitiativeTooltip(initiative); - } - onMarkerSelectionToggled(initiative: Initiative) { this.parent.mapui.toggleSelectInitiative(initiative); - this.view.refresh(); - } - - searchedInitiativeClicked(uri?: string) { - if (uri) this.view.onInitiativeClicked(uri); + this.view.refresh(false); } performSearch(text: string) { this.parent.mapui.performSearch(text); } - resetSearch() { - this.parent.mapui.resetSearch(); - } - changeSearchText(txt: string) { this.view.changeSearchText(txt); } diff --git a/src/map-app/app/state-manager.ts b/src/map-app/app/state-manager.ts index 36480c76..f775efb2 100644 --- a/src/map-app/app/state-manager.ts +++ b/src/map-app/app/state-manager.ts @@ -59,7 +59,7 @@ export class AppState { static applyTextSearch(initiatives: Set, textSearch?: TextSearch): Set { if (!textSearch) return initiatives; - if (textSearch.willMatch()) + if (textSearch.isEmpty()) return initiatives; return filterSet(initiatives, textSearch.predicate); } @@ -120,7 +120,7 @@ export class AppState { } get hasTextSearch(): boolean { - return this.textSearch.willMatch(); + return !this.textSearch.isEmpty(); } addTextSearch(textSearch: TextSearch): AppStateChange|undefined { @@ -138,10 +138,6 @@ export class AppState { ); return new AppStateChange(textSearch, result); } - - clearTextSearch(): AppStateChange|undefined { - return this.addTextSearch(new TextSearch('')); - } addPropEquality(propEq: PropEquality): AppStateChange|undefined { const oldPropFilter = this.propFilters[propEq.propName]; @@ -200,6 +196,17 @@ export class AppState { ); return new AppStateChange(action, result); } + + removePropEqualitiesAndClearTextSearch(): AppStateChange|undefined { + if (Object.keys(this.propFilters).length === 0 && this.textSearch.searchText === '') + return undefined; // No change + + const result = new AppState( + this.allInitiatives, + this.allInitiatives + ); + return new AppStateChange(undefined, result); + } } @@ -233,7 +240,7 @@ export class TextSearch { .trim(); // trim whitespace from front and back } - willMatch() { + isEmpty() { return this.normSearchText === ''; } } @@ -335,10 +342,6 @@ export class StateManager { this.onChange(stateChange); } - clearTextSearch(): void { - return this.textSearch(new TextSearch('')); - } - propFilter(propEq: PropEquality): void { const stateChange = this.stack.current.result.addPropEquality(propEq); if (stateChange === undefined) @@ -363,6 +366,14 @@ export class StateManager { this.onChange(stateChange); } + clearFiltersAndSearch(): void { + const stateChange = this.stack.current.result.removePropEqualitiesAndClearTextSearch(); + if (stateChange === undefined) + return; // No change + this.stack.push(stateChange); + this.onChange(stateChange); + } + altValues(propName: string) { return this.stack.current.result.altValues(propName); } diff --git a/src/map-app/app/view/map.ts b/src/map-app/app/view/map.ts index ee2ad7da..4c4afafd 100644 --- a/src/map-app/app/view/map.ts +++ b/src/map-app/app/view/map.ts @@ -9,7 +9,7 @@ import { EventBus } from "../../eventbus"; import { MarkerManager } from "../marker-manager"; import { PhraseBook } from "../../localisations"; import { Box2d } from "../../common-types"; -import { isFiniteBox2d } from "../../utils"; +import { getViewportWidth, isFiniteBox2d } from "../../utils"; export class MapView extends BaseView { readonly map: Map; @@ -188,7 +188,7 @@ export class MapView extends BaseView { this.map.zoomControl.setPosition("bottomright"); - this.map.on('click', (e) => this.onInitiativeClicked(e)); + this.map.on('click', (e) => this.onMapClicked(e)); this.map.on('load', (e) => this.onLoad(e)); this.map.on('resize', (e) => this.onResize(e)); @@ -221,7 +221,7 @@ export class MapView extends BaseView { } } - private onInitiativeClicked(me: leaflet.LeafletMouseEvent): void { + private onMapClicked(me: leaflet.LeafletMouseEvent): void { // Deselect any selected markers if (me.originalEvent.ctrlKey && me.latlng) { MapView.copyTextToClipboard(me.latlng.lat + "," + me.latlng.lng); @@ -235,7 +235,7 @@ export class MapView extends BaseView { private onResize(_: leaflet.ResizeEvent) { this.map.invalidateSize(); - console.log("Map resize", window.outerWidth); + console.log("Map resize", getViewportWidth()); } fitBounds(data: EventBus.Map.BoundsData) { diff --git a/src/map-app/app/view/map/marker.ts b/src/map-app/app/view/map/marker.ts index c9a4d8c2..7108f7a6 100644 --- a/src/map-app/app/view/map/marker.ts +++ b/src/map-app/app/view/map/marker.ts @@ -119,16 +119,13 @@ export class MapMarkerView extends BaseView { this.presenter.notifySelectionToggled(this.presenter.initiative); } else { console.log(this.presenter.initiative); - // this.presenter.notifySelectionSet(this.initiative); - EventBus.Directory.initiativeClicked.pub(this.presenter.initiative); + EventBus.Map.initiativeClicked.pub(this.presenter.initiative); } } setUnselected(initiative: Initiative) { //close pop-up this.presenter.mapUI.map?.closePopup(); - //close information on the left hand side (for smaller screens) - EventBus.Sidebar.hideInitiative.pub(); //reset the map vars and stop the zoom event from triggering selectInitiative ({target}) method //change the color of an initiative with a location @@ -210,7 +207,7 @@ export class MapMarkerView extends BaseView { let deselectInitiative = (e: leaflet.LeafletEvent) => { if (factory.geoClusterGroup.getVisibleParent(marker) !== marker) { this.setUnselected(initiative); - EventBus.Directory.initiativeClicked.pub(undefined); // deselects + EventBus.Map.initiativeClicked.pub(undefined); // deselects factory.geoClusterGroup.off("animationend", deselectInitiative); } } diff --git a/src/map-app/app/view/sidebar.ts b/src/map-app/app/view/sidebar.ts index ac43ed16..30e5c90a 100644 --- a/src/map-app/app/view/sidebar.ts +++ b/src/map-app/app/view/sidebar.ts @@ -3,6 +3,8 @@ import { EventBus } from '../../eventbus'; import * as d3 from 'd3'; import { SidebarPresenter } from '../presenter/sidebar'; import { BaseView } from './base'; +import { canDisplayInitiativePopups } from '../../utils'; +import { Initiative } from '../model/initiative'; export class SidebarView extends BaseView { @@ -21,7 +23,7 @@ export class SidebarView extends BaseView { .attr("class", "w3-btn") .attr("style","background-color: " + this.sidebarButtonColour) .attr("title", this.presenter.mapui.labels.showDirectory) - .on("click", () => this.showSidebar()) + .on("click", () => this.presenter.showSidebar()) .append("i") .attr("class", "fa fa-angle-right"); } @@ -43,10 +45,9 @@ export class SidebarView extends BaseView { .attr("class", "w3-button w3-border-0 ml-auto") .attr("title", labels.showDirectory) .on("click", () => { - this.presenter.mapui.resetSearch(); - //notify zoom + this.presenter.mapui.clearFiltersAndSearch(); + this.hideInitiativeList(); this.presenter.changeSidebar("directory"); - this.showInitiativeList(); //deselect EventBus.Markers.needToShowLatestSelection.pub([]); @@ -61,10 +62,8 @@ export class SidebarView extends BaseView { .attr("class", "w3-button w3-border-0") .attr("title", labels.showSearch) .on("click", () => { - this.hideInitiativeList(); - //deselect - EventBus.Markers.needToShowLatestSelection.pub([]); this.presenter.changeSidebar("initiatives"); + document.getElementById("search-box")?.focus(); }) .append("i") .attr("class", "fa fa-search"); @@ -111,9 +110,6 @@ export class SidebarView extends BaseView { if (event.propertyName === "transform") { d3.select("#map-app-sidebar-button").on("click", () => this.hideSidebar()); } - //select input textbox if possible - if (document.getElementById("search-box") != null) - document.getElementById("search-box")?.focus(); this.updateSidebarWidth(); }, false @@ -125,8 +121,6 @@ export class SidebarView extends BaseView { d3.select("#map-app-sidebar i").attr("class", "fa fa-angle-left"); this.presenter.changeSidebar(); // Refresh the content of the sidebar - if (document.getElementById("dir-filter") && window.outerWidth >= 1080) - document.getElementById("dir-filter")?.focus(); } updateSidebarWidth() { @@ -194,14 +188,8 @@ export class SidebarView extends BaseView { } //else show it const sidebar = d3.select("#map-app-sidebar"); - const sidebarButton = d3.select("#map-app-sidebar-button"); d3.select(".w3-btn").attr("title", this.presenter.mapui.labels.hideDirectory); - const initiativeListSidebar = d3.select("#sea-initiatives-list-sidebar"); - if (!initiativeListSidebar.empty() && !sidebarButton.empty()) - initiativeListSidebar.insert(() => sidebarButton.node(), // moves sidebarButton - "#sea-initiatives-list-sidebar-content"); - sidebar .on( "transitionend", @@ -219,9 +207,7 @@ export class SidebarView extends BaseView { hideInitiativeList() { const sidebar = d3.select("#map-app-sidebar"); - const sidebarButton = d3.select("#map-app-sidebar-button"); - sidebar.insert(() => sidebarButton.node(), // moves sidebarButton - "#sea-initiatives-list-sidebar"); + sidebar.on( "transitionend", (event: TransitionEvent) => { @@ -236,5 +222,26 @@ export class SidebarView extends BaseView { .classed("sea-sidebar-list-initiatives", false); d3.select(".w3-btn").attr("title", this.presenter.mapui.labels.hideDirectory); - } + } + + populateInitiativeSidebar(initiative: Initiative, initiativeContent: string) { + let initiativeSidebar = d3.select("#sea-initiative-sidebar"); + let initiativeContentElement = this.d3selectAndClear( + "#sea-initiative-sidebar-content" + ); + initiativeContentElement + .append("button") + .attr("class", "w3-button w3-border-0 ml-auto sidebar-button") + .attr("title", `${this.presenter.mapui.labels.close} ${initiative.name}`) + .on("click", () => EventBus.Map.initiativeClicked.pub(undefined)) + .append("i") + .attr("class", "fa " + "fa-times"); + initiativeContentElement + .append("div") + .html(initiativeContent); + initiativeSidebar.classed("sea-initiative-sidebar-open", true); + + if (!canDisplayInitiativePopups()) + EventBus.Sidebar.showSidebar.pub(); + } } diff --git a/src/map-app/app/view/sidebar/base.ts b/src/map-app/app/view/sidebar/base.ts index f8bcf8e9..d9dcbf9f 100644 --- a/src/map-app/app/view/sidebar/base.ts +++ b/src/map-app/app/view/sidebar/base.ts @@ -1,7 +1,9 @@ // Set up the various sidebars import { BaseSidebarPresenter, NavigationCallback } from '../../presenter/sidebar/base'; import { BaseView } from '../base'; -import { d3Selection } from '../d3-utils'; +import { d3DivSelection, d3Selection } from '../d3-utils'; +import { toString as _toString } from '../../../utils'; +import { EventBus } from "../../../eventbus"; /// Base class of all sidebar views export abstract class BaseSidebarView extends BaseView { @@ -11,8 +13,6 @@ export abstract class BaseSidebarView extends BaseView { static readonly accordionClasses = "w3-bar-item w3-tiny w3-light-grey w3-padding-small" + BaseSidebarView.hoverColour; static readonly sectionClasses = "w3-bar-item w3-small w3-white w3-padding-small"; - - abstract readonly presenter: BaseSidebarPresenter; @@ -83,12 +83,103 @@ export abstract class BaseSidebarView extends BaseView { } } - refresh() { + /** + * Refreshes the sidebar view + * + * @param changed true if we changed to this sidebar, false if it was already showing and we're + * just refreshing it. + */ + refresh(changed: boolean) { this.loadFixedSection(); this.loadHistoryNavigation(); // back and forward buttons this.loadScrollableSection(); + this.refreshSearchResults(); } -} + /** + * Display a list of all initiatives that match the current filters, in a separate pane to the + * right of the sidebar or, if on mobile, in the sidebar. + */ + refreshSearchResults() { + const appState = this.presenter.parent.mapui.currentItem(); + const labels = this.presenter.parent.mapui.labels; + const initiatives = Array.from(appState.visibleInitiatives); + + const selection = this.d3selectAndClear("#sea-initiatives-list-sidebar-content"); + const initiativesListSidebarHeader = selection.append("div").attr("class", "initiatives-list-sidebar-header"); + + this.createInitiativeListSidebarHeader(initiativesListSidebarHeader) + + selection + .append("p") + .classed("filter-count", true) + .text(initiatives.length ? `${initiatives.length} ${labels.matchingResults}`: labels.nothingMatched); + + const list = selection.append("ul").classed("sea-initiatives-list", true); + for (let initiative of initiatives) { + let activeClass = ""; + let nongeoClass = ""; + if (this.presenter.parent.mapui.isSelected(initiative)) { + activeClass = "sea-initiative-active"; + } + + if (!initiative.hasLocation()) { + nongeoClass = "sea-initiative-non-geo"; + } + + list + .append("li") + .text(_toString(initiative.name)) + .attr("data-uid", _toString(initiative.uri)) + .classed(activeClass, true) + .classed(nongeoClass, true) + .on("click", ()=> this.presenter.onInitiativeClicked(initiative)) + .on("mouseover", () => this.presenter.onInitiativeMouseoverInSidebar(initiative)) + .on("mouseout", () => this.presenter.onInitiativeMouseoutInSidebar(initiative)); + } + } + private createInitiativeListSidebarHeader(container: d3DivSelection) { + const labels = this.presenter.parent.mapui.labels; + if (this.presenter.parent.showingDirectory()) { + container + .append("button") + // mobile only since these buttons already exist in the sidebar on larger screens + .attr("class", "w3-button w3-border-0 mobile-only") + .attr("title", labels.showDirectory) + .on("click", function () { + EventBus.Map.clearFiltersAndSearch.pub(); + EventBus.Sidebar.hideInitiativeList.pub(); + EventBus.Sidebar.showDirectory.pub(); + EventBus.Markers.needToShowLatestSelection.pub([]); + }) + .append("i") + .attr("class", "fa fa-bars"); + } + + if (this.presenter.parent.showingSearch()) { + container + .append("button") + .attr("class", "w3-button w3-border-0 mobile-only") + .attr("title", labels.showSearch) + .on("click", function () { + EventBus.Sidebar.hideInitiativeList.pub(); + EventBus.Sidebar.showInitiatives.pub(); + document.getElementById("search-box")?.focus(); + }) + .append("i") + .attr("class", "fa fa-search"); + } + + container + .append("p") + .attr("class", "ml-auto clear-filters-button") + .text(labels.clearFilters) + .on("click", () => { + EventBus.Map.clearFiltersAndSearch.pub(); + EventBus.Sidebar.hideInitiativeList.pub(); + EventBus.Markers.needToShowLatestSelection.pub([]); + }) + } +} diff --git a/src/map-app/app/view/sidebar/directory.ts b/src/map-app/app/view/sidebar/directory.ts index 7fa36d3d..d17a7d61 100644 --- a/src/map-app/app/view/sidebar/directory.ts +++ b/src/map-app/app/view/sidebar/directory.ts @@ -6,7 +6,6 @@ import { Initiative } from "../../model/initiative"; import { DirectorySidebarPresenter } from "../../presenter/sidebar/directory"; import { d3Selection } from "../d3-utils"; import { BaseSidebarView } from "./base"; -import { toString as _toString } from "../../../utils"; import { propDefToVocabUri } from "../../model/data-services"; function uriToTag(uri: string) { @@ -28,9 +27,18 @@ export class DirectorySidebarView extends BaseSidebarView { this.title = presenter.parent.mapui.labels.directory; } + refresh(changed: boolean) { + this.loadHistoryNavigation(); + if (changed) { + // only need to load these if we changed to the directory sidebar + this.loadFixedSection(); + this.loadScrollableSection(); + } + this.refreshSearchResults(); + } - populateFixedSelection(selection: d3Selection): void { + populateFixedSelection(selection: d3Selection): void { const that = this; let sidebarTitle = this.title; const container = selection @@ -68,11 +76,11 @@ export class DirectorySidebarView extends BaseSidebarView { d3.selectAll("li.sea-directory-field").attr("hidden", null); else { const a = d3.selectAll("li.sea-directory-field"); - a.attr("hidden", null); // Hide everything + a.attr("hidden", true); // Hide everything a.filter(function () { // Unhide elements which have matching text return d3.select(this).text().toLowerCase().includes(input); - }).attr("hidden", true); + }).attr("hidden", null); //appear and set dissapear after seconds //cancel last dissapear if new one is coming in d3.select("#dir-filter") @@ -88,9 +96,7 @@ export class DirectorySidebarView extends BaseSidebarView { // dissapear in 1 sec this.dissapear = window.setTimeout(dissapear, 1000); - } - } populateScrollableSelection(selection: d3Selection) { @@ -134,8 +140,9 @@ export class DirectorySidebarView extends BaseSidebarView { .classed(classname, true) .classed("sea-directory-field", true) .on("click", (event: MouseEvent) => { - this.presenter.parent.mapui.resetSearch(); - this.listInitiativesForSelection(propName, propValue); // key may be null + this.presenter.parent.mapui.clearFiltersAndSearch(); + this.presenter.parent.mapui.changeFilters(propName, propValue); + EventBus.Sidebar.showInitiativeList.pub(); this.resetFilterSearch(); d3.select(".sea-field-active").classed("sea-field-active", false); if (event.currentTarget instanceof Element) @@ -166,227 +173,4 @@ export class DirectorySidebarView extends BaseSidebarView { if (values) addItems(directoryPropName, values); } - - - // selectionKey may be null, for the special 'Every item' case - listInitiativesForSelection(propName: string, propValue?: string) { - const labels = this.presenter.parent.mapui.labels; - const vocabs = this.presenter.parent.mapui.dataServices.getVocabs(); - const props = this.presenter.parent.mapui.config.fields(); - const lang = this.presenter.parent.mapui.config.getLanguage(); - const initiatives = this.presenter.getInitiativesForFieldAndSelectionKey( - propName, - propValue - ); - - //deselect all - this.presenter.clearLatestSelection(); - - this.presenter.notifyMapNeedsToNeedsToBeZoomedAndPanned(initiatives); - - const sidebar = d3.select("#map-app-sidebar"); - const sidebarButton = d3.select("#map-app-sidebar-button"); - d3.select(".w3-btn").attr("title", labels.hideDirectory); - const initiativeListSidebar = d3.select("#sea-initiatives-list-sidebar"); - const selection = this.d3selectAndClear("#sea-initiatives-list-sidebar-content"); - - // Add the heading (we need to determine the label as this may be stored in the data or - // in the list of values in the presenter) - let label = ''; // will be updated - let classname = 'sea-field-all'; // default - - if (propValue === undefined) { - // The "all" case. - label = this.presenter.getAllEntriesLabel(propName); - // classname remains at default - } - else { - const propDef = props[propName]; - const propUri = propDefToVocabUri(propDef); - - if (vocabs !== undefined && propUri) { - // This is a vocab field. - label = vocabs.getTerm(propValue, lang); - classname = `sea-field-${uriToTag(propValue)}`; - } else { - // The value comes from the data directly - // label from propValue - label = propValue; - classname = `sea-field-${labelToTag(propValue)}`; - } - } - - if (!initiativeListSidebar.empty() && !sidebarButton.empty()) { - initiativeListSidebar.insert(() => sidebarButton.node(), - "#sea-initiatives-list-sidebar-content"); - const seaFieldClasses = initiativeListSidebar.attr("class") - .split(/\s+/).filter(c => c.match(/^sea-field-/)); - initiativeListSidebar.classed(seaFieldClasses.join(' '), false); - initiativeListSidebar.classed(classname, true); - } - - this.presenter.parent.mapui.changeFilters(propName, propValue); - - //setup sidebar buttons in initiative list - const sidebarBtnHolder = selection.append("div").attr("class", "initiative-list-sidebar-btn-wrapper"); - - - sidebarBtnHolder - .append("button") - .attr("class", "w3-button w3-border-0 initiative-list-sidebar-btn") - .attr("title", labels.showSearch) - .on("click", function () { - - EventBus.Sidebar.hideInitiativeList.pub(); - EventBus.Markers.needToShowLatestSelection.pub([]); - EventBus.Sidebar.showInitiatives.pub(); - }) - .append("i") - .attr("class", "fa fa-search"); - - - - sidebarBtnHolder - .append("button") - .attr("class", "w3-button w3-border-0") - .attr("title", labels.showInfo) - .on("click", function () { - EventBus.Sidebar.hideInitiativeList.pub(); - EventBus.Markers.needToShowLatestSelection.pub([]); - EventBus.Sidebar.showAbout.pub(); - }) - .append("i") - .attr("class", "fa fa-info-circle"); - - - - if (true) { - sidebarBtnHolder - .append("button") - .attr("class", "w3-button w3-border-0") - .attr("title", labels.showDatasets) - .on("click", function () { - EventBus.Sidebar.hideInitiativeList.pub(); - EventBus.Markers.needToShowLatestSelection.pub([]); - EventBus.Sidebar.showDatasets.pub(); - }) - .append("i") - .attr("class", "fa fa-database"); - } - - - - sidebarBtnHolder - .append("button") - .attr("class", "w3-button w3-border-0 ml-auto sidebar-button") - .attr("title", labels.close + label) - .on("click", () => { - this.presenter.removeFilters(propName); - }) - .append("i") - .attr("class", "fa " + "fa-times"); - - - - selection - .append("button") - .attr("class", "w3-button w3-border-0 ml-auto sidebar-button sidebar-normal-size-close-btn") - .attr("title", labels.close + label) - .on("click", () => { - this.presenter.removeFilters(propName); - }) - .append("i") - .attr("class", "fa " + "fa-times"); - - selection - .append("h2") - .classed("sea-field", true) - .text(label) - .on("click", () => { - this.presenter.notifyMapNeedsToNeedsToBeZoomedAndPanned(initiatives); - }); - const list = selection.append("ul").classed("sea-initiative-list", true); - for (let initiative of initiatives) { - let activeClass = ""; - let nongeoClass = ""; - if (this.presenter.parent.mapui.isSelected(initiative)) { - activeClass = "sea-initiative-active"; - } - - if (!initiative.hasLocation()) { - nongeoClass = "sea-initiative-non-geo"; - } - - list - .append("li") - .text(_toString(initiative.name)) - .attr("data-uid", _toString(initiative.uri)) - .classed(activeClass, true) - .classed(nongeoClass, true) - .on("click", function () { - EventBus.Directory.initiativeClicked.pub(initiative); - }) - .on("mouseover", () => { - this.presenter.onInitiativeMouseoverInSidebar(initiative); - }) - .on("mouseout", () => { - this.presenter.onInitiativeMouseoutInSidebar(initiative); - }); - - - } - sidebar - .on( - "transitionend", - (event: TransitionEvent) => { - const target = event.target as HTMLElement | null | undefined; // Need some coercion here - if (target?.className === "w3-btn") - return; - if (event.propertyName === "transform") - this.presenter.parent.view.updateSidebarWidth(); - }, - false - ) - .classed("sea-sidebar-list-initiatives", true); - } - - populateInitiativeSidebar(initiative: Initiative, initiativeContent: string) { - // Highlight the correct initiative in the directory - d3.select(".sea-initiative-active").classed("sea-initiative-active", false); - d3.select('[data-uid="' + initiative.uri + '"]').classed( - "sea-initiative-active", - true - ); - let initiativeSidebar = d3.select("#sea-initiative-sidebar"); - let initiativeContentElement = this.d3selectAndClear( - "#sea-initiative-sidebar-content" - ); - initiativeContentElement - .append("button") - .attr("class", "w3-button w3-border-0 ml-auto sidebar-button") - .attr("title", `${this.presenter.parent.mapui.labels.close} ${initiative.name}`) - .on("click", function () { - EventBus.Directory.initiativeClicked.pub(undefined); - }) - .append("i") - .attr("class", "fa " + "fa-times"); - initiativeContentElement - .append("div") - .html(initiativeContent); - initiativeSidebar.classed("sea-initiative-sidebar-open", true); - // if (document.getElementById("map-app-leaflet-map").clientWidth < 800) - if (window.outerWidth < 800) - EventBus.Sidebar.showSidebar.pub(); - } - - deselectInitiativeSidebar() { - d3.select(".sea-initiative-active").classed("sea-initiative-active", false); - let initiativeSidebar = d3.select("#sea-initiative-sidebar"); - // let initiativeContentElement = d3.select("#sea-initiative-sidebar-content"); - // initiativeContentElement.html(initiativeContent); - initiativeSidebar.classed("sea-initiative-sidebar-open", false); - d3.select(".sea-search-initiative-active") - .classed("sea-search-initiative-active", false); - } - } diff --git a/src/map-app/app/view/sidebar/initiatives.ts b/src/map-app/app/view/sidebar/initiatives.ts index 69a44237..5bf2abdc 100644 --- a/src/map-app/app/view/sidebar/initiatives.ts +++ b/src/map-app/app/view/sidebar/initiatives.ts @@ -3,8 +3,7 @@ import * as d3 from 'd3'; import { BaseSidebarView } from './base'; import { InitiativesSidebarPresenter } from '../../presenter/sidebar/initiatives'; import type { d3Selection, d3DivSelection } from '../d3-utils'; -import { Initiative } from '../../model/initiative'; -import { toString as _toString } from '../../../utils'; +import { EventBus } from '../../../eventbus'; import { SentryValues } from '../base'; import { propDefToVocabUri } from '../../model/data-services'; @@ -29,23 +28,16 @@ export class InitiativesSidebarView extends BaseSidebarView { this.changeSearchText(this.presenter.parent.mapui.getSearchText()); + this.createFilterCount(container); + //advanced search const advancedSearchContainer = container .append("div") this.createAdvancedSearch(advancedSearchContainer); - } - onInitiativeClicked(id: string) { - d3.select(".sea-search-initiative-active") - .classed("sea-search-initiative-active", false); - - d3.select('[data-uid="' + id + '"]') - .classed( - "sea-search-initiative-active", - true - ); + this.createApplyFiltersButton(container); } getSearchText(): string { @@ -56,25 +48,32 @@ export class InitiativesSidebarView extends BaseSidebarView { d3.select("#search-box").property("value", txt); } - createSearchBox(selection: d3DivSelection) { - + createSearchBox(container: d3DivSelection) { const submitCallback = (event: Event) => { event.preventDefault(); // prevent page reloads on submit - const searchText = this.getSearchText() - this.presenter.performSearch(searchText); + + const oldSearchText = this.presenter.parent.mapui.getSearchText() + const newSearchText = this.getSearchText(); + + if (newSearchText !== oldSearchText) { + this.presenter.performSearch(newSearchText); + } }; - const selection2 = selection + const form = container .append("form") .attr("id", "map-app-search-form") .attr( "class", "w3-card-2 w3-round map-app-search-form" ) - .on("submit", submitCallback) + .on("submit", submitCallback); + + const selection = form .append("div") .attr("class", "w3-border-0"); - selection2 + + selection .append("div") .attr("class", "w3-col") .attr("title", this.presenter.parent.mapui.labels.clickToSearch) @@ -84,7 +83,8 @@ export class InitiativesSidebarView extends BaseSidebarView { .attr("class", "w3-btn w3-border-0") .append("i") .attr("class", "w3-xlarge fa fa-search"); - selection2 + + selection .append("div") .attr("class", "w3-rest") .append("input") @@ -93,12 +93,32 @@ export class InitiativesSidebarView extends BaseSidebarView { .attr("type", "search") .attr("placeholder", this.presenter.parent.mapui.labels.searchInitiatives) .attr("autocomplete", "off") - // If we don't submit the search on blur, selecting another filter will reset the search text - // https://github.com/digitalcommons/mykomap/issues/197 + // If we don't submit the search on blur, selecting another filter will reset the search text + // https://github.com/digitalcommons/mykomap/issues/197 .on("blur", submitCallback); + } + private createFilterCount(container: d3DivSelection) { + const appState = this.presenter.parent.mapui.currentItem(); + const labels = this.presenter.parent.mapui.labels; + + const count = appState.visibleInitiatives.size; + const filterApplied = appState.hasPropFilters || appState.hasTextSearch; - document.getElementById("search-box")?.focus(); + const selection = container + .append("div") + .attr("class", "map-app-search-filter-count") + .append("p"); + + selection + .append("span") + .attr("class", "filter-count-value") + .text(count); + + selection + .append("span") + .attr("class", "filter-count-descriptor") + .text(` ${filterApplied ? labels.matchingResults : labels.directoryEntries}`); } // used in the dropdown to change the filter @@ -205,60 +225,20 @@ export class InitiativesSidebarView extends BaseSidebarView { } } - populateScrollableSelection(selection: d3Selection) { + private createApplyFiltersButton(container: d3DivSelection) { const labels = this.presenter.parent.mapui.labels; - const appState = this.presenter.parent.mapui.currentItem(); - // add clear button - selection + // add apply filters button on mobile + container .append("div") - .attr("class", "w3-container w3-center sidebar-button-container") - .attr("id", "clearSearchFilterBtn") + .attr("class", "w3-container w3-center mobile-only apply-filters-button-container") .append("button") - .attr("class", "w3-button w3-black") - .property("disabled", this.presenter.isBackButtonDisabled() || undefined) - .text(labels.clearFilters) - .on("click", () => { - this.presenter.resetSearch(); - }); - - switch (appState.visibleInitiatives.size) { - case 0: // No matches - selection - .append("div") - .attr("class", "w3-container w3-center") - .append("p") - .text(labels.nothingMatched); - break; - - default: // Matches - this.populateSelectionWithListOfInitiatives( - selection, appState.visibleInitiatives - ); - } + .attr("class", "w3-button w3-round w3-border-dark-grey apply-filters-button") + .text(labels.applyFilters.toLocaleUpperCase()) + .on("click", () => EventBus.Sidebar.showInitiativeList.pub()); } - private populateSelectionWithListOfInitiatives( - selection: d3Selection, - initiatives: Set - ) { - const initiativeClass = "w3-bar-item w3-button w3-mobile srch-initiative"; - initiatives.forEach((initiative) => { - const uri = _toString(initiative.uri, null); - if (!uri) { - console.error("initiative with no uri field! ignoring...", initiative); - return; - } - selection - .append("button") - .attr("class", initiativeClass) - .classed("sea-initiative-non-geo", !initiative.hasLocation()) - .attr("data-uid", uri) - .attr("title", this.presenter.parent.mapui.labels.clickForDetailsHereAndMap) - .on("click", () => this.presenter.initClicked(initiative)) - .on("mouseover", () => this.presenter.onInitiativeMouseoverInSidebar(initiative) ) - .on("mouseout", () => this.presenter.onInitiativeMouseoutInSidebar(initiative) ) - .text(_toString(initiative.name)); - }); + populateScrollableSelection(selection: d3Selection) { + // No scrollable section } } diff --git a/src/map-app/eventbus.ts b/src/map-app/eventbus.ts index 76ddb73d..66f3ba66 100644 --- a/src/map-app/eventbus.ts +++ b/src/map-app/eventbus.ts @@ -18,8 +18,6 @@ export namespace EventBus { //export const filterDataset = new PostalEvent("Datasets.filterDataset"); } export namespace Directory { - export const initiativeClicked = new PostalTopic("Directory.initiativeClicked"); // deselected if undefined - export const initiativeClickedHideSidebar = new PostalTopic("Directory.InitiativeClickedSidebar.hideSidebar"); } export namespace Initiatives { export interface DatasetError { error: Error; dataset?: string; } @@ -32,8 +30,6 @@ export namespace EventBus { export namespace Initiative { export const created = new PostalTopic("Initiative.created"); export const refreshed = new PostalTopic("Initiative.refreshed"); - export const searchedInitiativeClicked = new PostalTopic("Initiative.searchedInitiativeClicked"); - //export const selected = new PostalTopic("Initiative.selected"); } export namespace Map { export interface ZoomOptions { @@ -77,7 +73,9 @@ export namespace EventBus { export const needToShowInitiativeTooltip = new PostalTopic("Map.needToShowInitiativeTooltip"); export const needsToBeZoomedAndPanned = new PostalTopic("Map.needsToBeZoomedAndPanned"); export const selectAndZoomOnInitiative = new PostalTopic("Map.selectAndZoomOnInitiative"); + export const initiativeClicked = new PostalTopic("Map.initiativeClicked"); export const setActiveArea = new PostalTopic("Map.setActiveArea"); + export const clearFiltersAndSearch = new PostalTopic("Map.clearFiltersAndSearch"); } export namespace Marker { export const selectionToggled = new PostalTopic("Marker.SelectionToggled"); @@ -87,7 +85,7 @@ export namespace EventBus { export const needToShowLatestSelection = new PostalTopic("Markers.needToShowLatestSelection"); } export namespace Sidebar { - export const hideInitiative = new PostalTopic("Sidebar.hideInitiative"); + export const showInitiativeList = new PostalTopic("Sidebar.showInitiativeList"); export const hideInitiativeList = new PostalTopic("Sidebar.hideInitiativeList"); export const hideInitiativeSidebar = new PostalTopic("Sidebar.hideInitiativeSidebar"); export const hideSidebar = new PostalTopic("Sidebar.hideSidebar"); diff --git a/src/map-app/index.ts b/src/map-app/index.ts index 0806f324..b87b89e3 100644 --- a/src/map-app/index.ts +++ b/src/map-app/index.ts @@ -122,43 +122,52 @@ export function webRun(window: Window, base_config: ConfigData): void { mapApp.innerHTML = `
+
+
+ +
+ +
+
- +
+
+
+
+
+ +
-
- -
-
-
+
`; const attrs = parseAttributes(mapApp, base_config.attr_namespace || ''); diff --git a/src/map-app/localisations.ts b/src/map-app/localisations.ts index d1956a82..0f1a2675 100644 --- a/src/map-app/localisations.ts +++ b/src/map-app/localisations.ts @@ -40,6 +40,7 @@ export interface PhraseBook { allEntries: string; and: string; any: string; + applyFilters: string; clearFilters: string; clickToSearch: string; clickForDetailsHereAndMap: string; @@ -49,11 +50,13 @@ export interface PhraseBook { countries: string; datasets: string; directory: string; + directoryEntries: string; errorLoading: string; hideDirectory: string; in: string; loading: string; mapDisclaimer: string; + matchingResults: string; mixedSources: string; noLocation: string; notAvailable: string; @@ -87,20 +90,23 @@ export const phraseBooks: PhraseBooks = { allEntries: "All Entries", and: "AND", any: "Any", + applyFilters: "Apply filters", + clearFilters: "Clear filters", clickToSearch: "Click to search", clickForDetailsHereAndMap: "Click to see details here and on map", - clearFilters: "Clear Filters", close: "Close ", contact: "Contact", contributers: "contributers", countries: "Countries", datasets: "Datasets", directory: "Directory", + directoryEntries: "directory entries", errorLoading: "Error loading", hideDirectory: "Hide directory", in: "in", loading: "Loading", mapDisclaimer: "This map contains indications of areas where there are disputes over territories. The ICA does not endorse or accept the boundaries depicted on the map.", + matchingResults: "matching results", mixedSources: "Mixed Sources", noLocation: "No location available", notAvailable: "N / A", @@ -129,6 +135,7 @@ export const phraseBooks: PhraseBooks = { allEntries: "Toutes les entrées", and: "ET", any: "Tout afficher", + applyFilters: "Appliquer les filtres", clearFilters: "Réinitialiser les filtres", clickToSearch: "Cliquez pour rechercher", clickForDetailsHereAndMap: "Cliquez pour voir les détails ici et sur la carte", @@ -138,11 +145,13 @@ export const phraseBooks: PhraseBooks = { countries: "Des pays", datasets: "Ensembles de données", directory: "Annuaire", + directoryEntries: "entrées d'annuaire", errorLoading: "FIXME", hideDirectory: "Masquer l’annuaire", in: "dans", loading: "Chargement des données", mapDisclaimer: "Cette carte contient des indications sur les zones où il y a des différends au sujet territorial. L'ACI n'approuve ni n'accepte les frontières représentées sur la carte.", + matchingResults: "résultats correspondants", mixedSources: "Sources mixtes", noLocation: "Aucun emplacement disponible", notAvailable: "Pas disponible", @@ -171,6 +180,7 @@ export const phraseBooks: PhraseBooks = { allEntries: "Todas las entradas", and: "Y", any: "Qualquiera", + applyFilters: "Aplicar filtros", clearFilters: "Borrar filtros", clickToSearch: "Haga clic para buscar", clickForDetailsHereAndMap: "Haga clic para ver detalles aquí y en el mapa", @@ -180,11 +190,13 @@ export const phraseBooks: PhraseBooks = { countries: "Países", datasets: "Conjuntos de datos", directory: "Directorio", + directoryEntries: "entradas de directorio", errorLoading: "FIXME", hideDirectory: "Ocultar directorio", in: "en", loading: "Cargando…", mapDisclaimer: "Este mapa contiene indicaciones de zonas donde hay disputas territoriales. La ACI no respalda ni acepta las fronteras representadas en el mapa.", + matchingResults: "resultados coincidentes", mixedSources: "Fuentes mixtas", noLocation: "No hay ubicación disponible", notAvailable: "No disponible", @@ -213,6 +225,7 @@ export const phraseBooks: PhraseBooks = { allEntries: "모든 항목들", and: "그리고", any: "아무거나", + applyFilters: "필터 적용", clearFilters: "필터 지우기", clickToSearch: "클릭하여 검색", clickForDetailsHereAndMap: "여기와 지도에서 세부정보를 보려면 클릭하세요.", @@ -222,11 +235,13 @@ export const phraseBooks: PhraseBooks = { countries: "국가", datasets: "데자료", directory: "디렉토리", + directoryEntries: "디렉토리 항목", errorLoading: "FIXME", hideDirectory: "디렉토리 숨기기", in: "에", loading: "로딩", mapDisclaimer: "이 지도에는 영토에 대한 분쟁이 있는 지역의 표시가 포함되어 있습니다. ICA는 지도에 표시된 경계를 승인하거나 수락하지 않습니다.", + matchingResults: "일치하는 결과", mixedSources: "혼합", noLocation: "사용 가능한 위치 없음", notAvailable: "사용 불가", diff --git a/src/map-app/styles/style.css b/src/map-app/styles/style.css index 54b0402d..1ec5ebc9 100644 --- a/src/map-app/styles/style.css +++ b/src/map-app/styles/style.css @@ -127,10 +127,8 @@ body { } /* All */ -.sea-initiatives-list-sidebar.sea-field-all, -.sea-field-all .sea-initiatives-list-sidebar-content, -.sea-field-all:hover, -.sea-field-all.sea-field-active { +.sea-initiatives-list-sidebar, +.sea-initiatives-list-sidebar-content { background: var(--dark) !important; } @@ -163,8 +161,15 @@ body { color: #ccc; } -.sidebar-button-container{ - margin-top: 5px; +.apply-filters-button-container{ + margin-top: 28px; +} + +.apply-filters-button{ + border: solid; + border-width: 2px; + font-weight: 450; + font-size: 0.9rem; } /* Arts, Media, Culture & Leisure. */ @@ -376,7 +381,7 @@ body { transform: translateX(-100%); transition: transform 0.16s cubic-bezier(0.215, 0.61, 0.355, 1); font-family: var(--sea-font-family-sans-serif); - font-weight: 300; + font-weight: 250; color: var(--dark); background: white; } @@ -407,7 +412,7 @@ body { display: flex; flex-direction: column; padding: 0 0 1.4em; - font-weight: 400; + font-weight: 300; } .sea-initiatives-list-sidebar-content { height: 100%; @@ -433,10 +438,6 @@ body { transform: translateX(-100%); } -.sea-sidebar-open.sea-sidebar-list-initiatives .sea-initiatives-list-sidebar { - transform: translateX(var(--sidebar-width)); -} - .sea-initiatives-list-sidebar { right: 0; background-color: var(--dark); @@ -458,11 +459,12 @@ body { font-size: 1.4rem; } -.sea-sidebar h2.sea-field { - padding: 0.5rem 1rem 0.7rem; +.sea-sidebar p.filter-count { + padding: 0.5rem 1rem 0.7rem 1.25rem; margin: 0; - line-height: 1.3; font-size: 1.3rem; + font-weight: 500; + cursor: default; } .sea-sidebar h3 { @@ -471,9 +473,6 @@ body { } /* Map */ -.map-app-display-container { - z-index: 30; -} div.map-app-map-container { overflow: hidden; @@ -492,6 +491,10 @@ div.map-app-map-container { margin-top: 1em; } +.sea-sidebar-open.sea-sidebar-list-initiatives .map-app-sidebar-button { + transform: translateX(var(--sidebar-width)); +} + .map-app-sidebar-button button { padding: 0 0.45em; font-size: 2.4em; @@ -501,9 +504,12 @@ div.map-app-map-container { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } +.map-app-sidebar-button button:hover { + transform: translateX(0px); +} + .w3-btn:hover { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - transform: translateX(0px); } .map-app-sidebar-header { @@ -549,21 +555,21 @@ input[type="search"] { } .sea-directory-list, -.sea-initiative-list { +.sea-initiatives-list { list-style: none; font-size: 1.05em; padding: 0; margin: 0 0 1.6em; } -.sea-initiative-list { +.sea-initiatives-list { margin: 0; overflow-y: scroll; -webkit-overflow-scrolling: touch; } .sea-directory-list li, -.sea-initiative-list li { +.sea-initiatives-list li { margin: 0; padding: 0.4rem 1rem 0.4rem 1.25rem; user-select: none; @@ -574,10 +580,9 @@ input[type="search"] { color: white; background-color: var(--dark); cursor: pointer; - /* font-weight: 400; */ } -.sea-initiative-list li:hover, +.sea-initiatives-list li:hover, .sea-sidebar h2.sea-field:hover { color: var(--dark); background-color: white; @@ -589,11 +594,6 @@ input[type="search"] { background-color: white; } -.sea-search-initiative-active { - color: white; - background-color: var(--dark); -} - .ml-auto { margin-left: auto; } @@ -611,6 +611,15 @@ input[type="search"] { width: 100%; } +.map-app-search-filter-count p { + font-size: 1.2rem; + color: var(--dark); +} + +.map-app-search-filter-count .filter-count-value { + color: var(--maroon); +} + .sea-sidebar, .w3-modal { z-index: 40; @@ -707,7 +716,7 @@ input[type="search"] { .sea-initiative-popup p, .sea-initiative-sidebar-content p { - font-weight: 300; + font-weight: 250; font-size: 0.9rem; margin: 0.5rem 0; } @@ -723,28 +732,38 @@ input[type="search"] { margin-top: 0.7rem; } -@media (max-device-width: 1080px) { - .sea-initiatives-list-sidebar { +@media (min-width:1081px) { + .mobile-only { display:none!important } +} + +.sea-sidebar-open.sea-sidebar-list-initiatives .sea-initiatives-list-sidebar { + transform: translateX(var(--sidebar-width)); +} + +@media (max-width: 1080px) { + .sea-sidebar-open.sea-sidebar-list-initiatives { + /* on smaller screen, shift the main portion of the sidebar off the screen */ transform: translateX(-100%); z-index: 100; } - .sea-sidebar-list-initiatives .sea-initiatives-list-sidebar { - transform: translateX(0%); - } - - .sea-sidebar-open.sea-sidebar-list-initiatives .sea-initiatives-list-sidebar { - transform: translateX(0); + .sea-initiatives-list-sidebar { + transform: translateX(-100%); + z-index: 100; } } -@media (max-device-width: 800px) { +@media (max-width: 800px) { .sea-initiative-popup { display: none; } .sea-initiative-sidebar-open { - transform: translateX(0); + transform: translateX(0%); + } + + .sea-sidebar-open.sea-sidebar-list-initiatives .sea-initiative-sidebar-open { + transform: translateX(100%); } } @@ -830,21 +849,24 @@ input[type="search"] { text-align: center; } -.initiative-list-sidebar-btn-wrapper { - display: none; +.initiatives-list-sidebar-header { + display: flex; + justify-content: flex-start; + font-size: 1.4rem; + flex: 0 0 auto; + border-bottom: 1.5px solid var(--dark-gray); + margin-bottom: 0.7rem; } -@media (max-device-width: 1080px) { - .initiative-list-sidebar-btn-wrapper { - display: flex; - justify-content: flex-start; - font-size: 1.4em; - flex: 0 0 auto; - } +.clear-filters-button { + font-size: 1rem; + text-decoration: underline; + margin-right: 1.2rem; + cursor: pointer; +} - .sidebar-normal-size-close-btn { - display: none; - } +.clear-filters-button:active { + color: var(--gray); } #logo-holder { @@ -856,7 +878,7 @@ input[type="search"] { } /* Shrink the logo on small displays to avoit it overflowing */ -@media (max-device-width: 768px) { +@media (max-width: 768px) { #logo-holder { height: 2.5rem; } diff --git a/src/map-app/undo-stack.ts b/src/map-app/undo-stack.ts index 2d9b73dd..a9370a98 100644 --- a/src/map-app/undo-stack.ts +++ b/src/map-app/undo-stack.ts @@ -39,7 +39,10 @@ export class WalkableStack1 { assert(this.storage.length > this.index, "storage should always have the initial state element"); this.index += 1; - this.storage[this.index] = obj; // sets storage.length to index+1 + this.storage[this.index] = obj; + + // remove later items from the stack + this.storage.length = this.index + 1; } // Get the current item in the stack diff --git a/src/map-app/utils.ts b/src/map-app/utils.ts index 9d4d6707..544a5c79 100644 --- a/src/map-app/utils.ts +++ b/src/map-app/utils.ts @@ -251,3 +251,23 @@ export function filterSet(set: Set, predicate: Predicate): Set { return result; } +/** + * This matches with the value of `@media (width)` in the css on all browsers. + */ +export const getViewportWidth = () => + Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); + +/** + * We can display an expanded sidebar if the viewport is wide enough (over 1080 pixels), otherwise + * we collapse to a single sidebar. + */ +export const canDisplayExpandedSidebar = () => + getViewportWidth() > 1080; + + + /** + * We can display initiative popups if the viewport is wide enough (over 800 pixels), otherwise we + * display the info in the sidebar. + */ +export const canDisplayInitiativePopups = () => + getViewportWidth() > 800;