diff --git a/README.md b/README.md
index fa6ccbdda..ebbfeee29 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Head Start is a web-based knowledge mapping software intended to give researcher
### Client
To get started, clone this repository. Next, duplicate the file `config.example.js` in the root folder and rename it to `config.js`.
-Make sure to have `npm` version 6.11.3 installed (it comes with Node.js 10.17.0, best way to install is with [nvm](https://github.com/nvm-sh/nvm), `nvm install 10.17.0`) and run the following two commands to build the Headstart client:
+Make sure to have installed `node` version >= 14.18.1 and `npm` version >=8.1.1 (best way to install is with [nvm](https://github.com/nvm-sh/nvm), `nvm install 14.18.1`) and run the following two commands to build the Headstart client:
npm install
npm run dev
@@ -24,9 +24,12 @@ To run Headstart on a different server (e.g. Apache), you need to set the public
* Dev: specify the full path including protocol, e.g. `http://localhost/headstart/dist`
* Production: specify the full path excluding protocol, e.g. `//example.org/headstart/dist`
-Point your browser to the following address:
+Point your browser to one of the following addresses:
- http://localhost:8080/examples/local_files/index.html
+ http://localhost:8080/local_files/
+ http://localhost:8080/local_streamgraph/
+ http://localhost:8080/project_website/base.html
+ http://localhost:8080/project_website/pubmed.html
If everything has worked out, you should see the visualization shown above.
@@ -42,7 +45,7 @@ See [Installing and configuring the server](doc/server_config.md) for instructio
Maintainer: [Peter Kraker](https://github.com/pkraker) ([pkraker@openknowledgemaps.org](mailto:pkraker@openknowledgemaps.org))
-Authors: [Maxi Schramm](https://github.com/tanteuschi), [Christopher Kittel](https://github.com/chreman), [Asura Enkhbayar](https://github.com/Bubblbu), [Scott Chamberlain](https://github.com/sckott), [Rainer Bachleitner](https://github.com/rbachleitner), [Yael Stein](https://github.com/jaels), [Thomas Arrow](https://github.com/tarrow), [Mike Skaug](https://github.com/mikeskaug), [Philipp Weissensteiner](https://github.com/wpp), and the [Open Knowledge Maps team](http://openknowledgemaps.org/team)
+Authors: [Maxi Schramm](https://github.com/tanteuschi), [Christopher Kittel](https://github.com/chreman), [Jan Konstant](https://github.com/konstiman), [Asura Enkhbayar](https://github.com/Bubblbu), [Scott Chamberlain](https://github.com/sckott), [Rainer Bachleitner](https://github.com/rbachleitner), [Yael Stein](https://github.com/jaels), [Thomas Arrow](https://github.com/tarrow), [Mike Skaug](https://github.com/mikeskaug), [Philipp Weissensteiner](https://github.com/wpp), and the [Open Knowledge Maps team](http://openknowledgemaps.org/team)
## Features
diff --git a/examples/gsheets/data-config.js b/examples/gsheets/data-config.js
index 42a636382..c3d909821 100644
--- a/examples/gsheets/data-config.js
+++ b/examples/gsheets/data-config.js
@@ -26,7 +26,6 @@ var data_config = {
content_based: true,
is_evaluation: true,
- evaluation_service: ["matomo"],
is_force_areas: true,
area_force_alpha: 0.03,
diff --git a/examples/local_streamgraph/README.md b/examples/local_streamgraph/README.md
index fc62b3878..739487559 100644
--- a/examples/local_streamgraph/README.md
+++ b/examples/local_streamgraph/README.md
@@ -11,7 +11,7 @@ module.exports = {
Then the process is the same as with the local files example: start the dev server npm start. When the build is done, the streamgraph can be accessed at
-`http://localhost:8080/examples/local_streamgraph/index.html`
+`http://localhost:8080/local_streamgraph/`
In future, it might be nice to come up with a solution that doesn't involve config file, but uses for example a system variable or a specific npm run command.
diff --git a/examples/project_website/base.html b/examples/project_website/base.html
index 122a67f23..ba370c0ff 100644
--- a/examples/project_website/base.html
+++ b/examples/project_website/base.html
@@ -2,7 +2,7 @@
-
+
diff --git a/examples/project_website/data-config_base.js b/examples/project_website/data-config_base.js
index 3907d6697..c9fd4d020 100644
--- a/examples/project_website/data-config_base.js
+++ b/examples/project_website/data-config_base.js
@@ -38,7 +38,6 @@ var data_config = {
show_keywords: true,
is_evaluation: true,
- evaluation_service: ["matomo"],
use_hypothesis: true,
diff --git a/examples/project_website/data-config_pubmed.js b/examples/project_website/data-config_pubmed.js
index 97e4f8710..d7aca734a 100644
--- a/examples/project_website/data-config_pubmed.js
+++ b/examples/project_website/data-config_pubmed.js
@@ -37,7 +37,6 @@ var data_config = {
show_keywords: true,
is_evaluation: true,
- evaluation_service: ["matomo"],
use_hypothesis: true,
diff --git a/examples/project_website/pubmed.html b/examples/project_website/pubmed.html
index f56c11460..03773ad87 100644
--- a/examples/project_website/pubmed.html
+++ b/examples/project_website/pubmed.html
@@ -2,7 +2,7 @@
-
+
diff --git a/examples/viper/js/data-config_openaire.js b/examples/viper/js/data-config_openaire.js
index 405117c30..57e853b56 100644
--- a/examples/viper/js/data-config_openaire.js
+++ b/examples/viper/js/data-config_openaire.js
@@ -64,7 +64,6 @@ var data_config = {
},
is_evaluation: true,
- evaluation_service: ["ga", "matomo"],
use_hypothesis: true,
diff --git a/vis/js/actions/index.js b/vis/js/actions/index.js
index 16c72852e..0f2e16af2 100644
--- a/vis/js/actions/index.js
+++ b/vis/js/actions/index.js
@@ -20,7 +20,6 @@ export const NOT_QUEUED_IN_ANIMATION = [
"ZOOM_OUT",
"SELECT_PAPER",
"DESELECT_PAPER",
- "DESELECT_PAPER_BACKLINK",
"FILE_CLICKED",
"SEARCH",
"FILTER",
@@ -29,20 +28,23 @@ export const NOT_QUEUED_IN_ANIMATION = [
export const zoomIn = (
selectedAreaData,
- source = null,
callback,
- alreadyZoomed = false
+ alreadyZoomed = false,
+ isFromBackButton = false,
+ selectedPaperData = null
) => ({
type: "ZOOM_IN",
selectedAreaData,
- source,
callback,
alreadyZoomed,
+ isFromBackButton,
+ selectedPaperData,
});
-export const zoomOut = (callback) => ({
+export const zoomOut = (callback, isFromBackButton = false) => ({
type: "ZOOM_OUT",
callback,
+ isFromBackButton,
});
/**
@@ -97,10 +99,11 @@ export const filter = (id) => ({ type: "FILTER", id });
export const sort = (id) => ({ type: "SORT", id });
-export const selectPaper = (paper) => ({
+export const selectPaper = (paper, isFromBackButton = false) => ({
type: "SELECT_PAPER",
safeId: paper.safe_id,
paper,
+ isFromBackButton,
});
export const deselectPaper = () => ({ type: "DESELECT_PAPER" });
@@ -114,10 +117,6 @@ export const showPreview = (paper) => ({ type: "SHOW_PREVIEW", paper });
export const hidePreview = () => ({ type: "HIDE_PREVIEW" });
-export const deselectPaperBacklink = () => ({
- type: "DESELECT_PAPER_BACKLINK",
-});
-
export const updateDimensions = (chart, list) => ({
type: "RESIZE",
listHeight: list.height,
diff --git a/vis/js/components/KnowledgeMap.js b/vis/js/components/KnowledgeMap.js
index 7c7eb88f1..41d4eaf72 100644
--- a/vis/js/components/KnowledgeMap.js
+++ b/vis/js/components/KnowledgeMap.js
@@ -34,7 +34,6 @@ const KnowledgeMap = (props) => {
};
const handleOtherAreaZoomIn = (bubble) => {
- handleDeselectPaper();
handleZoomIn(bubble, true);
trackMatomoEvent("Knowledge map", "Zoom in", "Bubble");
};
diff --git a/vis/js/components/Streamgraph.js b/vis/js/components/Streamgraph.js
index 01eb979c5..27e10b192 100644
--- a/vis/js/components/Streamgraph.js
+++ b/vis/js/components/Streamgraph.js
@@ -25,7 +25,7 @@ import {
TOOLTIP_OFFSET,
} from "../utils/streamgraph";
-import { deselectPaper, zoomIn, zoomOut } from "../actions";
+import { zoomIn, zoomOut } from "../actions";
/**
* Class representing the streamgraph visualization.
@@ -541,7 +541,6 @@ const mapStateToProps = (state) => ({
const mapDispatchToProps = (dispatch) => ({
onAreaClick: (stream) => {
- dispatch(deselectPaper());
dispatch(
zoomIn({ title: stream.key, color: stream.color, docIds: stream.docIds })
);
diff --git a/vis/js/components/listentries/BasicListEntries.js b/vis/js/components/listentries/BasicListEntries.js
index 4573c469b..15100ce9c 100644
--- a/vis/js/components/listentries/BasicListEntries.js
+++ b/vis/js/components/listentries/BasicListEntries.js
@@ -15,8 +15,7 @@ import { shorten } from "../../utils/string";
const BasicListEntries = ({
displayedData,
handleZoomIn,
- handleSelectPaper,
- handleDeselectPaper,
+ handleSelectPaperWithZoom,
handlePDFClick,
handleAreaMouseover,
handleAreaMouseout,
@@ -31,16 +30,14 @@ const BasicListEntries = ({
if (disableClicks) {
return;
}
- handleSelectPaper(paper);
- handleZoomIn(paper);
+ handleSelectPaperWithZoom(paper);
};
const handleAreaClick = (paper) => {
if (disableClicks) {
return;
}
- handleDeselectPaper();
- handleZoomIn(paper, "list-area");
+ handleZoomIn(paper);
};
return (
diff --git a/vis/js/components/listentries/ClassificationListEntries.js b/vis/js/components/listentries/ClassificationListEntries.js
index 53e4ce698..d3ca14ac7 100644
--- a/vis/js/components/listentries/ClassificationListEntries.js
+++ b/vis/js/components/listentries/ClassificationListEntries.js
@@ -27,7 +27,7 @@ const ClassificationListEntries = ({
disableClicks,
handleZoomIn,
handleSelectPaper,
- handleDeselectPaper,
+ handleSelectPaperWithZoom,
handlePDFClick,
handleAreaMouseover,
handleAreaMouseout,
@@ -37,9 +37,10 @@ const ClassificationListEntries = ({
if (disableClicks) {
return;
}
- handleSelectPaper(paper);
if (!isStreamgraph) {
- handleZoomIn(paper);
+ handleSelectPaperWithZoom(paper);
+ } else {
+ handleSelectPaper(paper);
}
};
@@ -47,8 +48,7 @@ const ClassificationListEntries = ({
if (disableClicks) {
return;
}
- handleDeselectPaper();
- handleZoomIn(paper, "list-area");
+ handleZoomIn(paper);
};
return (
diff --git a/vis/js/components/listentries/StandardListEntries.js b/vis/js/components/listentries/StandardListEntries.js
index fa0b88e8b..f9decbf01 100644
--- a/vis/js/components/listentries/StandardListEntries.js
+++ b/vis/js/components/listentries/StandardListEntries.js
@@ -34,7 +34,7 @@ const StandardListEntries = ({
disableClicks,
handleZoomIn,
handleSelectPaper,
- handleDeselectPaper,
+ handleSelectPaperWithZoom,
handlePDFClick,
handleAreaMouseover,
handleAreaMouseout,
@@ -44,9 +44,10 @@ const StandardListEntries = ({
if (disableClicks) {
return;
}
- handleSelectPaper(paper);
if (!isStreamgraph) {
- handleZoomIn(paper);
+ handleSelectPaperWithZoom(paper);
+ } else {
+ handleSelectPaper(paper);
}
};
@@ -54,8 +55,7 @@ const StandardListEntries = ({
if (disableClicks) {
return;
}
- handleDeselectPaper();
- handleZoomIn(paper, "list-area");
+ handleZoomIn(paper);
};
return (
diff --git a/vis/js/default-config.js b/vis/js/default-config.js
index 81423a107..2a5545f12 100644
--- a/vis/js/default-config.js
+++ b/vis/js/default-config.js
@@ -94,8 +94,6 @@ var config = {
show_loading_screen: false,
//evaluation mode/events logging
is_evaluation: false,
- //which evaluation service to use. can also be an array. currently possible: "log", "matomo" and "ga"
- evaluation_service: "log",
//enable logging of mouseover events (use only temporarily as it creates A LOT of logging events)
enable_mouseover_evaluation: false,
//whether to embed the okmaps credit
diff --git a/vis/js/headstart.js b/vis/js/headstart.js
index 9128a0602..f062b251e 100644
--- a/vis/js/headstart.js
+++ b/vis/js/headstart.js
@@ -32,68 +32,6 @@ HeadstartFSM.prototype = {
}
},
- // TODO delete this completely
- recordAction: function(id, category, action, user, timestamp, additional_params, post_data) {
-
- if(!config.is_evaluation) {
- return;
- }
-
- let services = config.evaluation_service;
-
- if(typeof services === "string") {
- services = [services];
- }
-
- if (services.includes("log")) {
- this.recordActionLog(category, action, id, user, timestamp, additional_params, post_data);
- }
- if (services.includes("ga")) {
- this.recordActionGA(category, action, id, user, timestamp, additional_params, post_data);
- }
- },
-
- recordActionLog: function(id, category, action, user, type, timestamp, additional_params, post_data) {
- timestamp = (typeof timestamp !== 'undefined') ? (escape(timestamp)) : ("");
- additional_params = (typeof additional_params !== 'undefined') ? ('&' + additional_params) : ("");
- if(typeof post_data !== 'undefined') {
- post_data = {post_data:post_data};
- } else {
- post_data = {};
- }
-
- let php_script = config.server_url + "services/writeActionToLog.php";
-
- $.ajax({
- url: php_script +
- '?user=' + user +
- '&category=' + category +
- '&action=' + action +
- '&item=' + encodeURI(id) +
- '&type=' + type +
- '&item_timestamp=' + timestamp + additional_params + '&jsoncallback=?',
- type: "POST",
- data: post_data,
- dataType: "json",
- success: function(output) {
- console.log(output);
- }
- });
- },
-
- recordActionGA: function(category, action, id) {
- //gtag.js
- if(typeof gtag === "function") {
- gtag('event', action, {
- 'event_category': category,
- 'event_label': id
- });
- //analytics.js
- } else if (typeof ga === "function") {
- ga('send', 'event', category, action, id);
- }
- },
-
markProjectChanged: function (id) {
let php_script = config.server_url + "services/markProjectChanged.php";
diff --git a/vis/js/intermediate.js b/vis/js/intermediate.js
index 7c18f1f7e..124df1aa9 100644
--- a/vis/js/intermediate.js
+++ b/vis/js/intermediate.js
@@ -13,6 +13,8 @@ import {
applyForceAreas,
applyForcePapers,
preinitializeStore,
+ zoomIn,
+ zoomOut,
} from "./actions";
import { STREAMGRAPH_MODE } from "./reducers/chartType";
@@ -23,6 +25,10 @@ import logAction from "./utils/actionLogger";
import { getChartSize, getListSize } from "./utils/dimensions";
import Headstart from "./components/Headstart";
import { sanitizeInputData } from "./utils/data";
+import { createAnimationCallback } from "./utils/eventhandlers";
+import { removeQueryParams, handleUrlAction } from "./utils/url";
+import debounce from "./utils/debounce";
+import { handleTitleAction } from "./utils/title";
/**
* Class to sit between the "old" mediator and the
@@ -41,13 +47,18 @@ class Intermediate {
createRepeatedInitializeMiddleware(this),
createChartTypeMiddleware(),
createRescaleMiddleware(rescaleCallback),
- createRecordActionMiddleware()
+ createRecordActionMiddleware(),
+ createQueryParameterMiddleware(),
+ createPageTitleMiddleware(this)
);
this.store = createStore(rootReducer, middleware);
}
renderFrontend(config) {
+ this.config = config;
+ this.originalTitle = document.title;
+
this.store.dispatch(preinitializeStore(config));
ReactDOM.render(
@@ -56,20 +67,27 @@ class Intermediate {
,
document.getElementById("app-container")
);
+
+ window.addEventListener(
+ "popstate",
+ debounce(this.onBackButtonClick.bind(this), 300)
+ );
}
initStore(config, context, mapData, streamData) {
const { size, width, height } = getChartSize(config, context);
const list = getListSize(config, context, size);
- const sanitizedMapData = sanitizeInputData(mapData);
+ this.config = config;
+ this.sanitizedMapData = sanitizeInputData(mapData);
+ this.streamData = streamData;
this.store.dispatch(
initializeStore(
config,
context,
- sanitizedMapData,
- streamData,
+ this.sanitizedMapData,
+ this.streamData,
size,
width,
height,
@@ -87,6 +105,135 @@ class Intermediate {
this.applyForceLayout();
}
+
+ // enable this for ability to share link to a zoomed bubble / paper
+ // if (queryParams.has("paper")) {
+ // this.selectUrlPaper();
+ // } else {
+ // this.zoomUrlArea();
+ // }
+ // remove the following lines if the previous line is uncommented
+ const queryParams = new URLSearchParams(window.location.search);
+ const paramsToRemove = [];
+ if (queryParams.has("area")) {
+ paramsToRemove.push("area");
+ }
+ if (queryParams.has("paper")) {
+ paramsToRemove.push("paper");
+ }
+ if (paramsToRemove.length > 0) {
+ removeQueryParams(...paramsToRemove);
+ }
+ }
+
+ /**
+ * Function for when the browser back button is clicked.
+ *
+ * This function is the reason zoom-in and zoom-out actions are also queued
+ * in the queue middleware.
+ *
+ * For a better user experience, it should be debounced.
+ */
+ onBackButtonClick() {
+ const queryParams = new URLSearchParams(window.location.search);
+
+ if (!queryParams.has("area")) {
+ if (this.config.is_streamgraph && queryParams.has("paper")) {
+ removeQueryParams("paper");
+ }
+ this.store.dispatch(
+ zoomOut(createAnimationCallback(this.store.dispatch), true)
+ );
+
+ return;
+ }
+
+ // this can be optimized: if the area is the same as before, simply
+ // deselect the paper or select a different one
+
+ if (queryParams.has("area") && !queryParams.has("paper")) {
+ this.zoomUrlArea();
+
+ return;
+ }
+
+ if (queryParams.has("paper")) {
+ this.selectUrlPaper();
+ }
+ }
+
+ /**
+ * Selects the paper that's specified in the query params.
+ */
+ selectUrlPaper() {
+ if (this.config.is_streamgraph) {
+ // paper cannot be selected in streamgraph
+ removeQueryParams("area", "paper");
+ return;
+ }
+
+ const params = new URLSearchParams(window.location.search);
+
+ if (!params.has("paper")) {
+ return;
+ }
+
+ const zoomedPaper = params.get("paper");
+
+ const paper = this.sanitizedMapData.find((p) => p.safe_id === zoomedPaper);
+
+ if (!paper) {
+ return;
+ }
+
+ this.store.dispatch(
+ zoomIn(
+ { title: paper.area, uri: paper.area_uri },
+ createAnimationCallback(this.store.dispatch),
+ this.store.getState().zoom,
+ true,
+ paper
+ )
+ );
+ }
+
+ /**
+ * Zooms into an area that's specified in the query params.
+ */
+ zoomUrlArea() {
+ const params = new URLSearchParams(window.location.search);
+
+ if (!params.has("area")) {
+ return;
+ }
+
+ const zoomedArea = params.get("area");
+
+ if (this.config.is_streamgraph) {
+ // the instant zoom in doesn't work for streamgraph, because the data is not processed here yet (this.streamData)
+ // proper data processing refactoring is necessary
+ // this is a workaround that simply zooms out
+ removeQueryParams("area");
+ this.store.dispatch(
+ zoomOut(createAnimationCallback(this.store.dispatch), true)
+ );
+ return;
+ }
+
+ const area = this.sanitizedMapData.find((a) => a.area_uri == zoomedArea);
+
+ if (!area) {
+ return;
+ }
+
+ this.store.dispatch(
+ zoomIn(
+ { title: area.area, uri: area.area_uri },
+ createAnimationCallback(this.store.dispatch),
+ this.store.getState().zoom,
+ true
+ )
+ );
}
// triggered on window resize
@@ -132,7 +279,10 @@ function createActionQueueMiddleware(intermediate) {
if (getState().animation !== null) {
if (!ALLOWED_IN_ANIMATION.includes(action.type)) {
- if (!NOT_QUEUED_IN_ANIMATION.includes(action.type)) {
+ if (
+ !NOT_QUEUED_IN_ANIMATION.includes(action.type) ||
+ action.isFromBackButton
+ ) {
actionQueue.push({ ...action });
}
action.canceled = true;
@@ -145,7 +295,13 @@ function createActionQueueMiddleware(intermediate) {
if (action.type === "STOP_ANIMATION") {
while (actionQueue.length > 0) {
const queuedAction = actionQueue.shift();
- dispatch(queuedAction);
+ if (!NOT_QUEUED_IN_ANIMATION.includes(queuedAction.type)) {
+ dispatch(queuedAction);
+ } else {
+ requestAnimationFrame(() => {
+ dispatch(queuedAction);
+ });
+ }
}
}
@@ -266,8 +422,35 @@ function createFileChangeMiddleware() {
function createRecordActionMiddleware() {
return function ({ getState }) {
return (next) => (action) => {
- const state = getState();
- logAction(action, state);
+ if (!action.canceled) {
+ const state = getState();
+ logAction(action, state);
+ }
+ return next(action);
+ };
+ };
+}
+
+function createQueryParameterMiddleware() {
+ return function () {
+ return (next) => (action) => {
+ if (!action.canceled && !action.isFromBackButton) {
+ handleUrlAction(action);
+ }
+
+ return next(action);
+ };
+ };
+}
+
+function createPageTitleMiddleware(itm) {
+ return function ({ getState }) {
+ return (next) => (action) => {
+ if (!action.canceled && !action.isFromBackButton) {
+ const state = getState();
+ handleTitleAction(action, itm.originalTitle, state);
+ }
+
return next(action);
};
};
diff --git a/vis/js/mediator.js b/vis/js/mediator.js
index a68ec6a4b..2e49f1eb1 100644
--- a/vis/js/mediator.js
+++ b/vis/js/mediator.js
@@ -32,7 +32,7 @@ var MyMediator = function() {
this.fileData = [];
this.mediator = new Mediator();
this.manager = new ModuleManager();
- this.intermediate_layer = new Intermediate(this.rescale_map, this.record_action);
+ this.intermediate_layer = new Intermediate(this.rescale_map);
this.init();
this.init_state();
};
@@ -55,9 +55,6 @@ MyMediator.prototype = {
// bubbles events
this.mediator.subscribe("bubbles_update_data_and_areas", this.bubbles_update_data_and_areas);
-
- // misc
- this.mediator.subscribe("record_action", this.record_action);
},
init_state: function() {
@@ -151,10 +148,6 @@ MyMediator.prototype = {
mediator.render_frontend();
},
- record_action: function(id, category, action, user, type, timestamp, additional_params, post_data) {
- window.headstartInstance.recordAction(id, category, action, user, type, timestamp, additional_params, post_data);
- },
-
dimensions_update: function() {
mediator.intermediate_layer.updateDimensions(config, io.context);
},
diff --git a/vis/js/reducers/modals.js b/vis/js/reducers/modals.js
index 109a0fc46..8b2e0dc54 100644
--- a/vis/js/reducers/modals.js
+++ b/vis/js/reducers/modals.js
@@ -94,6 +94,17 @@ const modals = (
...state,
openCitationModal: false,
};
+ case "ZOOM_IN":
+ case "ZOOM_OUT":
+ return {
+ ...state,
+ openInfoModal: false,
+ openEmbedModal: false,
+ openViperEditModal: false,
+ openCitationModal: false,
+ previewedPaper: null,
+ };
+
default:
return state;
}
diff --git a/vis/js/reducers/selectedPaper.js b/vis/js/reducers/selectedPaper.js
index 4a669b6f8..826f478c8 100644
--- a/vis/js/reducers/selectedPaper.js
+++ b/vis/js/reducers/selectedPaper.js
@@ -4,16 +4,20 @@ const selectedPaper = (state = null, action) => {
}
switch (action.type) {
+ case "INITIALIZE":
case "ZOOM_OUT":
- return null;
case "SCALE":
- return null;
- case "INITIALIZE":
- return null;
case "DESELECT_PAPER":
return null;
- case "DESELECT_PAPER_BACKLINK":
- return null;
+ case "ZOOM_IN": {
+ if (!action.selectedPaperData) {
+ return null;
+ }
+
+ return {
+ safeId: action.selectedPaperData.safe_id,
+ };
+ }
case "SELECT_PAPER":
return {
safeId: action.safeId,
diff --git a/vis/js/utils/actionLogger.js b/vis/js/utils/actionLogger.js
index 628083b46..60df1e71d 100644
--- a/vis/js/utils/actionLogger.js
+++ b/vis/js/utils/actionLogger.js
@@ -18,9 +18,55 @@ const logAction = (action, state) => {
// TODO trackSiteSearch ?
// https://developer.matomo.org/guides/tracking-javascript-guide
return trackMatomoEvent("List controls", "Search", "Search box");
+ case "ZOOM_IN":
+ return trackZoomIn(action, state);
+ case "ZOOM_OUT":
+ return trackZoomOut(action);
+ case "SELECT_PAPER":
+ return trackSelectPaper(action);
+ case "DESELECT_PAPER":
+ return trackDeselectPaper(action);
default:
return;
}
};
export default logAction;
+
+const trackZoomIn = (action, state) => {
+ if (!action.isFromBackButton) {
+ return;
+ }
+
+ if (action.selectedPaperData) {
+ return trackSelectPaper(action);
+ }
+
+ if (state.selectedPaper) {
+ return trackDeselectPaper(action);
+ }
+
+ trackMatomoEvent("Browser buttons", "Zoom in", "Back/Forward button");
+};
+
+const trackZoomOut = (action) => {
+ if (action.isFromBackButton) {
+ trackMatomoEvent("Browser buttons", "Zoom out", "Back/Forward button");
+ }
+};
+
+const trackSelectPaper = (action) => {
+ if (action.isFromBackButton) {
+ trackMatomoEvent("Browser buttons", "Select paper", "Back/Forward button");
+ }
+};
+
+const trackDeselectPaper = (action) => {
+ if (action.isFromBackButton) {
+ trackMatomoEvent(
+ "Browser buttons",
+ "Deselect paper",
+ "Back/Forward button"
+ );
+ }
+};
diff --git a/vis/js/utils/eventhandlers.js b/vis/js/utils/eventhandlers.js
index 0576694e3..4ac2d9c0f 100644
--- a/vis/js/utils/eventhandlers.js
+++ b/vis/js/utils/eventhandlers.js
@@ -5,7 +5,6 @@ import {
showPreview,
selectPaper,
deselectPaper,
- deselectPaperBacklink,
stopAnimation,
hoverBubble,
hoverPaper,
@@ -16,12 +15,10 @@ import {
* @param {Function} dispatch
*/
export const mapDispatchToListEntriesProps = (dispatch) => ({
- // TODO remove the source after refactoring
- handleZoomIn: (paper, source = null) =>
+ handleZoomIn: (paper) =>
dispatch(
zoomIn(
{ title: paper.area, uri: paper.area_uri },
- source,
createAnimationCallback(dispatch)
)
),
@@ -29,8 +26,18 @@ export const mapDispatchToListEntriesProps = (dispatch) => ({
handleAreaMouseover: (paper) => dispatch(highlightArea(paper)),
handleAreaMouseout: () => dispatch(highlightArea(null)),
handleSelectPaper: (paper) => dispatch(selectPaper(paper)),
+ handleSelectPaperWithZoom: (paper) =>
+ dispatch(
+ zoomIn(
+ { title: paper.area, uri: paper.area_uri },
+ createAnimationCallback(dispatch),
+ false,
+ false,
+ paper
+ )
+ ),
handleDeselectPaper: () => dispatch(deselectPaper()),
- handleBacklinkClick: () => dispatch(deselectPaperBacklink()),
+ handleBacklinkClick: () => dispatch(deselectPaper()),
});
/**
@@ -42,7 +49,6 @@ export const mapDispatchToMapEntriesProps = (dispatch) => ({
dispatch(
zoomIn(
{ title: area.title, uri: area.area_uri },
- null,
createAnimationCallback(dispatch),
alreadyZoomed
)
@@ -51,7 +57,8 @@ export const mapDispatchToMapEntriesProps = (dispatch) => ({
handleDeselectPaper: () => dispatch(deselectPaper()),
handleSelectPaper: (paper) => dispatch(selectPaper(paper)),
changeBubbleOrder: (uri) => dispatch(hoverBubble(uri)),
- changePaperOrder: (safeId, enlargeFactor) => dispatch(hoverPaper(safeId, enlargeFactor)),
+ changePaperOrder: (safeId, enlargeFactor) =>
+ dispatch(hoverPaper(safeId, enlargeFactor)),
});
/**
diff --git a/vis/js/utils/title.js b/vis/js/utils/title.js
new file mode 100644
index 000000000..952dab952
--- /dev/null
+++ b/vis/js/utils/title.js
@@ -0,0 +1,61 @@
+/**
+ * Sets correct page title based on the current action and state.
+ *
+ * @param {Object} action the Redux action object
+ * @param {string} defaultTitle the original page title Headstart has when it loads
+ * @param {Object} state the Redux state object
+ */
+export const handleTitleAction = (action, defaultTitle, state) => {
+ switch (action.type) {
+ case "ZOOM_IN":
+ document.title = getZoomInTitle(
+ action.selectedAreaData,
+ action.selectedPaperData,
+ defaultTitle
+ );
+ return;
+ case "ZOOM_OUT":
+ document.title = defaultTitle;
+ return;
+ case "SELECT_PAPER":
+ document.title = getSelectPaperTitle(action.paper, defaultTitle);
+ return;
+ case "DESELECT_PAPER":
+ document.title = getDeselectPaperTitle(defaultTitle, state);
+ return;
+ default:
+ return;
+ }
+};
+
+const getZoomInTitle = (areaData, paperData, defaultTitle) => {
+ if (!areaData) {
+ return document.title;
+ }
+
+ if (paperData) {
+ return `${paperData.title} | ${defaultTitle}`;
+ }
+
+ return `${areaData.title} | ${defaultTitle}`;
+};
+
+const getSelectPaperTitle = (paperData, defaultTitle) => {
+ if (!paperData) {
+ return document.title;
+ }
+
+ return `${paperData.title} | ${defaultTitle}`;
+};
+
+const getDeselectPaperTitle = (defaultTitle, state) => {
+ if (state.selectedBubble) {
+ return getZoomInTitle(
+ { title: state.selectedBubble.title },
+ null,
+ defaultTitle
+ );
+ }
+
+ return defaultTitle;
+};
diff --git a/vis/js/utils/url.js b/vis/js/utils/url.js
new file mode 100644
index 000000000..9b9b9e89d
--- /dev/null
+++ b/vis/js/utils/url.js
@@ -0,0 +1,100 @@
+/**
+ * Adds a parameter into the URL query (without redirect).
+ * @param {string} key parameter name
+ * @param {string} value parameter value
+ */
+export const addQueryParam = (key, value) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set(key, value);
+
+ window.history.pushState("", "", url.pathname + url.search);
+};
+
+/**
+ * Removes a parameter from the URL query (without redirect).
+ * @param {string} key parameter name
+ */
+export const removeQueryParams = (...keys) => {
+ const url = new URL(window.location.href);
+ keys.forEach((key) => {
+ url.searchParams.delete(key);
+ });
+
+ window.history.pushState("", "", url.pathname + url.search);
+};
+
+const addRemoveQueryParams = (paramsToAdd, paramsToRemove) => {
+ const url = new URL(window.location.href);
+
+ Object.keys(paramsToAdd).forEach((key) => {
+ url.searchParams.set(key, paramsToAdd[key]);
+ });
+
+ paramsToRemove.forEach((key) => {
+ url.searchParams.delete(key);
+ });
+
+ window.history.pushState("", "", url.pathname + url.search);
+};
+
+/**
+ * Changes page url based on the current Redux action.
+ *
+ * @param {Object} action the Redux action object
+ */
+export const handleUrlAction = (action) => {
+ switch (action.type) {
+ case "ZOOM_IN":
+ return handleZoomIn(action);
+ case "ZOOM_OUT":
+ return handleZoomOut();
+ case "SELECT_PAPER":
+ return handleSelectPaper(action);
+ case "DESELECT_PAPER":
+ return handleDeselectPaper();
+ default:
+ return;
+ }
+};
+
+const handleZoomIn = (action) => {
+ if (!action.selectedAreaData) {
+ return;
+ }
+
+ if (action.selectedPaperData) {
+ addRemoveQueryParams(
+ {
+ area:
+ typeof action.selectedAreaData.uri !== "undefined"
+ ? action.selectedAreaData.uri
+ : action.selectedAreaData.title,
+ paper: action.selectedPaperData.safe_id,
+ },
+ []
+ );
+
+ return;
+ }
+
+ addRemoveQueryParams(
+ {
+ area:
+ typeof action.selectedAreaData.uri !== "undefined"
+ ? action.selectedAreaData.uri
+ : action.selectedAreaData.title,
+ },
+ ["paper"]
+ );
+};
+
+const handleZoomOut = () => removeQueryParams("area", "paper");
+
+const handleSelectPaper = (action) => {
+ if (!action.safeId) {
+ return;
+ }
+ addQueryParam("paper", action.safeId);
+};
+
+const handleDeselectPaper = () => removeQueryParams("paper");
diff --git a/vis/test/component/list.test.js b/vis/test/component/list.test.js
index 0971a2c7a..8601d54b5 100644
--- a/vis/test/component/list.test.js
+++ b/vis/test/component/list.test.js
@@ -17,7 +17,6 @@ import {
zoomIn,
selectPaper,
highlightArea,
- deselectPaperBacklink,
showPreview,
deselectPaper,
} from "../../js/actions";
@@ -1071,7 +1070,7 @@ describe("List entries component", () => {
it("triggers a correct backlink click action in linkedcat (zoomed)", () => {
const PAPER = linkedcatData.find((p) => p.safe_id === "AC15093982");
- const EXPECTED_PAYLOAD = deselectPaperBacklink();
+ const EXPECTED_PAYLOAD = deselectPaper();
const storeObject = setup(
{ list: [PAPER] },
{ show: true },
diff --git a/vis/test/store/animation.test.js b/vis/test/store/animation.test.js
index b7541d937..74e21a917 100644
--- a/vis/test/store/animation.test.js
+++ b/vis/test/store/animation.test.js
@@ -16,7 +16,7 @@ describe("animation state", () => {
const INITIAL_STATE = null;
const F = jest.fn();
- const result = reducer(INITIAL_STATE, zoomIn({}, undefined, F, false));
+ const result = reducer(INITIAL_STATE, zoomIn({}, F, false));
const EXPECTED_RESULT = {
type: "ZOOM_IN",
diff --git a/vis/test/store/areas.test.js b/vis/test/store/areas.test.js
index d8bc2357b..7cf137c45 100644
--- a/vis/test/store/areas.test.js
+++ b/vis/test/store/areas.test.js
@@ -79,7 +79,6 @@ describe("areas state", () => {
zoomIn(
{ uri: INITIALIZED_STATE.list[0].area_uri },
undefined,
- undefined,
false
)
);
diff --git a/vis/test/store/selectedPaper.test.js b/vis/test/store/selectedPaper.test.js
index e2b85fd2c..d1fba873d 100644
--- a/vis/test/store/selectedPaper.test.js
+++ b/vis/test/store/selectedPaper.test.js
@@ -1,9 +1,4 @@
-import {
- zoomOut,
- selectPaper,
- deselectPaper,
- deselectPaperBacklink,
-} from "../../js/actions";
+import { zoomOut, selectPaper, deselectPaper } from "../../js/actions";
import selectedPaperReducer from "../../js/reducers/selectedPaper";
@@ -14,7 +9,7 @@ describe("list state", () => {
id: "some-id",
safe_id: "some-safe-id",
title: "some title",
- }
+ };
const EXPECTED_ACTION = {
type: "SELECT_PAPER",
safeId: PAPER.safe_id,
@@ -29,13 +24,6 @@ describe("list state", () => {
};
expect(deselectPaper()).toEqual(EXPECTED_ACTION);
});
-
- it("should create a deselect paper backlink action", () => {
- const EXPECTED_ACTION = {
- type: "DESELECT_PAPER_BACKLINK",
- };
- expect(deselectPaperBacklink()).toEqual(EXPECTED_ACTION);
- });
});
describe("reducers", () => {
@@ -77,21 +65,6 @@ describe("list state", () => {
expect(result).toEqual(EXPECTED_STATE);
});
- it("should deselect the paper (from backlink)", () => {
- const PAPER = {
- safe_id: "some-id",
- };
-
- const INITIAL_STATE = {
- safeId: PAPER.safe_id,
- };
- const EXPECTED_STATE = null;
-
- const result = selectedPaperReducer(INITIAL_STATE, deselectPaperBacklink());
-
- expect(result).toEqual(EXPECTED_STATE);
- });
-
it("should deselect the paper (on zoom out)", () => {
const PAPER = {
safe_id: "some-id",
diff --git a/vis/test/store/zoom.test.js b/vis/test/store/zoom.test.js
index 0699fc4d9..e069caf71 100644
--- a/vis/test/store/zoom.test.js
+++ b/vis/test/store/zoom.test.js
@@ -13,36 +13,19 @@ describe("zoom state", () => {
const EXPECTED_ACTION = {
type: "ZOOM_IN",
- source: null,
selectedAreaData: DATA,
alreadyZoomed: false,
callback: undefined,
+ isFromBackButton: false,
};
expect(zoomIn(DATA)).toEqual(EXPECTED_ACTION);
});
- it("should create a zoom-in action with source", () => {
- const DATA = {
- title: "some title",
- url: "http://example.com",
- };
-
- const SOURCE = "some-source";
-
- const EXPECTED_ACTION = {
- type: "ZOOM_IN",
- source: SOURCE,
- selectedAreaData: DATA,
- alreadyZoomed: false,
- callback: undefined,
- };
- expect(zoomIn(DATA, SOURCE)).toEqual(EXPECTED_ACTION);
- });
-
it("should create a zoom-out action", () => {
const EXPECTED_ACTION = {
type: "ZOOM_OUT",
callback: undefined,
+ isFromBackButton: false,
};
expect(zoomOut()).toEqual(EXPECTED_ACTION);
});
diff --git a/webpack.config.js b/webpack.config.js
index 08f84a5c8..984f373ac 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -19,10 +19,12 @@ const common = {
},
devServer: {
- static: { directory: path.join( __dirname ) },
+ static: {
+ directory: path.resolve(__dirname, 'examples/'),
+ },
allowedHosts: "all",
host: "0.0.0.0",
- devMiddleware: { publicPath: '/dist/' }
+ devMiddleware: { publicPath: '/dist/' },
},
resolve: {