From 5cec3ed5dc7d31df2a06e066bf08c4c1d1d4edeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maria=20=C3=96hrman?= Date: Wed, 8 May 2024 13:48:24 +0200 Subject: [PATCH 01/99] feat: use Karp API v7.0.0 - update related words query - update autocomplete calls --- app/scripts/components/autoc.js | 2 +- app/scripts/services.js | 215 +++++++++++++------------------- 2 files changed, 90 insertions(+), 127 deletions(-) diff --git a/app/scripts/components/autoc.js b/app/scripts/components/autoc.js index 66551b4ce..70269690f 100644 --- a/app/scripts/components/autoc.js +++ b/app/scripts/components/autoc.js @@ -165,7 +165,7 @@ angular.module("korpApp").component("autoc", { ctrl.getLemgrams = function (input, morphologies, corporaIDs) { const deferred = $q.defer() - const http = lexicons.getLemgrams(input, morphologies, corporaIDs, ctrl.variant === "affix") + const http = lexicons.getLemgrams(input, morphologies, corporaIDs) http.then(function (data) { data.forEach(function (item) { if (ctrl.variant === "affix") { diff --git a/app/scripts/services.js b/app/scripts/services.js index ff7ed3180..8cefc3d1b 100644 --- a/app/scripts/services.js +++ b/app/scripts/services.js @@ -392,172 +392,135 @@ korpApp.factory("lexicons", [ "$q", "$http", function ($q, $http) { - const karpURL = "https://ws.spraakbanken.gu.se/ws/karp/v4" + const karpURL = "https://spraakbanken4.it.gu.se/karp/v7" + // query for saldom resource to find all entries that have wf as a non-compound word form + const wfQuery = (wf) => + "inflectionTable(and(equals|writtenForm|" + + wf + + "||not(equals|msd|c||equals|msd|ci||equals|msd|cm||equals|msd|sms)))" return { getLemgrams(wf, resources, corporaIDs) { const deferred = $q.defer() - const args = { - q: wf, - resource: $.isArray(resources) ? resources.join(",") : resources, - mode: "external", - } - $http({ method: "GET", - url: `${karpURL}/autocomplete`, - params: args, + url: `${karpURL}/query/${resources.join(",")}`, + params: { + q: wfQuery(wf), + path: "entry.lemgram", + }, }) - .then(function (response) { - let { data } = response - if (data === null) { - return deferred.resolve([]) - } else { - // Pick the lemgrams. Would be nice if this was done by the backend instead. - const karpLemgrams = _.map( - data.hits.hits, - (entry) => entry._source.FormRepresentations[0].lemgram - ) - - if (karpLemgrams.length === 0) { - deferred.resolve([]) - return - } + .then(({ data }) => { + if (data.total === 0) { + deferred.resolve([]) + return + } - let lemgram = karpLemgrams.join(",") - const corpora = corporaIDs.join(",") - const headers = authenticationProxy.getAuthorizationHeader() - return $http( - httpConfAddMethod({ - url: settings["korp_backend_url"] + "/lemgram_count", - params: { - lemgram: lemgram, - count: "lemgram", - corpus: corpora, - }, - headers, - }) - ).then(({ data }) => { - delete data.time - const allLemgrams = [] - for (lemgram in data) { - const count = data[lemgram] - allLemgrams.push({ lemgram: lemgram, count: count }) - } - for (let klemgram of karpLemgrams) { - if (!data[klemgram]) { - allLemgrams.push({ lemgram: klemgram, count: 0 }) - } - } - return deferred.resolve(allLemgrams) + const karpLemgrams = data.hits + $http( + httpConfAddMethod({ + url: settings["korp_backend_url"] + "/lemgram_count", + params: { + lemgram: karpLemgrams.join(","), + count: "lemgram", + corpus: corporaIDs.join(","), + }, + headers: authenticationProxy.getAuthorizationHeader(), }) - } + ).then(({ data }) => { + delete data.time + const allLemgrams = [] + for (let lemgram in data) { + const count = data[lemgram] + allLemgrams.push({ lemgram: lemgram, count: count }) + } + for (let klemgram of karpLemgrams) { + if (!data[klemgram]) { + allLemgrams.push({ lemgram: klemgram, count: 0 }) + } + } + deferred.resolve(allLemgrams) + }) }) - .catch((response) => deferred.resolve([])) + .catch(() => deferred.resolve([])) return deferred.promise }, getSenses(wf) { const deferred = $q.defer() - const args = { - q: wf, - resource: "saldom", - mode: "external", - } - $http({ method: "GET", - url: `${karpURL}/autocomplete`, - params: args, + url: `${karpURL}/query/saldom`, + params: { + q: wfQuery(wf), + path: "entry.lemgram", + }, }) - .then((response) => { - let { data } = response - if (data === null) { - return deferred.resolve([]) - } else { - let karpLemgrams = _.map( - data.hits.hits, - (entry) => entry._source.FormRepresentations[0].lemgram - ) - if (karpLemgrams.length === 0) { - deferred.resolve([]) - return - } + .then(({ data }) => { + if (data.total === 0) { + deferred.resolve([]) + return + } - karpLemgrams = karpLemgrams.slice(0, 100) + const karpLemgrams = data.hits.slice(0, 100) + if (karpLemgrams.length === 0) { + deferred.resolve([]) + return + } - const senseargs = { - q: `extended||and|lemgram|equals|${karpLemgrams.join("|")}`, - resource: "saldo", - show: "sense,primary", + $http({ + method: "GET", + url: `${karpURL}/query/saldo`, + params: { + q: + "or(" + + _.map(karpLemgrams, (lemgram) => `equals|lemgrams|${lemgram}`).join("||") + + ")", + path: "entry", size: 500, - } - - return $http({ - method: "GET", - url: `${karpURL}/minientry`, - params: senseargs, + }, + }) + .then(function ({ data }) { + const senses = _.map(data.hits, ({ senseID, primary }) => ({ + sense: senseID, + desc: primary, + })) + deferred.resolve(senses) }) - .then(function ({ data }) { - if (data.hits.total === 0) { - deferred.resolve([]) - return - } - const senses = _.map(data.hits.hits, (entry) => ({ - sense: entry._source.Sense[0].senseid, - desc: - entry._source.Sense[0].SenseRelations && - entry._source.Sense[0].SenseRelations.primary, - })) - deferred.resolve(senses) - }) - .catch((response) => deferred.resolve([])) - } + .catch(() => deferred.resolve([])) }) - .catch((response) => deferred.resolve([])) + .catch(() => deferred.resolve([])) return deferred.promise }, relatedWordSearch(lemgram) { const def = $q.defer() $http({ - url: `${karpURL}/minientry`, - method: "GET", + url: `${karpURL}/query/saldo`, params: { - q: `extended||and|lemgram|equals|${lemgram}`, - show: "sense", - resource: "saldo", + q: `equals|lemgrams|${lemgram}`, + path: "entry.senseID", }, - }).then(function ({ data }) { - if (data.hits.total === 0) { + }).then(({ data }) => { + if (data.total === 0) { def.resolve([]) } else { - const senses = _.map(data.hits.hits, (entry) => entry._source.Sense[0].senseid) - $http({ - url: `${karpURL}/minientry`, - method: "GET", + url: `${karpURL}/query/swefn`, params: { - q: `extended||and|LU|equals|${senses.join("|")}`, - show: "LU,sense", - resource: "swefn", + q: "and(" + _.map(data.hits, (sense) => `equals|LUs|${sense}`).join("||") + ")", + path: "entry", }, - }).then(function ({ data }) { - if (data.hits.total === 0) { - def.resolve([]) - } else { - const eNodes = _.map(data.hits.hits, (entry) => ({ - label: entry._source.Sense[0].senseid.replace("swefn--", ""), - words: entry._source.Sense[0].LU, - })) - - return def.resolve(eNodes) - } + }).then(({ data }) => { + const eNodes = _.map(data.hits, (entry) => ({ + label: entry.swefnID, + words: entry.LUs, + })) + def.resolve(eNodes) }) } }) - return def.promise }, } From 59dd037936ff14560a063c406647dfa665bdee7d Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 27 May 2024 14:08:37 +0200 Subject: [PATCH 02/99] docs(changelog): Restore 9.5.3 link --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f58e718cb..f431a7541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -213,6 +213,7 @@ [unreleased]: https://github.com/spraakbanken/korp-frontend/compare/master...dev [9.6.0]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.6.0 +[9.5.3]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.3 [9.5.2]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.2 [9.5.1]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.1 [9.5.0]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.0 From 7f263d3306de1976be668a3f582931db78aaf9ce Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 27 May 2024 17:00:00 +0200 Subject: [PATCH 03/99] fix(local): Restore "of" --- app/translations/locale-eng.json | 1 + app/translations/locale-swe.json | 1 + 2 files changed, 2 insertions(+) diff --git a/app/translations/locale-eng.json b/app/translations/locale-eng.json index 6e7a2eff8..4e0671c25 100644 --- a/app/translations/locale-eng.json +++ b/app/translations/locale-eng.json @@ -281,6 +281,7 @@ "corpselector_lastupdate": "Last update", "corpselector_supports": "Supports extended context.", "corpselector_limited": "The corpus is protected.", + "corpselector_of": " of ", "corpselector_year": "Tokens in material dated", "corpselector_undated": "Tokens in undated material", "corpselector_all": "Available", diff --git a/app/translations/locale-swe.json b/app/translations/locale-swe.json index ea1ad5e71..1b3704d26 100644 --- a/app/translations/locale-swe.json +++ b/app/translations/locale-swe.json @@ -281,6 +281,7 @@ "corpselector_lastupdate": "Senast uppdaterad", "corpselector_supports": "Stödjer utökad kontextvisning.", "corpselector_limited": "Korpusen är skyddad.", + "corpselector_of": " av ", "corpselector_year": "Antal token i material daterat", "corpselector_undated": "Antal token i odaterat material", "corpselector_all": "Tillgängligt", From 5515f8054536c6917a807db100772a2c85e98fa3 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 28 May 2024 14:30:02 +0200 Subject: [PATCH 04/99] refactor: Clean up locale fetching --- app/scripts/data_init.js | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/app/scripts/data_init.js b/app/scripts/data_init.js index 22d99ad47..bed867b5a 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.js @@ -10,41 +10,27 @@ import { httpConfAddMethodFetch } from "@/util" // TODO it would be better only to load additional languages when there is a language change async function initLocales() { - const packages = ["locale", "corpora"] - const prefix = "translations" const locData = {} - const defs = [] - for (let langObj of settings["languages"]) { + for (const langObj of settings["languages"]) { const lang = langObj.value locData[lang] = {} - for (let pkg of packages) { - let file = pkg + "-" + lang + ".json" - file = prefix + "/" + file - const def = new Promise((resolve) => { - fetch(file) - .then((response) => { - if (response.status >= 300) { - throw new Error() - } - response.json().then((data) => { - _.extend(locData[lang], data) - }) - resolve() - }) - .catch(() => { - resolve() - console.log("No language file: ", file) - }) - }) + for (const pkg of ["locale", "corpora"]) { + const file = `translations/${pkg}-${lang}.json` + const def = fetch(file) + .then(async (response) => { + if (response.status >= 300) throw new Error() + const data = await response.json() + Object.assign(locData[lang], data) + }) + .catch(() => { + console.log("No language file: ", file) + }) defs.push(def) } } - for (const def of defs) { - await def - } - + await Promise.all(defs) window.loc_data = locData return locData } From 20a0ccfd9a9c42b8bb754f0a919e18808221a218 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 28 May 2024 20:07:48 +0200 Subject: [PATCH 05/99] refactor: Replace Raphael with Chart.js for pie chart --- CHANGELOG.md | 6 + app/index.js | 1 - .../components/corpus-distribution-chart.ts | 65 ++++ .../corpus_chooser/corpus-time-graph.ts | 4 +- app/scripts/components/statistics.js | 106 ++----- app/scripts/pie-widget.js | 290 ------------------ app/translations/locale-eng.json | 5 +- app/translations/locale-swe.json | 5 +- package.json | 2 - yarn.lock | 17 - 10 files changed, 99 insertions(+), 402 deletions(-) create mode 100644 app/scripts/components/corpus-distribution-chart.ts delete mode 100644 app/scripts/pie-widget.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f431a7541..eab7df77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Changed + +- Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics + ## [9.6.0] - 2024-05-27 ### Added diff --git a/app/index.js b/app/index.js index 97d47787f..2f057c617 100644 --- a/app/index.js +++ b/app/index.js @@ -56,7 +56,6 @@ require("angular-filter/index.js") require("./lib/jquery.tooltip.pack.js") -require("./scripts/pie-widget.js") require("./scripts/widgets.js") require("./scripts/main.js") require("./scripts/app.js") diff --git a/app/scripts/components/corpus-distribution-chart.ts b/app/scripts/components/corpus-distribution-chart.ts new file mode 100644 index 000000000..b72613988 --- /dev/null +++ b/app/scripts/components/corpus-distribution-chart.ts @@ -0,0 +1,65 @@ +/** @format */ +import angular, { IController, IRootScopeService } from "angular" +import { Chart } from "chart.js" +import { html } from "@/util" + +const defaultMode: Mode = "relative" + +angular.module("korpApp").component("corpusDistributionChart", { + template: html` + + `, + bindings: { + row: "<", + }, + controller: [ + "$rootScope", + function ($rootScope: IRootScopeService) { + const $ctrl = this as CorpusDistributionChartController + let chart: Chart<"pie"> + + const getValues = (mode: Mode) => $ctrl.row.map((corpus) => corpus.values[mode == "relative" ? 1 : 0]) + + $ctrl.$onInit = () => { + chart = new Chart("distribution-chart", { + type: "pie", + data: { + labels: $ctrl.row.map((corpus) => corpus.title), + datasets: [{ data: getValues(defaultMode) }], + }, + options: { + locale: $rootScope["lang"], + plugins: { + legend: { + display: false, + }, + }, + }, + }) + + setTimeout(() => { + const radioList = ($("#statistics_switch") as any).radioList({ + selected: defaultMode, + change: () => { + const mode = radioList.radioList("getSelected").attr("data-mode") + chart.data.datasets[0].data = getValues(mode) + chart.update() + }, + }) + }) + } + }, + ], +}) + +type CorpusDistributionChartController = IController & { + row: { title: string; values: [number, number] }[] +} + +type Mode = "relative" | "absolute" diff --git a/app/scripts/components/corpus_chooser/corpus-time-graph.ts b/app/scripts/components/corpus_chooser/corpus-time-graph.ts index 3ce8c65a2..266589920 100644 --- a/app/scripts/components/corpus_chooser/corpus-time-graph.ts +++ b/app/scripts/components/corpus_chooser/corpus-time-graph.ts @@ -2,7 +2,7 @@ import angular, { IRootScopeService } from "angular" import range from "lodash/range" import { Chart } from "chart.js/auto" -import { getLang, loc } from "@/i18n" +import { loc } from "@/i18n" import { calculateYearTicks, getSeries, @@ -94,7 +94,7 @@ angular.module("korpApp").component("corpusTimeGraph", { minBarLength: 2, }, }, - locale: getLang(), + locale: $rootScope["lang"], plugins: { legend: { display: false, diff --git a/app/scripts/components/statistics.js b/app/scripts/components/statistics.js index 59f0262d8..dc34af6ba 100644 --- a/app/scripts/components/statistics.js +++ b/app/scripts/components/statistics.js @@ -1,12 +1,12 @@ /** @format */ import angular from "angular" import _ from "lodash" -import "components-jqueryui/ui/widgets/dialog.js" import settings from "@/settings" -import { formatRelativeHits, html } from "@/util" +import { html } from "@/util" import { loc, locObj } from "@/i18n" import { getCqp } from "../../config/statistics_config.js" import { expandOperators } from "@/cqp_parser/cqp.js" +import "@/components/corpus-distribution-chart" angular.module("korpApp").component("statistics", { template: html` @@ -166,15 +166,18 @@ angular.module("korpApp").component("statistics", { }, controller: [ "$rootScope", + "$scope", + "$uibModal", "searches", "backend", - function ($rootScope, searches, backend) { + function ($rootScope, $scope, $uibModal, searches, backend) { const $ctrl = this $ctrl.noRowsError = false $ctrl.doSort = true $ctrl.sortColumn = null $ctrl.mapRelative = true + $scope.row = null $ctrl.$onInit = () => { $(window).resize( @@ -446,88 +449,27 @@ angular.module("korpApp").component("statistics", { } function showPieChart(rowId) { - let statsSwitchInstance - const pieChartCurrentRowId = rowId + const row = $ctrl.data.find((row) => row.rowId == rowId) - const getDataItems = (rowId, valueType) => { - const dataItems = [] - if (valueType === "relative") { - valueType = 1 - } else { - valueType = 0 - } - for (let row of $ctrl.data) { - if (row.rowId === rowId) { - for (let corpus of $ctrl.searchParams.corpora) { - const freq = row[corpus + "_value"][valueType] - const freqStr = formatRelativeHits(freq.toString(), $rootScope.lang) - const title = locObj(settings.corpora[corpus.toLowerCase()]["title"]) - dataItems.push({ - value: freq, - caption: `${title}: ${freqStr}`, - shape_id: rowId, - }) - } - break - } - } - return dataItems - } + $scope.rowData = $ctrl.searchParams.corpora.map((corpus) => ({ + title: locObj(settings.corpora[corpus.toLowerCase()]["title"]), + values: row[corpus + "_value"], // [absolute, relative] + })) - $("#dialog").remove() - - const relHitsString = loc("statstable_relfigures_hits") - $("
") - .appendTo("body") - .append( - html`
- -
-

- ${relHitsString} -

-
` - ) - .dialog({ - width: 400, - height: 500, - close() { - return $("#pieDiv").remove() - }, - }) - .css("opacity", 0) - .parent() - .find(".ui-dialog-title") - .localeKey("statstable_hitsheader_lemgram") - - $("#dialog").fadeTo(400, 1) - $("#dialog").find("a").blur() // Prevents the focus of the first link in the "dialog" - - const stats2Instance = $("#chartFrame").pie_widget({ - container_id: "chartFrame", - data_items: getDataItems(rowId, "relative"), - }) - statsSwitchInstance = $("#statistics_switch").radioList({ - change: () => { - let loc - const typestring = statsSwitchInstance.radioList("getSelected").attr("data-mode") - stats2Instance.pie_widget("newData", getDataItems(pieChartCurrentRowId, typestring)) - if (typestring === "absolute") { - loc = "statstable_absfigures_hits" - } else { - loc = "statstable_relfigures_hits" - } - return $("#hitsDescription").localeKey(loc) - }, - selected: "relative", + const modal = $uibModal.open({ + template: html` + + + `, + scope: $scope, + windowClass: "!text-base", }) + // Ignore rejection from closing the modal + modal.result.catch(() => {}) } $ctrl.resizeGrid = (resizeColumns) => { diff --git a/app/scripts/pie-widget.js b/app/scripts/pie-widget.js deleted file mode 100644 index e20fda80c..000000000 --- a/app/scripts/pie-widget.js +++ /dev/null @@ -1,290 +0,0 @@ -/** @format */ -import Raphael from "raphael" - -const pie_widget = { - options: { - container_id: "", - data_items: "", - diameter: 300, - sort_desc: true, - offset_x: 0, - offset_y: 0, - }, - - shapes: [], - canvas: null, - _create() { - this.shapes = this.initDiagram(this.options.data_items) - }, - - resizeDiagram(newDiameter) { - if (newDiameter >= 150) { - $(this.container_id).width(newDiameter + 60) - $(this.container_id).height(newDiameter + 60) - this.options.diameter = newDiameter - this.newData(this.options.data_items, false) - } - }, - - newData(data_items) { - this.canvas.remove() - this.options.data_items = data_items - this.shapes = this.initDiagram(data_items) - }, - - _constructSVGPath(highlight, circleTrack, continueArc, offsetX, offsetY, radius, part) { - let str = `M${offsetX + radius},${offsetY + radius}` - if (part === 1.0) { - // Special case, make two arc halves - str += `\nm -${radius}, 0\n` - str += `a ${radius},${radius} 0 1,0 ${radius * 2},0` - str += `a ${radius},${radius} 0 1,0 -${radius * 2},0` - str += " Z" - return str - } else { - let lineToArcX, lineToArcY - const radians = (part + circleTrack["accumulatedArc"]) * 2 * Math.PI - str += " L" - if (continueArc) { - lineToArcX = circleTrack["lastArcX"] - lineToArcY = circleTrack["lastArcY"] - } else { - lineToArcX = offsetX + radius - lineToArcY = offsetY - } - if (highlight) { - // make piece stand out - let newX, newY - const degree = Math.acos((lineToArcY - offsetY - radius) / radius) - if (lineToArcX - offsetX - radius < 0) { - newX = radius * 1.1 * Math.sin(degree) - newY = radius * 1.1 * Math.cos(degree) - } else { - newX = -(radius * 1.1) * Math.sin(degree) - newY = radius * 1.1 * Math.cos(degree) - } - lineToArcX = offsetX + radius - newX - lineToArcY = offsetY + radius + newY - } - str += lineToArcX + "," + lineToArcY - if (highlight) { - str += ` A${radius * 1.1},${radius * 1.1}` - } else { - str += ` A${radius},${radius}` - } - str += " 0 " - if (part > 0.5) { - // Makes the arc always go the long way instead of taking a shortcut - str += "1" - } else { - str += "0" - } - str += ",1 " - let x2 = offsetX + radius + Math.sin(radians) * radius - let y2 = offsetY + radius - Math.cos(radians) * radius - if (!highlight) { - circleTrack["lastArcX"] = x2 - circleTrack["lastArcY"] = y2 - } - if (highlight) { - const endDegree = Math.acos((y2 - offsetY - radius) / radius) - if (x2 < offsetX + radius) { - x2 = offsetX + radius - radius * 1.1 * Math.sin(endDegree) - y2 = offsetX + radius + radius * 1.1 * Math.cos(endDegree) - } else { - x2 = offsetX + radius + radius * 1.1 * Math.sin(endDegree) - y2 = offsetX + radius + radius * 1.1 * Math.cos(endDegree) - } - } - str += x2 + "," + y2 - if (!highlight) { - if (continueArc) { - circleTrack["accumulatedArc"] += part - } else { - circleTrack["accumulatedArc"] = part - } - } - str += " Z" - return str - } - }, - - _makeSVGPie(pieparts, radius) { - const nowthis = this - const mouseEnter = function (event) { - this.attr({ - opacity: 0.7, - cursor: "move", - }) - nowthis._highlight(this) - // Fire callback "enteredArc": - const callback = nowthis.options.enteredArc - if ($.isFunction(callback)) { - callback(nowthis.eventArc(this)) - } - } - - const mouseExit = function (event) { - nowthis._deHighlight(this) - // Fire callback "exitedArc": - const callback = nowthis.options.exitedArc - if ($.isFunction(callback)) { - callback(nowthis.eventArc(this)) - } - } - - const r = Raphael(this.options.container_id) - this.canvas = r - const pieTrack = [] - pieTrack["accumulatedArc"] = 0 - pieTrack["lastArcX"] = 0 - pieTrack["lastArcY"] = 0 - const SVGArcObjects = [] - let first = true - for (let fvalue of pieparts) { - const partOfTotal = fvalue["share"] - if (partOfTotal !== 0) { - const bufferPieTrack = [] - bufferPieTrack["accumulatedArc"] = pieTrack["accumulatedArc"] - bufferPieTrack["lastArcX"] = pieTrack["lastArcX"] - bufferPieTrack["lastArcY"] = pieTrack["lastArcY"] - const origPath = nowthis._constructSVGPath(false, pieTrack, !first, 30, 30, radius, partOfTotal) - const newPiece = r.path(origPath) - const newPieceDOMNode = newPiece.node - newPieceDOMNode["continue"] = !first - newPieceDOMNode["offsetX"] = 30 - newPieceDOMNode["offsetY"] = 30 - newPieceDOMNode["radius"] = radius - newPieceDOMNode["shape_id"] = fvalue["shape_id"] - newPieceDOMNode["caption"] = fvalue["caption"] - newPieceDOMNode["part"] = partOfTotal - newPieceDOMNode["track"] = bufferPieTrack - newPieceDOMNode["origpath"] = origPath - $(newPieceDOMNode).tooltip({ - delay: 80, - bodyHandler() { - return this.caption || "" - }, - }) - - newPiece.mouseover(mouseEnter) - newPiece.mouseout(mouseExit) - newPiece.click(function (event) { - // Fire callback "clickedArc": - const callback = nowthis.options.clickedArc - if ($.isFunction(callback)) { - callback(nowthis.eventArc(this)) - } - }) - - newPiece.attr({ fill: fvalue["color"] }) - newPiece.attr({ stroke: "white" }) - newPiece.attr({ opacity: 0.7 }) - newPiece.attr({ "stroke-linejoin": "miter" }) - SVGArcObjects.push(newPiece) - if (first) { - first = false - } - } - } - - return SVGArcObjects - }, - - _sortDataDescending(indata) { - const sortedData = indata.slice(0) - return sortedData.sort((a, b) => b["value"] - a["value"]) - }, - - initDiagram(indata) { - // Creates the diagram from the data in <> formatting like <>, returns array of the SVG arc objects - // <> is an array with "value","id" and "caption" - // "value" is the numeric value of the item, "id" is to connect the SVG arc item to other stuff, and "caption" is to add tooltip etc. - let fvalue - const defaultOptions = { - colors: [ - "90-#C0C7E0-#D0D7F0:50-#D0D7F0", - "90-#E7C1D4-#F7D1E4:50-#F7D1E4", - "90-#DDECC5-#EDFCD5:50-#EDFCD5", - "90-#EFE3C8-#FFF3D8:50-#FFF3D8", - "90-#BADED8-#CAEEE8:50-#CAEEE8", - "90-#EFCDC8-#FFDDD8:50-#FFDDD8", - ], - } - - const sortedData = this.options.sort_desc ? this._sortDataDescending(indata) : indata - - // Calculate the sum of the array - let total = 0 - for (fvalue of sortedData) { - total += fvalue["value"] - } - - // Piece of cake! - const piePieceDefinitions = [] - let acc = 0 - let colorCount = 0 - for (fvalue of sortedData) { - const relative = fvalue["value"] / total - acc += fvalue["value"] - const itemID = fvalue["shape_id"] - const itemCaption = fvalue["caption"] - piePieceDefinitions.push({ - share: relative, - color: defaultOptions["colors"][colorCount], - shape_id: itemID, - caption: itemCaption, - }) - colorCount = (colorCount + 1) % defaultOptions["colors"].length - } - return this._makeSVGPie(piePieceDefinitions, this.options.diameter * 0.5) - }, - - _highlight(item) { - const n = item.node - const newpath = this._constructSVGPath( - true, - n["track"], - n["continue"], - n["offsetX"], - n["offsetY"], - n["radius"], - n["part"] - ) - return item.attr({ path: newpath }) - }, - - _deHighlight(item) { - const n = item.node - return item.animate({ path: n["origpath"] }, 400, "elastic") - }, - - highlightArc(itemID) { - for (let shape in this.shapes) { - const n = this.shapes[shape].node - if ((n && n.shape_id) === itemID) { - // Highlight the arc - this._highlight(this.shapes[shape]) - return true - } - } - }, - - deHighlightArc(itemID) { - for (let shape in this.shapes) { - const n = this.shapes[shape].node - if ((n && n.shape_id) === itemID) { - // Highlight the arc - this._deHighlight(this.shapes[shape]) - return true - } - } - }, - eventArc(item) { - // Return the clicked arc's ID - return item.node["shape_id"] - }, -} - -let widget = require("components-jqueryui/ui/widget") -widget("hp.pie_widget", pie_widget) // create the widget diff --git a/app/translations/locale-eng.json b/app/translations/locale-eng.json index 4e0671c25..3bb388238 100644 --- a/app/translations/locale-eng.json +++ b/app/translations/locale-eng.json @@ -292,10 +292,7 @@ "statstable_absfreq": "absolute frequency", "statstable_absfigures": "Absolute frequencies", "statstable_relfigures": "Relative frequencies", - "statstable_absfigures_hits": "Hits per corpus, absolute frequencies", - "statstable_relfigures_hits": "Hits per corpus, relative frequencies.", - "statstable_hitsheader": "Hits for ", - "statstable_hitsheader_lemgram": "Hits", + "statstable_distribution": "Hits per corpus", "statstable_exp_csv": "CSV (semicolon separated values)", "statstable_exp_tsv": "TSV (tab separated values)", "statstable_export": "Export", diff --git a/app/translations/locale-swe.json b/app/translations/locale-swe.json index 1b3704d26..12af4da5a 100644 --- a/app/translations/locale-swe.json +++ b/app/translations/locale-swe.json @@ -292,10 +292,7 @@ "statstable_absfreq": "absolut frekvens", "statstable_absfigures": "Absoluta frekvenser", "statstable_relfigures": "Relativa frekvenser", - "statstable_absfigures_hits": "Träffar per korpus, absoluta frekvenser.", - "statstable_relfigures_hits": "Träffar per korpus, relativa frekvenser.", - "statstable_hitsheader": "Träffar för ", - "statstable_hitsheader_lemgram": "Träffar", + "statstable_distribution": "Träffar per korpus", "statstable_exp_csv": "CSV (semikolonseparerade värden)", "statstable_exp_tsv": "TSV (tabseparerade värden)", "statstable_export": "Exportera", diff --git a/package.json b/package.json index 7c1b68dae..de7e6a834 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "leaflet.markercluster": "^1.5.3", "lodash": "^4.17.21", "moment": "2.29.4", - "raphael": "2.3.0", "rickshaw": "1.7.1", "slickgrid": "3.0.3", "tailwindcss": "3.2.4", @@ -33,7 +32,6 @@ "@types/jquery": "^3.5.29", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.118", - "@types/raphael": "^2.3.9", "@types/rickshaw": "^0.0.31", "autoprefixer": "^10.2.4", "chromedriver": "^122.0.4", diff --git a/yarn.lock b/yarn.lock index e8da67a21..e16cffe4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -305,11 +305,6 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/raphael@^2.3.9": - version "2.3.9" - resolved "https://registry.yarnpkg.com/@types/raphael/-/raphael-2.3.9.tgz#d53bb8930431524f42987a8a19815c0d42a61eb5" - integrity sha512-K1dZwoLNvEN+mvleFU/t2swG9Z4SE5Vub7dA5wDYojH0bVTQ8ZAP+lNsl91t1njdu/B+roSEL4QXC67I7Hpiag== - "@types/retry@0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -1653,11 +1648,6 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== -eve-raphael@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" - integrity sha512-jrxnPsCGqng1UZuEp9DecX/AuSyAszATSjf4oEcRxvfxa1Oux4KkIPKBAAWWnpdwfARtr+Q0o9aPYWjsROD7ug== - eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -3460,13 +3450,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raphael@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/raphael/-/raphael-2.3.0.tgz#eabeb09dba861a1d4cee077eaafb8c53f3131f89" - integrity sha512-w2yIenZAQnp257XUWGni4bLMVxpUpcIl7qgxEgDIXtmSypYtlNxfXWpOBxs7LBTps5sDwhRnrToJrMUrivqNTQ== - dependencies: - eve-raphael "0.5.0" - raw-body@2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" From 9fc0bdf6388bd11ffeae92926bf4afc77a6e50a9 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 13:42:16 +0200 Subject: [PATCH 06/99] refactor: Remove c alias for console --- CHANGELOG.md | 1 + app/index.js | 2 -- app/scripts/components/auth/basic_auth.js | 2 +- app/scripts/components/extended/cqp-term.js | 2 +- app/scripts/components/extended/extended-parallel.js | 2 +- app/scripts/components/extended/extended-standard.js | 4 ++-- app/scripts/components/sidebar.js | 2 +- app/scripts/components/statistics.js | 2 +- app/scripts/components/trend-diagram.js | 4 ++-- app/scripts/controllers/comparison_controller.js | 2 +- app/scripts/controllers/kwic_controller.js | 4 ++-- app/scripts/controllers/statistics_controller.js | 8 ++++---- app/scripts/controllers/word_picture_controller.js | 2 +- app/scripts/cqp_parser/CQPParser.js | 2 -- app/scripts/cqp_parser/CQPParser.peggy | 2 -- app/scripts/extended.js | 2 +- app/scripts/filter_directives.js | 2 +- app/scripts/jq_extensions.js | 2 +- app/scripts/main.js | 4 ++-- 19 files changed, 23 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eab7df77b..e900476c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics +- Removed the global `c` alias for `console` ## [9.6.0] - 2024-05-27 diff --git a/app/index.js b/app/index.js index 2f057c617..abc5e93bb 100644 --- a/app/index.js +++ b/app/index.js @@ -28,8 +28,6 @@ require("angular") require("jquerylocalize") -window.c = console - try { // modes-files are optional and have customizing code require(`modes/${currentMode}_mode.js`) diff --git a/app/scripts/components/auth/basic_auth.js b/app/scripts/components/auth/basic_auth.js index b814ad7d1..23d9490f5 100644 --- a/app/scripts/components/auth/basic_auth.js +++ b/app/scripts/components/auth/basic_auth.js @@ -70,7 +70,7 @@ const login = (usr, pass, saveLogin) => { return dfd.resolve(data) }) .fail(function (xhr, status, error) { - c.log("auth fail", arguments) + console.log("auth fail", arguments) return dfd.reject() }) diff --git a/app/scripts/components/extended/cqp-term.js b/app/scripts/components/extended/cqp-term.js index ea92f7df9..25e48497c 100644 --- a/app/scripts/components/extended/cqp-term.js +++ b/app/scripts/components/extended/cqp-term.js @@ -106,7 +106,7 @@ angular.module("korpApp").component("extendedCqpTerm", { } let confObj = ctrl.typeMapping && ctrl.typeMapping[type] if (!confObj) { - c.log("confObj missing", type, ctrl.typeMapping) + console.log("confObj missing", type, ctrl.typeMapping) return } diff --git a/app/scripts/components/extended/extended-parallel.js b/app/scripts/components/extended/extended-parallel.js index 63c1967da..8525fdea3 100644 --- a/app/scripts/components/extended/extended-parallel.js +++ b/app/scripts/components/extended/extended-parallel.js @@ -114,7 +114,7 @@ angular.module("korpApp").component("extendedParallel", { try { return expandOperators(cqp) } catch (e) { - c.log("parallel cqp parsing error", e) + console.log("parallel cqp parsing error", e) return cqp } } diff --git a/app/scripts/components/extended/extended-standard.js b/app/scripts/components/extended/extended-standard.js index 03b2c2fe5..be9dc76a1 100644 --- a/app/scripts/components/extended/extended-standard.js +++ b/app/scripts/components/extended/extended-standard.js @@ -95,8 +95,8 @@ angular.module("korpApp").component("extendedStandard", { try { updateExtendedCQP() } catch (e) { - c.log("Failed to parse CQP", ctrl.cqp) - c.log("Error", e) + console.log("Failed to parse CQP", ctrl.cqp) + console.log("Error", e) } ctrl.validateFreeOrder() diff --git a/app/scripts/components/sidebar.js b/app/scripts/components/sidebar.js index d4e57b71b..f82f62a15 100644 --- a/app/scripts/components/sidebar.js +++ b/app/scripts/components/sidebar.js @@ -222,7 +222,7 @@ angular.module("korpApp").component("sidebar", { posItems.push([key, output]) } } catch (e) { - c.log("failed to render custom attribute", e) + console.log("failed to render custom attribute", e) } } return [posItems, structItems] diff --git a/app/scripts/components/statistics.js b/app/scripts/components/statistics.js index dc34af6ba..718564bd4 100644 --- a/app/scripts/components/statistics.js +++ b/app/scripts/components/statistics.js @@ -404,7 +404,7 @@ angular.module("korpApp").component("statistics", { const selectedAttributes = _.filter($ctrl.mapAttributes, "selected") if (selectedAttributes.length > 1) { - c.log("Warning: more than one selected attribute, choosing first") + console.log("Warning: more than one selected attribute, choosing first") } const selectedAttribute = selectedAttributes[0] diff --git a/app/scripts/components/trend-diagram.js b/app/scripts/components/trend-diagram.js index f531da05e..e4f9ea4be 100644 --- a/app/scripts/components/trend-diagram.js +++ b/app/scripts/components/trend-diagram.js @@ -654,7 +654,7 @@ angular.module("korpApp").component("trendDiagram", { try { abs_y = series.abs_data[i].y } catch (e) { - c.log("i", i, x) + console.log("i", i, x) } const rel = series.name + ": " + formattedY @@ -761,7 +761,7 @@ angular.module("korpApp").component("trendDiagram", { $timeout(() => renderGraph(Rickshaw, graphData, cqp, labelMapping, currentZoom, showTotal)) } catch (e) { $timeout(() => { - c.error("graph crash", e) + console.error("graph crash", e) $ctrl.localUpdateLoading(false) $ctrl.error = true }) diff --git a/app/scripts/controllers/comparison_controller.js b/app/scripts/controllers/comparison_controller.js index ae164ba6b..d75bf30a0 100644 --- a/app/scripts/controllers/comparison_controller.js +++ b/app/scripts/controllers/comparison_controller.js @@ -80,7 +80,7 @@ korpApp.directive("compareCtrl", () => ({ const attrVal = token[attrI] if (attrKey.includes("_.")) { - c.log("error, attribute key contains _.") + console.log("error, attribute key contains _.") } const attribute = attributes[attrKey] diff --git a/app/scripts/controllers/kwic_controller.js b/app/scripts/controllers/kwic_controller.js index 5f2e76a16..999c48d76 100644 --- a/app/scripts/controllers/kwic_controller.js +++ b/app/scripts/controllers/kwic_controller.js @@ -185,9 +185,9 @@ export class KwicCtrl { req.fail((jqXHR, status, errorThrown) => { $timeout(() => { - c.log("kwic fail") + console.log("kwic fail") if (s.ignoreAbort) { - c.log("stats ignoreabort") + console.log("stats ignoreabort") return } s.loading = false diff --git a/app/scripts/controllers/statistics_controller.js b/app/scripts/controllers/statistics_controller.js index 506741b56..bcde10860 100644 --- a/app/scripts/controllers/statistics_controller.js +++ b/app/scripts/controllers/statistics_controller.js @@ -101,14 +101,14 @@ korpApp.directive("statsResultCtrl", () => ({ function (textStatus, err) { const arguments_ = arguments $timeout(() => { - c.log("fail", arguments_) - c.log( + console.log("fail", arguments_) + console.log( "stats fail", s.loading, _.map(s.proxy.pendingRequests, (item) => item.readyState) ) if (s.ignoreAbort) { - c.log("stats ignoreabort") + console.log("stats ignoreabort") return } s.loading = false @@ -123,7 +123,7 @@ korpApp.directive("statsResultCtrl", () => ({ } s.resultError = (data) => { - c.error("json fetch error: ", data) + console.error("json fetch error: ", data) s.loading = false s.resetView() s.error = true diff --git a/app/scripts/controllers/word_picture_controller.js b/app/scripts/controllers/word_picture_controller.js index 1378b360a..8b6918836 100644 --- a/app/scripts/controllers/word_picture_controller.js +++ b/app/scripts/controllers/word_picture_controller.js @@ -81,7 +81,7 @@ korpApp.directive("wordpicCtrl", () => ({ }) def.fail((jqXHR, status, errorThrown) => { $timeout(() => { - c.log("def fail", status) + console.log("def fail", status) if (s.ignoreAbort) { return } diff --git a/app/scripts/cqp_parser/CQPParser.js b/app/scripts/cqp_parser/CQPParser.js index 23c7d9a9c..579edc065 100644 --- a/app/scripts/cqp_parser/CQPParser.js +++ b/app/scripts/cqp_parser/CQPParser.js @@ -1920,8 +1920,6 @@ function peg$parse(input, options) { return output } - var c = console - peg$result = peg$startRuleFunction(); if (options.peg$library) { diff --git a/app/scripts/cqp_parser/CQPParser.peggy b/app/scripts/cqp_parser/CQPParser.peggy index 8a576bf3c..60d5baf33 100644 --- a/app/scripts/cqp_parser/CQPParser.peggy +++ b/app/scripts/cqp_parser/CQPParser.peggy @@ -10,8 +10,6 @@ }) return output } - - var c = console } diff --git a/app/scripts/extended.js b/app/scripts/extended.js index 30d2affbf..cea063a07 100644 --- a/app/scripts/extended.js +++ b/app/scripts/extended.js @@ -84,7 +84,7 @@ const selectController = (autocomplete) => [ $scope.input = _.includes(data, $scope.input) ? $scope.input : $scope.dataset[0][0] } }, - () => c.log("struct_values error") + () => console.log("struct_values error") ) } diff --git a/app/scripts/filter_directives.js b/app/scripts/filter_directives.js index 2cd866aa5..5193609f4 100644 --- a/app/scripts/filter_directives.js +++ b/app/scripts/filter_directives.js @@ -208,7 +208,7 @@ korpApp.factory("globalFilterService", [ } else if (_.every(values, (val) => Number.isInteger(val))) { return _.reduce(values, (a, b) => a + b, 0) } else { - c.error("Cannot merge objects a and b") + console.error("Cannot merge objects a and b") } } diff --git a/app/scripts/jq_extensions.js b/app/scripts/jq_extensions.js index c9007bac5..2b5de8bac 100644 --- a/app/scripts/jq_extensions.js +++ b/app/scripts/jq_extensions.js @@ -100,7 +100,7 @@ $.fn.localeKey = function(key) { // Creating a jQuery plugin: $.generateFile = function(script, data) { - c.log("generateFile", script, data); + console.log("generateFile", script, data); data = data || {}; // Creating a 1 by 1 px invisible iframe: diff --git a/app/scripts/main.js b/app/scripts/main.js index f9c51026b..b957941f6 100644 --- a/app/scripts/main.js +++ b/app/scripts/main.js @@ -40,13 +40,13 @@ function initApp() { try { angular.bootstrap(document, ["korpApp"]) } catch (error) { - c.error(error) + console.error(error) } try { updateSearchHistory() } catch (error1) { - c.error("ERROR setting corpora from location", error1) + console.error("ERROR setting corpora from location", error1) } if (process.env.ENVIRONMENT == "staging") { From a3707385a404b90ed984a65a88f109f43a25a739 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 13:48:13 +0200 Subject: [PATCH 07/99] refactor: Import CSV lib directly --- CHANGELOG.md | 1 + app/index.js | 1 - app/scripts/components/statistics.js | 1 + app/scripts/components/trend-diagram.js | 1 + app/scripts/kwic_download.js | 1 + 5 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e900476c0..df83b7c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Removed the global `c` alias for `console` +- Removed global `CSV`, import the library instead ## [9.6.0] - 2024-05-27 diff --git a/app/index.js b/app/index.js index abc5e93bb..e0d9c9d9d 100644 --- a/app/index.js +++ b/app/index.js @@ -45,7 +45,6 @@ require("slickgrid/slick.interactions.js") require("./scripts/jq_extensions.js") window.moment = require("moment") -window.CSV = require("comma-separated-values/csv") require("leaflet") require("leaflet.markercluster") diff --git a/app/scripts/components/statistics.js b/app/scripts/components/statistics.js index 718564bd4..d6bff7503 100644 --- a/app/scripts/components/statistics.js +++ b/app/scripts/components/statistics.js @@ -1,6 +1,7 @@ /** @format */ import angular from "angular" import _ from "lodash" +import CSV from "comma-separated-values/csv" import settings from "@/settings" import { html } from "@/util" import { loc, locObj } from "@/i18n" diff --git a/app/scripts/components/trend-diagram.js b/app/scripts/components/trend-diagram.js index e4f9ea4be..b937e3800 100644 --- a/app/scripts/components/trend-diagram.js +++ b/app/scripts/components/trend-diagram.js @@ -1,6 +1,7 @@ /** @format */ import angular from "angular" import _ from "lodash" +import CSV from "comma-separated-values/csv" import settings from "@/settings" import graphProxyFactory from "@/backend/graph-proxy" import { expandOperators } from "@/cqp_parser/cqp" diff --git a/app/scripts/kwic_download.js b/app/scripts/kwic_download.js index e3fb2ac4a..0309ad1cb 100644 --- a/app/scripts/kwic_download.js +++ b/app/scripts/kwic_download.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" const korpApp = angular.module("korpApp") From 2a25d5332492be8fe8db8d1bf7bc8eb7ccd751ad Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 14:15:44 +0200 Subject: [PATCH 08/99] refactor: Import moment lib directly --- CHANGELOG.md | 2 +- app/index.js | 2 -- app/scripts/components/trend-diagram.js | 1 + app/scripts/corpus_listing.js | 1 + app/scripts/cqp_parser/cqp.js | 1 + app/scripts/extended.js | 1 + app/scripts/kwic_download.js | 1 + app/scripts/trend_diagram/trend_util.js | 2 ++ app/scripts/video_controllers.js | 2 ++ 9 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df83b7c2c..9f309176b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Removed the global `c` alias for `console` -- Removed global `CSV`, import the library instead +- Removed globals `CSV` and `moment`, import the libraries instead ## [9.6.0] - 2024-05-27 diff --git a/app/index.js b/app/index.js index e0d9c9d9d..78f98871d 100644 --- a/app/index.js +++ b/app/index.js @@ -44,8 +44,6 @@ require("slickgrid/slick.interactions.js") require("./scripts/jq_extensions.js") -window.moment = require("moment") - require("leaflet") require("leaflet.markercluster") require("leaflet-providers") diff --git a/app/scripts/components/trend-diagram.js b/app/scripts/components/trend-diagram.js index b937e3800..b8c8faa72 100644 --- a/app/scripts/components/trend-diagram.js +++ b/app/scripts/components/trend-diagram.js @@ -1,6 +1,7 @@ /** @format */ import angular from "angular" import _ from "lodash" +import moment from "moment" import CSV from "comma-separated-values/csv" import settings from "@/settings" import graphProxyFactory from "@/backend/graph-proxy" diff --git a/app/scripts/corpus_listing.js b/app/scripts/corpus_listing.js index d0163e24d..4a200a461 100644 --- a/app/scripts/corpus_listing.js +++ b/app/scripts/corpus_listing.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import moment from "moment" import settings from "@/settings" import { locationSearchGet } from "@/util" import { loc } from "@/i18n" diff --git a/app/scripts/cqp_parser/cqp.js b/app/scripts/cqp_parser/cqp.js index 2dadb2944..594d48041 100644 --- a/app/scripts/cqp_parser/cqp.js +++ b/app/scripts/cqp_parser/cqp.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import moment from "moment" import settings from "@/settings" import { parse } from "./CQPParser" diff --git a/app/scripts/extended.js b/app/scripts/extended.js index cea063a07..95512bc27 100644 --- a/app/scripts/extended.js +++ b/app/scripts/extended.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import moment from "moment" import settings from "@/settings" import { html, regescape, unregescape } from "@/util" import { loc, locAttribute } from "@/i18n" diff --git a/app/scripts/kwic_download.js b/app/scripts/kwic_download.js index 0309ad1cb..14113dfb2 100644 --- a/app/scripts/kwic_download.js +++ b/app/scripts/kwic_download.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import moment from "moment" import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" diff --git a/app/scripts/trend_diagram/trend_util.js b/app/scripts/trend_diagram/trend_util.js index 231893651..d5373ab70 100644 --- a/app/scripts/trend_diagram/trend_util.js +++ b/app/scripts/trend_diagram/trend_util.js @@ -1,4 +1,6 @@ /** @format */ +import moment from "moment" + export function getTimeCQP(time, zoom, coarseGranularity) { let timecqp const m = moment(time * 1000) diff --git a/app/scripts/video_controllers.js b/app/scripts/video_controllers.js index cb1d1d3e7..d3054de5b 100644 --- a/app/scripts/video_controllers.js +++ b/app/scripts/video_controllers.js @@ -1,4 +1,6 @@ /** @format */ +import moment from "moment" + const korpApp = angular.module("korpApp") korpApp.controller("VideoCtrl", [ From 24cb176a83a03ca62df82d5af09838441f6c54a1 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 14:47:37 +0200 Subject: [PATCH 09/99] fix(news): show on load --- CHANGELOG.md | 4 ++++ app/scripts/components/newsdesk.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f309176b..d7f07c0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Removed the global `c` alias for `console` - Removed globals `CSV` and `moment`, import the libraries instead +### Fixed + +- News were sometimes not shown immediately after fetch + ## [9.6.0] - 2024-05-27 ### Added diff --git a/app/scripts/components/newsdesk.ts b/app/scripts/components/newsdesk.ts index c706285e9..4f094c25d 100644 --- a/app/scripts/components/newsdesk.ts +++ b/app/scripts/components/newsdesk.ts @@ -45,6 +45,8 @@ angular.module("korpApp").component("newsdesk", { $scope.items = await fetchNews() // The watcher may not yet be in place when the fetch finishes. $ctrl.updateItemsFiltered() + // Since this is async, we need to tell AngularJS to notice new values. + $scope.$digest() } catch (error) { console.error("Error fetching news:", error) $scope.isEnabled = false From 735a97527cccaf6301029499cb29f080a4f58beb Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 15:56:39 +0200 Subject: [PATCH 10/99] refactor(locale): Move window.lang to $rootScope --- CHANGELOG.md | 1 + app/scripts/app.js | 16 +++++++--------- .../components/corpus_chooser/corpus-chooser.js | 2 +- .../components/corpus_chooser/info-box.js | 4 ++-- app/scripts/components/deptree/deptree.js | 2 +- app/scripts/components/frontpage.ts | 6 +++--- app/scripts/components/header.js | 4 ++-- app/scripts/components/kwic.js | 6 +++--- app/scripts/components/word-picture.js | 2 +- app/scripts/directives.js | 8 ++++---- app/scripts/filter_directives.js | 4 ++-- app/scripts/i18n/index.ts | 3 ++- app/scripts/statistics.ts | 2 +- app/scripts/util.ts | 6 ++++-- doc/frontend_devel.md | 2 +- 15 files changed, 35 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f07c0b7..662930b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Removed the global `c` alias for `console` +- Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) - Removed globals `CSV` and `moment`, import the libraries instead ### Fixed diff --git a/app/scripts/app.js b/app/scripts/app.js index 3fdf28cb0..83d5114e1 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -83,7 +83,6 @@ korpApp.run([ async function ($rootScope, $location, $locale, tmhDynamicLocale, tmhDynamicLocaleCache, $q, $timeout, $uibModal) { const s = $rootScope s._settings = settings - window.lang = s.lang = $location.search().lang || settings["default_language"] s.extendedCQP = null @@ -112,8 +111,7 @@ korpApp.run([ } // Update global variables - s.lang = lang - window.lang = lang + $rootScope["lang"] = lang // Trigger jQuery Localize $("body").localize() @@ -198,7 +196,7 @@ korpApp.run([ // access partly or fully denied to selected corpora if (settings.corpusListing.corpora.length == loginNeededFor.length) { s.openErrorModal({ - content: "{{'access_denied' | loc:lang}}", + content: "{{'access_denied' | loc:$root.lang}}", buttonText: "go_to_start", onClose: () => { window.location.href = window.location.href.split("?")[0] @@ -206,9 +204,9 @@ korpApp.run([ }) } else { s.openErrorModal({ - content: html`
{{'access_partly_denied' | loc:lang}}:
+ content: html`
{{'access_partly_denied' | loc:$root.lang}}:
${loginNeededHTML()}
-
{{'access_partly_denied_continue' | loc:lang}}
`, +
{{'access_partly_denied_continue' | loc:$root.lang}}
`, onClose: () => { const neededIds = loginNeededFor.map((corpus) => corpus.id) let newIds = selectedIds.filter((corpusId) => !neededIds.includes(corpusId)) @@ -222,7 +220,7 @@ korpApp.run([ } else { // login needed before access can be checked s.openErrorModal({ - content: html`{{'login_needed_for_corpora' | loc:lang}}:{{'login_needed_for_corpora' | loc:$root.lang}}:${loginNeededHTML()}`, onClose: () => { s.waitForLogin = true @@ -233,7 +231,7 @@ korpApp.run([ } else if (!selectedIds.every((r) => allCorpusIds.includes(r))) { // some corpora missing s.openErrorModal({ - content: `{{'corpus_not_available' | loc:lang}}`, + content: `{{'corpus_not_available' | loc:$root.lang}}`, onClose: () => { let newIds = selectedIds.filter((corpusId) => allCorpusIds.includes(corpusId)) if (newIds.length == 0) { @@ -265,7 +263,7 @@ korpApp.run([ ng-click="closeModal()" class="btn bg-blue-500 text-white font-bold mt-3" > - {{'${buttonText}' | loc:lang }} + {{'${buttonText}' | loc:$root.lang }} OK
diff --git a/app/scripts/components/corpus_chooser/corpus-chooser.js b/app/scripts/components/corpus_chooser/corpus-chooser.js index b78a1a779..5c2646eff 100644 --- a/app/scripts/components/corpus_chooser/corpus-chooser.js +++ b/app/scripts/components/corpus_chooser/corpus-chooser.js @@ -25,7 +25,7 @@ angular.module("korpApp").component("corpusChooser", { >
{{ $ctrl.selectCount }} - {{ 'corpselector_of' | loc:$root.lang }} + {{ 'corpselector_of' | loc }} {{ $ctrl.totalCount }} {{'corpselector_selectedmultiple' | loc:$root.lang }} diff --git a/app/scripts/components/corpus_chooser/info-box.js b/app/scripts/components/corpus_chooser/info-box.js index c54208b16..8032df186 100644 --- a/app/scripts/components/corpus_chooser/info-box.js +++ b/app/scripts/components/corpus_chooser/info-box.js @@ -11,9 +11,9 @@ angular.module("korpApp").component("ccInfoBox", {

- {{ $ctrl.title | locObj:lang }} + {{ $ctrl.title | locObj:$root.lang }}

-
+
diff --git a/app/scripts/components/frontpage.ts b/app/scripts/components/frontpage.ts index ef29d00f8..06eb4b2ed 100644 --- a/app/scripts/components/frontpage.ts +++ b/app/scripts/components/frontpage.ts @@ -16,14 +16,14 @@ export default angular.module("korpApp").component("frontpage", { >

- {{$root._settings['mode']['label'] | locObj:lang}} + {{$root._settings['mode']['label'] | locObj:$root.lang}}

-
+
diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 31491923a..24155e61a 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -81,8 +81,8 @@ angular.module("korpApp").component("header", {
{{langObj.label | locObj:lang}}{{langObj.label | locObj:$root.lang}}
diff --git a/app/scripts/components/kwic.js b/app/scripts/components/kwic.js index 1ca6c2be0..4e8977b2e 100644 --- a/app/scripts/components/kwic.js +++ b/app/scripts/components/kwic.js @@ -28,7 +28,7 @@ angular.module("korpApp").component("kwic", { ng-style='{width : corpus.relative + "%"}' ng-class="{odd : $index % 2 != 0, even : $index % 2 == 0}" ng-click="$ctrl.pageEvent(corpus.page)" - uib-tooltip="{{corpus.rtitle | locObj:lang}}: {{corpus.abs}}" + uib-tooltip="{{corpus.rtitle | locObj:$root.lang}}: {{corpus.abs}}" tooltip-placement='{{$last? "left":"top"}}' append-to-body="false" > @@ -58,7 +58,7 @@ angular.module("korpApp").component("kwic", {
- {{sentence.newCorpus | locObj:lang}} + {{sentence.newCorpus | locObj:$root.lang}} {{'no_context_support' | loc:$root.lang}} @@ -106,7 +106,7 @@ angular.module("korpApp").component("kwic", { ng-class="{corpus_info : sentence.newCorpus, not_corpus_info : !sentence.newCorpus, linked_sentence : sentence.isLinked, even : $even, odd : $odd}" > {{sentence.newCorpus | locObj:lang}}{{sentence.newCorpus | locObj:$root.lang}}{{'no_context_support' | loc:$root.lang}}
diff --git a/app/scripts/directives.js b/app/scripts/directives.js index 319f379ec..a03701f14 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -383,7 +383,7 @@ korpApp.directive("reduceSelect", [
{{ "reduce_text" | loc:$root.lang }}: - {{keyItems[selected[0]].label | locObj:lang}} + {{keyItems[selected[0]].label | locObj:$root.lang}} (+{{ numberAttributes - 1 }}) @@ -395,7 +395,7 @@ korpApp.directive("reduceSelect", [
  • - {{keyItems['word'].label | locObj:lang }} + {{keyItems['word'].label | locObj:$root.lang }} Aa @@ -405,14 +405,14 @@ korpApp.directive("reduceSelect", [ ng-click="toggleSelected(item.value, $event)" ng-class="item.selected ? 'selected':''" class="attribute"> - {{item.label | locObj:lang }} + {{item.label | locObj:$root.lang }}
  • {{'sentence_attr' | loc:$root.lang}}
  • - {{item.label | locObj:lang }} + {{item.label | locObj:$root.lang }}
diff --git a/app/scripts/filter_directives.js b/app/scripts/filter_directives.js index 5193609f4..3d096704f 100644 --- a/app/scripts/filter_directives.js +++ b/app/scripts/filter_directives.js @@ -57,10 +57,10 @@ korpApp.directive("globalFilter", [ diff --git a/app/scripts/i18n/index.ts b/app/scripts/i18n/index.ts index 827a40b7c..1c7f021d0 100644 --- a/app/scripts/i18n/index.ts +++ b/app/scripts/i18n/index.ts @@ -1,11 +1,12 @@ /** @format */ import isObject from "lodash/isObject" import settings from "@/settings" +import { getService } from "@/util" import type { LangLocMap, LangMap, LocLangMap, LocMap } from "@/i18n/types" /** Get the current UI language. */ export function getLang(): string { - return (window as any).lang || settings["default_language"] + return getService("$rootScope")["lang"] || settings["default_language"] } /** diff --git a/app/scripts/statistics.ts b/app/scripts/statistics.ts index 9eae9ea62..78c79a60f 100644 --- a/app/scripts/statistics.ts +++ b/app/scripts/statistics.ts @@ -15,7 +15,7 @@ const createStatisticsService = function () { ): SlickgridColumn[] { const valueFormatter: SlickgridFormatter = function (row, cell, value, columnDef, dataContext) { const [absolute, relative] = [...dataContext[columnDef.id + "_value"]] - return hitCountHtml(absolute, relative, (window as any).lang as string) + return hitCountHtml(absolute, relative) } const corporaKeys = _.keys(corpora) diff --git a/app/scripts/util.ts b/app/scripts/util.ts index c6262da3e..2e80b7a8a 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -115,7 +115,8 @@ export class SelectionManager { * @param lang The locale to use. * @returns A string with the number nicely formatted. */ -export function formatRelativeHits(x: number | string, lang: string) { +export function formatRelativeHits(x: number | string, lang?: string) { + lang = lang || getLang() return Number(x).toLocaleString(lang, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) } @@ -126,7 +127,8 @@ export function formatRelativeHits(x: number | string, lang: string) { * @param lang The locale to use. * @returns A HTML snippet. */ -export function hitCountHtml(absolute: number, relative: number, lang: string) { +export function hitCountHtml(absolute: number, relative: number, lang?: string) { + lang = lang || getLang() const relativeHtml = `${formatRelativeHits(relative, lang)}` // TODO Remove outer span? // TODO Flexbox? diff --git a/doc/frontend_devel.md b/doc/frontend_devel.md index e8fc7e7b9..6a4e64932 100644 --- a/doc/frontend_devel.md +++ b/doc/frontend_devel.md @@ -580,7 +580,7 @@ Korp does runtime DOM manipulation when the user changes language. Using an Angu
{{'my_key' | loc}}
-Sometimes it is necessary to use `loc:lang` or even `loc:$root.lang`, instead of just `loc`. +For proper reactivity, it is generally necessary to use `loc:lang` or even `loc:$root.lang`, instead of just `loc`. Add `my_key` to `/translations/corpora-.json` for all `lang`. From 17a72b3b31a22e6101201e3b428800b081bce874 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 29 May 2024 16:38:12 +0200 Subject: [PATCH 11/99] refactor(locale): Move window.loc_data to $rootScope --- CHANGELOG.md | 1 + app/lib/jquery.localize.js | 4 ++-- app/scripts/app.js | 7 +++++++ app/scripts/data_init.js | 11 ++++++----- app/scripts/i18n/index.ts | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 662930b1f..effab9fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Removed the global `c` alias for `console` - Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) +- Removed global `loc_data`, use `$rootScope["loc_data"]` instead (outside Angular: `getService("$rootScope")["loc_data"]`) - Removed globals `CSV` and `moment`, import the libraries instead ### Fixed diff --git a/app/lib/jquery.localize.js b/app/lib/jquery.localize.js index 233525f06..03286cf05 100644 --- a/app/lib/jquery.localize.js +++ b/app/lib/jquery.localize.js @@ -1,5 +1,5 @@ const { default: settings } = require("@/settings"); -const { locationSearchGet } = require("@/util"); +const { locationSearchGet, getService } = require("@/util"); (function($) { dl_cache = {} @@ -45,7 +45,7 @@ const { locationSearchGet } = require("@/util"); $.fn.localize = function() { //TODO: make this less slow. var lang = locationSearchGet("lang") || settings["default_language"]; - var data = loc_data[lang]; + var data = getService("$rootScope")["loc_data"][lang]; this.find("[rel^=localize]").each(function(i, elem) { var elem = $(elem); var key = elem.attr("rel").match(/localize\[(.*?)\]/)[1]; diff --git a/app/scripts/app.js b/app/scripts/app.js index 83d5114e1..b4cd6f181 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -11,6 +11,7 @@ import "@/components/searchtabs" import "@/components/frontpage" import "@/components/results" import "@/components/korp-error" +import { initLocales, locDataPromise } from "./data_init" // load all custom components let customComponents = {} @@ -132,6 +133,11 @@ korpApp.run([ $rootScope.mapTabs = [] $rootScope.textTabs = [] + // This fetch was started in data_init.js, but only here can we store the result. + const initLocalesPromise = initLocales().then((data) => + $rootScope.$apply(() => ($rootScope["loc_data"] = data)) + ) + s.waitForLogin = false /** Recursively collect the corpus ids found in a corpus folder */ @@ -303,6 +309,7 @@ korpApp.run([ }) initializeCorpusSelection(getCorporaFromHash()) + await initLocalesPromise }, ]) diff --git a/app/scripts/data_init.js b/app/scripts/data_init.js index bed867b5a..c5daffd52 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.js @@ -1,5 +1,6 @@ /** @format */ import _ from "lodash" +import memoize from "lodash/memoize" import settings, { setDefaultConfigValues } from "@/settings" import currentMode from "@/mode" import timeProxyFactory from "@/backend/time-proxy" @@ -8,8 +9,9 @@ import { CorpusListing } from "./corpus_listing" import { ParallelCorpusListing } from "./parallel/corpus_listing" import { httpConfAddMethodFetch } from "@/util" +// Using memoize, this will only fetch once and then return the same promise when called again. // TODO it would be better only to load additional languages when there is a language change -async function initLocales() { +export const initLocales = memoize(async () => { const locData = {} const defs = [] for (const langObj of settings["languages"]) { @@ -31,9 +33,8 @@ async function initLocales() { } await Promise.all(defs) - window.loc_data = locData return locData -} +}) async function getInfoData() { const params = { @@ -242,7 +243,8 @@ export async function fetchInitialData(authDef) { await authDef } - const translationFiles = initLocales() + // Start fetching locales asap. Await and read it later, in the Angular context. + initLocales() const config = await getConfig() const modeSettings = transformConfig(config) @@ -270,5 +272,4 @@ export async function fetchInitialData(authDef) { settings["time_data"] = await timeDef } } - await translationFiles } diff --git a/app/scripts/i18n/index.ts b/app/scripts/i18n/index.ts index 1c7f021d0..c5caab7a6 100644 --- a/app/scripts/i18n/index.ts +++ b/app/scripts/i18n/index.ts @@ -18,7 +18,7 @@ export function getLang(): string { export function loc(key: string, lang?: string) { lang = lang || getLang() try { - return ((window as any).loc_data as LangLocMap)[lang][key] + return (getService("$rootScope")["loc_data"] as LangLocMap)[lang][key] } catch (e) { return key } From a757fcf6277e7eb01bca80cfcac1aa03edbba769 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 30 May 2024 16:15:27 +0200 Subject: [PATCH 12/99] refactor: radioList component --- CHANGELOG.md | 4 ++ app/scripts/app.js | 3 -- .../components/corpus-distribution-chart.ts | 50 +++++++++++-------- app/scripts/components/header.js | 26 ++++++---- app/scripts/components/radio-list.ts | 50 +++++++++++++++++++ app/scripts/main.js | 10 ---- app/scripts/widgets.js | 46 ----------------- app/styles/styles.scss | 9 ---- 8 files changed, 100 insertions(+), 98 deletions(-) create mode 100644 app/scripts/components/radio-list.ts delete mode 100644 app/scripts/widgets.js diff --git a/CHANGELOG.md b/CHANGELOG.md index effab9fc5..f5c98ef3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,14 @@ ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics + +### Refactoring + - Removed the global `c` alias for `console` - Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) - Removed global `loc_data`, use `$rootScope["loc_data"]` instead (outside Angular: `getService("$rootScope")["loc_data"]`) - Removed globals `CSV` and `moment`, import the libraries instead +- Converted the "radioList" JQuery widget to a component ### Fixed diff --git a/app/scripts/app.js b/app/scripts/app.js index b4cd6f181..c6d061a76 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -116,9 +116,6 @@ korpApp.run([ // Trigger jQuery Localize $("body").localize() - - // Update language switcher - $("#languages").radioList("select", lang) }) $(document).keyup(function (event) { diff --git a/app/scripts/components/corpus-distribution-chart.ts b/app/scripts/components/corpus-distribution-chart.ts index b72613988..b934d0339 100644 --- a/app/scripts/components/corpus-distribution-chart.ts +++ b/app/scripts/components/corpus-distribution-chart.ts @@ -1,18 +1,15 @@ /** @format */ -import angular, { IController, IRootScopeService } from "angular" +import angular, { type IController, type IRootScopeService, type IScope } from "angular" import { Chart } from "chart.js" import { html } from "@/util" - -const defaultMode: Mode = "relative" +import { loc } from "@/i18n" +import { type Option } from "@/components/radio-list" angular.module("korpApp").component("corpusDistributionChart", { template: html` `, bindings: { @@ -20,18 +17,30 @@ angular.module("korpApp").component("corpusDistributionChart", { }, controller: [ "$rootScope", - function ($rootScope: IRootScopeService) { + "$scope", + function ($rootScope: IRootScopeService, $scope: CorpusDistributionChartScope) { const $ctrl = this as CorpusDistributionChartController + $ctrl.modeOptions = [ + { + value: "relative", + label: loc("statstable_relfigures", $rootScope["lang"]), + }, + { + value: "absolute", + label: loc("statstable_absfigures", $rootScope["lang"]), + }, + ] + $scope.mode = "relative" let chart: Chart<"pie"> - const getValues = (mode: Mode) => $ctrl.row.map((corpus) => corpus.values[mode == "relative" ? 1 : 0]) + const getValues = () => $ctrl.row.map((corpus) => corpus.values[$scope.mode == "relative" ? 1 : 0]) $ctrl.$onInit = () => { chart = new Chart("distribution-chart", { type: "pie", data: { labels: $ctrl.row.map((corpus) => corpus.title), - datasets: [{ data: getValues(defaultMode) }], + datasets: [{ data: getValues() }], }, options: { locale: $rootScope["lang"], @@ -42,24 +51,23 @@ angular.module("korpApp").component("corpusDistributionChart", { }, }, }) - - setTimeout(() => { - const radioList = ($("#statistics_switch") as any).radioList({ - selected: defaultMode, - change: () => { - const mode = radioList.radioList("getSelected").attr("data-mode") - chart.data.datasets[0].data = getValues(mode) - chart.update() - }, - }) - }) } + + $scope.$watch("mode", () => { + chart.data.datasets[0].data = getValues() + chart.update() + }) }, ], }) +type CorpusDistributionChartScope = IScope & { + mode: Mode +} + type CorpusDistributionChartController = IController & { row: { title: string; values: [number, number] }[] + modeOptions: Option[] } type Mode = "relative" | "absolute" diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 24155e61a..2203fa466 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -10,6 +10,7 @@ import settings from "@/settings" import currentMode from "@/mode" import { collatorSort, html } from "@/util" import "@/components/corpus_chooser/corpus-chooser" +import "@/components/radio-list" angular.module("korpApp").component("header", { template: html` @@ -77,14 +78,8 @@ angular.module("korpApp").component("header", {
- + + {{'about_cite_header' | loc:$root.lang}} { + // Watcher gets called with `undefined` on init. + if (!$scope.lang) return + $rootScope["lang"] = $scope.lang + // Set url param if different from default. + $location.search("lang", $scope.lang !== settings["default_language"] ? $scope.lang : null) + }) + $ctrl.citeClick = () => { $rootScope.show_modal = "about" } @@ -221,6 +228,7 @@ angular.module("korpApp").component("header", { $ctrl.visible = $ctrl.modes.slice(0, N_VISIBLE) $rootScope.$watch("lang", () => { + $scope.lang = $rootScope.lang $ctrl.menu = collatorSort($ctrl.modes.slice(N_VISIBLE), "label", $rootScope.lang) const i = _.map($ctrl.menu, "mode").indexOf(currentMode) diff --git a/app/scripts/components/radio-list.ts b/app/scripts/components/radio-list.ts new file mode 100644 index 000000000..ef2899159 --- /dev/null +++ b/app/scripts/components/radio-list.ts @@ -0,0 +1,50 @@ +/** @format */ +import angular, { IController, type IScope } from "angular" +import { html } from "@/util" + +export default angular.module("korpApp").component("radioList", { + template: html` + + | + + {{ option.label | locObj:$root.lang }} + + + `, + bindings: { + options: "<", + }, + require: { + ngModelCtrl: "^ngModel", + }, + controller: [ + "$scope", + function ($scope: RadioListScope) { + const $ctrl: RadioListController = this + $scope.value = undefined + + $ctrl.$onInit = () => { + $ctrl.ngModelCtrl.$render = () => { + $scope.value = $ctrl.ngModelCtrl.$viewValue + } + } + + $ctrl.select = (value: string) => { + $scope.value = value + $ctrl.ngModelCtrl.$setViewValue(value) + $ctrl.ngModelCtrl.$setTouched() + $ctrl.ngModelCtrl.$setDirty() + } + }, + ], +}) + +type RadioListScope = IScope & { + value?: string +} + +type RadioListController = IController & { + options: Option[] +} + +export type Option = { label: string; value: V } diff --git a/app/scripts/main.js b/app/scripts/main.js index b957941f6..df7da7270 100644 --- a/app/scripts/main.js +++ b/app/scripts/main.js @@ -7,7 +7,6 @@ import { fetchInitialData } from "@/data_init" import currentMode from "@/mode" import * as authenticationProxy from "@/components/auth/auth" import korpLogo from "../img/korp.svg" -import { locationSearchSet } from "./util" const createSplashScreen = () => { const splash = document.getElementById("preload") @@ -65,15 +64,6 @@ function initApp() { } }) - $("#languages").radioList({ - change() { - const currentLang = $(this).radioList("getSelected").data("mode") - locationSearchSet("lang", currentLang !== settings["default_language"] ? currentLang : null) - }, - // TODO: this does nothing? - selected: settings["default_language"], - }) - // this is to hide all ugly markup before Angular is fully loaded $("#main").css("display", "block") $("#main").animate({ opacity: 1 }, function () { diff --git a/app/scripts/widgets.js b/app/scripts/widgets.js deleted file mode 100644 index b8e134aaa..000000000 --- a/app/scripts/widgets.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @format */ -let widget = require("components-jqueryui/ui/widget") - -widget("korp.radioList", { - options: { - change: $.noop, - separator: "|", - selected: "default", - }, - - _create() { - this._super() - const self = this - $.each(this.element, function () { - return $(this) - .children() - .wrap("
  • ") - .click(function () { - if (!$(this).is(".radioList_selected")) { - self.select($(this).data("mode")) - return self._trigger("change", $(this).data("mode")) - } - }) - .parent() - .prepend($("").text(self.options.separator)) - .wrapAll("
      ") - }) - - this.element.find(".inline_list span:first").remove() - return this.select(this.options.selected) - }, - - select(mode) { - this.options.selected = mode - const target = this.element.find("a").filter(function () { - return $(this).data("mode") === mode - }) - this.element.find(".radioList_selected").removeClass("radioList_selected") - this.element.find(target).addClass("radioList_selected") - return this.element - }, - - getSelected() { - return this.element.find(".radioList_selected") - }, -}) diff --git a/app/styles/styles.scss b/app/styles/styles.scss index 8ab60ebe5..12c851cbf 100644 --- a/app/styles/styles.scss +++ b/app/styles/styles.scss @@ -257,10 +257,6 @@ label.placeholder { opacity: 0.6; } -#languages { - margin-top : -1px; -} - #query_table { white-space: nowrap; margin-bottom: 15px; @@ -998,11 +994,6 @@ a { } } -.radioList_selected { - color: black !important; - cursor: default; -} - .hits_picture_table { border : 1px solid lightgrey; width: 100%; From af030ec285c3f510019e4d17b7e114f07f5fbb1b Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 30 May 2024 16:22:16 +0200 Subject: [PATCH 13/99] fix: radioList css, broken require --- app/index.js | 1 - app/styles/styles.scss | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/index.js b/app/index.js index 78f98871d..faf2bd1a1 100644 --- a/app/index.js +++ b/app/index.js @@ -51,7 +51,6 @@ require("angular-filter/index.js") require("./lib/jquery.tooltip.pack.js") -require("./scripts/widgets.js") require("./scripts/main.js") require("./scripts/app.js") require("./scripts/search_controllers.js") diff --git a/app/styles/styles.scss b/app/styles/styles.scss index 12c851cbf..b4896cc78 100644 --- a/app/styles/styles.scss +++ b/app/styles/styles.scss @@ -994,6 +994,11 @@ a { } } +.radioList_selected { + color: black !important; + cursor: default; +} + .hits_picture_table { border : 1px solid lightgrey; width: 100%; From 25c9d3ffc1514c53ef19c8e574d605f34e8f5885 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 13 Jun 2024 08:18:21 +0200 Subject: [PATCH 14/99] refactor: Unangularify kwic_download --- app/index.js | 2 - app/scripts/components/kwic.js | 11 +- app/scripts/kwic_download.js | 353 ++++++++++++++++----------------- 3 files changed, 172 insertions(+), 194 deletions(-) diff --git a/app/index.js b/app/index.js index faf2bd1a1..955a6bb38 100644 --- a/app/index.js +++ b/app/index.js @@ -55,8 +55,6 @@ require("./scripts/main.js") require("./scripts/app.js") require("./scripts/search_controllers.js") -require("./scripts/kwic_download.js") - require("./scripts/controllers/comparison_controller.js") require("./scripts/controllers/kwic_controller.js") require("./scripts/controllers/example_controller.js") diff --git a/app/scripts/components/kwic.js b/app/scripts/components/kwic.js index 4e8977b2e..3bfaa4cda 100644 --- a/app/scripts/components/kwic.js +++ b/app/scripts/components/kwic.js @@ -4,6 +4,7 @@ import _ from "lodash" import statemachine from "@/statemachine" import settings from "@/settings" import currentMode from "@/mode" +import { makeDownload } from "@/kwic_download" import { SelectionManager, html, setDownloadLinks } from "@/util" import "@/components/kwic-pager" import "@/components/kwic-word" @@ -170,8 +171,7 @@ angular.module("korpApp").component("kwic", { "$location", "$element", "$timeout", - "kwicDownload", - function ($location, $element, $timeout, kwicDownload) { + function ($location, $element, $timeout) { let $ctrl = this const selectionManager = new SelectionManager() @@ -278,12 +278,7 @@ angular.module("korpApp").component("kwic", { if (value === "") { return } - const [fileName, blobName] = kwicDownload.makeDownload( - ...value.split("/"), - $ctrl.kwic, - $ctrl.prevParams, - hits - ) + const [fileName, blobName] = makeDownload(...value.split("/"), $ctrl.kwic, $ctrl.prevParams, hits) $ctrl.download.fileName = fileName $ctrl.download.blobName = blobName $ctrl.download.selected = "" diff --git a/app/scripts/kwic_download.js b/app/scripts/kwic_download.js index 14113dfb2..55cf3e619 100644 --- a/app/scripts/kwic_download.js +++ b/app/scripts/kwic_download.js @@ -4,211 +4,196 @@ import moment from "moment" import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" -const korpApp = angular.module("korpApp") -korpApp.factory("kwicDownload", function () { - const emptyRow = function (length) { - return _.fill(new Array(length), "") - } - - const createFile = function (dataType, fileType, content) { - const date = moment().format("YYYYMMDD_HHmmss") - const filename = `korp_${dataType}_${date}.${fileType}` - const blobURL = window.URL.createObjectURL(new Blob([content], { type: `text/${fileType}` })) - return [filename, blobURL] - } - - const padRows = (data, length) => - _.map(data, function (row) { - const newRow = emptyRow(length) - newRow[0] = row - return newRow - }) - - const createSearchInfo = function (requestInfo, totalHits) { - const rows = [] - const fields = ["cqp", "context", "within", "sorting", "start", "end", "hits"] - for (let field of fields) { - var row - if (field === "cqp") { - row = `## CQP query: ${requestInfo.cqp}` - } - if (field === "context") { - row = `## context: ${requestInfo.default_context}` - } - if (field === "within") { - row = `## within: ${requestInfo.default_within}` - } - if (field === "sorting") { - const sorting = requestInfo.sort || "none" - row = `## sorting: ${sorting}` - } - if (field === "start") { - row = `## start: ${requestInfo.start}` - } - if (field === "end") { - const cqpQuery = "" - row = `## end: ${requestInfo.end}` - } - if (field === "hits") { - row = `## Total hits: ${totalHits}` - } - rows.push(row) +const emptyRow = (length) => _.fill(new Array(length), "") + +const padRows = (data, length) => _.map(data, (row) => [row, ...emptyRow(length - 1)]) + +function createFile(dataType, fileType, content) { + const date = moment().format("YYYYMMDD_HHmmss") + const filename = `korp_${dataType}_${date}.${fileType}` + const blobURL = window.URL.createObjectURL(new Blob([content], { type: `text/${fileType}` })) + return [filename, blobURL] +} + +function createSearchInfo(requestInfo, totalHits) { + const rows = [] + const fields = ["cqp", "context", "within", "sorting", "start", "end", "hits"] + for (let field of fields) { + var row + if (field === "cqp") { + row = `## CQP query: ${requestInfo.cqp}` + } + if (field === "context") { + row = `## context: ${requestInfo.default_context}` + } + if (field === "within") { + row = `## within: ${requestInfo.default_within}` + } + if (field === "sorting") { + const sorting = requestInfo.sort || "none" + row = `## sorting: ${sorting}` } - return rows + if (field === "start") { + row = `## start: ${requestInfo.start}` + } + if (field === "end") { + row = `## end: ${requestInfo.end}` + } + if (field === "hits") { + row = `## Total hits: ${totalHits}` + } + rows.push(row) } - - const transformDataToAnnotations = function (data, searchInfo) { - const headers = _.filter( - _.keys(data[1].tokens[0]), - (val) => val.indexOf("_") !== 0 && val !== "structs" && val !== "$$hashKey" && val !== "position" - ) - const columnCount = headers.length + 1 - let corpus - const res = padRows(searchInfo, columnCount) - res.push(["match"].concat(headers)) - for (let row of data) { - if (row.tokens) { - const textAttributes = [] - for (let attrName in row.structs) { - const attrValue = row.structs[attrName] - textAttributes.push(attrName + ': "' + attrValue + '"') - } - const hitInfo = emptyRow(columnCount) - hitInfo[0] = `# ${corpus}; text attributes: ${textAttributes.join(", ")}` - res.push(hitInfo) - - for (let token of row.tokens || []) { - let match = "" - for (let matchObj of [row.match].flat()) { - if (token.position >= matchObj.start && token.position < matchObj.end) { - match = "***" - break - } - } - const newRow = [match] - for (let field of headers) { - newRow.push(token[field]) + return rows +} + +function transformDataToAnnotations(data, searchInfo) { + const headers = _.filter( + _.keys(data[1].tokens[0]), + (val) => val.indexOf("_") !== 0 && val !== "structs" && val !== "$$hashKey" && val !== "position" + ) + const columnCount = headers.length + 1 + let corpus + const res = padRows(searchInfo, columnCount) + res.push(["match"].concat(headers)) + for (let row of data) { + if (row.tokens) { + const textAttributes = [] + for (let attrName in row.structs) { + const attrValue = row.structs[attrName] + textAttributes.push(attrName + ': "' + attrValue + '"') + } + const hitInfo = emptyRow(columnCount) + hitInfo[0] = `# ${corpus}; text attributes: ${textAttributes.join(", ")}` + res.push(hitInfo) + + for (let token of row.tokens || []) { + let match = "" + for (let matchObj of [row.match].flat()) { + if (token.position >= matchObj.start && token.position < matchObj.end) { + match = "***" + break } - res.push(newRow) } - } else if (row.newCorpus) { - corpus = locObj(row.newCorpus) + const newRow = [match] + for (let field of headers) { + newRow.push(token[field]) + } + res.push(newRow) } + } else if (row.newCorpus) { + corpus = locObj(row.newCorpus) } - - return res } - const transformDataToKWIC = function (data, searchInfo) { - let row - let corpus - const structHeaders = [] - let res = [] - for (row of data) { - if (row.tokens) { - if (row.isLinked) { - // parallell mode does not have matches or structs for the linked sentences - // current wordaround is to add all tokens to the left context - res.push(["", "", row.tokens.map((token) => token.word).join(" "), "", ""]) - continue - } + return res +} + +function transformDataToKWIC(data, searchInfo) { + let row + let corpus + const structHeaders = [] + let res = [] + for (row of data) { + if (row.tokens) { + if (row.isLinked) { + // parallell mode does not have matches or structs for the linked sentences + // current wordaround is to add all tokens to the left context + res.push(["", "", row.tokens.map((token) => token.word).join(" "), "", ""]) + continue + } - var attrName, token - const leftContext = [] - const match = [] - const rightContext = [] + var attrName, token + const leftContext = [] + const match = [] + const rightContext = [] - if (row.match instanceof Array) { - // the user has searched "not-in-order" and we cannot have a left, match and right context for the download - // put all data in leftContext - for (token of row.tokens) { - leftContext.push(token.word) - } - } else { - for (token of row.tokens.slice(0, row.match.start)) { - leftContext.push(token.word) - } - for (token of row.tokens.slice(row.match.start, row.match.end)) { - match.push(token.word) - } - for (token of row.tokens.slice(row.match.end, row.tokens.length)) { - rightContext.push(token.word) - } + if (row.match instanceof Array) { + // the user has searched "not-in-order" and we cannot have a left, match and right context for the download + // put all data in leftContext + for (token of row.tokens) { + leftContext.push(token.word) } - - const structs = [] - for (attrName in row.structs) { - if (!structHeaders.includes(attrName)) { - structHeaders.push(attrName) - } + } else { + for (token of row.tokens.slice(0, row.match.start)) { + leftContext.push(token.word) } - for (attrName of structHeaders) { - if (attrName in row.structs) { - structs.push(row.structs[attrName]) - } else { - structs.push("") - } + for (token of row.tokens.slice(row.match.start, row.match.end)) { + match.push(token.word) + } + for (token of row.tokens.slice(row.match.end, row.tokens.length)) { + rightContext.push(token.word) } - const newRow = [ - corpus, - row.match instanceof Array - ? row.match.map((match) => match.position).join(", ") - : row.match.position, - leftContext.join(" "), - match.join(" "), - rightContext.join(" "), - ].concat(structs) - res.push(newRow) - } else if (row.newCorpus) { - corpus = locObj(row.newCorpus) } - } - const headers = ["corpus", "match_position", "left context", "match", "right_context"].concat(structHeaders) - res = [headers].concat(res) - - res.push(emptyRow(headers.length)) - for (let row of padRows(searchInfo, headers.length)) { - res.push(row) + const structs = [] + for (attrName in row.structs) { + if (!structHeaders.includes(attrName)) { + structHeaders.push(attrName) + } + } + for (attrName of structHeaders) { + if (attrName in row.structs) { + structs.push(row.structs[attrName]) + } else { + structs.push("") + } + } + const newRow = [ + corpus, + row.match instanceof Array ? row.match.map((match) => match.position).join(", ") : row.match.position, + leftContext.join(" "), + match.join(" "), + rightContext.join(" "), + ].concat(structs) + res.push(newRow) + } else if (row.newCorpus) { + corpus = locObj(row.newCorpus) } - - return res } - const transformData = function (dataType, data, requestInfo, totalHits) { - const searchInfo = createSearchInfo(requestInfo, totalHits) - if (dataType === "annotations") { - return transformDataToAnnotations(data, searchInfo) - } - if (dataType === "kwic") { - return transformDataToKWIC(data, searchInfo) - } - } + const headers = ["corpus", "match_position", "left context", "match", "right_context"].concat(structHeaders) + res = [headers].concat(res) - const makeContent = function (fileType, transformedData) { - let dataDelimiter - if (fileType === "csv") { - dataDelimiter = "," - } - if (fileType === "tsv") { - dataDelimiter = "\t" - } + res.push(emptyRow(headers.length)) + for (let row of padRows(searchInfo, headers.length)) { + res.push(row) + } - const csv = new CSV(transformedData, { - delimiter: dataDelimiter, - }) + return res +} - return csv.encode() +function transformData(dataType, data, requestInfo, totalHits) { + const searchInfo = createSearchInfo(requestInfo, totalHits) + if (dataType === "annotations") { + return transformDataToAnnotations(data, searchInfo) } + if (dataType === "kwic") { + return transformDataToKWIC(data, searchInfo) + } +} - // dataType: either "kwic" or "annotations" - // fileType: either "csv" or "tsv" - // data: json from the backend - return { - makeDownload(dataType, fileType, data, requestInfo, totalHits) { - const tmp = transformData(dataType, data, requestInfo, totalHits) - const tmp2 = makeContent(fileType, tmp) - return createFile(dataType, fileType, tmp2) - }, +function makeContent(fileType, transformedData) { + let dataDelimiter + if (fileType === "csv") { + dataDelimiter = "," + } + if (fileType === "tsv") { + dataDelimiter = "\t" } -}) + + const csv = new CSV(transformedData, { + delimiter: dataDelimiter, + }) + + return csv.encode() +} + +// dataType: either "kwic" or "annotations" +// fileType: either "csv" or "tsv" +// data: json from the backend +export function makeDownload(dataType, fileType, data, requestInfo, totalHits) { + const tmp = transformData(dataType, data, requestInfo, totalHits) + const tmp2 = makeContent(fileType, tmp) + return createFile(dataType, fileType, tmp2) +} From 37f3d9203ac3324f0ba44f78b0afa327f7399c2b Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 13 Jun 2024 10:39:30 +0200 Subject: [PATCH 15/99] refactor(ts): kwic_download --- app/scripts/backend/kwic-proxy.ts | 10 +- .../{kwic_download.js => kwic_download.ts} | 138 +++++++++--------- 2 files changed, 78 insertions(+), 70 deletions(-) rename app/scripts/{kwic_download.js => kwic_download.ts} (58%) diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index 6ad9724e3..ba35ab961 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -135,7 +135,7 @@ const kwicProxyFactory = new Factory(KwicProxy) export default kwicProxyFactory /** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get */ -type KorpQueryParams = { +export type KorpQueryParams = { corpus: string cqp: string start?: number @@ -164,7 +164,7 @@ type MakeRequestOptions = { type Interval = { start: number; end: number } /** @see https://ws.spraakbanken.gu.se/docs/korp#tag/Concordance/paths/~1query/get */ -type KorpQueryResponse = { +export type KorpQueryResponse = { /** Search hits */ kwic: ApiKwic[] /** Total number of hits */ @@ -178,7 +178,7 @@ type KorpQueryResponse = { } /** Search hits */ -type ApiKwic = { +export type ApiKwic = { /** An object for each token in the context, with attribute values for that token */ tokens: Record[] /** Attribute values for the context (e.g. sentence) */ @@ -186,7 +186,9 @@ type ApiKwic = { /** Specifies the position of the match in the context. If `in_order` is false, `match` will consist of a list of match objects, one per highlighted word */ match: KwicMatch | KwicMatch[] /** Hits from aligned corpora if available, otherwise omitted */ - aligned: Record + aligned: { + [linkedCorpusId: `${string}-${string}`]: Record[] + } } /** Specifies the position of a match in a context */ diff --git a/app/scripts/kwic_download.js b/app/scripts/kwic_download.ts similarity index 58% rename from app/scripts/kwic_download.js rename to app/scripts/kwic_download.ts index 55cf3e619..eb49670e4 100644 --- a/app/scripts/kwic_download.js +++ b/app/scripts/kwic_download.ts @@ -3,61 +3,68 @@ import _ from "lodash" import moment from "moment" import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" +import { type ApiKwic, type KorpQueryParams } from "@/backend/kwic-proxy" +import { LangMap } from "./i18n/types" + +// This is what is returned by massageData in kwic.js +type Row = ApiKwic | LinkedKwic | CorpusHeading +// The annotations option is not available for parallel +type AnnotationsRow = ApiKwic | CorpusHeading + +type LinkedKwic = { + tokens: ApiKwic["tokens"] + isLinked: true + corpus: string +} + +type CorpusHeading = { + newCorpus: LangMap | string + noContext?: boolean +} -const emptyRow = (length) => _.fill(new Array(length), "") +const isKwic = (row: Row): row is ApiKwic => "tokens" in row && !isLinkedKwic(row) +const isLinkedKwic = (row: Row): row is LinkedKwic => "isLinked" in row +const isCorpusHeading = (row: Row): row is CorpusHeading => "newCorpus" in row -const padRows = (data, length) => _.map(data, (row) => [row, ...emptyRow(length - 1)]) +type TableRow = (string | number)[] -function createFile(dataType, fileType, content) { +const emptyRow = (length: number) => _.fill(new Array(length), "") + +const padRows = (data: string[], length: number) => _.map(data, (value) => [value, ...emptyRow(length - 1)]) + +function createFile(dataType: string, fileType: string, content: string) { const date = moment().format("YYYYMMDD_HHmmss") const filename = `korp_${dataType}_${date}.${fileType}` const blobURL = window.URL.createObjectURL(new Blob([content], { type: `text/${fileType}` })) return [filename, blobURL] } -function createSearchInfo(requestInfo, totalHits) { - const rows = [] - const fields = ["cqp", "context", "within", "sorting", "start", "end", "hits"] - for (let field of fields) { - var row - if (field === "cqp") { - row = `## CQP query: ${requestInfo.cqp}` - } - if (field === "context") { - row = `## context: ${requestInfo.default_context}` - } - if (field === "within") { - row = `## within: ${requestInfo.default_within}` - } - if (field === "sorting") { - const sorting = requestInfo.sort || "none" - row = `## sorting: ${sorting}` - } - if (field === "start") { - row = `## start: ${requestInfo.start}` - } - if (field === "end") { - row = `## end: ${requestInfo.end}` - } - if (field === "hits") { - row = `## Total hits: ${totalHits}` - } - rows.push(row) - } - return rows +function createSearchInfo(requestInfo: KorpQueryParams, totalHits: number) { + return [ + `## CQP query: ${requestInfo.cqp}`, + `## context: ${requestInfo.default_context}`, + `## within: ${requestInfo.default_within}`, + `## sorting: ${requestInfo.sort || "none"}`, + `## start: ${requestInfo.start}`, + `## end: ${requestInfo.end}`, + `## Total hits: ${totalHits}`, + ] } -function transformDataToAnnotations(data, searchInfo) { - const headers = _.filter( - _.keys(data[1].tokens[0]), +function transformDataToAnnotations(data: AnnotationsRow[], searchInfo: string[]) { + const firstTokensRow: ApiKwic = data.find((row) => isKwic(row)) as ApiKwic | undefined + if (!firstTokensRow) return undefined + + const headers = Object.keys(firstTokensRow.tokens[0]).filter( (val) => val.indexOf("_") !== 0 && val !== "structs" && val !== "$$hashKey" && val !== "position" ) + const columnCount = headers.length + 1 let corpus const res = padRows(searchInfo, columnCount) res.push(["match"].concat(headers)) - for (let row of data) { - if (row.tokens) { + for (const row of data) { + if (isKwic(row)) { const textAttributes = [] for (let attrName in row.structs) { const attrValue = row.structs[attrName] @@ -89,20 +96,14 @@ function transformDataToAnnotations(data, searchInfo) { return res } -function transformDataToKWIC(data, searchInfo) { - let row - let corpus - const structHeaders = [] - let res = [] - for (row of data) { - if (row.tokens) { - if (row.isLinked) { - // parallell mode does not have matches or structs for the linked sentences - // current wordaround is to add all tokens to the left context - res.push(["", "", row.tokens.map((token) => token.word).join(" "), "", ""]) - continue - } - +function transformDataToKWIC(data: Row[], searchInfo: string[]) { + let corpus: string + const structHeaders: string[] = [] + let res: TableRow[] = [] + for (const row of data) { + if (isCorpusHeading(row)) { + corpus = locObj(row.newCorpus) + } else if (isKwic(row)) { var attrName, token const leftContext = [] const match = [] @@ -139,7 +140,7 @@ function transformDataToKWIC(data, searchInfo) { structs.push("") } } - const newRow = [ + const newRow: TableRow = [ corpus, row.match instanceof Array ? row.match.map((match) => match.position).join(", ") : row.match.position, leftContext.join(" "), @@ -147,13 +148,15 @@ function transformDataToKWIC(data, searchInfo) { rightContext.join(" "), ].concat(structs) res.push(newRow) - } else if (row.newCorpus) { - corpus = locObj(row.newCorpus) + } else { + // parallell mode does not have matches or structs for the linked sentences + // current wordaround is to add all tokens to the left context + res.push(["", "", row.tokens.map((token) => token.word).join(" "), "", ""]) } } const headers = ["corpus", "match_position", "left context", "match", "right_context"].concat(structHeaders) - res = [headers].concat(res) + res = [headers, ...res] res.push(emptyRow(headers.length)) for (let row of padRows(searchInfo, headers.length)) { @@ -163,17 +166,17 @@ function transformDataToKWIC(data, searchInfo) { return res } -function transformData(dataType, data, requestInfo, totalHits) { +function transformData(dataType: "annotations" | "kwic", data: Row[], requestInfo: KorpQueryParams, totalHits: number) { const searchInfo = createSearchInfo(requestInfo, totalHits) if (dataType === "annotations") { - return transformDataToAnnotations(data, searchInfo) + return transformDataToAnnotations(data as AnnotationsRow[], searchInfo) } if (dataType === "kwic") { return transformDataToKWIC(data, searchInfo) } } -function makeContent(fileType, transformedData) { +function makeContent(fileType: "csv" | "tsv", transformedData: TableRow[]): string { let dataDelimiter if (fileType === "csv") { dataDelimiter = "," @@ -189,11 +192,14 @@ function makeContent(fileType, transformedData) { return csv.encode() } -// dataType: either "kwic" or "annotations" -// fileType: either "csv" or "tsv" -// data: json from the backend -export function makeDownload(dataType, fileType, data, requestInfo, totalHits) { - const tmp = transformData(dataType, data, requestInfo, totalHits) - const tmp2 = makeContent(fileType, tmp) - return createFile(dataType, fileType, tmp2) +export function makeDownload( + dataType: "annotations" | "kwic", + fileType: "csv" | "tsv", + data: Row[], + requestInfo: KorpQueryParams, + totalHits: number +) { + const table = transformData(dataType, data, requestInfo, totalHits) + const csv = makeContent(fileType, table) + return createFile(dataType, fileType, csv) } From 6c81264373b9bfab3a4c7f09b68c374970666851 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 17 Jun 2024 15:34:17 +0200 Subject: [PATCH 16/99] chore: Remove empty file search_controllers.js --- app/index.js | 1 - app/scripts/search_controllers.js | 0 2 files changed, 1 deletion(-) delete mode 100644 app/scripts/search_controllers.js diff --git a/app/index.js b/app/index.js index 955a6bb38..f2343fab6 100644 --- a/app/index.js +++ b/app/index.js @@ -53,7 +53,6 @@ require("./lib/jquery.tooltip.pack.js") require("./scripts/main.js") require("./scripts/app.js") -require("./scripts/search_controllers.js") require("./scripts/controllers/comparison_controller.js") require("./scripts/controllers/kwic_controller.js") diff --git a/app/scripts/search_controllers.js b/app/scripts/search_controllers.js deleted file mode 100644 index e69de29bb..000000000 From 67882b1124083702cbba74f2d080233f8a3208db Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 3 Jul 2024 14:47:33 +0200 Subject: [PATCH 17/99] fix: Drop tslint Dropped it already in 8cbb64d2 but readded it (mistakenly?) in 067f75de --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index de7e6a834..38c0d49f0 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "testserver": "KORP_HOST=localhost KORP_HTTPS= webpack-dev-server --config webpack.dev.js --port 9112 &", "build": "ENVIRONMENT=production webpack --config webpack.prod.js", "build:labb": "ENVIRONMENT=staging webpack --config webpack.prod.js", - "lint": "tslint --project tslint.json", "format": "prettier . --write --ignore-path .gitignore" } } From 2bb13d415bd5b0935bb38ee7a1a3297895b9e040 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 4 Jul 2024 15:30:01 +0200 Subject: [PATCH 18/99] refactor: Types for config/settings --- CHANGELOG.md | 6 +- app/scripts/backend/base-proxy.ts | 2 +- app/scripts/backend/graph-proxy.ts | 2 +- app/scripts/backend/kwic-proxy.ts | 6 +- app/scripts/backend/lemgram-proxy.ts | 2 +- app/scripts/backend/stats-proxy.ts | 4 +- app/scripts/backend/time-proxy.ts | 2 +- app/scripts/components/corpus-updates.ts | 7 +- app/scripts/components/frontpage.ts | 27 +++---- app/scripts/components/search-examples.ts | 13 +-- app/scripts/corpus_listing.js | 4 +- app/scripts/data_init.js | 19 +++-- app/scripts/i18n/index.ts | 6 +- app/scripts/i18n/types.ts | 4 + app/scripts/index.d.ts | 5 ++ app/scripts/news-service.ts | 15 ++-- app/scripts/parallel/stats_proxy.ts | 4 +- app/scripts/settings/README.md | 14 ++++ app/scripts/settings/app-settings.types.ts | 71 ++++++++++++++++ .../settings/config-transformed.types.ts | 38 +++++++++ app/scripts/settings/config.types.ts | 81 +++++++++++++++++++ .../{settings.js => settings/index.ts} | 11 +-- app/scripts/settings/settings.types.ts | 19 +++++ app/scripts/statistics.ts | 2 +- app/scripts/timeseries.ts | 4 +- app/scripts/util.ts | 18 ++--- 26 files changed, 309 insertions(+), 77 deletions(-) create mode 100644 app/scripts/index.d.ts create mode 100644 app/scripts/settings/README.md create mode 100644 app/scripts/settings/app-settings.types.ts create mode 100644 app/scripts/settings/config-transformed.types.ts create mode 100644 app/scripts/settings/config.types.ts rename app/scripts/{settings.js => settings/index.ts} (78%) create mode 100644 app/scripts/settings/settings.types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c98ef3e..06df5d1d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ ## [Unreleased] +### Added + +- TypeScript typings for config/settings + ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics -### Refactoring +### Refactored - Removed the global `c` alias for `console` - Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) diff --git a/app/scripts/backend/base-proxy.ts b/app/scripts/backend/base-proxy.ts index 84fbd1448..59b2a67f0 100644 --- a/app/scripts/backend/base-proxy.ts +++ b/app/scripts/backend/base-proxy.ts @@ -138,7 +138,7 @@ export default abstract class BaseProxy { } return _(corpus.split("|")) - .map((corpus) => parseInt(settings.corpora[corpus.toLowerCase()].info.Size)) + .map((corpus) => Number(settings.corpora[corpus.toLowerCase()].info.Size)) .reduce((a, b) => a + b, 0) }) this.total = _.reduce(tmp, (val1, val2) => val1 + val2, 0) diff --git a/app/scripts/backend/graph-proxy.ts b/app/scripts/backend/graph-proxy.ts index 7ab7aac59..2b6b1577e 100644 --- a/app/scripts/backend/graph-proxy.ts +++ b/app/scripts/backend/graph-proxy.ts @@ -57,7 +57,7 @@ export class GraphProxy extends BaseProxy { const def = $.Deferred() const ajaxSettings: AjaxSettings = { - url: settings["korp_backend_url"] + "/count_time", + url: settings.korp_backend_url + "/count_time", dataType: "json", data: params, diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index ba35ab961..35a7d59b5 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -42,7 +42,7 @@ export class KwicProxy extends BaseProxy { function getPageInterval(): Interval { const hpp: string | number = locationSearchGet("hpp") - const itemsPerPage = Number(hpp) || settings["hits_per_page_default"] + const itemsPerPage = Number(hpp) || settings.hits_per_page_default const start = (page || 0) * itemsPerPage const end = start + itemsPerPage - 1 return { start, end } @@ -52,7 +52,7 @@ export class KwicProxy extends BaseProxy { delete options.ajaxParams.command const data: KorpQueryParams = { - default_context: settings["default_overview_context"], + default_context: settings.default_overview_context, ...getPageInterval(), ...options.ajaxParams, } @@ -97,7 +97,7 @@ export class KwicProxy extends BaseProxy { this.prevRequest = data this.prevParams = data const ajaxSettings: AjaxSettings = { - url: settings["korp_backend_url"] + "/" + command, + url: settings.korp_backend_url + "/" + command, data: data, beforeSend(req, settings) { self.prevRequest = settings diff --git a/app/scripts/backend/lemgram-proxy.ts b/app/scripts/backend/lemgram-proxy.ts index 476d22f0f..5cfbeec4a 100644 --- a/app/scripts/backend/lemgram-proxy.ts +++ b/app/scripts/backend/lemgram-proxy.ts @@ -27,7 +27,7 @@ export class LemgramProxy extends BaseProxy { this.prevParams = params const ajaxSettings: AjaxSettings = { - url: settings["korp_backend_url"] + "/relations", + url: settings.korp_backend_url + "/relations", data: params, success() { diff --git a/app/scripts/backend/stats-proxy.ts b/app/scripts/backend/stats-proxy.ts index a89c1533a..3a794195a 100644 --- a/app/scripts/backend/stats-proxy.ts +++ b/app/scripts/backend/stats-proxy.ts @@ -74,7 +74,7 @@ export class StatsProxy extends BaseProxy { const reduceValLabels = _.map(reduceVals, function (reduceVal) { if (reduceVal === "word") { - return settings["word_label"] + return settings.word_label } const maybeReduceAttr = settings.corpusListing.getCurrentAttributes(settings.corpusListing.getReduceLang())[ reduceVal @@ -109,7 +109,7 @@ export class StatsProxy extends BaseProxy { this.prevParams = data const def: JQuery.Deferred = $.Deferred() - const url = settings["korp_backend_url"] + "/count" + const url = settings.korp_backend_url + "/count" const ajaxSettings: AjaxSettings> = { url, data, diff --git a/app/scripts/backend/time-proxy.ts b/app/scripts/backend/time-proxy.ts index c60b25b7f..5161f051b 100644 --- a/app/scripts/backend/time-proxy.ts +++ b/app/scripts/backend/time-proxy.ts @@ -14,7 +14,7 @@ export class TimeProxy extends BaseProxy { const dfd = $.Deferred() const ajaxSettings: AjaxSettings = { - url: settings["korp_backend_url"] + "/timespan", + url: settings.korp_backend_url + "/timespan", data, } const xhr = $.ajax(httpConfAddMethod(ajaxSettings)) as JQuery.jqXHR diff --git a/app/scripts/components/corpus-updates.ts b/app/scripts/components/corpus-updates.ts index 1b93265b1..737321f7c 100644 --- a/app/scripts/components/corpus-updates.ts +++ b/app/scripts/components/corpus-updates.ts @@ -3,6 +3,7 @@ import angular, { IScope } from "angular" import moment from "moment" import settings from "@/settings" import { html } from "@/util" +import { LangMap } from "@/i18n/types" export default angular.module("korpApp").component("corpusUpdates", { template: html` @@ -44,7 +45,7 @@ export default angular.module("korpApp").component("corpusUpdates", { $scope.expanded = false $ctrl.$onInit = () => { - if (settings["frontpage"]?.["corpus_updates"]) { + if (settings.frontpage?.corpus_updates) { const limitDate = moment().subtract(6, "months") // Find most recently updated corpora $scope.recentUpdates = settings.corpusListing.corpora @@ -74,7 +75,5 @@ type CorpusUpdatesScope = IScope & { type Corpus = { info: { Updated: string } - title: TranslatedString | string + title: LangMap | string } - -type TranslatedString = { [lang: string]: string } diff --git a/app/scripts/components/frontpage.ts b/app/scripts/components/frontpage.ts index 06eb4b2ed..a80448379 100644 --- a/app/scripts/components/frontpage.ts +++ b/app/scripts/components/frontpage.ts @@ -5,29 +5,22 @@ import { isEnabled } from "@/news-service" import "@/components/corpus-updates" import "@/components/newsdesk" import "@/components/search-examples" +import settings from "@/settings" export default angular.module("korpApp").component("frontpage", { template: html`
      -
      -
      - -
      -

      - {{$root._settings['mode']['label'] | locObj:$root.lang}} -

      -
      +
      +
      + +
      +

      {{modeLabel | locObj:$root.lang}}

      +
      - +
      @@ -45,6 +38,10 @@ export default angular.module("korpApp").component("frontpage", { $ctrl.showDescription = false $scope.newsdeskIsEnabled = isEnabled() + $scope.description = settings.description + $scope.modeDescription = settings.mode_description + $scope.modeLabel = settings.mode?.label + $scope.examples = settings.frontpage?.examples $ctrl.hasResult = () => searches.activeSearch || diff --git a/app/scripts/components/search-examples.ts b/app/scripts/components/search-examples.ts index 1e5658922..b259ae505 100644 --- a/app/scripts/components/search-examples.ts +++ b/app/scripts/components/search-examples.ts @@ -4,6 +4,7 @@ import _ from "lodash" import statemachine from "@/statemachine" import { html } from "@/util" import settings from "@/settings" +import { SearchExample } from "@/settings/app-settings.types" export default angular.module("korpApp").component("searchExamples", { template: html` @@ -31,7 +32,7 @@ export default angular.module("korpApp").component("searchExamples", { $ctrl.$onInit = () => { // Find search query examples - const examples = settings["frontpage"]?.["examples"] + const examples = settings.frontpage?.examples if (examples) { // Pick three random examples $scope.examples = _.shuffle(examples).slice(0, 3) @@ -41,7 +42,7 @@ export default angular.module("korpApp").component("searchExamples", { $ctrl.setSearch = (params: Record) => { if (params.corpus) { const corpora = params.corpus.split(",") - $rootScope._settings.corpusListing.select(corpora) + settings.corpusListing.select(corpora) $rootScope.$broadcast("corpuschooserchange", corpora) } if (params.cqp) { @@ -56,11 +57,3 @@ export default angular.module("korpApp").component("searchExamples", { type SearchExamplesScope = IScope & { examples?: SearchExample[] } - -type SearchExample = { - label: TranslatedString | string - hint: TranslatedString | string - params: Record -} - -type TranslatedString = { [lang: string]: string } diff --git a/app/scripts/corpus_listing.js b/app/scripts/corpus_listing.js index 4a200a461..295027a0a 100644 --- a/app/scripts/corpus_listing.js +++ b/app/scripts/corpus_listing.js @@ -92,7 +92,7 @@ export class CorpusListing { return this._mapping_intersection(attrs) } - getStructAttrs() { + getStructAttrs(lang) { return this.structAttributes } @@ -192,7 +192,7 @@ export class CorpusListing { return true } - stringifySelected() { + stringifySelected(onlyMain) { return _.map(this.selected, "id") .map((a) => a.toUpperCase()) .join(",") diff --git a/app/scripts/data_init.js b/app/scripts/data_init.js index c5daffd52..dccef4423 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.js @@ -120,9 +120,14 @@ async function getConfig() { return await response.json() } +/** + * + * @param {import("@/settings/config.types").Config} modeSettings + * @returns {import("@/settings/config.types").ConfigTransformed} + */ function transformConfig(modeSettings) { // only if the current mode is parallel, we load the special code required - if (modeSettings["parallel"]) { + if (modeSettings.parallel) { require("./parallel/corpus_listing.js") require("./parallel/stats_proxy.ts") } @@ -134,15 +139,15 @@ function transformConfig(modeSettings) { } } - rename(modeSettings["attributes"], "pos_attributes", "attributes") + rename(modeSettings.attributes, "pos_attributes", "attributes") // take the backend configuration format for attributes and expand it // TODO the internal representation should be changed to a new, more compact one. - for (const corpusId in modeSettings["corpora"]) { - const corpus = modeSettings["corpora"][corpusId] + for (const corpusId in modeSettings.corpora) { + const corpus = modeSettings.corpora[corpusId] - if (corpus["title"] == undefined) { - corpus["title"] = corpusId + if (corpus.title == undefined) { + corpus.title = corpusId } rename(corpus, "pos_attributes", "attributes") @@ -151,7 +156,7 @@ function transformConfig(modeSettings) { const attrs = {} const newAttrList = [] for (const attrIdx in attrList) { - const attr = modeSettings["attributes"][attrType][attrList[attrIdx]] + const attr = modeSettings.attributes[attrType][attrList[attrIdx]] attrs[attr.name] = attr newAttrList.push(attr.name) } diff --git a/app/scripts/i18n/index.ts b/app/scripts/i18n/index.ts index c5caab7a6..19e898e8b 100644 --- a/app/scripts/i18n/index.ts +++ b/app/scripts/i18n/index.ts @@ -6,7 +6,7 @@ import type { LangLocMap, LangMap, LocLangMap, LocMap } from "@/i18n/types" /** Get the current UI language. */ export function getLang(): string { - return getService("$rootScope")["lang"] || settings["default_language"] + return getService("$rootScope")["lang"] || settings.default_language } /** @@ -37,8 +37,8 @@ export function locObj(map: LangMap | string, lang?: string): string | undefined lang = lang || getLang() if (map[lang]) { return map[lang] - } else if (map[settings["default_language"]]) { - return map[settings["default_language"]] + } else if (map[settings.default_language]) { + return map[settings.default_language] } // fall back to the first value if neither the selected or default language are available diff --git a/app/scripts/i18n/types.ts b/app/scripts/i18n/types.ts index 501c4d137..9cc04f046 100644 --- a/app/scripts/i18n/types.ts +++ b/app/scripts/i18n/types.ts @@ -15,3 +15,7 @@ export type LangLocMap = LangMap /** UI strings keyed first by localization key and then by language. */ export type LocLangMap = LocMap + +export type LangString = string | LangMap + +export type Labeled = { label: LangString; value: T } diff --git a/app/scripts/index.d.ts b/app/scripts/index.d.ts new file mode 100644 index 000000000..953a4cabf --- /dev/null +++ b/app/scripts/index.d.ts @@ -0,0 +1,5 @@ + +declare module "korp_config" { + const settings: import("@/settings/settings.types").Settings + export = settings +} \ No newline at end of file diff --git a/app/scripts/news-service.ts b/app/scripts/news-service.ts index e8c695a34..1d90a253e 100644 --- a/app/scripts/news-service.ts +++ b/app/scripts/news-service.ts @@ -2,13 +2,14 @@ import Yaml from "js-yaml" import settings from "./settings" import moment from "moment" +import { LangString } from "./i18n/types" export function isEnabled(): boolean { - return !!settings["news_url"] + return !!settings.news_url } export async function fetchNews(): Promise { - const response = await fetch(settings["news_url"]) + const response = await fetch(settings.news_url) const feedYaml: string = await response.text() const itemsRaw = Yaml.load(feedYaml) as NewsItemRaw[] @@ -37,14 +38,12 @@ const formatDate = (date: Date) => moment(date).format("YYYY-MM-DD") type NewsItemRaw = { created: Date expires?: Date - title: string | TranslatedString - body: string | TranslatedString + title: LangString + body: LangString } export type NewsItem = { created: string - title: string | TranslatedString - body: string | TranslatedString + title: LangString + body: LangString } - -type TranslatedString = { [lang: string]: string } diff --git a/app/scripts/parallel/stats_proxy.ts b/app/scripts/parallel/stats_proxy.ts index 82afac7c5..54ecd8822 100644 --- a/app/scripts/parallel/stats_proxy.ts +++ b/app/scripts/parallel/stats_proxy.ts @@ -1,11 +1,13 @@ /** @format */ import statsProxyFactory, { StatsProxy } from "@/backend/stats-proxy" import settings from "@/settings" +import { type ParallelCorpusListing } from "./corpus_listing" class ParallelStatsProxy extends StatsProxy { makeParameters(reduceVals: string[], cqp: string, ignoreCase: boolean) { let params = super.makeParameters(reduceVals, cqp, ignoreCase) - params.within = settings.corpusListing.getAttributeQuery("within").replace(/\|.*?:/g, ":") + const corpusListing = settings.corpusListing as ParallelCorpusListing + params.within = corpusListing.getAttributeQuery("within").replace(/\|.*?:/g, ":") return params } } diff --git a/app/scripts/settings/README.md b/app/scripts/settings/README.md new file mode 100644 index 000000000..0aba1345d --- /dev/null +++ b/app/scripts/settings/README.md @@ -0,0 +1,14 @@ +# Settings and config + +**Config** refers to a body of configuration that mainly describes the corpus data, including the _modes_ that are used to organize them, and the _attributes_ which are found in the data. +The config is mainly fetched from the backend, which does little more than read it from a bunch of YAML files. +Once fetched, they are transformed from its current structure to a previous version of the structure – only because we haven't put time into updating the usage in code yet. +(This should probably be remedied after introduction of more TypeScript.) + +**Settings** is configuration for the frontend app. It is read from the configuration directory (confusingly, as `config.yml`; see [frontend_devel.md](../../doc/frontend_devel.md)). + +However, the transformed config and the settings are then merged into the same object. +This object is then used with `import settings from "@/settings"`. + +As an exception to the serializable structure of `settings`, the **corpus listing** singleton object (`CorpusListing` or `ParallelCorpusListing`) lives as `settings.corpusListing`. +This is weird and should probably change. \ No newline at end of file diff --git a/app/scripts/settings/app-settings.types.ts b/app/scripts/settings/app-settings.types.ts new file mode 100644 index 000000000..6bdf835b6 --- /dev/null +++ b/app/scripts/settings/app-settings.types.ts @@ -0,0 +1,71 @@ +/** + * @file Typings for frontend settings as can be loaded from configuration directory. + * @format + */ + +import { Labeled, LangMap, LangString } from "@/i18n/types" +import { Attribute } from "./config.types" + +export type AppSettings = { + auth_module?: string | { module: string; options: Record } + autocomplete?: boolean + backendURLMaxLength: number + common_struct_types?: Record + corpus_config_url?: string + corpus_info_link?: { + url_template: string + label: LangString + } + cqp_prio: string[] + default_options?: Record + default_language: string + default_overview_context: string + default_reading_context: string + default_within?: Record + description?: string + download_cgi_script?: string + download_formats: string[] + download_format_params: Record> + enable_backend_kwic_download?: boolean + enable_frontend_kwic_download?: boolean + frontpage?: { + corpus_updates?: boolean + examples?: SearchExample[] + } + group_statistics: string[] + has_timespan: boolean + hits_per_page_values: number[] + hits_per_page_default: number + /** codes for translation ISO-639-1 to 639-2 */ + iso_languages: Record + korp_backend_url: string + languages?: Labeled[] + map_center?: { lat: number; lng: number; zoom: number } + map_enabled?: boolean + markup: Record + matomo?: { + url?: string + site?: number + development?: { url?: string; site?: number } + staging?: { url?: string; site?: number } + production?: { url?: string; site?: number } + } + news_url?: string + reduce_word_attribute_selector: "union" | "intersection" + reduce_struct_attribute_selector: "union" | "intersection" + statistics_search_default: boolean + urnResolver?: string + visible_modes: number + word_label: Record + word_picture?: boolean + word_picture_tagset?: Record + word_picture_conf?: Record +} + +export type SearchExample = { + label: LangString + hint?: LangString + params: Record +} + +export type WordPictureDef = Array<"_" | { rel: string; css_class: string }> diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts new file mode 100644 index 000000000..3a7d82e17 --- /dev/null +++ b/app/scripts/settings/config-transformed.types.ts @@ -0,0 +1,38 @@ +/** + * @file Typings for config as transformed after being fetched from backend + * @format + */ + +import { LangMap } from "@/i18n/types" +import { Attribute, Config, Corpus, CustomAttribute, Folder } from "./config.types" + +export type ConfigTransformed = Omit & { + corpora: Record + folders: Record + mode: { + label: string | LangMap + } +} + +export type CorpusTransformed = Omit< + Corpus, + "pos_attributes" | "struct_attributes" | "custom_attributes" | "within" | "context" +> & { + attributes: Record + struct_attributes: Record + custom_attributes?: Record + _attributes_order: string[] + _struct_attributes_order: string[] + _custom_attributes_order: string[] + within: Record + context: Record + info: { + Name: string + Size: string | number + Sentences: string | number + Updated?: string + FirstDate?: string + LastDate?: string + Protected?: boolean + } +} diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts new file mode 100644 index 000000000..747305482 --- /dev/null +++ b/app/scripts/settings/config.types.ts @@ -0,0 +1,81 @@ +/** + * @file Typings for config as fetched from backend. + * @format + */ + +import { Labeled, LangMap, LangString } from "@/i18n/types" + +export type Config = { + attributes: { + pos_attributes: Record + struct_attributes: Record + custom_attributes?: Record + } + corpora: Record + folders?: Record + label: LangString + map_enabled?: boolean + mode_description?: LangString + modes: { + mode: string + label: LangString + }[] + order?: number + parallel?: boolean + preselected_corpora?: string[] +} + +export type Corpus = { + context: Labeled[] + description: LangString + id: string + pos_attributes: string[] + struct_attributes: string[] + custom_attributes: string[] + reading_mode?: boolean + title?: LangString + within: Labeled[] +} + +export type Folder = { + description?: LangString + title: LangString +} & ({ subfolders?: Record } | { corpora?: string[] }) + +export type Attribute = { + dataset?: Record + display_type?: "hidden" + escape?: boolean + extended_component?: string + extended_template?: string + external_search?: string + group_by?: "group_by" | "group_by_struct" + hide_compare?: boolean + hide_extended?: boolean + hide_sidebar?: boolean + hide_statistics?: boolean + internal_search?: boolean + is_struct_attr?: boolean + label: LangString + name: string + opts?: Record | false + order?: number + pattern?: string + ranked?: boolean + sidebar_component?: string | { name: string; options: Record } + sidebar_info_url?: string + sidebar_hide_label?: boolean + stats_cqp?: string + stats_stringify?: string + stringify?: string + translation?: Record + type?: "set" | "url" +} + +export type CustomAttribute = { + custom_type: string + label: LangString + name: string + pattern?: string + sidebar_component?: string | { name: string; options: Record } +} diff --git a/app/scripts/settings.js b/app/scripts/settings/index.ts similarity index 78% rename from app/scripts/settings.js rename to app/scripts/settings/index.ts index a975b297c..c2bf1dea7 100644 --- a/app/scripts/settings.js +++ b/app/scripts/settings/index.ts @@ -1,13 +1,14 @@ /** @format */ import _ from "lodash" import settings from "korp_config" +import { AppSettings } from "./app-settings.types" export default settings -if (process.env.ENVIRONMENT != "production") window.settings = settings +if (process.env.ENVIRONMENT != "production") (window as any).settings = settings settings.markup = { - msd: require("../markup/msd.html"), + msd: require("@/../markup/msd.html"), } /** @@ -15,7 +16,7 @@ settings.markup = { */ export function setDefaultConfigValues() { // Default values for some settings properties - const settingsDefaults = { + const settingsDefaults: Partial = { hits_per_page_values: [25, 50, 75, 100], group_statistics: [], // The default maximum URI length for Apache is 8190 but keep @@ -42,8 +43,8 @@ export function setDefaultConfigValues() { // Default values depending on other settings values, possibly // assigned a default value above - const settingsDefaultsDep = { - hits_per_page_default: settings["hits_per_page_values"][0], + const settingsDefaultsDep: Partial = { + hits_per_page_default: settings.hits_per_page_values[0], } _.defaults(settings, settingsDefaultsDep) diff --git a/app/scripts/settings/settings.types.ts b/app/scripts/settings/settings.types.ts new file mode 100644 index 000000000..bf7c15425 --- /dev/null +++ b/app/scripts/settings/settings.types.ts @@ -0,0 +1,19 @@ +/** + * @file Typings for settings as they are stored and used in the frontend. + * @format + */ + +import { CorpusListing } from "@/corpus_listing" +import { AppSettings } from "./app-settings.types" +import { ConfigTransformed } from "./config-transformed.types" + +export type Settings = AppSettings & + ConfigTransformed & { + // Populated in data_init.js fetchInitialData() using the `/timespan` API + time_data: [ + [number, number][], // Token count per year + number // Undated tokens + ] + // Set in data_init.js fetchInitialData() + corpusListing: CorpusListing + } diff --git a/app/scripts/statistics.ts b/app/scripts/statistics.ts index 78c79a60f..20967db92 100644 --- a/app/scripts/statistics.ts +++ b/app/scripts/statistics.ts @@ -106,7 +106,7 @@ const createStatisticsService = function () { type: "korpStatistics", data, reduceVals, - groupStatistics: settings["group_statistics"], + groupStatistics: settings.group_statistics, } as StatisticsWorkerMessage) } diff --git a/app/scripts/timeseries.ts b/app/scripts/timeseries.ts index 371794a51..82017d9bd 100644 --- a/app/scripts/timeseries.ts +++ b/app/scripts/timeseries.ts @@ -22,10 +22,10 @@ export function calculateYearTicks(min: number, max: number) { // Time data is fetched in data_init.js, to also provide data for search result trend diagram (?) /** Data size per year of all corpora. */ -export const getTimeDataPairs = (): [number, number][] => settings["time_data"][0] +export const getTimeDataPairs = (): [number, number][] => settings.time_data[0] /** Data size of unknown year in all corpora. */ -export const getCountUndated = (): number => settings["time_data"][1] +export const getCountUndated = (): number => settings.time_data[1] /** Get data size per year of all corpora. */ export const getSeries = () => fromPairs(getTimeDataPairs()) as YearSeries diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 2e80b7a8a..13d349a36 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -285,8 +285,8 @@ export function setDownloadLinks(xhr_settings: JQuery.AjaxSettings, result_data) } $("#download-links").append("") i = 0 - while (i < settings["download_formats"].length) { - const format = settings["download_formats"][i] + while (i < settings.download_formats.length) { + const format = settings.download_formats[i] // NOTE: Using attribute rel="localize[...]" to localize the // title attribute requires a small change to // lib/jquery.localize.js. Without that, we could use @@ -305,17 +305,17 @@ export function setDownloadLinks(xhr_settings: JQuery.AjaxSettings, result_data) query_params: xhr_settings.url, format, korp_url: window.location.href, - korp_server_url: settings["korp_backend_url"], + korp_server_url: settings.korp_backend_url, corpus_config: JSON.stringify(result_corpora_settings), corpus_config_info_keys: ["metadata", "licence", "homepage", "compiler"].join(","), urn_resolver: settings.urnResolver, } if ("downloadFormatParams" in settings) { - if ("*" in settings["download_format_params"]) { - $.extend(download_params, settings["download_format_params"]["*"]) + if ("*" in settings.download_format_params) { + $.extend(download_params, settings.download_format_params["*"]) } - if (format in settings["download_format_params"]) { - $.extend(download_params, settings["download_format_params"][format]) + if (format in settings.download_format_params) { + $.extend(download_params, settings.download_format_params[format]) } } option.appendTo("#download-links").data("params", download_params) @@ -330,7 +330,7 @@ export function setDownloadLinks(xhr_settings: JQuery.AjaxSettings, result_data) if (!params) { return } - ;($ as any).generateFile(settings["download_cgi_script"], params) + ;($ as any).generateFile(settings.download_cgi_script, params) const self = $(this) return setTimeout(() => self.val("init"), 1000) }) @@ -403,7 +403,7 @@ export function httpConfAddMethodFetch( url: string, params: Record ): { url: string; request?: RequestInit } { - if (calcUrlLength(url, params) > settings["backendURLMaxLength"]) { + if (calcUrlLength(url, params) > settings.backendURLMaxLength) { const body = new FormData() for (const key in params) { body.append(key, params[key]) From 15ba09800ea74542fb75142a26f976b3cf16beda Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 5 Jul 2024 09:04:57 +0200 Subject: [PATCH 19/99] refactor: Moar LangString --- app/scripts/settings/app-settings.types.ts | 2 +- app/scripts/settings/config-transformed.types.ts | 4 ++-- app/scripts/statistics.ts | 9 ++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/scripts/settings/app-settings.types.ts b/app/scripts/settings/app-settings.types.ts index 6bdf835b6..b20c9f455 100644 --- a/app/scripts/settings/app-settings.types.ts +++ b/app/scripts/settings/app-settings.types.ts @@ -3,7 +3,7 @@ * @format */ -import { Labeled, LangMap, LangString } from "@/i18n/types" +import { Labeled, LangString } from "@/i18n/types" import { Attribute } from "./config.types" export type AppSettings = { diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts index 3a7d82e17..7c3049a52 100644 --- a/app/scripts/settings/config-transformed.types.ts +++ b/app/scripts/settings/config-transformed.types.ts @@ -3,14 +3,14 @@ * @format */ -import { LangMap } from "@/i18n/types" +import { LangString } from "@/i18n/types" import { Attribute, Config, Corpus, CustomAttribute, Folder } from "./config.types" export type ConfigTransformed = Omit & { corpora: Record folders: Record mode: { - label: string | LangMap + label: LangString } } diff --git a/app/scripts/statistics.ts b/app/scripts/statistics.ts index 20967db92..5b28f8b89 100644 --- a/app/scripts/statistics.ts +++ b/app/scripts/statistics.ts @@ -5,13 +5,14 @@ import { reduceStringify } from "../config/statistics_config" import type { StatsNormalized, StatisticsWorkerMessage, StatisticsWorkerResult, SearchParams } from "./statistics.types" import { hitCountHtml } from "@/util" import { Row } from "./statistics_worker" +import { LangString } from "./i18n/types" const pieChartImg = require("../img/stats2.png") const createStatisticsService = function () { const createColumns = function ( corpora: Record, reduceVals: string[], - reduceValLabels: Label[] + reduceValLabels: LangString[] ): SlickgridColumn[] { const valueFormatter: SlickgridFormatter = function (row, cell, value, columnDef, dataContext) { const [absolute, relative] = [...dataContext[columnDef.id + "_value"]] @@ -83,7 +84,7 @@ const createStatisticsService = function () { originalCorpora: string, data: StatsNormalized, reduceVals: string[], - reduceValLabels: Label[], + reduceValLabels: LangString[], ignoreCase: boolean, prevNonExpandedCQP: string ) { @@ -120,7 +121,7 @@ export type SlickgridColumn = { field: string formatter: SlickgridFormatter name?: string - translation?: Label + translation?: LangString sortable?: boolean minWidth?: number maxWidth?: number @@ -128,8 +129,6 @@ export type SlickgridColumn = { headerCssClass?: string } -type Label = string | Record - type SlickgridFormatter = ( // There's currently no Korp code that uses these first three args row: unknown, From 22eb9a44228cf6eb63758c650f477c245b90bc85 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 5 Jul 2024 10:20:55 +0200 Subject: [PATCH 20/99] refactor(ts): CQP --- CHANGELOG.md | 1 + app/scripts/components/statistics.js | 2 +- app/scripts/cqp_parser/{cqp.js => cqp.ts} | 92 +++++++++++++++-------- app/scripts/cqp_parser/cqp.types.ts | 43 +++++++++++ 4 files changed, 105 insertions(+), 33 deletions(-) rename app/scripts/cqp_parser/{cqp.js => cqp.ts} (63%) create mode 100644 app/scripts/cqp_parser/cqp.types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 06df5d1d5..a9f8eb98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - TypeScript typings for config/settings +- TypeScript typings for CQP queries ### Changed diff --git a/app/scripts/components/statistics.js b/app/scripts/components/statistics.js index d6bff7503..994504555 100644 --- a/app/scripts/components/statistics.js +++ b/app/scripts/components/statistics.js @@ -6,7 +6,7 @@ import settings from "@/settings" import { html } from "@/util" import { loc, locObj } from "@/i18n" import { getCqp } from "../../config/statistics_config.js" -import { expandOperators } from "@/cqp_parser/cqp.js" +import { expandOperators } from "@/cqp_parser/cqp" import "@/components/corpus-distribution-chart" angular.module("korpApp").component("statistics", { diff --git a/app/scripts/cqp_parser/cqp.js b/app/scripts/cqp_parser/cqp.ts similarity index 63% rename from app/scripts/cqp_parser/cqp.js rename to app/scripts/cqp_parser/cqp.ts index 594d48041..4315e1364 100644 --- a/app/scripts/cqp_parser/cqp.js +++ b/app/scripts/cqp_parser/cqp.ts @@ -1,16 +1,27 @@ /** @format */ import _ from "lodash" -import moment from "moment" +import moment, { type Moment } from "moment" import settings from "@/settings" -import { parse } from "./CQPParser" +import { parse as parse_ } from "./CQPParser" +import type { Condition, CqpQuery, DateRange, OperatorKorp } from "./cqp.types" +/** Parse CQP string to syntax tree. */ +// Rename to be able to add typing. +const parse: (input: string, options?: never) => CqpQuery = parse_ export { parse } -export function parseDateInterval(op, val, expanded_format) { - let out +/** + * Create CQP expression for a date interval condition. + * + * @param opKorp Operator to use if not using `expanded_format` + * @param val An array like `[fromdate, todate, fromtime, totime]` + * @param expanded_format Whether to convert to standard CQP or keep Korp-specific operators + */ +export function parseDateInterval(opKorp: OperatorKorp, val: DateRange, expanded_format?: boolean) { + let out: string val = _.invokeMap(val, "toString") if (!expanded_format) { - return `$date_interval ${op} '${val.join(",")}'` + return `$date_interval ${opKorp} '${val.join(",")}'` } const [fromdate, todate, fromtime, totime] = val @@ -25,7 +36,7 @@ export function parseDateInterval(op, val, expanded_format) { text_timeto: totime, } - op = function (field, operator, valfield) { + function op(field: string, operator: string, valfield?: string) { val = valfield ? fieldMapping[valfield] : fieldMapping[field] return `int(_.${field}) ${operator} ${val}` } @@ -62,7 +73,12 @@ export function parseDateInterval(op, val, expanded_format) { return out } -export function stringify(cqp_obj, expanded_format) { +/** + * Serialize syntax tree to CQP string. + * @param cqp_obj Syntax tree + * @param expanded_format Whether to convert to standard CQP or keep Korp-specific operators + */ +export function stringify(cqp_obj: CqpQuery, expanded_format?: boolean): string { if (expanded_format == null) { expanded_format = false } @@ -80,13 +96,13 @@ export function stringify(cqp_obj, expanded_format) { continue } - const outer_and_array = [] + const outer_and_array: string[][] = [] for (let and_array of token.and_block) { - const or_array = [] + const or_array: string[] = [] for (let { type, op, val, flags } of and_array) { var out if (expanded_format) { - ;[val, op] = { + ;[val, op] = ({ "^=": [val + ".*", "="], "_=": [`.*${val}.*`, "="], "&=": [`.*${val}`, "="], @@ -104,7 +120,7 @@ export function stringify(cqp_obj, expanded_format) { not_incontains_contains: [`.*${val}.*`, "not contains"], ends_with_contains: [`.*${val}`, "contains"], not_ends_with_contains: [`.*${val}`, "not contains"], - }[op] || [val, op] + }[op] || [val, op]) as [string | DateRange, OperatorKorp] } let flagstr = "" @@ -115,7 +131,7 @@ export function stringify(cqp_obj, expanded_format) { if (type === "word" && val === "") { out = "" } else if (settings.corpusListing.isDateInterval(type)) { - out = parseDateInterval(op, val, expanded_format) + out = parseDateInterval(op, val as DateRange, expanded_format) } else { out = `${type} ${op} \"${val}\"` } @@ -129,7 +145,7 @@ export function stringify(cqp_obj, expanded_format) { } } - let or_out = outer_and_array.map((x) => (x.length > 1 ? `(${x.join(" | ")})` : x.join(" | "))) + let or_out: string[] = outer_and_array.map((x) => (x.length > 1 ? `(${x.join(" | ")})` : x.join(" | "))) if (token.bound) { or_out = _.compact(or_out) @@ -149,46 +165,58 @@ export function stringify(cqp_obj, expanded_format) { return output.join(" ") } -export const expandOperators = (cqpstr) => stringify(parse(cqpstr), true) +export const expandOperators = (cqpstr: string) => stringify(parse(cqpstr), true) -export function getTimeInterval(obj) { - let from = [] - let to = [] +/** + * Find first and last date in any date interval conditions. + * @param obj Syntax tree + * @returns `[from, to]` as Moment objects, or `undefined` if query has no intervals + */ +export function getTimeInterval(obj: CqpQuery): [Moment, Moment] | undefined { + let froms: Moment[] = [] + let tos: Moment[] = [] for (let token of obj) { for (let or_block of token.and_block) { for (let item of or_block) { if (item.type === "date_interval") { - from.push(moment(`${item.val[0]}${item.val[2]}`, "YYYYMMDDhhmmss")) - to.push(moment(`${item.val[1]}${item.val[3]}`, "YYYYMMDDhhmmss")) + froms.push(moment(`${item.val[0]}${item.val[2]}`, "YYYYMMDDhhmmss")) + tos.push(moment(`${item.val[1]}${item.val[3]}`, "YYYYMMDDhhmmss")) } } } } - if (!from.length) { + if (!froms.length) { return } - from = _.minBy(from, (mom) => mom.toDate()) - to = _.maxBy(to, (mom) => mom.toDate()) + const from = _.minBy(froms, (m) => m.toDate()) + const to = _.maxBy(tos, (m) => m.toDate()) return [from, to] } -export function prioSort(cqpObjs) { - const getPrio = function (and_array) { - const numbers = _.map(and_array, (item) => _.indexOf(settings["cqp_prio"], item.type)) +/** + * Sort the conditions in each token according to the `cqp_prio` setting. + */ +export function prioSort(cqpObjs: CqpQuery) { + const getPrio = function (or_block: Condition[]) { + const numbers = _.map(or_block, (item) => _.indexOf(settings.cqp_prio, item.type)) return Math.min(...(numbers || [])) } for (let token of cqpObjs) { - token.and_block = _.sortBy(token.and_block, getPrio).reverse() + token.and_block = (_.sortBy(token.and_block, getPrio) as Condition[][]).reverse() } return cqpObjs } -// assume cqpObj2 to contain fewer tokens than cqpObj1 -export function mergeCqpExprs(cqpObj1, cqpObj2) { +/** + * Create intersection of two queries. + * + * The second query is assumed to contain fewer tokens than the first. + */ +export function mergeCqpExprs(cqpObj1: CqpQuery, cqpObj2: CqpQuery) { for (let i = 0; i < cqpObj2.length; i++) { const token = cqpObj2[i] for (let j = 0; j < cqpObj1.length; j++) { @@ -202,14 +230,14 @@ export function mergeCqpExprs(cqpObj1, cqpObj2) { } /** Check if a query has any wildcards (`[]`) */ -export const hasWildcard = (cqpObjs) => cqpObjs.some((token) => stringify([token]).indexOf("[]") === 0) +export const hasWildcard = (cqpObjs: CqpQuery) => cqpObjs.some((token) => stringify([token]).indexOf("[]") === 0) /** Check if a query has any tokens with repetition */ -export const hasRepetition = (cqpObjs) => cqpObjs.some((token) => token.repeat) +export const hasRepetition = (cqpObjs: CqpQuery) => cqpObjs.some((token) => token.repeat) /** Check if a query has any structure boundaries, e.g. sentence start */ -export const hasStruct = (cqpObjs) => cqpObjs.some((token) => token.struct) +export const hasStruct = (cqpObjs: CqpQuery) => cqpObjs.some((token) => token.struct) /** Determine whether a query will work with the in_order option */ -export const supportsInOrder = (cqpObjs) => +export const supportsInOrder = (cqpObjs: CqpQuery) => cqpObjs.length > 1 && !hasWildcard(cqpObjs) && !hasRepetition(cqpObjs) && !hasStruct(cqpObjs) diff --git a/app/scripts/cqp_parser/cqp.types.ts b/app/scripts/cqp_parser/cqp.types.ts new file mode 100644 index 000000000..a98bbc71d --- /dev/null +++ b/app/scripts/cqp_parser/cqp.types.ts @@ -0,0 +1,43 @@ +/** @format */ +export type CqpQuery = CqpToken[] + +export type CqpToken = { + and_block: Condition[][] + bound?: Record<"lbound" | "rbound", true> + /** `[min]` or `[min, max]` */ + repeat?: [number] | [number, number] + struct?: string + start?: number +} + +export type Condition = { + type: string + op: OperatorKorp + val: string | DateRange + flags: Record +} + +/** Should be `[fromdate, todate, fromtime, totime]` */ +export type DateRange = (string | number)[] + +export type Operator = "=" | "!=" | "contains" | "not contains" + +export type OperatorKorp = + | Operator + | "^=" + | "_=" + | "&=" + | "*=" + | "!*=" + | "rank_contains" + | "not_rank_contains" + | "highest_rank" + | "not_highest_rank" + | "regexp_contains" + | "not_regexp_contains" + | "starts_with_contains" + | "not_starts_with_contains" + | "incontains_contains" + | "not_incontains_contains" + | "ends_with_contains" + | "not_ends_with_contains" From 741a8b5afccfffee6f0a6e9ea9a35644c30b73a6 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 5 Jul 2024 14:31:30 +0200 Subject: [PATCH 21/99] refactor: Moar LangString --- app/scripts/components/corpus-updates.ts | 4 ++-- app/scripts/i18n/index.ts | 4 ++-- app/scripts/kwic_download.ts | 4 ++-- app/scripts/settings/config.types.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/scripts/components/corpus-updates.ts b/app/scripts/components/corpus-updates.ts index 737321f7c..305dd128d 100644 --- a/app/scripts/components/corpus-updates.ts +++ b/app/scripts/components/corpus-updates.ts @@ -3,7 +3,7 @@ import angular, { IScope } from "angular" import moment from "moment" import settings from "@/settings" import { html } from "@/util" -import { LangMap } from "@/i18n/types" +import { LangString } from "@/i18n/types" export default angular.module("korpApp").component("corpusUpdates", { template: html` @@ -75,5 +75,5 @@ type CorpusUpdatesScope = IScope & { type Corpus = { info: { Updated: string } - title: LangMap | string + title: LangString } diff --git a/app/scripts/i18n/index.ts b/app/scripts/i18n/index.ts index 19e898e8b..3d3a3177a 100644 --- a/app/scripts/i18n/index.ts +++ b/app/scripts/i18n/index.ts @@ -2,7 +2,7 @@ import isObject from "lodash/isObject" import settings from "@/settings" import { getService } from "@/util" -import type { LangLocMap, LangMap, LocLangMap, LocMap } from "@/i18n/types" +import type { LangLocMap, LangString, LocLangMap, LocMap } from "@/i18n/types" /** Get the current UI language. */ export function getLang(): string { @@ -30,7 +30,7 @@ export function loc(key: string, lang?: string) { * @param lang The code of the language to translate to. Defaults to the global current language. * @returns The translated string, or undefined if no translation is found. */ -export function locObj(map: LangMap | string, lang?: string): string | undefined { +export function locObj(map: LangString, lang?: string): string | undefined { if (!map) return undefined if (typeof map == "string") return map diff --git a/app/scripts/kwic_download.ts b/app/scripts/kwic_download.ts index eb49670e4..257c19077 100644 --- a/app/scripts/kwic_download.ts +++ b/app/scripts/kwic_download.ts @@ -4,7 +4,7 @@ import moment from "moment" import CSV from "comma-separated-values/csv" import { locObj } from "@/i18n" import { type ApiKwic, type KorpQueryParams } from "@/backend/kwic-proxy" -import { LangMap } from "./i18n/types" +import { LangString } from "./i18n/types" // This is what is returned by massageData in kwic.js type Row = ApiKwic | LinkedKwic | CorpusHeading @@ -18,7 +18,7 @@ type LinkedKwic = { } type CorpusHeading = { - newCorpus: LangMap | string + newCorpus: LangString noContext?: boolean } diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts index 747305482..eaf70a8e5 100644 --- a/app/scripts/settings/config.types.ts +++ b/app/scripts/settings/config.types.ts @@ -3,7 +3,7 @@ * @format */ -import { Labeled, LangMap, LangString } from "@/i18n/types" +import { Labeled, LangString } from "@/i18n/types" export type Config = { attributes: { From 2129a39c3437d73f7aec54221d973c93ebe3f38e Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 5 Jul 2024 18:16:16 +0200 Subject: [PATCH 22/99] refactor(ts): CorpusListing --- CHANGELOG.md | 7 +- app/scripts/backend/kwic-proxy.ts | 2 +- app/scripts/components/corpus-updates.ts | 11 +- .../{corpus_listing.js => corpus_listing.ts} | 157 +++++++++--------- app/scripts/data_init.js | 4 +- .../{corpus_listing.js => corpus_listing.ts} | 109 +++++------- .../settings/config-transformed.types.ts | 3 + app/scripts/settings/config.types.ts | 2 + app/scripts/timeseries.ts | 6 +- app/scripts/util.ts | 3 +- 10 files changed, 138 insertions(+), 166 deletions(-) rename app/scripts/{corpus_listing.js => corpus_listing.ts} (73%) rename app/scripts/parallel/{corpus_listing.js => corpus_listing.ts} (59%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f8eb98e..063104cdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ ### Added -- TypeScript typings for config/settings -- TypeScript typings for CQP queries +- TypeScript typings for: + - config/settings + - CQP queries + - CorpusListing ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics +- In the `CorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures ### Refactored diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index 35a7d59b5..ecff220ea 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -41,7 +41,7 @@ export class KwicProxy extends BaseProxy { } function getPageInterval(): Interval { - const hpp: string | number = locationSearchGet("hpp") + const hpp = locationSearchGet("hpp") const itemsPerPage = Number(hpp) || settings.hits_per_page_default const start = (page || 0) * itemsPerPage const end = start + itemsPerPage - 1 diff --git a/app/scripts/components/corpus-updates.ts b/app/scripts/components/corpus-updates.ts index 305dd128d..0ba0c9130 100644 --- a/app/scripts/components/corpus-updates.ts +++ b/app/scripts/components/corpus-updates.ts @@ -3,7 +3,7 @@ import angular, { IScope } from "angular" import moment from "moment" import settings from "@/settings" import { html } from "@/util" -import { LangString } from "@/i18n/types" +import { CorpusTransformed } from "@/settings/config-transformed.types" export default angular.module("korpApp").component("corpusUpdates", { template: html` @@ -67,13 +67,8 @@ export default angular.module("korpApp").component("corpusUpdates", { type CorpusUpdatesScope = IScope & { LIMIT: number - recentUpdates: Corpus[] | null - recentUpdatesFiltered: Corpus[] | null + recentUpdates: CorpusTransformed[] | null + recentUpdatesFiltered: CorpusTransformed[] | null expanded: boolean toggleExpanded: (to?: boolean) => void } - -type Corpus = { - info: { Updated: string } - title: LangString -} diff --git a/app/scripts/corpus_listing.js b/app/scripts/corpus_listing.ts similarity index 73% rename from app/scripts/corpus_listing.js rename to app/scripts/corpus_listing.ts index 295027a0a..e6cef51c0 100644 --- a/app/scripts/corpus_listing.js +++ b/app/scripts/corpus_listing.ts @@ -1,18 +1,39 @@ /** @format */ import _ from "lodash" -import moment from "moment" +import moment, { type Moment } from "moment" import settings from "@/settings" import { locationSearchGet } from "@/util" -import { loc } from "@/i18n" +import { locObj } from "@/i18n" +import { Attribute } from "./settings/config.types" +import { CorpusTransformed } from "./settings/config-transformed.types" +import { LangString } from "./i18n/types" + +export type Filter = { + settings: Attribute + corpora: string[] +} + +export type AttributeOption = { + group: "word" | "word_attr" | "sentence_attr" + value: string + label: LangString +} export class CorpusListing { - constructor(corpora) { + corpora: CorpusTransformed[] + selected: CorpusTransformed[] + struct: Record + structAttributes: Record + commonAttributes: Record + _wordGroup: AttributeOption + + constructor(corpora: Record) { this.struct = corpora this.corpora = _.values(corpora) this.selected = [] } - get(key) { + get(key: string) { return this.struct[key] } @@ -20,11 +41,11 @@ export class CorpusListing { return this.corpora } - map(func) { + map(func: (corpus: CorpusTransformed) => T): T[] { return _.map(this.corpora, func) } - subsetFactory(idArray) { + subsetFactory(idArray: string[]) { // returns a new CorpusListing instance from an id subset. idArray = _.invokeMap(idArray, "toLowerCase") const cl = new CorpusListing(_.pick(this.struct, ...idArray)) @@ -34,41 +55,41 @@ export class CorpusListing { } // only applicable for parallel corpora - getReduceLang() {} + getReduceLang(): string { + return "" + } // Returns an array of all the selected corpora's IDs in uppercase getSelectedCorpora() { return _.map(this.selected, "id") } - select(idArray) { - this.selected = _.values(_.pick.apply(this, [this.struct].concat(idArray))) - + select(idArray: string[]): void { + this.selected = idArray.map((id) => this.struct[id]) this.updateAttributes() } - mapSelectedCorpora(f) { + mapSelectedCorpora(f: (corpus: CorpusTransformed) => T): T[] { return _.map(this.selected, f) } // takes an array of mapping objs and returns their intersection - _mapping_intersection(mappingArray) { - return _.reduce( - mappingArray, - function (a, b) { - const keys_intersect = _.intersection(_.keys(a), _.keys(b)) + _mapping_intersection(mappingArray: T[]): T { + return ( + _.reduce(mappingArray, function (a: T, b: T) { + const keys_intersect = _.intersection(_.keys(a), _.keys(b)) as (keyof T)[] const to_mergea = _.pick(a, ...keys_intersect) const to_mergeb = _.pick(b, ...keys_intersect) return _.merge({}, to_mergea, to_mergeb) - } || {} + }) || ({} as T) ) } - _mapping_union(mappingArray) { - return _.reduce(mappingArray, (a, b) => _.merge(a, b), {}) + _mapping_union(mappingArray: T[]): T { + return _.reduce(mappingArray, (a, b) => _.merge(a, b), {} as T) } - getCurrentAttributes(lang) { + getCurrentAttributes(lang?: string) { // lang not used here, only in parallel mode const attrs = this.mapSelectedCorpora((corpus) => corpus.attributes) return this._invalidateAttrs(attrs) @@ -80,7 +101,7 @@ export class CorpusListing { return this._mapping_intersection(attrs) } - getStructAttrsIntersection() { + getStructAttrsIntersection(lang?: string): Record { const attrs = this.mapSelectedCorpora(function (corpus) { for (let key in corpus["struct_attributes"]) { const value = corpus["struct_attributes"][key] @@ -92,11 +113,11 @@ export class CorpusListing { return this._mapping_intersection(attrs) } - getStructAttrs(lang) { + getStructAttrs(lang?: string): Record { return this.structAttributes } - _getStructAttrs() { + _getStructAttrs(): Record { const attrs = this.mapSelectedCorpora(function (corpus) { for (let key in corpus["struct_attributes"]) { const value = corpus["struct_attributes"][key] @@ -113,7 +134,7 @@ export class CorpusListing { // TODO this code merges datasets from attributes with the same name and // should be moved to the code for extended controller "datasetSelect" - const withDataset = _.filter(_.toPairs(rest), (item) => item[1].dataset) + const withDataset: [string, Attribute][] = _.toPairs(rest).filter((item) => item[1].dataset) $.each(withDataset, function (i, item) { const key = item[0] const val = item[1] @@ -138,8 +159,7 @@ export class CorpusListing { /** Compile list of filters applicable to all selected corpora. */ getDefaultFilters() { - /** @type {Object.} */ - const attrs = {} + const attrs: Record = {} // Collect filters of all selected corpora for (let corpus of this.selected) { @@ -165,7 +185,7 @@ export class CorpusListing { return attrs } - _invalidateAttrs(attrs) { + _invalidateAttrs(attrs: Record[]) { const union = this._mapping_union(attrs) const intersection = this._mapping_intersection(attrs) $.each(union, function (key, value) { @@ -180,7 +200,7 @@ export class CorpusListing { } // returns true if coprus has all attrs, else false - corpusHasAttrs(corpus, attrs) { + corpusHasAttrs(corpus: string, attrs: string[]): boolean { for (let attr of attrs) { if ( attr !== "word" && @@ -192,34 +212,34 @@ export class CorpusListing { return true } - stringifySelected(onlyMain) { + stringifySelected(onlyMain?: boolean): string { return _.map(this.selected, "id") .map((a) => a.toUpperCase()) .join(",") } - stringifyAll() { + stringifyAll(): string { return _.map(this.corpora, "id") .map((a) => a.toUpperCase()) .join(",") } - getWithinKeys() { + getWithinKeys(): string[] { const struct = _.map(this.selected, (corpus) => _.keys(corpus.within)) - return _.union(...(struct || [])) + return _.union(...struct) } - getContextQueryStringFromCorpusId(corpus_ids, prefer, avoid) { + getContextQueryStringFromCorpusId(corpus_ids: string[], prefer: string, avoid: string): string { const corpora = _.map(corpus_ids, (corpus_id) => settings.corpora[corpus_id.toLowerCase()]) return this.getContextQueryStringFromCorpora(_.compact(corpora), prefer, avoid) } - getContextQueryString(prefer, avoid) { + getContextQueryString(prefer: string, avoid: string): string { return this.getContextQueryStringFromCorpora(this.selected, prefer, avoid) } - getContextQueryStringFromCorpora(corpora, prefer, avoid) { - const output = [] + getContextQueryStringFromCorpora(corpora: CorpusTransformed[], prefer: string, avoid: string) { + const output: string[] = [] for (let corpus of corpora) { const contexts = _.keys(corpus.context) if (!contexts.includes(prefer)) { @@ -232,10 +252,10 @@ export class CorpusListing { return _(output).compact().join() } - getWithinParameters() { - const defaultWithin = locationSearchGet("within") || _.keys(settings["default_within"])[0] + getWithinParameters(): { default_within: string; within: string } { + const defaultWithin = locationSearchGet("within") || _.keys(settings.default_within)[0] - const output = [] + const output: string[] = [] for (let corpus of this.selected) { const withins = _.keys(corpus.within) if (!withins.includes(defaultWithin)) { @@ -246,7 +266,7 @@ export class CorpusListing { return { default_within: defaultWithin, within } } - getCommonWithins() { + getCommonWithins(): Record { // only return withins that are available in every selected corpus const allWithins = this.selected.map((corp) => corp.within) const withins = allWithins.reduce( @@ -269,7 +289,7 @@ export class CorpusListing { return withins } - getTimeInterval() { + getTimeInterval(): [number, number] { const all = _(this.selected) .map("time") .filter((item) => item != null) @@ -282,11 +302,8 @@ export class CorpusListing { return [_.first(all), _.last(all)] } - getMomentInterval() { - let from, to - const toUnix = (item) => item.unix() - - const infoGetter = (prop) => { + getMomentInterval(): [Moment, Moment] { + const infoGetter = (prop: "FirstDate" | "LastDate") => { return _(this.selected) .map("info") .map(prop) @@ -298,28 +315,20 @@ export class CorpusListing { const froms = infoGetter("FirstDate") const tos = infoGetter("LastDate") - if (!froms.length) { - from = null - } else { - from = _.minBy(froms, toUnix) - } - if (!tos.length) { - to = null - } else { - to = _.maxBy(tos, toUnix) - } + const from = _.minBy(froms, (item) => item.unix()) || null + const to = _.maxBy(tos, (item) => item.unix()) || null return [from, to] } - getTitleObj(corpus) { + getTitleObj(corpus: string): LangString { return this.struct[corpus].title } /* * Avoid triggering watches etc. by only creating this object once. */ - getWordGroup() { + getWordGroup(): AttributeOption { if (!this._wordGroup) { this._wordGroup = { group: "word", @@ -330,31 +339,27 @@ export class CorpusListing { return this._wordGroup } - getWordAttributeGroups(lang, setOperator) { - let allAttrs - if (setOperator === "union") { - allAttrs = this.getCurrentAttributes(lang) - } else { - allAttrs = this.getCurrentAttributesIntersection() - } + getWordAttributeGroups(lang: string, setOperator: "union" | "intersection"): AttributeOption[] { + const allAttrs = + setOperator === "union" ? this.getCurrentAttributes(lang) : this.getCurrentAttributesIntersection() - const attrs = [] + const attrs: AttributeOption[] = [] for (let key in allAttrs) { const obj = allAttrs[key] if (obj["display_type"] !== "hidden") { - attrs.push(_.extend({ group: "word_attr", value: key }, obj)) + attrs.push({ group: "word_attr", value: key, ...obj }) } } return attrs } - getWordAttribute(attribute, lang) { + getWordAttribute(attribute: string, lang: string): Attribute { const attributes = this.getCurrentAttributes(lang) return attributes[attribute] } - getStructAttributeGroups(lang, setOperator) { + getStructAttributeGroups(lang: string, setOperator: "union" | "intersection"): AttributeOption[] { let allAttrs if (setOperator === "union") { allAttrs = this.getStructAttrs(lang) @@ -364,28 +369,28 @@ export class CorpusListing { const common = this.commonAttributes - let sentAttrs = [] + let sentAttrs: AttributeOption[] = [] const object = _.extend({}, common, allAttrs) for (let key in object) { const obj = object[key] if (obj["display_type"] !== "hidden") { - sentAttrs.push(_.extend({ group: "sentence_attr", value: key }, obj)) + sentAttrs.push({ group: "sentence_attr", value: key, ...obj }) } } - sentAttrs = _.sortBy(sentAttrs, (item) => loc(item.label)) + sentAttrs = _.sortBy(sentAttrs, (item) => locObj(item.label)) return sentAttrs } - getAttributeGroups(lang) { + getAttributeGroups(lang: string): AttributeOption[] { const word = this.getWordGroup() const attrs = this.getWordAttributeGroups(lang, "union") const sentAttrs = this.getStructAttributeGroups(lang, "union") return [word].concat(attrs, sentAttrs) } - getStatsAttributeGroups(lang) { + getStatsAttributeGroups(lang: string): AttributeOption[] { const word = this.getWordGroup() const wordOp = settings["reduce_word_attribute_selector"] || "union" @@ -400,13 +405,13 @@ export class CorpusListing { // update attributes so that we don't need to check them multiple times // currently done only for common and struct attributes, but code for // positional could be added here, but is tricky because parallel mode lang might be needed - updateAttributes() { + updateAttributes(): void { const common_keys = _.compact(_.flatten(_.map(this.selected, (corp) => _.keys(corp.common_attributes)))) this.commonAttributes = _.pick(settings["common_struct_types"], ...common_keys) this.structAttributes = this._getStructAttrs() } - isDateInterval(type) { + isDateInterval(type: string): boolean { if (_.isEmpty(type)) { return false } diff --git a/app/scripts/data_init.js b/app/scripts/data_init.js index dccef4423..bae0d2271 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.js @@ -128,8 +128,8 @@ async function getConfig() { function transformConfig(modeSettings) { // only if the current mode is parallel, we load the special code required if (modeSettings.parallel) { - require("./parallel/corpus_listing.js") - require("./parallel/stats_proxy.ts") + require("./parallel/corpus_listing") + require("./parallel/stats_proxy") } function rename(obj, from, to) { diff --git a/app/scripts/parallel/corpus_listing.js b/app/scripts/parallel/corpus_listing.ts similarity index 59% rename from app/scripts/parallel/corpus_listing.js rename to app/scripts/parallel/corpus_listing.ts index 9c9465f4a..2827203c2 100644 --- a/app/scripts/parallel/corpus_listing.js +++ b/app/scripts/parallel/corpus_listing.ts @@ -3,63 +3,59 @@ import _ from "lodash" import settings from "@/settings" import { CorpusListing } from "@/corpus_listing" import { locationSearchGet } from "@/util" +import { CorpusTransformed } from "@/settings/config-transformed.types" +import { Attribute } from "@/settings/config.types" +import { LangString } from "@/i18n/types" export class ParallelCorpusListing extends CorpusListing { - constructor(corpora) { + activeLangs: string[] + + constructor(corpora: Record) { super(corpora) - const hash = window.location.hash.substr(2) - for (let item of hash.split("&")) { - var parts = item.split("=") - if (parts[0] == "parallel_corpora") { - this.setActiveLangs(parts[1].split(",")) - break - } - } + // Cannot use Angular helpers (`locationSearchGet`) here, it's not initialized yet. + const activeLangs = new URLSearchParams(window.location.hash.slice(2)).get("parallel_corpora") + this.setActiveLangs(activeLangs.split(",")) } - select(idArray) { - this.selected = [] - $.each(idArray, (i, id) => { - const corp = this.struct[id] - this.selected = this.selected.concat(this.getLinked(corp, true, false)) - }) - - this.selected = _.uniq(this.selected) + select(idArray: string[]): void { + this.selected = _.uniq(idArray.flatMap((id) => this.getLinked(this.struct[id]))) this.updateAttributes() } - setActiveLangs(langlist) { + setActiveLangs(langlist: string[]): void { this.activeLangs = langlist } - getReduceLang() { + getReduceLang(): string { return this.activeLangs[0] } - getCurrentAttributes(lang) { + getCurrentAttributes(lang?: string): Record { if (_.isEmpty(lang)) { lang = settings.corpusListing.getReduceLang() } const corpora = _.filter(this.selected, (item) => item.lang === lang) - const struct = _.reduce(corpora, (a, b) => $.extend({}, a.attributes, b.attributes), {}) - return struct + return corpora.reduce((attrs, corpus) => ({ ...attrs, ...corpus.attributes }), {} as Record) } - getStructAttrs(lang) { + getStructAttrs(lang?: string): Record { if (_.isEmpty(lang)) { lang = settings.corpusListing.getReduceLang() } const corpora = _.filter(this.selected, (item) => item.lang === lang) - const struct = _.reduce(corpora, (a, b) => $.extend({}, a["struct_attributes"], b["struct_attributes"]), {}) - $.each(struct, (key, val) => (val["is_struct_attr"] = true)) + const struct = corpora.reduce( + (attrs, corpus) => ({ ...attrs, ...corpus.struct_attributes }), + {} as Record + ) + Object.values(struct).forEach((attr) => (attr.is_struct_attr = true)) return struct } - getStructAttrsIntersection(lang) { + getStructAttrsIntersection(lang: string): Record { const corpora = _.filter(this.selected, (item) => item.lang === lang) const attrs = _.map(corpora, function (corpus) { for (let key in corpus["struct_attributes"]) { @@ -72,45 +68,20 @@ export class ParallelCorpusListing extends CorpusListing { return this._mapping_intersection(attrs) } - getLinked(corp, andSelf, only_selected) { - if (andSelf == null) { - andSelf = false - } - if (only_selected == null) { - only_selected = true - } - const target = only_selected ? this.selected : this.struct - let output = _.filter(target, (item) => (corp["linked_to"] || []).includes(item.id)) - if (andSelf) { - output = [corp].concat(output) - } - return output + getLinked(corp: CorpusTransformed, only_selected?: boolean) { + const target = only_selected ? this.selected : Object.values(this.struct) + let output: CorpusTransformed[] = target.filter((item) => (corp["linked_to"] || []).includes(item.id)) + return [corp].concat(output) } - getEnabledByLang(lang, andSelf, flatten) { - if (andSelf == null) { - andSelf = false - } - if (flatten == null) { - flatten = true - } + getEnabledByLang(lang: string): CorpusTransformed[][] { const corps = _.filter(this.selected, (item) => item["lang"] === lang) - const output = _(corps) - .map((item) => { - return this.getLinked(item, andSelf) - }) - .value() - - if (flatten) { - return _.flatten(output) - } else { - return output - } + return corps.map((item) => this.getLinked(item, true)) } - getLinksFromLangs(activeLangs) { + getLinksFromLangs(activeLangs: string[]): CorpusTransformed[][] { if (activeLangs.length === 1) { - return this.getEnabledByLang(activeLangs[0], true, false) + return this.getEnabledByLang(activeLangs[0]) } // get the languages that are enabled given a list of active languages const main = _.filter(this.selected, (corp) => corp.lang === activeLangs[0]) @@ -131,7 +102,7 @@ export class ParallelCorpusListing extends CorpusListing { return output } - getAttributeQuery(attr) { + getAttributeQuery(attr: "context" | "within"): string { // gets the within and context queries const struct = this.getLinksFromLangs(this.activeLangs) @@ -143,12 +114,7 @@ export class ParallelCorpusListing extends CorpusListing { const other = corps.slice(1) const pair = _.map(other, function (corp) { - let a - if (mainIsPivot) { - a = _.keys(corp[attr])[0] - } else { - a = _.keys(corps[0][attr])[0] - } + const a = mainIsPivot ? _.keys(corp[attr])[0] : _.keys(corps[0][attr])[0] return mainId + "|" + corp.id.toUpperCase() + ":" + a }) return output.push(pair) @@ -157,17 +123,17 @@ export class ParallelCorpusListing extends CorpusListing { return output.join(",") } - getContextQueryString() { + getContextQueryString(): string { return this.getAttributeQuery("context") } - getWithinParameters() { + getWithinParameters(): { default_within: string; within: string } { const defaultWithin = locationSearchGet("within") || _.keys(settings["default_within"])[0] const within = this.getAttributeQuery("within") return { default_within: defaultWithin, within } } - stringifySelected(onlyMain) { + stringifySelected(onlyMain?: boolean): string { let struct = this.getLinksFromLangs(this.activeLangs) if (onlyMain) { struct = _.map(struct, (pair) => { @@ -182,7 +148,6 @@ export class ParallelCorpusListing extends CorpusListing { } const output = [] - // $.each(struct, function(i, item) { for (let i = 0; i < struct.length; i++) { const item = struct[i] var main = item[0] @@ -194,11 +159,11 @@ export class ParallelCorpusListing extends CorpusListing { return output.join(",") } - get(corpusID) { + get(corpusID: string): CorpusTransformed { return this.struct[corpusID.split("|")[1]] } - getTitleObj(corpusID) { + getTitleObj(corpusID: string): LangString { return this.get(corpusID).title } } diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts index 7c3049a52..9a2b45b5f 100644 --- a/app/scripts/settings/config-transformed.types.ts +++ b/app/scripts/settings/config-transformed.types.ts @@ -35,4 +35,7 @@ export type CorpusTransformed = Omit< LastDate?: string Protected?: boolean } + common_attributes?: Record + time?: Record + non_time?: number } diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts index eaf70a8e5..283202342 100644 --- a/app/scripts/settings/config.types.ts +++ b/app/scripts/settings/config.types.ts @@ -29,6 +29,8 @@ export type Corpus = { context: Labeled[] description: LangString id: string + lang?: string + pivot?: boolean pos_attributes: string[] struct_attributes: string[] custom_attributes: string[] diff --git a/app/scripts/timeseries.ts b/app/scripts/timeseries.ts index 82017d9bd..07196eb91 100644 --- a/app/scripts/timeseries.ts +++ b/app/scripts/timeseries.ts @@ -32,16 +32,14 @@ export const getSeries = () => fromPairs(getTimeDataPairs()) as YearSeries /** Get data size per year of selected corpora. */ export function getSeriesSelected() { - const corpora: { time?: YearSeries }[] = settings.corpusListing.selected // `pickBy` removes zeroes. - const series = corpora.map((corpus) => ("time" in corpus ? pickBy(corpus.time) : {})) + const series = settings.corpusListing.selected.map((corpus) => ("time" in corpus ? pickBy(corpus.time) : {})) return sumYearSeries(...series) } /** Get data size of unknown year in selected corpora */ export function getCountUndatedSelected() { - const corpora: { non_time?: number }[] = settings.corpusListing.selected - return corpora.reduce((sum, corpus) => sum + (corpus["non_time"] || 0), 0) + return settings.corpusListing.selected.reduce((sum, corpus) => sum + (corpus["non_time"] || 0), 0) } /** Get first and last year in all available corpora. */ diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 13d349a36..956f47497 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -40,7 +40,8 @@ export const withService = (name: K, fn: (servi * Get values from the URL search string via Angular. * Only use this in code outside Angular. Inside, use `$location.search()`. */ -export const locationSearchGet = (key: string) => withService("$location", ($location) => $location.search()[key]) +export const locationSearchGet = (key: string): string => + withService("$location", ($location) => $location.search()[key]) /** * Set values in the URL search string via Angular. From d3d634e0ae675afd8d5d5968a56d1d6756c71878 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 8 Jul 2024 21:45:01 +0200 Subject: [PATCH 23/99] fix: ParallelCorpusListing activeLangs --- CHANGELOG.md | 2 +- app/scripts/parallel/corpus_listing.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 063104cdc..5d605fdcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics -- In the `CorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures +- In the `ParallelCorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures ### Refactored diff --git a/app/scripts/parallel/corpus_listing.ts b/app/scripts/parallel/corpus_listing.ts index 2827203c2..f4642d11c 100644 --- a/app/scripts/parallel/corpus_listing.ts +++ b/app/scripts/parallel/corpus_listing.ts @@ -14,7 +14,7 @@ export class ParallelCorpusListing extends CorpusListing { super(corpora) // Cannot use Angular helpers (`locationSearchGet`) here, it's not initialized yet. - const activeLangs = new URLSearchParams(window.location.hash.slice(2)).get("parallel_corpora") + const activeLangs = new URLSearchParams(window.location.hash.slice(2)).get("parallel_corpora") || "" this.setActiveLangs(activeLangs.split(",")) } From 9dbd0b31ef3a2245540fa2d405dcee202b637c9e Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 8 Jul 2024 10:13:29 +0200 Subject: [PATCH 24/99] refactor(ts): mode --- app/scripts/{mode.js => mode.ts} | 2 ++ 1 file changed, 2 insertions(+) rename app/scripts/{mode.js => mode.ts} (87%) diff --git a/app/scripts/mode.js b/app/scripts/mode.ts similarity index 87% rename from app/scripts/mode.js rename to app/scripts/mode.ts index 46e13ba37..a84c8b8d8 100644 --- a/app/scripts/mode.js +++ b/app/scripts/mode.ts @@ -1,3 +1,5 @@ +/** @format */ + const currentMode = new URLSearchParams(window.location.search).get("mode") || "default" export default currentMode From 1daa2fe3f5047ba2bef0a09e3b34c20ee132a114 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 8 Jul 2024 11:47:03 +0200 Subject: [PATCH 25/99] refactor(ts): data_init --- app/scripts/backend/time-proxy.ts | 9 +- app/scripts/{data_init.js => data_init.ts} | 114 +++++++++--------- app/scripts/settings/app-settings.types.ts | 3 +- .../settings/config-transformed.types.ts | 12 +- app/scripts/settings/config.types.ts | 2 + app/scripts/settings/corpus-info.types.ts | 41 +++++++ app/scripts/settings/settings.types.ts | 2 +- doc/frontend_devel.md | 3 +- 8 files changed, 117 insertions(+), 69 deletions(-) rename app/scripts/{data_init.js => data_init.ts} (70%) create mode 100644 app/scripts/settings/corpus-info.types.ts diff --git a/app/scripts/backend/time-proxy.ts b/app/scripts/backend/time-proxy.ts index 5161f051b..7a26da26a 100644 --- a/app/scripts/backend/time-proxy.ts +++ b/app/scripts/backend/time-proxy.ts @@ -6,7 +6,7 @@ import type { AjaxSettings, Granularity, Histogram, KorpResponse, NumericString import { Factory, httpConfAddMethod } from "@/util" export class TimeProxy extends BaseProxy { - makeRequest() { + makeRequest(): JQueryDeferred { const data: KorpTimespanParams = { granularity: "y", corpus: settings.corpusListing.stringifyAll(), @@ -110,3 +110,10 @@ type KorpTimespanResponse = KorpResponse<{ /** Execution time in seconds */ time: number }> + +/** Data returned after slight mangling. */ +export type TimeData = [ + Record, // Same as KorpTimespanResponse.corpora + [number, number][], // Tokens per time period, as pairs ordered by time period + number // Tokens in undated material +] diff --git a/app/scripts/data_init.js b/app/scripts/data_init.ts similarity index 70% rename from app/scripts/data_init.js rename to app/scripts/data_init.ts index bae0d2271..5ef552feb 100644 --- a/app/scripts/data_init.js +++ b/app/scripts/data_init.ts @@ -8,13 +8,17 @@ import * as treeUtil from "./components/corpus_chooser/util" import { CorpusListing } from "./corpus_listing" import { ParallelCorpusListing } from "./parallel/corpus_listing" import { httpConfAddMethodFetch } from "@/util" +import { Labeled, LangLocMap, LocMap } from "./i18n/types" +import { CorpusInfoResponse } from "./settings/corpus-info.types" +import { Config } from "./settings/config.types" +import { ConfigTransformed } from "./settings/config-transformed.types" // Using memoize, this will only fetch once and then return the same promise when called again. // TODO it would be better only to load additional languages when there is a language change export const initLocales = memoize(async () => { - const locData = {} - const defs = [] - for (const langObj of settings["languages"]) { + const locData: LangLocMap = {} + const defs: Promise[] = [] + for (const langObj of settings.languages) { const lang = langObj.value locData[lang] = {} for (const pkg of ["locale", "corpora"]) { @@ -22,7 +26,7 @@ export const initLocales = memoize(async () => { const def = fetch(file) .then(async (response) => { if (response.status >= 300) throw new Error() - const data = await response.json() + const data = (await response.json()) as LocMap Object.assign(locData[lang], data) }) .catch(() => { @@ -36,37 +40,37 @@ export const initLocales = memoize(async () => { return locData }) -async function getInfoData() { +async function getInfoData(): Promise { const params = { corpus: _.map(settings.corpusListing.corpora, "id") .map((a) => a.toUpperCase()) .join(","), } - const { url, request } = httpConfAddMethodFetch(settings["korp_backend_url"] + "/corpus_info", params) + const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/corpus_info", params) const response = await fetch(url, request) - const data = await response.json() + const data = (await response.json()) as CorpusInfoResponse for (let corpus of settings.corpusListing.corpora) { - corpus["info"] = data["corpora"][corpus.id.toUpperCase()]["info"] - const privateStructAttrs = [] + corpus.info = data.corpora[corpus.id.toUpperCase()].info + const privateStructAttrs: string[] = [] for (let attr of data["corpora"][corpus.id.toUpperCase()].attrs.s) { if (attr.indexOf("__") !== -1) { privateStructAttrs.push(attr) } } - corpus["private_struct_attributes"] = privateStructAttrs + corpus.private_struct_attributes = privateStructAttrs } } -async function getTimeData() { +async function getTimeData(): Promise<[[number, number][], number]> { const timeProxy = timeProxyFactory.create() const args = await timeProxy.makeRequest() let [dataByCorpus, all_timestruct, rest] = args if (all_timestruct.length == 0) { - return [[], []] + return [[], 0] } // this adds data to the corpora in settings @@ -89,23 +93,23 @@ async function getTimeData() { return [all_timestruct, rest] } -async function getConfig() { +async function getConfig(): Promise { // Load static corpus config if it exists. try { - const corpusConfig = require(`modes/${currentMode}_corpus_config.json`) + const corpusConfig = require(`modes/${currentMode}_corpus_config.json`) as Config console.log(`Using static corpus config`) return corpusConfig } catch {} - let configUrl + let configUrl: string // The corpora to include can be defined elsewhere can in a mode - if (settings["corpus_config_url"]) { - configUrl = await settings["corpus_config_url"]() + if (settings.corpus_config_url) { + configUrl = await settings.corpus_config_url() } else { const labParam = process.env.ENVIRONMENT == "staging" ? "&include_lab" : "" - configUrl = `${settings["korp_backend_url"]}/corpus_config?mode=${currentMode}${labParam}` + configUrl = `${settings.korp_backend_url}/corpus_config?mode=${currentMode}${labParam}` } - let response + let response: Response try { response = await fetch(configUrl) } catch (error) { @@ -117,22 +121,24 @@ async function getConfig() { throw Error("Something wrong with corpus config") } - return await response.json() + return (await response.json()) as Config } /** + * Transform the raw config fetched form backend, to a structure that frontend code can handle. * - * @param {import("@/settings/config.types").Config} modeSettings - * @returns {import("@/settings/config.types").ConfigTransformed} + * TODO: Use the `Config` and `ConfigTransformed` types, not `any`. + * + * @see ./settings/README.md */ -function transformConfig(modeSettings) { +function transformConfig(modeSettings: any): ConfigTransformed { // only if the current mode is parallel, we load the special code required if (modeSettings.parallel) { require("./parallel/corpus_listing") require("./parallel/stats_proxy") } - function rename(obj, from, to) { + function rename(obj: T, from: keyof T, to: keyof T): void { if (obj[from]) { obj[to] = obj[from] delete obj[from] @@ -168,10 +174,10 @@ function transformConfig(modeSettings) { // TODO use the new format instead // remake the new format of witihns and contex to the old const sortingArr = ["sentence", "paragraph", "text", "1 sentence", "1 paragraph", "1 text"] - function contextWithinFix(list) { + function contextWithinFix(list: Labeled[]) { // sort the list so that sentence is before paragraph list.sort((a, b) => sortingArr.indexOf(a.value) - sortingArr.indexOf(b.value)) - const res = {} + const res: Record = {} for (const elem of list) { res[elem.value] = elem.value } @@ -181,50 +187,46 @@ function transformConfig(modeSettings) { corpus["context"] = contextWithinFix(corpus["context"]) } - delete modeSettings["attributes"] + delete modeSettings.attributes - if (!modeSettings["folders"]) { - modeSettings["folders"] = {} + if (!modeSettings.folders) { + modeSettings.folders = {} } return modeSettings } -function setInitialCorpora() { +/** Determine initial corpus selection and mark them selected in the CorpusListing. */ +function setInitialCorpora(): void { // if no preselectedCorpora is defined, use all of them - if (!(settings["preselected_corpora"] && settings["preselected_corpora"].length)) { + if (!(settings.preselected_corpora && settings.preselected_corpora.length)) { // if all corpora in mode is limited_access, make them all preselected if (settings.corpusListing.corpora.filter((corpus) => !corpus.limited_access).length == 0) { - settings["preselected_corpora"] = _.map( + settings.preselected_corpora = _.map( _.filter(settings.corpusListing.corpora, (corpus) => !corpus.hide), "id" ) // else filter out the ones with limited_access } else { - settings["preselected_corpora"] = _.map( + settings.preselected_corpora = _.map( _.filter(settings.corpusListing.corpora, (corpus) => !(corpus.hide || corpus.limited_access)), "id" ) } } else { let expandedCorpora = [] - for (let preItem of settings["preselected_corpora"]) { + for (let preItem of settings.preselected_corpora) { preItem = preItem.replace(/^__/g, "") - expandedCorpora = [].concat(expandedCorpora, treeUtil.getAllCorporaInFolders(settings["folders"], preItem)) + expandedCorpora = [].concat(expandedCorpora, treeUtil.getAllCorporaInFolders(settings.folders, preItem)) } // folders expanded, save - settings["preselected_corpora"] = expandedCorpora + settings.preselected_corpora = expandedCorpora } const corpusParam = new URLSearchParams(window.location.hash.slice(2)).get("corpus") - let currentCorpora - if (corpusParam) { - currentCorpora = _.flatten( - _.map(corpusParam.split(","), (val) => treeUtil.getAllCorporaInFolders(settings["folders"], val)) - ) - } else { - currentCorpora = settings["preselected_corpora"] - } + const currentCorpora = corpusParam + ? _.flatten(_.map(corpusParam.split(","), (val) => treeUtil.getAllCorporaInFolders(settings.folders, val))) + : settings.preselected_corpora settings.corpusListing.select(currentCorpora) } @@ -234,17 +236,17 @@ function setInitialCorpora() { * It both fetches data, such as config and populates the * `settings` object. */ -export async function fetchInitialData(authDef) { - settings["korp_backend_url"] = settings["korp_backend_url"].trim() - if (settings["korp_backend_url"].slice(-1) == "/") { - settings["korp_backend_url"] = settings["korp_backend_url"].slice(0, -1) +export async function fetchInitialData(authDef: Promise) { + settings.korp_backend_url = settings.korp_backend_url.trim() + if (settings.korp_backend_url.slice(-1) == "/") { + settings.korp_backend_url = settings.korp_backend_url.slice(0, -1) } - if (!settings["korp_backend_url"].startsWith("http")) { + if (!settings.korp_backend_url.startsWith("http")) { console.error('"korp_backend_url" in config.yml must start with http:// or https://') return } - if (settings["config_dependent_on_authentication"]) { + if (settings.config_dependent_on_authentication) { await authDef } @@ -257,24 +259,24 @@ export async function fetchInitialData(authDef) { setDefaultConfigValues() - if (!settings["parallel"]) { + if (!settings.parallel) { settings.corpusListing = new CorpusListing(settings.corpora) } else { settings.corpusListing = new ParallelCorpusListing(settings.corpora) } // if the previous config calls didn't yield any corpora, don't ask for info or time - if (!_.isEmpty(settings["corpora"])) { + if (!_.isEmpty(settings.corpora)) { const infoDef = getInfoData() - let timeDef - if (settings["has_timespan"]) { + let timeDef: Promise<[[number, number][], number]> + if (settings.has_timespan) { timeDef = getTimeData() } setInitialCorpora() await infoDef - if (settings["has_timespan"]) { - settings["time_data"] = await timeDef + if (settings.has_timespan) { + settings.time_data = await timeDef } } } diff --git a/app/scripts/settings/app-settings.types.ts b/app/scripts/settings/app-settings.types.ts index b20c9f455..0e60d3c75 100644 --- a/app/scripts/settings/app-settings.types.ts +++ b/app/scripts/settings/app-settings.types.ts @@ -11,7 +11,8 @@ export type AppSettings = { autocomplete?: boolean backendURLMaxLength: number common_struct_types?: Record - corpus_config_url?: string + config_dependent_on_authentication?: boolean + corpus_config_url?: () => Promise corpus_info_link?: { url_template: string label: LangString diff --git a/app/scripts/settings/config-transformed.types.ts b/app/scripts/settings/config-transformed.types.ts index 9a2b45b5f..b0266da07 100644 --- a/app/scripts/settings/config-transformed.types.ts +++ b/app/scripts/settings/config-transformed.types.ts @@ -5,6 +5,7 @@ import { LangString } from "@/i18n/types" import { Attribute, Config, Corpus, CustomAttribute, Folder } from "./config.types" +import { CorpusInfoInfo } from "./corpus-info.types" export type ConfigTransformed = Omit & { corpora: Record @@ -24,17 +25,10 @@ export type CorpusTransformed = Omit< _attributes_order: string[] _struct_attributes_order: string[] _custom_attributes_order: string[] + private_struct_attributes: string[] within: Record context: Record - info: { - Name: string - Size: string | number - Sentences: string | number - Updated?: string - FirstDate?: string - LastDate?: string - Protected?: boolean - } + info: CorpusInfoInfo common_attributes?: Record time?: Record non_time?: number diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts index 283202342..216c4520d 100644 --- a/app/scripts/settings/config.types.ts +++ b/app/scripts/settings/config.types.ts @@ -28,8 +28,10 @@ export type Config = { export type Corpus = { context: Labeled[] description: LangString + hide?: boolean id: string lang?: string + limited_access?: boolean pivot?: boolean pos_attributes: string[] struct_attributes: string[] diff --git a/app/scripts/settings/corpus-info.types.ts b/app/scripts/settings/corpus-info.types.ts new file mode 100644 index 000000000..80590f1ca --- /dev/null +++ b/app/scripts/settings/corpus-info.types.ts @@ -0,0 +1,41 @@ +/** @format */ + +/** + * Response from the `/corpus_info` API. + * See https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_info/get + */ +export type CorpusInfoResponse = { + corpora: Record + total_sentences: number + total_size: number +} + +export type CorpusInfo = { + /** Lists of attribute names present in the corpus. */ + attrs: CorpusInfoAttrs + /** Miscellaneous information about the corpus given by Corpus Workbench, including any key-value pairs from the corresponding .info file. */ + info: CorpusInfoInfo +} + +export type CorpusInfoAttrs = { + /** Positional attributes */ + p: string[] + /** Structural attributes */ + s: string[] + /** Align attributes, for linked corpora */ + a: string[] +} + +export type CorpusInfoInfo = { + Name?: string + Size?: `${number}` + Charset?: string + Sentences?: `${number}` + Saldo?: `${number}` + FirstDate?: `${number}-${number}-${number} ${number}:${number}:${number}` | "" + LastDate?: string + Updated?: `${number}-${number}-${number}` + Protected?: "true" | "false" | "" + DateResolution?: string + KorpModes?: string +} diff --git a/app/scripts/settings/settings.types.ts b/app/scripts/settings/settings.types.ts index bf7c15425..30016aad9 100644 --- a/app/scripts/settings/settings.types.ts +++ b/app/scripts/settings/settings.types.ts @@ -3,7 +3,7 @@ * @format */ -import { CorpusListing } from "@/corpus_listing" +import { type CorpusListing } from "@/corpus_listing" import { AppSettings } from "./app-settings.types" import { ConfigTransformed } from "./config-transformed.types" diff --git a/doc/frontend_devel.md b/doc/frontend_devel.md index 6a4e64932..89be7c8bf 100644 --- a/doc/frontend_devel.md +++ b/doc/frontend_devel.md @@ -108,7 +108,8 @@ settings that affect the frontend. - __common_struct_types__ - Object with attribute name as a key and attribute definition as value. Attributes that may be added automatically to a corpus. See [backend documentation](https://github.com/spraakbanken/korp-backend) for more information about how to define attributes. -- __corpus_config_url__ - String. Configuration for the selected mode is fetched from here at app initialization. If not given, the default is `/corpus_config?mode=`, see the [`corpus_config`](https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get) API. +- __config_dependent_on_authentication__ - Boolean. If true, backend config will not be fetched until login check has finished. +- __corpus_config_url__ - Async function returning a url string. Configuration for the selected mode is fetched from here at app initialization. If not given, the default is `/corpus_config?mode=`, see the [`corpus_config`](https://ws.spraakbanken.gu.se/docs/korp#tag/Information/paths/~1corpus_config/get) API. - __corpus_info_link__ - Object. Use this to render a link for each corpus in the corpus chooser. - __url_template__ - String or translation object. A URL containing a token "%s", which will be replaced with the corpus id. - __label__ - String or translation object. The label is the the same for all corpora. From 6498d3ac2f920eaa132b454e5d91e4882a0d512f Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 8 Jul 2024 21:46:53 +0200 Subject: [PATCH 26/99] refactor(ts): data_init transformConfig --- app/scripts/components/header.js | 8 -- app/scripts/data_init.ts | 157 +++++++++++++-------------- app/scripts/settings/config.types.ts | 2 +- app/scripts/util.ts | 4 + 4 files changed, 81 insertions(+), 90 deletions(-) diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 2203fa466..a17ec954a 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -247,14 +247,6 @@ angular.module("korpApp").component("header", { const modeParam = modeId === "default" ? "" : `?mode=${modeId}` return [location.pathname, modeParam, langParam] } - - for (let mode of $ctrl.modes) { - mode.selected = false - if (mode.mode === currentMode) { - settings.mode = mode - mode.selected = true - } - } }, ], }) diff --git a/app/scripts/data_init.ts b/app/scripts/data_init.ts index 5ef552feb..2ccc5780b 100644 --- a/app/scripts/data_init.ts +++ b/app/scripts/data_init.ts @@ -7,11 +7,11 @@ import timeProxyFactory from "@/backend/time-proxy" import * as treeUtil from "./components/corpus_chooser/util" import { CorpusListing } from "./corpus_listing" import { ParallelCorpusListing } from "./parallel/corpus_listing" -import { httpConfAddMethodFetch } from "@/util" +import { fromKeys, httpConfAddMethodFetch } from "@/util" import { Labeled, LangLocMap, LocMap } from "./i18n/types" import { CorpusInfoResponse } from "./settings/corpus-info.types" -import { Config } from "./settings/config.types" -import { ConfigTransformed } from "./settings/config-transformed.types" +import { Attribute, Config, Corpus, CustomAttribute } from "./settings/config.types" +import { ConfigTransformed, CorpusTransformed } from "./settings/config-transformed.types" // Using memoize, this will only fetch once and then return the same promise when called again. // TODO it would be better only to load additional languages when there is a language change @@ -40,27 +40,23 @@ export const initLocales = memoize(async () => { return locData }) -async function getInfoData(): Promise { - const params = { - corpus: _.map(settings.corpusListing.corpora, "id") - .map((a) => a.toUpperCase()) - .join(","), - } - const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/corpus_info", params) +type InfoData = Record> +/** + * Fetch CWB corpus info (Size, Updated etc). + */ +async function getInfoData(corpusIds: string[]): Promise { + const params = { corpus: corpusIds.map((id) => id.toUpperCase()).join(",") } + const { url, request } = httpConfAddMethodFetch(settings.korp_backend_url + "/corpus_info", params) const response = await fetch(url, request) const data = (await response.json()) as CorpusInfoResponse - for (let corpus of settings.corpusListing.corpora) { - corpus.info = data.corpora[corpus.id.toUpperCase()].info - const privateStructAttrs: string[] = [] - for (let attr of data["corpora"][corpus.id.toUpperCase()].attrs.s) { - if (attr.indexOf("__") !== -1) { - privateStructAttrs.push(attr) - } - } - corpus.private_struct_attributes = privateStructAttrs - } + return fromKeys(corpusIds, (corpusId) => ({ + info: data.corpora[corpusId.toUpperCase()].info, + private_struct_attributes: data.corpora[corpusId.toUpperCase()].attrs.s.filter( + (name) => name.indexOf("__") !== -1 + ), + })) } async function getTimeData(): Promise<[[number, number][], number]> { @@ -131,68 +127,64 @@ async function getConfig(): Promise { * * @see ./settings/README.md */ -function transformConfig(modeSettings: any): ConfigTransformed { - // only if the current mode is parallel, we load the special code required - if (modeSettings.parallel) { - require("./parallel/corpus_listing") - require("./parallel/stats_proxy") - } - - function rename(obj: T, from: keyof T, to: keyof T): void { - if (obj[from]) { - obj[to] = obj[from] - delete obj[from] - } - } - - rename(modeSettings.attributes, "pos_attributes", "attributes") - +function transformConfig(config: Config, infos: InfoData): ConfigTransformed { // take the backend configuration format for attributes and expand it // TODO the internal representation should be changed to a new, more compact one. - for (const corpusId in modeSettings.corpora) { - const corpus = modeSettings.corpora[corpusId] - + function transformCorpus(corpus: Corpus): CorpusTransformed { if (corpus.title == undefined) { - corpus.title = corpusId + corpus.title = corpus.id } - rename(corpus, "pos_attributes", "attributes") - for (const attrType of ["attributes", "struct_attributes", "custom_attributes"]) { - const attrList = corpus[attrType] - const attrs = {} - const newAttrList = [] - for (const attrIdx in attrList) { - const attr = modeSettings.attributes[attrType][attrList[attrIdx]] - attrs[attr.name] = attr - newAttrList.push(attr.name) - } - // attrs is an object of attribute settings - corpus[attrType] = attrs - // attrList is an ordered list of the preferred order of attributes - corpus[`_${attrType}_order`] = newAttrList + function transformAttributes(type: T) { + console.log(corpus.id, type, corpus) + type AttrType = T extends "custom_attributes" ? CustomAttribute : Attribute + const attrs = _.fromPairs( + corpus[type]?.map((name) => { + const attr = config.attributes[type][name] as AttrType + return [attr.name, attr] + }) || [] + ) + const order = corpus[type]?.map((name) => config.attributes[type][name].name) || [] + return { attrs, order } } - // TODO use the new format instead - // remake the new format of witihns and contex to the old - const sortingArr = ["sentence", "paragraph", "text", "1 sentence", "1 paragraph", "1 text"] - function contextWithinFix(list: Labeled[]) { - // sort the list so that sentence is before paragraph - list.sort((a, b) => sortingArr.indexOf(a.value) - sortingArr.indexOf(b.value)) - const res: Record = {} - for (const elem of list) { - res[elem.value] = elem.value - } - return res + + const { attrs: attributes, order: _attributes_order } = transformAttributes("pos_attributes") + const { attrs: struct_attributes, order: _struct_attributes_order } = transformAttributes("struct_attributes") + const { attrs: custom_attributes, order: _custom_attributes_order } = transformAttributes("custom_attributes") + + return { + ..._.omit(corpus, "pos_attributes"), + attributes, + struct_attributes, + custom_attributes, + _attributes_order, + _struct_attributes_order, + _custom_attributes_order, + context: contextWithinFix(corpus["context"]), + within: contextWithinFix(corpus["within"]), + info: infos[corpus.id].info, + private_struct_attributes: infos[corpus.id].private_struct_attributes, } - corpus["within"] = contextWithinFix(corpus["within"]) - corpus["context"] = contextWithinFix(corpus["context"]) } - delete modeSettings.attributes + // TODO use the new format instead + // remake the new format of witihns and contex to the old + function contextWithinFix(list: Labeled[]) { + // sort the list so that sentence is before paragraph + const sortingArr = ["sentence", "paragraph", "text", "1 sentence", "1 paragraph", "1 text"] + list.sort((a, b) => sortingArr.indexOf(a.value) - sortingArr.indexOf(b.value)) + return _.fromPairs(list.map((elem) => [elem.value, elem.value])) + } + + const modes = config.modes.map((mode) => ({ ...mode, selected: mode.mode == currentMode })) - if (!modeSettings.folders) { - modeSettings.folders = {} + return { + folders: {}, + ..._.omit(config, "pos_attributes", "corpora"), + corpora: _.mapValues(config.corpora, transformCorpus), + modes, + mode: modes.find((mode) => mode.selected), } - return modeSettings } /** Determine initial corpus selection and mark them selected in the CorpusListing. */ @@ -252,10 +244,19 @@ export async function fetchInitialData(authDef: Promise) { // Start fetching locales asap. Await and read it later, in the Angular context. initLocales() + + // Fetch corpus configuration and metadata const config = await getConfig() - const modeSettings = transformConfig(config) + const infos = await getInfoData(Object.keys(config.corpora)) + // Add config and metadata to settings + const configTransformed = transformConfig(config, infos) + Object.assign(settings, configTransformed) - _.assign(settings, modeSettings) + // only if the current mode is parallel, we load the special code required + if (config.parallel) { + require("./parallel/corpus_listing") + require("./parallel/stats_proxy") + } setDefaultConfigValues() @@ -265,18 +266,12 @@ export async function fetchInitialData(authDef: Promise) { settings.corpusListing = new ParallelCorpusListing(settings.corpora) } - // if the previous config calls didn't yield any corpora, don't ask for info or time + // if the previous config calls didn't yield any corpora, don't ask for time if (!_.isEmpty(settings.corpora)) { - const infoDef = getInfoData() - let timeDef: Promise<[[number, number][], number]> - if (settings.has_timespan) { - timeDef = getTimeData() - } setInitialCorpora() - await infoDef if (settings.has_timespan) { - settings.time_data = await timeDef + settings.time_data = await getTimeData() } } } diff --git a/app/scripts/settings/config.types.ts b/app/scripts/settings/config.types.ts index 216c4520d..1712e5cfe 100644 --- a/app/scripts/settings/config.types.ts +++ b/app/scripts/settings/config.types.ts @@ -35,7 +35,7 @@ export type Corpus = { pivot?: boolean pos_attributes: string[] struct_attributes: string[] - custom_attributes: string[] + custom_attributes?: string[] reading_mode?: boolean title?: LangString within: Labeled[] diff --git a/app/scripts/util.ts b/app/scripts/util.ts index 956f47497..e986e3664 100644 --- a/app/scripts/util.ts +++ b/app/scripts/util.ts @@ -15,6 +15,10 @@ import { LangMap } from "./i18n/types" /** Use html`
      html here
      ` to enable formatting template strings with Prettier. */ export const html = String.raw +/** Create an object from a list of keys and a function for creating corresponding values. */ +export const fromKeys = (keys: K[], getValue: (key: K) => T) => + Object.fromEntries(keys.map((key) => [key, getValue(key)])) + /** Mapping from service names to their TS types. */ type ServiceTypes = { $controller: IControllerService From f78646dcf57fe02ff064c4f96bb7495306ccf2dd Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 9 Jul 2024 13:52:15 +0200 Subject: [PATCH 27/99] fix: remove stray console.log --- app/scripts/data_init.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/scripts/data_init.ts b/app/scripts/data_init.ts index 2ccc5780b..893da3390 100644 --- a/app/scripts/data_init.ts +++ b/app/scripts/data_init.ts @@ -136,7 +136,6 @@ function transformConfig(config: Config, infos: InfoData): ConfigTransformed { } function transformAttributes(type: T) { - console.log(corpus.id, type, corpus) type AttrType = T extends "custom_attributes" ? CustomAttribute : Attribute const attrs = _.fromPairs( corpus[type]?.map((name) => { From 71e95e83c4c9872b8eeb8a4330f02a8f47227bd3 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 9 Jul 2024 14:04:51 +0200 Subject: [PATCH 28/99] refactor(ts): index, korp.module --- README.md | 3 +-- app/{index.js => index.ts} | 19 +++++++++++++------ app/scripts/index.d.ts | 3 ++- .../{korp.module.js => korp.module.ts} | 1 + webpack.common.js | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) rename app/{index.js => index.ts} (90%) rename app/scripts/{korp.module.js => korp.module.ts} (97%) diff --git a/README.md b/README.md index b3b92811d..7385dac2e 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ imgPath = require("img/image.png") myTemplate = `` ``` -Most dependencies are only specified in `app/index.js` and where needed -added to the `window`-object. +Some dependencies are only specified in `app/index.ts`. About the current loaders in `webpack.config.js`: - `pug` and `html` files: all `src`-attributes in `` tags and all `href`s in `` tags will be diff --git a/app/index.js b/app/index.ts similarity index 90% rename from app/index.js rename to app/index.ts index f2343fab6..685d6256e 100644 --- a/app/index.js +++ b/app/index.ts @@ -3,8 +3,21 @@ import $ from "jquery" import currentMode from "@/mode" import { locationSearchGet } from "@/util" +declare global { + interface Window { + jQuery: JQueryStatic + $: JQueryStatic + /** + * TODO Remove, currently used in tests + * @deprecated + */ + locationSearch: (key: string) => string + } +} + window.jQuery = $ window.$ = $ +window.locationSearch = locationSearchGet require("slickgrid/slick.grid.css") require("./styles/ui_mods.css") @@ -24,8 +37,6 @@ require("./styles/textreader.css") require("components-jqueryui/ui/widget.js") -require("angular") - require("jquerylocalize") try { @@ -71,7 +82,3 @@ require("./scripts/directives.js") require("./scripts/directives/scroll.js") require("./scripts/filter_directives.js") require("./scripts/matomo.js") - -// TODO Remove, currently used in tests -/** @deprecated */ -window.locationSearch = locationSearchGet diff --git a/app/scripts/index.d.ts b/app/scripts/index.d.ts index 953a4cabf..64779c222 100644 --- a/app/scripts/index.d.ts +++ b/app/scripts/index.d.ts @@ -1,5 +1,6 @@ +/** @format */ declare module "korp_config" { const settings: import("@/settings/settings.types").Settings export = settings -} \ No newline at end of file +} diff --git a/app/scripts/korp.module.js b/app/scripts/korp.module.ts similarity index 97% rename from app/scripts/korp.module.js rename to app/scripts/korp.module.ts index e77ff5992..69746980c 100644 --- a/app/scripts/korp.module.js +++ b/app/scripts/korp.module.ts @@ -1,4 +1,5 @@ /** @format */ +import angular from "angular" import "angular-ui-bootstrap" import "angular-spinner" import "angular-ui-sortable" diff --git a/webpack.common.js b/webpack.common.js index 59b2f260a..0e20f81e6 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -174,7 +174,7 @@ module.exports = { (e) => e.message.includes("Can't resolve 'modes"), ], entry: { - index: "./app/index.js", + index: "./app/index.ts", worker: "./app/scripts/statistics_worker.ts", }, output: { From 72fced8a1c01c67f59873f6f38ed5581b3501abe Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 9 Jul 2024 15:26:56 +0200 Subject: [PATCH 29/99] refactor(ts): main --- app/index.ts | 2 +- app/scripts/index.d.ts | 10 ++++++++++ app/scripts/{main.js => main.ts} | 21 ++++++++++++--------- 3 files changed, 23 insertions(+), 10 deletions(-) rename app/scripts/{main.js => main.ts} (82%) diff --git a/app/index.ts b/app/index.ts index 685d6256e..7d449fe8a 100644 --- a/app/index.ts +++ b/app/index.ts @@ -62,7 +62,7 @@ require("angular-filter/index.js") require("./lib/jquery.tooltip.pack.js") -require("./scripts/main.js") +require("./scripts/main") require("./scripts/app.js") require("./scripts/controllers/comparison_controller.js") diff --git a/app/scripts/index.d.ts b/app/scripts/index.d.ts index 64779c222..71c2b0d5f 100644 --- a/app/scripts/index.d.ts +++ b/app/scripts/index.d.ts @@ -4,3 +4,13 @@ declare module "korp_config" { const settings: import("@/settings/settings.types").Settings export = settings } + +declare module "*.svg" { + const content: any + export default content +} + +declare module "*.png" { + const content: any + export default content +} diff --git a/app/scripts/main.js b/app/scripts/main.ts similarity index 82% rename from app/scripts/main.js rename to app/scripts/main.ts index df7da7270..17cb8bbec 100644 --- a/app/scripts/main.js +++ b/app/scripts/main.ts @@ -6,17 +6,19 @@ import { updateSearchHistory } from "@/history" import { fetchInitialData } from "@/data_init" import currentMode from "@/mode" import * as authenticationProxy from "@/components/auth/auth" +import { html } from "@/util" import korpLogo from "../img/korp.svg" +import korpFail from "../img/korp_fail.svg" +import angular from "angular" const createSplashScreen = () => { const splash = document.getElementById("preload") - splash.innerHTML = `` + splash.innerHTML = html`` } const createErrorScreen = () => { - const korpFail = require("../img/korp_fail.svg") const elem = document.getElementById("preload") - elem.innerHTML = ` + elem.innerHTML = html`
      Sorry, Korp doesn't seem to work right now @@ -44,8 +46,8 @@ function initApp() { try { updateSearchHistory() - } catch (error1) { - console.error("ERROR setting corpora from location", error1) + } catch (error) { + console.error("ERROR setting corpora from location", error) } if (process.env.ENVIRONMENT == "staging") { @@ -55,10 +57,11 @@ function initApp() { $("body").addClass(`mode-${currentMode}`) $("#search_history").change(function (event) { - const target = $(this).find(":selected") - if (_.includes(["http://", "https:/"], target.val().slice(0, 7))) { - location.href = target.val() - } else if (target.is(".clear")) { + const optionElement = $(this).find(":selected") + const value = optionElement.val() as string + if (_.includes(["http://", "https:/"], value.slice(0, 7))) { + location.href = value + } else if (optionElement.is(".clear")) { jStorage.set("searches", []) updateSearchHistory() } From 75f208afcb54ad91469f53f7ca9364e631933213 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 9 Jul 2024 16:52:02 +0200 Subject: [PATCH 30/99] refactor(ts): app, RootScope type --- CHANGELOG.md | 2 + app/index.ts | 2 +- app/scripts/{app.js => app.ts} | 112 ++++++++++-------- app/scripts/components/compare-search.js | 2 +- .../components/corpus-distribution-chart.ts | 11 +- .../corpus_chooser/corpus-time-graph.ts | 7 +- app/scripts/components/extended/cqp-term.js | 2 +- app/scripts/components/frontpage.ts | 3 +- app/scripts/components/search-examples.ts | 3 +- app/scripts/i18n/index.ts | 4 +- app/scripts/root-scope.types.ts | 25 ++++ app/scripts/services.js | 9 +- app/scripts/settings/app-settings.types.ts | 2 + app/scripts/settings/config.types.ts | 4 +- app/scripts/util.ts | 3 +- package.json | 2 + yarn.lock | 21 +++- 17 files changed, 140 insertions(+), 74 deletions(-) rename app/scripts/{app.js => app.ts} (75%) create mode 100644 app/scripts/root-scope.types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d605fdcc..d043f3473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ - config/settings - CQP queries - CorpusListing + - `$rootScope` ### Changed - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - In the `ParallelCorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures +- Removed the `mapper` template filter; change `x | mapper:f` to `f(x)` ### Refactored diff --git a/app/index.ts b/app/index.ts index 7d449fe8a..6b17d5795 100644 --- a/app/index.ts +++ b/app/index.ts @@ -63,7 +63,7 @@ require("angular-filter/index.js") require("./lib/jquery.tooltip.pack.js") require("./scripts/main") -require("./scripts/app.js") +require("./scripts/app") require("./scripts/controllers/comparison_controller.js") require("./scripts/controllers/kwic_controller.js") diff --git a/app/scripts/app.js b/app/scripts/app.ts similarity index 75% rename from app/scripts/app.js rename to app/scripts/app.ts index c6d061a76..090c7e967 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.ts @@ -1,9 +1,25 @@ /** @format */ import _ from "lodash" +import { + ICacheObject, + ICompileProvider, + IComponentOptions, + ILocaleService, + ILocationProvider, + ILocationService, + IQService, + IScope, + ITimeoutService, + ui, +} from "angular" import korpApp from "./korp.module" import settings from "@/settings" import statemachine from "@/statemachine" import * as authenticationProxy from "@/components/auth/auth" +import { initLocales } from "@/data_init" +import { RootScope } from "@/root-scope.types" +import { Folder } from "./settings/config.types" +import { CorpusTransformed } from "./settings/config-transformed.types" import { html } from "@/util" import { loc, locObj } from "@/i18n" import "@/components/header" @@ -11,10 +27,9 @@ import "@/components/searchtabs" import "@/components/frontpage" import "@/components/results" import "@/components/korp-error" -import { initLocales, locDataPromise } from "./data_init" // load all custom components -let customComponents = {} +let customComponents: Record = {} try { customComponents = require("custom/components.js").default @@ -25,18 +40,14 @@ for (const componentName in customComponents) { korpApp.component(componentName, customComponents[componentName]) } -korpApp.filter("mapper", () => (item, f) => f(item)) korpApp.filter("loc", () => loc) korpApp.filter("locObj", () => locObj) -korpApp.filter("replaceEmpty", function () { - return function (input) { - if (input === "") { - return "–" - } else { - return input - } - } -}) +korpApp.filter( + "replaceEmpty", + () => + (input: T) => + input === "" ? "–" : input +) authenticationProxy.initAngular() @@ -46,13 +57,13 @@ authenticationProxy.initAngular() */ korpApp.config([ "tmhDynamicLocaleProvider", - (tmhDynamicLocaleProvider) => + (tmhDynamicLocaleProvider: tmh.tmh.IDynamicLocaleProvider) => tmhDynamicLocaleProvider.localeLocationPattern("translations/angular-locale_{{locale}}.js"), ]) korpApp.config([ "$uibTooltipProvider", - ($uibTooltipProvider) => + ($uibTooltipProvider: ui.bootstrap.ITooltipProvider) => $uibTooltipProvider.options({ appendToBody: true, }), @@ -62,14 +73,15 @@ korpApp.config([ * Makes the hashPrefix "" instead of "!" which means our URL:s are ?mode=test#?lang=eng * instead of ?mode=test#!?lang=eng */ -korpApp.config(["$locationProvider", ($locationProvider) => $locationProvider.hashPrefix("")]) +korpApp.config(["$locationProvider", ($locationProvider: ILocationProvider) => $locationProvider.hashPrefix("")]) /** * "blob" must be added to the trusted URL:s, otherwise downloading KWIC and statistics etc. will not work */ korpApp.config([ "$compileProvider", - ($compileProvider) => $compileProvider.aHrefSanitizationTrustedUrlList(/^\s*(https?|ftp|mailto|tel|file|blob):/), + ($compileProvider: ICompileProvider) => + $compileProvider.aHrefSanitizationTrustedUrlList(/^\s*(https?|ftp|mailto|tel|file|blob):/), ]) korpApp.run([ @@ -81,7 +93,16 @@ korpApp.run([ "$q", "$timeout", "$uibModal", - async function ($rootScope, $location, $locale, tmhDynamicLocale, tmhDynamicLocaleCache, $q, $timeout, $uibModal) { + async function ( + $rootScope: RootScope, + $location: ILocationService, + $locale: ILocaleService, + tmhDynamicLocale: tmh.tmh.IDynamicLocale, + tmhDynamicLocaleCache: ICacheObject, + $q: IQService, + $timeout: ITimeoutService, + $uibModal: ui.bootstrap.IModalService + ) { const s = $rootScope s._settings = settings @@ -90,7 +111,8 @@ korpApp.run([ /** This deferred is used to signal that the filter feature is ready. */ s.globalFilterDef = $q.defer() - s.searchtabs = () => $(".search_tabs > ul").scope().tabset.tabs + type BootstrapTabsetScope = IScope & { tabset: { tabs: any } } + s.searchtabs = () => ($(".search_tabs > ul").scope() as BootstrapTabsetScope).tabset.tabs // Listen to url changes like #?lang=swe s.$on("$locationChangeSuccess", () => { @@ -104,7 +126,7 @@ korpApp.run([ // Find the configured 3-letter UI language matching the new 2-letter locale const lang = settings["languages"] .map((language) => language.value) - .find((lang3) => tmhDynamicLocaleCache.get(lang3)?.id == $locale.id) + .find((lang3) => tmhDynamicLocaleCache.get(lang3)?.id == $locale.id) if (!lang) { console.warn(`No locale matching "${$locale.id}"`) @@ -115,7 +137,7 @@ korpApp.run([ $rootScope["lang"] = lang // Trigger jQuery Localize - $("body").localize() + ;($("body") as any).localize() }) $(document).keyup(function (event) { @@ -131,30 +153,31 @@ korpApp.run([ $rootScope.textTabs = [] // This fetch was started in data_init.js, but only here can we store the result. - const initLocalesPromise = initLocales().then((data) => + const initLocalesPromise = initLocales().then((data): void => $rootScope.$apply(() => ($rootScope["loc_data"] = data)) ) s.waitForLogin = false /** Recursively collect the corpus ids found in a corpus folder */ - function collectCorpusIdsInFolder(folder) { + function collectCorpusIdsInFolder(folder: Folder): string[] { if (!folder) return [] // Collect direct child corpora const ids = folder.corpora || [] // Recurse into subfolders and add - for (const subfolder of Object.values(folder.subfolders || {})) { + const subfolders = folder.subfolders || {} + for (const subfolder of Object.values(subfolders)) { ids.push(...collectCorpusIdsInFolder(subfolder)) } return ids } - async function initializeCorpusSelection(selectedIds) { + async function initializeCorpusSelection(selectedIds: string[]): Promise { // Resolve any folder ids to the contained corpus ids - const corpusIds = [] + const corpusIds: string[] = [] for (const id of selectedIds) { // If it is a corpus, copy the id if (settings.corpora[id]) { @@ -169,11 +192,11 @@ korpApp.run([ // Replace the possibly mixed list with the list of corpus-only ids selectedIds = corpusIds - let loginNeededFor = [] + let loginNeededFor: CorpusTransformed[] = [] - for (let corpusId of selectedIds) { + for (const corpusId of selectedIds) { const corpusObj = settings.corpora[corpusId] - if (corpusObj && corpusObj["limited_access"]) { + if (corpusObj && corpusObj.limited_access) { if (!authenticationProxy.hasCredential(corpusId.toUpperCase())) { loginNeededFor.push(corpusObj) } @@ -182,7 +205,7 @@ korpApp.run([ const allCorpusIds = settings.corpusListing.corpora.map((corpus) => corpus.id) - if (settings["initialization_checks"] && (await settings["initialization_checks"](s))) { + if (settings.initialization_checks && (await settings.initialization_checks(s))) { // custom initialization code called } else if (_.isEmpty(settings.corpora)) { // no corpora @@ -236,10 +259,8 @@ korpApp.run([ s.openErrorModal({ content: `{{'corpus_not_available' | loc:$root.lang}}`, onClose: () => { - let newIds = selectedIds.filter((corpusId) => allCorpusIds.includes(corpusId)) - if (newIds.length == 0) { - newIds = settings["preselected_corpora"] - } + const validIds = selectedIds.filter((corpusId) => allCorpusIds.includes(corpusId)) + const newIds = validIds.length >= 0 ? validIds : settings["preselected_corpora"] initializeCorpusSelection(newIds) }, }) @@ -252,8 +273,9 @@ korpApp.run([ // TODO the top bar could show even though the modal is open, // thus allowing switching modes or language when an error has occured. - s.openErrorModal = ({ content, resolvable = true, onClose = null, buttonText = null, translations = {} }) => { - const s = $rootScope.$new(true) + s.openErrorModal = ({ content, resolvable = true, onClose = null, buttonText = null }) => { + type ModalScope = IScope & { closeModal: () => void } + const s = $rootScope.$new(true) as ModalScope const useCustomButton = !_.isEmpty(buttonText) @@ -277,8 +299,6 @@ korpApp.run([ keyboard: false, }) - s.translations = translations - s.closeModal = () => { if (onClose && resolvable) { modal.close() @@ -287,15 +307,9 @@ korpApp.run([ } } - function getCorporaFromHash() { - let selectedIds - let { corpus } = $location.search() - if (corpus) { - selectedIds = corpus.split(",") - } else { - selectedIds = settings["preselected_corpora"] || [] - } - return selectedIds + function getCorporaFromHash(): string[] { + const corpus: string = $location.search().corpus + return corpus ? corpus.split(",") : settings["preselected_corpora"] || [] } statemachine.listen("login", function () { @@ -310,11 +324,11 @@ korpApp.run([ }, ]) -korpApp.filter("trust", ["$sce", ($sce) => (input) => $sce.trustAsHtml(input)]) +korpApp.filter("trust", ["$sce", ($sce) => (input: string) => $sce.trustAsHtml(input)]) // Passing `lang` seems to be necessary to have the string updated when switching language. // Can fall back on using $rootScope for numbers that will anyway be re-rendered when switching language. korpApp.filter("prettyNumber", [ "$rootScope", - ($rootScope) => (input, lang) => Number(input).toLocaleString(lang || $rootScope.lang), + ($rootScope) => (input: string, lang: string) => Number(input).toLocaleString(lang || $rootScope.lang), ]) -korpApp.filter("maxLength", () => (val) => val.length > 39 ? val.slice(0, 36) + "…" : val) +korpApp.filter("maxLength", () => (val: string) => val.length > 39 ? val.slice(0, 36) + "…" : val) diff --git a/app/scripts/components/compare-search.js b/app/scripts/components/compare-search.js index d4710302c..85f56636d 100644 --- a/app/scripts/components/compare-search.js +++ b/app/scripts/components/compare-search.js @@ -33,7 +33,7 @@ angular.module("korpApp").component("compareSearch", { {{'compare_reduce' | loc:$root.lang}} - -
      -
      -
      -

      {{'compare_save_header' | loc:$root.lang}}

      - -
      - -
      -
      - -
      - -
      -
      \ -`, - restrict: "E", - replace: true, - scope: { - onSearch: "&", - onSearchSave: "&", - disabled: "<", - }, - link(scope, elem, attr) { - let at, my - const s = scope - - s.disabled = angular.isDefined(s.disabled) ? s.disabled : false - - s.pos = attr.pos || "bottom" - s.togglePopover = function (event) { - if (s.isPopoverVisible) { - s.popHide() - } else { - s.popShow() - } - event.preventDefault() - return event.stopPropagation() - } - - const popover = elem.find(".popover") - s.onPopoverClick = function (event) { - if (event.target !== popover.find(".btn")[0]) { - event.preventDefault() - return event.stopPropagation() - } - } - s.isPopoverVisible = false - const trans = { - bottom: "top", - top: "bottom", - right: "left", - left: "right", - } - const horizontal = ["top", "bottom"].includes(s.pos) - if (horizontal) { - my = `center ${trans[s.pos]}` - at = `center ${s.pos}+10` - } else { - my = trans[s.pos] + " center" - at = s.pos + "+10 center" - } - - const onEscape = function (event) { - if (event.which === 27) { - // escape - s.popHide() - return false - } - } - - s.popShow = function () { - s.isPopoverVisible = true - popover - .fadeIn("fast") - .focus() - .position({ - my, - at, - of: elem.find(".opener"), - }) - - $rootElement.on("keydown", onEscape) - $rootElement.on("click", s.popHide) - } - - s.popHide = function () { - s.isPopoverVisible = false - popover.fadeOut("fast") - $rootElement.off("keydown", onEscape) - $rootElement.off("click", s.popHide) - } - - s.onSubmit = function () { - s.popHide() - s.onSearchSave({ name: s.name }) - } - - s.onSendClick = () => s.onSearch() - }, - }), -]) - korpApp.directive("meter", () => ({ template: `\
      diff --git a/app/scripts/directives/search-submit.ts b/app/scripts/directives/search-submit.ts new file mode 100644 index 000000000..4005a50c7 --- /dev/null +++ b/app/scripts/directives/search-submit.ts @@ -0,0 +1,124 @@ +/** @format */ +import _ from "lodash" +import angular, { IScope } from "angular" +import { html } from "@/util" + +type SearchSubmitScope = IScope & { + disabled: boolean + name: string + onSearch: () => void + onSearchSave: (params: { name: string }) => void + isPopoverVisible: boolean + pos: string + togglePopover: (event: Event) => void + popHide: () => void + popShow: () => void + onSubmit: () => void + onSendClick: (event: Event) => void + onPopoverClick: (event: Event) => void +} + +angular.module("korpApp").directive("searchSubmit", [ + "$rootElement", + ($rootElement) => ({ + template: html`
      +
      + + +
      +
      +
      +

      {{'compare_save_header' | loc:$root.lang}}

      +
      +
      + +
      +
      + +
      +
      +
      +
      `, + restrict: "E", + replace: true, + scope: { + onSearch: "&", + onSearchSave: "&", + disabled: "<", + }, + link(scope: SearchSubmitScope, elem, attr) { + const s = scope + + s.disabled = angular.isDefined(s.disabled) ? s.disabled : false + + s.pos = attr.pos || "bottom" + s.togglePopover = function (event) { + if (s.isPopoverVisible) { + s.popHide() + } else { + s.popShow() + } + event.preventDefault() + event.stopPropagation() + } + + const popover = elem.find(".popover") + s.onPopoverClick = function (event) { + if (event.target !== popover.find(".btn")[0]) { + event.preventDefault() + event.stopPropagation() + } + } + s.isPopoverVisible = false + const trans = { + bottom: "top", + top: "bottom", + right: "left", + left: "right", + } + const horizontal = ["top", "bottom"].includes(s.pos) + const my = horizontal ? `center ${trans[s.pos]}` : `${trans[s.pos]} center` + const at = horizontal ? `center ${s.pos}+10` : `${s.pos}+10 center` + + const onEscape = function (event) { + if (event.which === 27) { + // escape + s.popHide() + return false + } + } + + s.popShow = function () { + s.isPopoverVisible = true + popover + .fadeIn("fast") + .focus() + .position({ my, at, of: elem.find(".opener") }) + + $rootElement.on("keydown", onEscape) + $rootElement.on("click", s.popHide) + } + + s.popHide = function () { + s.isPopoverVisible = false + popover.fadeOut("fast") + $rootElement.off("keydown", onEscape) + $rootElement.off("click", s.popHide) + } + + s.onSubmit = function () { + s.popHide() + s.onSearchSave({ name: s.name }) + } + + s.onSendClick = () => s.onSearch() + }, + }), +]) diff --git a/package.json b/package.json index 4d4f6be89..7ef3169ce 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@fortawesome/fontawesome-free": "6.2.1", "@types/angular-dynamic-locale": "^0.1.35", "@types/angular-ui-bootstrap": "^1.0.7", + "@types/jqueryui": "^1.12.23", "angular": "1.8.3", "angular-dynamic-locale": "0.1.38", "angular-filter": "0.5.17", diff --git a/yarn.lock b/yarn.lock index 1b9c8479e..728ad6889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -272,6 +272,13 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-5.1.4.tgz#0de3f6ca753e10d1600ce1864ae42cfd47cf9924" integrity sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w== +"@types/jquery@*": + version "3.5.30" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.30.tgz#888d584cbf844d3df56834b69925085038fd80f7" + integrity sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A== + dependencies: + "@types/sizzle" "*" + "@types/jquery@^3.5.29": version "3.5.29" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.29.tgz#3c06a1f519cd5fc3a7a108971436c00685b5dcea" @@ -279,6 +286,13 @@ dependencies: "@types/sizzle" "*" +"@types/jqueryui@^1.12.23": + version "1.12.23" + resolved "https://registry.yarnpkg.com/@types/jqueryui/-/jqueryui-1.12.23.tgz#06882d3fd91834f87c40320a0897b2d3fe17de35" + integrity sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA== + dependencies: + "@types/jquery" "*" + "@types/js-yaml@^4.0.9": version "4.0.9" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" From 968a15bb03a7524fe5d16ecd485cf3700f736f1a Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 13:25:20 +0200 Subject: [PATCH 57/99] refactor(ts): directive meter --- .../components/dynamic_tabs/compare-tabs.js | 2 + app/scripts/directives.js | 43 --------------- app/scripts/directives/meter.ts | 53 +++++++++++++++++++ 3 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 app/scripts/directives/meter.ts diff --git a/app/scripts/components/dynamic_tabs/compare-tabs.js b/app/scripts/components/dynamic_tabs/compare-tabs.js index 749b34834..676af4838 100644 --- a/app/scripts/components/dynamic_tabs/compare-tabs.js +++ b/app/scripts/components/dynamic_tabs/compare-tabs.js @@ -2,6 +2,8 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" +import "@/controllers/comparison_controller" +import "@/directives/meter" angular.module("korpApp").directive("compareTabs", () => ({ replace: true, diff --git a/app/scripts/directives.js b/app/scripts/directives.js index 3a51846eb..a1b3b5335 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -1,51 +1,8 @@ /** @format */ import _ from "lodash" -import { loc } from "./i18n" const korpApp = angular.module("korpApp") -korpApp.directive("meter", () => ({ - template: `\ -
      -
      -
      {{meter.abs}}
      -
      \ -`, - replace: true, - scope: { - meter: "=", - max: "=", - stringify: "=", - }, - link(scope, elem, attr) { - const zipped = _.zip(scope.meter.tokenLists, scope.stringify) - scope.displayWd = _.map(zipped, function (...args) { - const [tokens, stringify] = args[0] - return _.map(tokens, function (token) { - if (token === "|" || token === "") { - return "—" - } else { - return stringify(token) - } - }).join(" ") - }).join(";") - - scope.loglike = Math.abs(scope.meter.loglike) - - scope.tooltipHTML = `\ - ${loc("statstable_absfreq")}: ${scope.meter.abs} -
      - loglike: ${scope.loglike}\ -` - - const w = 394 - const part = scope.loglike / Math.abs(scope.max) - - const bkg = elem.find(".background") - return bkg.width(Math.round(part * w)) - }, -})) - korpApp.directive("popper", [ "$rootElement", ($rootElement) => ({ diff --git a/app/scripts/directives/meter.ts b/app/scripts/directives/meter.ts new file mode 100644 index 000000000..aee981f3d --- /dev/null +++ b/app/scripts/directives/meter.ts @@ -0,0 +1,53 @@ +/** @format */ +import _ from "lodash" +import angular, { IScope } from "angular" +import { loc } from "@/i18n" +import { html } from "@/util" +import { CompareItem } from "@/services/backend" + +type MeterScope = IScope & { + meter: CompareItem + max: number + stringify: ((x: string) => string)[] + displayWd: string + loglike: number + tooltipHTML: string +} + +angular.module("korpApp").directive("meter", () => ({ + template: html`
      +
      +
      {{meter.abs}}
      +
      `, + replace: true, + scope: { + meter: "=", + max: "=", + stringify: "=", + }, + link(scope: MeterScope, elem, attr) { + const zipped = _.zip(scope.meter.tokenLists, scope.stringify) + scope.displayWd = _.map(zipped, function (...args) { + const [tokens, stringify] = args[0] + return _.map(tokens, function (token) { + if (token === "|" || token === "") { + return "—" + } else { + return stringify(token) + } + }).join(" ") + }).join(";") + + scope.loglike = Math.abs(scope.meter.loglike) + + scope.tooltipHTML = html`${loc("statstable_absfreq")}: ${scope.meter.abs} +
      + loglike: ${scope.loglike}` + + const w = 394 + const part = scope.loglike / Math.abs(scope.max) + + const bkg = elem.find(".background") + bkg.width(Math.round(part * w)) + }, +})) From 1c33d9ebfc072899e4df927f669eac78262dc1d3 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 13:58:26 +0200 Subject: [PATCH 58/99] refactor(ts): directive popper --- app/scripts/components/datetime-picker.ts | 1 + app/scripts/components/extended/token.js | 1 + app/scripts/components/header.js | 1 + app/scripts/directives.js | 46 ---------------------- app/scripts/directives/popper.ts | 48 +++++++++++++++++++++++ app/scripts/extended.js | 1 + 6 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 app/scripts/directives/popper.ts diff --git a/app/scripts/components/datetime-picker.ts b/app/scripts/components/datetime-picker.ts index 91254b853..7c2c47eeb 100644 --- a/app/scripts/components/datetime-picker.ts +++ b/app/scripts/components/datetime-picker.ts @@ -2,6 +2,7 @@ import angular, { type ui, type IComponentController, type IScope } from "angular" import { html } from "@/util" import moment, { type Moment } from "moment" +import "@/directives/popper" angular.module("korpApp").component("datetimePicker", { template: html` diff --git a/app/scripts/components/extended/token.js b/app/scripts/components/extended/token.js index 4a37fc654..54edd45c2 100644 --- a/app/scripts/components/extended/token.js +++ b/app/scripts/components/extended/token.js @@ -2,6 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/extended/and-token" +import "@/directives/popper" angular.module("korpApp").component("extendedToken", { template: html` diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index a17ec954a..a4854dfca 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -11,6 +11,7 @@ import currentMode from "@/mode" import { collatorSort, html } from "@/util" import "@/components/corpus_chooser/corpus-chooser" import "@/components/radio-list" +import "@/directives/popper" angular.module("korpApp").component("header", { template: html` diff --git a/app/scripts/directives.js b/app/scripts/directives.js index a1b3b5335..64fdfeeb3 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -3,52 +3,6 @@ import _ from "lodash" const korpApp = angular.module("korpApp") -korpApp.directive("popper", [ - "$rootElement", - ($rootElement) => ({ - scope: {}, - link(scope, elem, attrs) { - const popup = elem.next() - popup.appendTo("body").hide() - const closePopup = () => popup.hide() - - if (attrs.noCloseOnClick == null) { - popup.on("click", function () { - closePopup() - return false - }) - } - - elem.on("click", function () { - const other = $(".popper_menu:visible").not(popup) - if (other.length) { - other.hide() - } - if (popup.is(":visible")) { - closePopup() - } else { - popup.show() - } - - const pos = { - my: attrs.my || "right top", - at: attrs.at || "bottom right", - of: elem, - } - if (scope.offset) { - pos.offset = scope.offset - } - - popup.position(pos) - - return false - }) - - return $rootElement.on("click", () => closePopup()) - }, - }), -]) - korpApp.directive("tabSpinner", () => ({ template: `\ diff --git a/app/scripts/directives/popper.ts b/app/scripts/directives/popper.ts new file mode 100644 index 000000000..85c7d6d29 --- /dev/null +++ b/app/scripts/directives/popper.ts @@ -0,0 +1,48 @@ +/** @format */ +import _ from "lodash" +import angular, { IRootElementService } from "angular" + +angular.module("korpApp").directive("popper", [ + "$rootElement", + ($rootElement: IRootElementService) => ({ + scope: {}, + link(scope, elem, attrs) { + const popup = elem.next() + popup.appendTo("body").hide() + + if (attrs.noCloseOnClick == null) { + popup.on("click", function () { + popup.hide() + return false + }) + } + + elem.on("click", function () { + // Hide other popper menus on the page + const other = $(".popper_menu:visible").not(popup) + if (other.length) { + other.hide() + } + + // Close this menu if visible, show if hidden + if (popup.is(":visible")) { + popup.hide() + } else { + popup.show() + } + + // See https://api.jqueryui.com/position/ + popup.position({ + my: attrs.my || "right top", + at: attrs.at || "bottom right", + of: elem, + }) + + return false + }) + + // Hide menu if any other part of the page is clicked + $rootElement.on("click", () => popup.hide()) + }, + }), +]) diff --git a/app/scripts/extended.js b/app/scripts/extended.js index f21d46102..f7c04d2f4 100644 --- a/app/scripts/extended.js +++ b/app/scripts/extended.js @@ -6,6 +6,7 @@ import { html, regescape, unregescape } from "@/util" import { loc, locAttribute } from "@/i18n" import "@/components/autoc" import "@/components/datetime-picker" +import "@/directives/popper" let customExtendedTemplates = {} From 6d575e37ed4658d1eaab96c730ae0cac9b2ed834 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 14:01:22 +0200 Subject: [PATCH 59/99] refactor(ts): directive tabSpinner --- app/scripts/components/dynamic_tabs/compare-tabs.js | 1 + app/scripts/components/dynamic_tabs/graph-tabs.js | 1 + app/scripts/components/dynamic_tabs/kwic-tabs.js | 1 + app/scripts/components/dynamic_tabs/map-tabs.js | 1 + app/scripts/components/dynamic_tabs/text-tabs.js | 1 + app/scripts/directives.js | 8 -------- app/scripts/directives/tab-spinner.ts | 12 ++++++++++++ 7 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 app/scripts/directives/tab-spinner.ts diff --git a/app/scripts/components/dynamic_tabs/compare-tabs.js b/app/scripts/components/dynamic_tabs/compare-tabs.js index 676af4838..d80f359a0 100644 --- a/app/scripts/components/dynamic_tabs/compare-tabs.js +++ b/app/scripts/components/dynamic_tabs/compare-tabs.js @@ -4,6 +4,7 @@ import { html } from "@/util" import "@/components/korp-error" import "@/controllers/comparison_controller" import "@/directives/meter" +import "@/directives/tab-spinner" angular.module("korpApp").directive("compareTabs", () => ({ replace: true, diff --git a/app/scripts/components/dynamic_tabs/graph-tabs.js b/app/scripts/components/dynamic_tabs/graph-tabs.js index 74b1c1fd6..24c5c1ca5 100644 --- a/app/scripts/components/dynamic_tabs/graph-tabs.js +++ b/app/scripts/components/dynamic_tabs/graph-tabs.js @@ -2,6 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/trend-diagram" +import "@/directives/tab-spinner" angular.module("korpApp").directive("graphTabs", () => ({ replace: true, diff --git a/app/scripts/components/dynamic_tabs/kwic-tabs.js b/app/scripts/components/dynamic_tabs/kwic-tabs.js index 020624846..3d842fa0e 100644 --- a/app/scripts/components/dynamic_tabs/kwic-tabs.js +++ b/app/scripts/components/dynamic_tabs/kwic-tabs.js @@ -3,6 +3,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" import "@/components/kwic" +import "@/directives/tab-spinner" // This is a directives because it needs `replace: true`, which is not supported in component angular.module("korpApp").directive("kwicTabs", () => ({ diff --git a/app/scripts/components/dynamic_tabs/map-tabs.js b/app/scripts/components/dynamic_tabs/map-tabs.js index 1799556ed..597200275 100644 --- a/app/scripts/components/dynamic_tabs/map-tabs.js +++ b/app/scripts/components/dynamic_tabs/map-tabs.js @@ -2,6 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" +import "@/directives/tab-spinner" angular.module("korpApp").directive("mapTabs", () => ({ replace: true, diff --git a/app/scripts/components/dynamic_tabs/text-tabs.js b/app/scripts/components/dynamic_tabs/text-tabs.js index c564cccc4..d5f5d229c 100644 --- a/app/scripts/components/dynamic_tabs/text-tabs.js +++ b/app/scripts/components/dynamic_tabs/text-tabs.js @@ -2,6 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" +import "@/directives/tab-spinner" angular.module("korpApp").directive("textTabs", () => ({ replace: true, diff --git a/app/scripts/directives.js b/app/scripts/directives.js index 64fdfeeb3..3d6ca43ef 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -3,14 +3,6 @@ import _ from "lodash" const korpApp = angular.module("korpApp") -korpApp.directive("tabSpinner", () => ({ - template: `\ - -\ -`, -})) - korpApp.directive("tabPreloader", () => ({ restrict: "E", scope: { diff --git a/app/scripts/directives/tab-spinner.ts b/app/scripts/directives/tab-spinner.ts new file mode 100644 index 000000000..cd50542c0 --- /dev/null +++ b/app/scripts/directives/tab-spinner.ts @@ -0,0 +1,12 @@ +/** @format */ +import _ from "lodash" +import angular from "angular" +import { html } from "@/util" + +angular.module("korpApp").directive("tabSpinner", () => ({ + template: html` + `, +})) From 1d5a3f30bfbf724cb1c14ea9b5b6d6da7b184426 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 14:03:19 +0200 Subject: [PATCH 60/99] refactor(ts): directive tabPreloader --- app/scripts/components/results.js | 1 + app/scripts/directives.js | 18 ------------------ app/scripts/directives/tab-preloader.ts | 23 +++++++++++++++++++++++ 3 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 app/scripts/directives/tab-preloader.ts diff --git a/app/scripts/components/results.js b/app/scripts/components/results.js index df56f3699..6e7ae06ac 100644 --- a/app/scripts/components/results.js +++ b/app/scripts/components/results.js @@ -11,6 +11,7 @@ import "@/components/kwic" import "@/components/statistics" import "@/components/sidebar" import "@/components/word-picture" +import "@/directives/tab-preloader" angular.module("korpApp").component("results", { template: html` diff --git a/app/scripts/directives.js b/app/scripts/directives.js index 3d6ca43ef..5a2c6846a 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -3,24 +3,6 @@ import _ from "lodash" const korpApp = angular.module("korpApp") -korpApp.directive("tabPreloader", () => ({ - restrict: "E", - scope: { - value: "=", - spinner: "=", - }, - replace: true, - template: `\ -
      -
      - -
      \ -`, - - link() {}, -})) - korpApp.directive("clickCover", () => ({ link(scope, elem, attr) { const cover = $("
      ").on("click", () => false) diff --git a/app/scripts/directives/tab-preloader.ts b/app/scripts/directives/tab-preloader.ts new file mode 100644 index 000000000..1c2d7f251 --- /dev/null +++ b/app/scripts/directives/tab-preloader.ts @@ -0,0 +1,23 @@ +/** @format */ +import _ from "lodash" +import angular from "angular" +import { html } from "@/util" + +angular.module("korpApp").directive("tabPreloader", () => ({ + restrict: "E", + scope: { + value: "=", + spinner: "=", + }, + replace: true, + template: html`
      +
      + +
      `, + + link() {}, +})) From a6e5c818ad44bc0cb802d75f562c52030422726c Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 14:09:50 +0200 Subject: [PATCH 61/99] refactor(ts): directive clickCover --- app/scripts/components/searchtabs.js | 1 + app/scripts/directives.js | 22 ---------------------- app/scripts/directives/click-cover.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 app/scripts/directives/click-cover.ts diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index 37826cbe8..fce8944a7 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -8,6 +8,7 @@ import "@/components/extended/extended-standard" import "@/components/extended/extended-parallel" import "@/components/advanced-search" import "@/components/compare-search" +import "@/directives/click-cover" angular.module("korpApp").component("searchtabs", { template: html` diff --git a/app/scripts/directives.js b/app/scripts/directives.js index 5a2c6846a..b2131546e 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -3,28 +3,6 @@ import _ from "lodash" const korpApp = angular.module("korpApp") -korpApp.directive("clickCover", () => ({ - link(scope, elem, attr) { - const cover = $("
      ").on("click", () => false) - - const pos = elem.css("position") || "static" - return scope.$watch( - () => scope.$eval(attr.clickCover), - function (val) { - if (val) { - elem.prepend(cover) - elem.css("pointer-events", "none") - return elem.css("position", "relative").addClass("covered") - } else { - cover.remove() - elem.css("pointer-events", "") - return elem.css("position", pos).removeClass("covered") - } - } - ) - }, -})) - // This directive is only used by the autoc-component (autoc.js) // It is therefore made to work with magic variables such as $scope.$ctrl.typeaheadIsOpen korpApp.directive("typeaheadClickOpen", [ diff --git a/app/scripts/directives/click-cover.ts b/app/scripts/directives/click-cover.ts new file mode 100644 index 000000000..8d1ce7233 --- /dev/null +++ b/app/scripts/directives/click-cover.ts @@ -0,0 +1,27 @@ +/** @format */ +import _ from "lodash" +import angular from "angular" + +//
      ...
      +// If the expression is true, the content is faded and cannot be clicked. +angular.module("korpApp").directive("clickCover", () => ({ + link(scope, elem, attr) { + const cover = $("
      ").on("click", () => false) + + const pos = elem.css("position") || "static" + scope.$watch( + () => scope.$eval(attr.clickCover), + (enabled: boolean) => { + if (enabled) { + elem.prepend(cover) + elem.css("pointer-events", "none") + elem.css("position", "relative").addClass("covered") + } else { + cover.remove() + elem.css("pointer-events", "") + elem.css("position", pos).removeClass("covered") + } + } + ) + }, +})) From 433b042096623ef6c80b596aeaa4b05c5fcd1fcb Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 15:18:52 +0200 Subject: [PATCH 62/99] refactor(ts): directive typeaheadClickOpen --- app/scripts/components/autoc.js | 1 + app/scripts/directives.js | 22 --------------- .../directives/typeahead-click-open.ts | 27 +++++++++++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 app/scripts/directives/typeahead-click-open.ts diff --git a/app/scripts/components/autoc.js b/app/scripts/components/autoc.js index 66551b4ce..2dd976a6d 100644 --- a/app/scripts/components/autoc.js +++ b/app/scripts/components/autoc.js @@ -4,6 +4,7 @@ import angular from "angular" import settings from "@/settings" import { html, lemgramToString, saldoToString } from "@/util" import { loc } from "@/i18n" +import "@/directives/typeahead-click-open" angular.module("korpApp").component("autoc", { template: html` diff --git a/app/scripts/directives.js b/app/scripts/directives.js index b2131546e..6d3a10b6f 100644 --- a/app/scripts/directives.js +++ b/app/scripts/directives.js @@ -3,28 +3,6 @@ import _ from "lodash" const korpApp = angular.module("korpApp") -// This directive is only used by the autoc-component (autoc.js) -// It is therefore made to work with magic variables such as $scope.$ctrl.typeaheadIsOpen -korpApp.directive("typeaheadClickOpen", [ - "$timeout", - ($timeout) => ({ - restrict: "A", - require: ["ngModel"], - link($scope, elem, attrs, ctrls) { - const triggerFunc = function (event) { - if (event.keyCode === 40 && !$scope.$ctrl.typeaheadIsOpen) { - const prev = ctrls[0].$modelValue || "" - if (prev) { - ctrls[0].$setViewValue("") - $timeout(() => ctrls[0].$setViewValue(`${prev}`)) - } - } - } - elem.bind("keyup", triggerFunc) - }, - }), -]) - korpApp.directive("reduceSelect", [ "$timeout", ($timeout) => ({ diff --git a/app/scripts/directives/typeahead-click-open.ts b/app/scripts/directives/typeahead-click-open.ts new file mode 100644 index 000000000..ae4d19ed0 --- /dev/null +++ b/app/scripts/directives/typeahead-click-open.ts @@ -0,0 +1,27 @@ +/** @format */ +import _ from "lodash" +import angular, { ITimeoutService } from "angular" + +// Enable triggering the autocomplete dropdown by pressing the down arrow key. +// This directive is only used by the autoc-component (autoc.js) +// It is therefore made to work with magic variables such as $scope.$ctrl.typeaheadIsOpen +angular.module("korpApp").directive("typeaheadClickOpen", [ + "$timeout", + ($timeout: ITimeoutService) => ({ + restrict: "A", + require: ["ngModel"], + link($scope, elem, attrs, ctrls: [angular.INgModelController]) { + elem.bind("keyup", (event) => { + if (event.key == "ArrowDown" && !($scope as any).$ctrl.typeaheadIsOpen) { + // Get entered text + const prev = ctrls[0].$modelValue || "" + // Empty and refill the input to trigger autocomplete + if (prev) { + ctrls[0].$setViewValue("") + $timeout(() => ctrls[0].$setViewValue(`${prev}`)) + } + } + }) + }, + }), +]) From 6de35606891d328e6ec3161bb11e613d50857fa7 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 16:14:14 +0200 Subject: [PATCH 63/99] refactor(ts): directive reduceSelect --- app/index.ts | 1 - app/scripts/components/searchtabs.js | 1 + app/scripts/directives.js | 147 ---------------------- app/scripts/directives/reduce-select.ts | 155 ++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 148 deletions(-) delete mode 100644 app/scripts/directives.js create mode 100644 app/scripts/directives/reduce-select.ts diff --git a/app/index.ts b/app/index.ts index 9b2bbba05..6fd47b210 100644 --- a/app/index.ts +++ b/app/index.ts @@ -84,7 +84,6 @@ require("./scripts/services/searches") require("./scripts/services/utils") require("./scripts/extended.js") require("./scripts/struct_services.js") -require("./scripts/directives.js") require("./scripts/directives/escaper") require("./scripts/directives/scroll") require("./scripts/directives/tab-hash") diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index fce8944a7..ea1c8da08 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -9,6 +9,7 @@ import "@/components/extended/extended-parallel" import "@/components/advanced-search" import "@/components/compare-search" import "@/directives/click-cover" +import "@/directives/reduce-select" angular.module("korpApp").component("searchtabs", { template: html` diff --git a/app/scripts/directives.js b/app/scripts/directives.js deleted file mode 100644 index 6d3a10b6f..000000000 --- a/app/scripts/directives.js +++ /dev/null @@ -1,147 +0,0 @@ -/** @format */ -import _ from "lodash" - -const korpApp = angular.module("korpApp") - -korpApp.directive("reduceSelect", [ - "$timeout", - ($timeout) => ({ - restrict: "AE", - scope: { - items: "=reduceItems", - selected: "=reduceSelected", - insensitive: "=reduceInsensitive", - lang: "=reduceLang", - onChange: "<", - }, - replace: true, - template: `\ -
      -
      -
      - {{ "reduce_text" | loc:$root.lang }}: - - {{keyItems[selected[0]].label | locObj:$root.lang}} - - - (+{{ numberAttributes - 1 }}) - - -
      -
      -
      -
        -
      • - - {{keyItems['word'].label | locObj:$root.lang }} - Aa -
      • - {{'word_attr' | loc:$root.lang}} -
      • - - {{item.label | locObj:$root.lang }} -
      • - {{'sentence_attr' | loc:$root.lang}} -
      • - - {{item.label | locObj:$root.lang }} -
      • -
      -
      -
      `, - - link(scope) { - scope.$watchCollection("items", function () { - if (scope.items) { - scope.keyItems = {} - for (let item of scope.items) { - scope.keyItems[item.value] = item - } - - scope.hasWordAttrs = _.find(scope.keyItems, { group: "word_attr" }) != undefined - scope.hasStructAttrs = _.find(scope.keyItems, { group: "sentence_attr" }) != undefined - - let somethingSelected = false - if (scope.selected && scope.selected.length > 0) { - for (let select of scope.selected) { - const item = scope.keyItems[select] - if (item) { - item.selected = true - somethingSelected = true - } - } - } - - if (!somethingSelected) { - scope.keyItems["word"].selected = true - } - - if (scope.insensitive) { - for (let insensitive of scope.insensitive) { - scope.keyItems[insensitive].insensitive = true - } - } - return updateSelected(scope) - } - }) - - var updateSelected = function (scope) { - scope.selected = _.map( - _.filter(scope.keyItems, (item, key) => item.selected), - "value" - ) - scope.numberAttributes = scope.selected.length - $timeout(() => scope.onChange()) - } - - scope.toggleSelected = function (value, event) { - const item = scope.keyItems[value] - const isLinux = window.navigator.userAgent.indexOf("Linux") !== -1 - if (event && ((!isLinux && event.altKey) || (isLinux && event.ctrlKey))) { - _.map(_.values(scope.keyItems), (item) => (item.selected = false)) - item.selected = true - } else { - item.selected = !item.selected - if (value === "word" && !item.selected) { - item.insensitive = false - scope.insensitive = [] - } - } - - updateSelected(scope) - - if (event) { - return event.stopPropagation() - } - } - - scope.toggleWordInsensitive = function (event) { - event.stopPropagation() - scope.keyItems["word"].insensitive = !scope.keyItems["word"].insensitive - if (scope.keyItems["word"].insensitive) { - scope.insensitive = ["word"] - } else { - scope.insensitive = [] - } - $timeout(() => scope.onChange()) - - if (!scope.keyItems["word"].selected) { - return scope.toggleSelected("word") - } - } - - scope.toggled = function (open) { - // if no element is selected when closing popop, select word - if (!open && scope.numberAttributes === 0) { - return $timeout(() => scope.toggleSelected("word"), 0) - } - } - }, - }), -]) diff --git a/app/scripts/directives/reduce-select.ts b/app/scripts/directives/reduce-select.ts new file mode 100644 index 000000000..982416977 --- /dev/null +++ b/app/scripts/directives/reduce-select.ts @@ -0,0 +1,155 @@ +/** @format */ +import _ from "lodash" +import angular, { IScope, ITimeoutService } from "angular" +import { html } from "@/util" +import { AttributeOption } from "@/corpus_listing" + +type ReduceSelectScope = IScope & { + items: Item[] + keyItems: Record + hasWordAttrs: boolean + hasStructAttrs: boolean + selected: string[] + insensitive: string[] + toggleSelected: (value: string, event?: MouseEvent) => void + toggleWordInsensitive: (event: MouseEvent) => void + onChange: () => void + toggled: (open: boolean) => void +} + +type Item = AttributeOption & { + selected?: boolean + insensitive?: boolean +} + +angular.module("korpApp").directive("reduceSelect", [ + "$timeout", + ($timeout: ITimeoutService) => ({ + restrict: "AE", + scope: { + items: "=reduceItems", + selected: "=reduceSelected", + insensitive: "=reduceInsensitive", + lang: "=reduceLang", + onChange: "<", + }, + replace: true, + template: html`
      +
      +
      + {{ "reduce_text" | loc:$root.lang }}: + {{keyItems[selected[0]].label | locObj:$root.lang}} + (+{{ selected.length - 1 }}) + +
      +
      +
      +
        +
      • + + {{keyItems['word'].label | locObj:$root.lang }} + Aa +
      • + {{'word_attr' | loc:$root.lang}} +
      • + + {{item.label | locObj:$root.lang }} +
      • + {{'sentence_attr' | loc:$root.lang}} +
      • + + {{item.label | locObj:$root.lang }} +
      • +
      +
      +
      `, + + link(scope: ReduceSelectScope) { + scope.$watchCollection("items", function () { + if (!scope.items) return + scope.keyItems = _.keyBy(scope.items, "value") + scope.hasWordAttrs = scope.items.some((item) => item.group == "word_attr") + scope.hasStructAttrs = scope.items.some((item) => item.group == "sentence_attr") + + for (const name of scope.selected || []) { + scope.keyItems[name].selected = true + } + for (const name of scope.insensitive || []) { + scope.keyItems[name].insensitive = true + } + + // If no selection given, default to selecting the word option + const hasSelection = scope.items.some((item) => item.selected) + if (!hasSelection) { + scope.keyItems["word"].selected = true + } + updateSelected() + }) + + function updateSelected() { + // If unselecting the word option, reset the insensitive flag. + if (!scope.keyItems["word"].selected) { + scope.keyItems["word"].insensitive = false + } + + scope.selected = scope.items.filter((item) => item.selected).map((item) => item.value) + scope.insensitive = scope.items.filter((item) => item.insensitive).map((item) => item.value) + + $timeout(() => scope.onChange()) + } + + scope.toggleSelected = function (value, event) { + event.stopPropagation() + const item = scope.keyItems[value] + const isLinux = window.navigator.userAgent.indexOf("Linux") !== -1 + if (isLinux ? event.ctrlKey : event.altKey) { + // Unselect all options and select only the given option + scope.items.forEach((item) => (item.selected = false)) + item.selected = true + } else { + item.selected = !item.selected + } + updateSelected() + } + + scope.toggleWordInsensitive = function (event) { + event.stopPropagation() + scope.keyItems["word"].insensitive = !scope.keyItems["word"].insensitive + if (!scope.keyItems["word"].selected) { + scope.keyItems["word"].selected = true + } + updateSelected() + } + + scope.toggled = function (open) { + // if no element is selected when closing popop, select word + if (!open && !scope.selected.length) { + scope.keyItems["word"].selected = true + updateSelected() + } + } + }, + }), +]) From 28c060d0b42f98ed171d9a7211899b36cc2a5341 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 26 Aug 2024 16:19:30 +0200 Subject: [PATCH 64/99] refactor: import all @/directives/* where used --- app/index.ts | 3 --- app/scripts/components/extended/tokens.js | 1 + app/scripts/components/results.js | 1 + app/scripts/components/searchtabs.js | 1 + app/scripts/directives/{scroll.ts => scroll-to-start.ts} | 0 app/scripts/extended.js | 1 + 6 files changed, 4 insertions(+), 3 deletions(-) rename app/scripts/directives/{scroll.ts => scroll-to-start.ts} (100%) diff --git a/app/index.ts b/app/index.ts index 6fd47b210..d72e9acb1 100644 --- a/app/index.ts +++ b/app/index.ts @@ -84,8 +84,5 @@ require("./scripts/services/searches") require("./scripts/services/utils") require("./scripts/extended.js") require("./scripts/struct_services.js") -require("./scripts/directives/escaper") -require("./scripts/directives/scroll") -require("./scripts/directives/tab-hash") require("./scripts/filter_directives.js") require("./scripts/matomo.js") diff --git a/app/scripts/components/extended/tokens.js b/app/scripts/components/extended/tokens.js index 0f56f310a..42338470e 100644 --- a/app/scripts/components/extended/tokens.js +++ b/app/scripts/components/extended/tokens.js @@ -7,6 +7,7 @@ import { html } from "@/util" import "@/components/extended/token" import "@/components/extended/struct-token" import "@/components/extended/add-box" +import "@/directives/scroll-to-start" angular.module("korpApp").component("extendedTokens", { template: html` diff --git a/app/scripts/components/results.js b/app/scripts/components/results.js index 6e7ae06ac..f4024998d 100644 --- a/app/scripts/components/results.js +++ b/app/scripts/components/results.js @@ -11,6 +11,7 @@ import "@/components/kwic" import "@/components/statistics" import "@/components/sidebar" import "@/components/word-picture" +import "@/directives/tab-hash" import "@/directives/tab-preloader" angular.module("korpApp").component("results", { diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index ea1c8da08..3fa935806 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -10,6 +10,7 @@ import "@/components/advanced-search" import "@/components/compare-search" import "@/directives/click-cover" import "@/directives/reduce-select" +import "@/directives/tab-hash" angular.module("korpApp").component("searchtabs", { template: html` diff --git a/app/scripts/directives/scroll.ts b/app/scripts/directives/scroll-to-start.ts similarity index 100% rename from app/scripts/directives/scroll.ts rename to app/scripts/directives/scroll-to-start.ts diff --git a/app/scripts/extended.js b/app/scripts/extended.js index f7c04d2f4..69e452926 100644 --- a/app/scripts/extended.js +++ b/app/scripts/extended.js @@ -6,6 +6,7 @@ import { html, regescape, unregescape } from "@/util" import { loc, locAttribute } from "@/i18n" import "@/components/autoc" import "@/components/datetime-picker" +import "@/directives/escaper" import "@/directives/popper" let customExtendedTemplates = {} From 3537684cde1f76447be67807e32addc974f9ea84 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 28 Aug 2024 11:16:52 +0200 Subject: [PATCH 65/99] refactor: import all @/services/* where used --- app/index.ts | 5 ----- app/scripts/components/advanced-search.js | 1 + app/scripts/components/autoc.js | 1 + app/scripts/components/compare-search.js | 2 ++ app/scripts/components/extended/extended-parallel.js | 1 + app/scripts/components/extended/extended-standard.js | 1 + app/scripts/components/frontpage.ts | 7 ++++--- app/scripts/components/header.js | 1 + app/scripts/components/results.js | 1 + app/scripts/components/searchtabs.js | 2 ++ app/scripts/components/sidebar.js | 1 + app/scripts/components/simple-search.js | 3 +++ app/scripts/components/statistics.js | 2 ++ app/scripts/controllers/example_controller.ts | 1 + app/scripts/controllers/kwic_controller.ts | 1 + app/scripts/controllers/statistics_controller.ts | 1 + app/scripts/controllers/word_picture_controller.ts | 1 + app/scripts/directives/tab-hash.ts | 1 + app/scripts/services/backend.ts | 1 + app/scripts/text_reader_controller.js | 1 + 20 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/index.ts b/app/index.ts index d72e9acb1..94334eaab 100644 --- a/app/index.ts +++ b/app/index.ts @@ -77,11 +77,6 @@ require("./scripts/controllers/word_picture_controller") require("./scripts/map_controllers.js") require("./scripts/text_reader_controller.js") require("./scripts/video_controllers.js") -require("./scripts/services/backend") -require("./scripts/services/compare-searches") -require("./scripts/services/lexicons") -require("./scripts/services/searches") -require("./scripts/services/utils") require("./scripts/extended.js") require("./scripts/struct_services.js") require("./scripts/filter_directives.js") diff --git a/app/scripts/components/advanced-search.js b/app/scripts/components/advanced-search.js index 0a7bbe846..fc8d6c2e5 100644 --- a/app/scripts/components/advanced-search.js +++ b/app/scripts/components/advanced-search.js @@ -1,6 +1,7 @@ /** @format */ import angular from "angular" import { html } from "@/util" +import "@/services/compare-searches" import "@/directives/search-submit" angular.module("korpApp").component("advancedSearch", { diff --git a/app/scripts/components/autoc.js b/app/scripts/components/autoc.js index 2dd976a6d..5d90b4666 100644 --- a/app/scripts/components/autoc.js +++ b/app/scripts/components/autoc.js @@ -4,6 +4,7 @@ import angular from "angular" import settings from "@/settings" import { html, lemgramToString, saldoToString } from "@/util" import { loc } from "@/i18n" +import "@/services/lexicons" import "@/directives/typeahead-click-open" angular.module("korpApp").component("autoc", { diff --git a/app/scripts/components/compare-search.js b/app/scripts/components/compare-search.js index 42f0ddf99..bcd6048f4 100644 --- a/app/scripts/components/compare-search.js +++ b/app/scripts/components/compare-search.js @@ -2,6 +2,8 @@ import angular from "angular" import _ from "lodash" import settings from "@/settings" +import "@/services/backend" +import "@/services/compare-searches" import { html, valfilter } from "@/util" angular.module("korpApp").component("compareSearch", { diff --git a/app/scripts/components/extended/extended-parallel.js b/app/scripts/components/extended/extended-parallel.js index 8525fdea3..bba174979 100644 --- a/app/scripts/components/extended/extended-parallel.js +++ b/app/scripts/components/extended/extended-parallel.js @@ -4,6 +4,7 @@ import _ from "lodash" import settings from "@/settings" import { expandOperators } from "@/cqp_parser/cqp" import { html } from "@/util" +import "@/services/searches" import "@/components/extended/tokens" angular.module("korpApp").component("extendedParallel", { diff --git a/app/scripts/components/extended/extended-standard.js b/app/scripts/components/extended/extended-standard.js index c65e58b0c..95137a46e 100644 --- a/app/scripts/components/extended/extended-standard.js +++ b/app/scripts/components/extended/extended-standard.js @@ -5,6 +5,7 @@ import statemachine from "@/statemachine" import settings from "@/settings" import { expandOperators, mergeCqpExprs, parse, stringify, supportsInOrder } from "@/cqp_parser/cqp" import { html } from "@/util" +import "@/services/compare-searches" import "@/components/extended/tokens" import "@/directives/search-submit" diff --git a/app/scripts/components/frontpage.ts b/app/scripts/components/frontpage.ts index 45e9c53e9..5562c6bdf 100644 --- a/app/scripts/components/frontpage.ts +++ b/app/scripts/components/frontpage.ts @@ -1,13 +1,14 @@ /** @format */ import angular from "angular" +import settings from "@/settings" +import { RootScope } from "@/root-scope.types" +import { SearchesService } from "@/services/searches" import { html } from "@/util" import { isEnabled } from "@/news-service" +import "@/services/searches" import "@/components/corpus-updates" import "@/components/newsdesk" import "@/components/search-examples" -import settings from "@/settings" -import { RootScope } from "@/root-scope.types" -import { SearchesService } from "@/services/searches" export default angular.module("korpApp").component("frontpage", { template: html` diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index a4854dfca..0a6749848 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -9,6 +9,7 @@ import guLogo from "../../img/gu_logo_sv_head.svg" import settings from "@/settings" import currentMode from "@/mode" import { collatorSort, html } from "@/util" +import "@/services/utils" import "@/components/corpus_chooser/corpus-chooser" import "@/components/radio-list" import "@/directives/popper" diff --git a/app/scripts/components/results.js b/app/scripts/components/results.js index f4024998d..9ef766a7f 100644 --- a/app/scripts/components/results.js +++ b/app/scripts/components/results.js @@ -1,6 +1,7 @@ /** @format */ import angular from "angular" import { html } from "@/util" +import "@/services/searches" import "@/components/dynamic_tabs/compare-tabs" import "@/components/dynamic_tabs/graph-tabs" import "@/components/dynamic_tabs/kwic-tabs" diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index 3fa935806..1d2126c7b 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -3,6 +3,8 @@ import angular from "angular" import _ from "lodash" import settings from "@/settings" import { html } from "@/util" +import "@/services/compare-searches" +import "@/services/searches" import "@/components/simple-search" import "@/components/extended/extended-standard" import "@/components/extended/extended-parallel" diff --git a/app/scripts/components/sidebar.js b/app/scripts/components/sidebar.js index f82f62a15..8375a912b 100644 --- a/app/scripts/components/sidebar.js +++ b/app/scripts/components/sidebar.js @@ -7,6 +7,7 @@ import settings from "@/settings" import { stringify } from "@/stringify.js" import { html, regescape, splitLemgram, safeApply } from "@/util" import { loc, locAttribute } from "@/i18n" +import "@/services/utils" import "@/components/deptree/deptree" let sidebarComponents = {} diff --git a/app/scripts/components/simple-search.js b/app/scripts/components/simple-search.js index bc3a05f5c..8a75bc630 100644 --- a/app/scripts/components/simple-search.js +++ b/app/scripts/components/simple-search.js @@ -5,6 +5,9 @@ import statemachine from "@/statemachine" import settings from "@/settings" import { expandOperators, mergeCqpExprs, parse, stringify, supportsInOrder } from "@/cqp_parser/cqp" import { html, regescape, saldoToHtml, unregescape } from "@/util" +import "@/services/compare-searches" +import "@/services/lexicons" +import "@/services/searches" import "@/components/autoc" import "@/directives/search-submit" diff --git a/app/scripts/components/statistics.js b/app/scripts/components/statistics.js index 994504555..ca6bbe40a 100644 --- a/app/scripts/components/statistics.js +++ b/app/scripts/components/statistics.js @@ -7,6 +7,8 @@ import { html } from "@/util" import { loc, locObj } from "@/i18n" import { getCqp } from "../../config/statistics_config.js" import { expandOperators } from "@/cqp_parser/cqp" +import "@/services/backend" +import "@/services/searches" import "@/components/corpus-distribution-chart" angular.module("korpApp").component("statistics", { diff --git a/app/scripts/controllers/example_controller.ts b/app/scripts/controllers/example_controller.ts index b52963a8f..1cde3d48b 100644 --- a/app/scripts/controllers/example_controller.ts +++ b/app/scripts/controllers/example_controller.ts @@ -8,6 +8,7 @@ import { KwicTab, RootScope } from "@/root-scope.types" import { KorpResponse, ProgressReport } from "@/backend/types" import { KorpQueryResponse } from "@/backend/kwic-proxy" import { UtilsService } from "@/services/utils" +import "@/services/utils" const korpApp = angular.module("korpApp") diff --git a/app/scripts/controllers/kwic_controller.ts b/app/scripts/controllers/kwic_controller.ts index 60d2ee48f..48f7df1ef 100644 --- a/app/scripts/controllers/kwic_controller.ts +++ b/app/scripts/controllers/kwic_controller.ts @@ -7,6 +7,7 @@ import { RootScope } from "@/root-scope.types" import { LocationService } from "@/urlparams" import { KorpResponse, ProgressReport } from "@/backend/types" import { UtilsService } from "@/services/utils" +import "@/services/utils" angular.module("korpApp").directive("kwicCtrl", () => ({ controller: KwicCtrl })) diff --git a/app/scripts/controllers/statistics_controller.ts b/app/scripts/controllers/statistics_controller.ts index 4779ddf02..b99f5aeb6 100644 --- a/app/scripts/controllers/statistics_controller.ts +++ b/app/scripts/controllers/statistics_controller.ts @@ -11,6 +11,7 @@ import { Dataset } from "@/statistics_worker" import { SearchParams } from "@/statistics.types" import { SlickgridColumn } from "@/statistics" import { SearchesService } from "@/services/searches" +import "@/services/searches" type StatsResultCtrlScope = IScope & { $parent: any diff --git a/app/scripts/controllers/word_picture_controller.ts b/app/scripts/controllers/word_picture_controller.ts index a0b06d050..0fe9c1d44 100644 --- a/app/scripts/controllers/word_picture_controller.ts +++ b/app/scripts/controllers/word_picture_controller.ts @@ -9,6 +9,7 @@ import { LocationService } from "@/urlparams" import { KorpResponse, ProgressReport } from "@/backend/types" import { WordPictureDefItem } from "@/settings/app-settings.types" import { SearchesService } from "@/services/searches" +import "@/services/searches" type WordpicCtrlScope = IScope & { $parent: any diff --git a/app/scripts/directives/tab-hash.ts b/app/scripts/directives/tab-hash.ts index d8cbf33ae..0f3d9952e 100644 --- a/app/scripts/directives/tab-hash.ts +++ b/app/scripts/directives/tab-hash.ts @@ -3,6 +3,7 @@ import _ from "lodash" import angular, { IScope, ITimeoutService } from "angular" import { UtilsService } from "@/services/utils" import { LocationService } from "@/urlparams" +import "@/services/utils" type TabHashScope = IScope & { activeTab: number diff --git a/app/scripts/services/backend.ts b/app/scripts/services/backend.ts index 6fcff5bbc..51202e6d7 100644 --- a/app/scripts/services/backend.ts +++ b/app/scripts/services/backend.ts @@ -9,6 +9,7 @@ import { httpConfAddMethod, httpConfAddMethodAngular } from "@/util" import { KorpStatsResponse, normalizeStatsData } from "@/backend/stats-proxy" import { MapResult, parseMapData } from "@/map_services" import { KorpQueryResponse } from "@/backend/kwic-proxy" +import "@/services/lexicons" export type BackendService = { requestCompare: (cmpObj1: SavedSearch, cmpObj2: SavedSearch, reduce: string[]) => IPromise diff --git a/app/scripts/text_reader_controller.js b/app/scripts/text_reader_controller.js index 059a8d589..ea71d97ae 100644 --- a/app/scripts/text_reader_controller.js +++ b/app/scripts/text_reader_controller.js @@ -2,6 +2,7 @@ import _ from "lodash" import statemachine from "./statemachine" import settings from "@/settings" +import "@/services/backend" import "@/components/readingmode" const korpApp = angular.module("korpApp") From 91fc00976cbf972000e8815e3bc15fea0d7083b9 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 28 Aug 2024 11:27:30 +0200 Subject: [PATCH 66/99] fix: guard for removed options in reduceSelect Fixes #384 --- app/scripts/directives/reduce-select.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/directives/reduce-select.ts b/app/scripts/directives/reduce-select.ts index 982416977..22a6aa374 100644 --- a/app/scripts/directives/reduce-select.ts +++ b/app/scripts/directives/reduce-select.ts @@ -94,10 +94,10 @@ angular.module("korpApp").directive("reduceSelect", [ scope.hasStructAttrs = scope.items.some((item) => item.group == "sentence_attr") for (const name of scope.selected || []) { - scope.keyItems[name].selected = true + if (name in scope.keyItems) scope.keyItems[name].selected = true } for (const name of scope.insensitive || []) { - scope.keyItems[name].insensitive = true + if (name in scope.keyItems) scope.keyItems[name].insensitive = true } // If no selection given, default to selecting the word option From d738f29687196a9cba0fa588a3c6d4783ce31335 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 28 Aug 2024 11:30:33 +0200 Subject: [PATCH 67/99] fix: guard for empty loginObj in basic_auth Fixes #387 --- app/scripts/components/auth/basic_auth.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/components/auth/basic_auth.ts b/app/scripts/components/auth/basic_auth.ts index bb71a9e44..18b3f08d3 100644 --- a/app/scripts/components/auth/basic_auth.ts +++ b/app/scripts/components/auth/basic_auth.ts @@ -75,7 +75,8 @@ export const login = (usr: string, pass: string, saveLogin: boolean): JQueryDefe return dfd } -export const hasCredential = (corpusId: string): boolean => state.loginObj.credentials?.includes(corpusId.toUpperCase()) +export const hasCredential = (corpusId: string): boolean => + state.loginObj?.credentials?.includes(corpusId.toUpperCase()) export const logout = (): void => { state.loginObj = undefined From 38534b82a7902cc5e56a67844485505ffe0f767e Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 28 Aug 2024 13:43:01 +0200 Subject: [PATCH 68/99] fix: paging in word picture example search Fixes #383 --- CHANGELOG.md | 1 + app/scripts/backend/kwic-proxy.ts | 4 +--- app/scripts/controllers/example_controller.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc29f7caf..b58519520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - Handle end seconds correctly [#378](https://github.com/spraakbanken/korp-frontend/issues/378) - Parsing of the simple input had been incomplete since way back - There was no word picture heading if lemgram +- Paging broken in word picture example search [#383](https://github.com/spraakbanken/korp-frontend/issues/383) ## [9.6.0] - 2024-05-27 diff --git a/app/scripts/backend/kwic-proxy.ts b/app/scripts/backend/kwic-proxy.ts index bf7963fd6..1d64fa827 100644 --- a/app/scripts/backend/kwic-proxy.ts +++ b/app/scripts/backend/kwic-proxy.ts @@ -49,7 +49,6 @@ export class KwicProxy extends BaseProxy { } const command = options.ajaxParams.command || "query" - delete options.ajaxParams.command const data: KorpQueryParams = { default_context: settings.default_overview_context, @@ -158,8 +157,7 @@ export type KorpQueryParams = { } export type KorpQueryRequestOptions = { - // TODO Should start,end,command really exist here as well as under ajaxParams? - command?: string + // TODO Should start,end really exist here as well as under ajaxParams? start?: number end?: number ajaxParams?: KorpQueryParams & { diff --git a/app/scripts/controllers/example_controller.ts b/app/scripts/controllers/example_controller.ts index 1cde3d48b..fe8964c29 100644 --- a/app/scripts/controllers/example_controller.ts +++ b/app/scripts/controllers/example_controller.ts @@ -131,7 +131,7 @@ class ExampleCtrl extends KwicCtrl { _.extend(opts.ajaxParams, { context, default_context: preferredContext }) s.loading = true - if (opts.command == "relations_sentences") { + if (opts.ajaxParams.command == "relations_sentences") { s.onExampleProgress = () => {} } else { s.onExampleProgress = s.onProgress From 505e49faad563b949479d2ab2a5f14f3220067d7 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 2 Sep 2024 16:23:16 +0200 Subject: [PATCH 69/99] chore: changelog Karp 7 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b58519520..14a728920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Removed global `loc_data`, use `$rootScope["loc_data"]` instead (outside Angular: `getService("$rootScope")["loc_data"]`) - Removed globals `CSV` and `moment`, import the libraries instead - Converted the "radioList" JQuery widget to a component +- Using Karp 7 backend instead of Karp 4 [#388](https://github.com/spraakbanken/korp-frontend/pull/388) ### Fixed From b88ea1bd6bd92c4932d81d351b6d5bc1f7055946 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 3 Sep 2024 15:15:22 +0200 Subject: [PATCH 70/99] fix: corpus heading size in kwic Fixes #389 --- CHANGELOG.md | 1 + app/scripts/components/kwic.js | 51 +++++++++++++++++----------------- app/styles/styles.scss | 47 ------------------------------- 3 files changed, 27 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a728920..034811a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Parsing of the simple input had been incomplete since way back - There was no word picture heading if lemgram - Paging broken in word picture example search [#383](https://github.com/spraakbanken/korp-frontend/issues/383) +- Incoherent style change to corpus heading when switching between KWIC and context view [#389](https://github.com/spraakbanken/korp-frontend/issues/389) ## [9.6.0] - 2024-05-27 diff --git a/app/scripts/components/kwic.js b/app/scripts/components/kwic.js index 3bfaa4cda..4c0ec6649 100644 --- a/app/scripts/components/kwic.js +++ b/app/scripts/components/kwic.js @@ -54,19 +54,19 @@ angular.module("korpApp").component("kwic", { - - -
      + + +
      {{sentence.newCorpus | locObj:$root.lang}} - {{'no_context_support' | loc:$root.lang}} + + ({{'no_context_support' | loc:$root.lang}}) +
      - - + + - + + - + - + - {{sentence.newCorpus | locObj:$root.lang}}{{'no_context_support' | loc:$root.lang}} + + {{sentence.newCorpus | locObj:$root.lang}} + + ({{'no_context_support' | loc:$root.lang}}) + + Date: Tue, 3 Sep 2024 15:53:39 +0200 Subject: [PATCH 71/99] fix: context view in example search Fixes #386 --- CHANGELOG.md | 1 + app/scripts/components/dynamic_tabs/kwic-tabs.js | 4 ++-- app/scripts/controllers/example_controller.ts | 7 ++----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034811a3c..59352f57b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - There was no word picture heading if lemgram - Paging broken in word picture example search [#383](https://github.com/spraakbanken/korp-frontend/issues/383) - Incoherent style change to corpus heading when switching between KWIC and context view [#389](https://github.com/spraakbanken/korp-frontend/issues/389) +- Context view broken in example search [#386](https://github.com/spraakbanken/korp-frontend/issues/386) ## [9.6.0] - 2024-05-27 diff --git a/app/scripts/components/dynamic_tabs/kwic-tabs.js b/app/scripts/components/dynamic_tabs/kwic-tabs.js index 3d842fa0e..937ec8667 100644 --- a/app/scripts/components/dynamic_tabs/kwic-tabs.js +++ b/app/scripts/components/dynamic_tabs/kwic-tabs.js @@ -17,7 +17,7 @@ angular.module("korpApp").directive("kwicTabs", () => ({
      ({ hits="hits" kwic-input="kwic" corpus-hits="corpusHits" - is-reading="reading_mode" + is-reading="kwicTab.readingMode" page="page" page-event="pageChange" context-change-event="toggleReading" diff --git a/app/scripts/controllers/example_controller.ts b/app/scripts/controllers/example_controller.ts index fe8964c29..d8fbcc943 100644 --- a/app/scripts/controllers/example_controller.ts +++ b/app/scripts/controllers/example_controller.ts @@ -17,7 +17,6 @@ type ScopeBase = Omit & IRepeatScope type ExampleCtrlScope = ScopeBase & { $parent: { $parent: any } closeTab: (idx: number, e: Event) => void - exampleReadingMode?: boolean hitsPictureData?: any hitspictureClick?: (page: number) => void kwicTab: KwicTab @@ -61,7 +60,7 @@ class ExampleCtrl extends KwicCtrl { s.newDynamicTab() s.isReadingMode = () => { - return s.exampleReadingMode + return s.kwicTab.readingMode } s.hitspictureClick = function (pageNumber) { @@ -73,10 +72,8 @@ class ExampleCtrl extends KwicCtrl { s.makeRequest() } - s.exampleReadingMode = s.kwicTab.readingMode - s.toggleReading = function () { - s.exampleReadingMode = !s.exampleReadingMode + s.kwicTab.readingMode = !s.kwicTab.readingMode if (s.getProxy().pendingRequests.length) { return $.when(...(s.getProxy().pendingRequests || [])).then(() => s.makeRequest()) } From 839df5b483f8fe038a66807571848582092de70c Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 3 Sep 2024 16:16:17 +0200 Subject: [PATCH 72/99] fix: simplify reading mode state The Kwic component had two flags for tracking reading mode, and they would be out of sync if the main Kwic had it enabled when a new KwicTab was created --- app/scripts/components/kwic.js | 16 +++++++--------- app/scripts/controllers/kwic_controller.ts | 6 +----- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/app/scripts/components/kwic.js b/app/scripts/components/kwic.js index 4c0ec6649..037c15a99 100644 --- a/app/scripts/components/kwic.js +++ b/app/scripts/components/kwic.js @@ -46,8 +46,8 @@ angular.module("korpApp").component("kwic", { hits-per-page="$ctrl.hitsPerPage" > - {{'show_reading' | loc:$root.lang}} - {{'show_kwic' | loc:$root.lang}} + {{'show_reading' | loc:$root.lang}} + {{'show_kwic' | loc:$root.lang}}
      @@ -257,12 +257,10 @@ angular.module("korpApp").component("kwic", { $ctrl._settings = settings $ctrl.toggleReading = () => { - $ctrl.readingMode = !$ctrl.readingMode + // Emit event; parent should update isReading $ctrl.contextChangeEvent() } - $ctrl.readingMode = $location.search().reading_mode - $ctrl.download = { options: [ { value: "", label: "download_kwic" }, @@ -655,7 +653,7 @@ angular.module("korpApp").component("kwic", { function selectNext() { let next - if (!$ctrl.readingMode) { + if (!$ctrl.isReading) { const i = getCurrentRow().index($element.find(".token_selected").get(0)) next = getCurrentRow().get(i + 1) if (next == null) { @@ -670,7 +668,7 @@ angular.module("korpApp").component("kwic", { function selectPrev() { let prev - if (!$ctrl.readingMode) { + if (!$ctrl.isReading) { const i = getCurrentRow().index($element.find(".token_selected").get(0)) if (i === 0) { return @@ -686,7 +684,7 @@ angular.module("korpApp").component("kwic", { function selectUp() { let prevMatch const current = selectionManager.selected - if (!$ctrl.readingMode) { + if (!$ctrl.isReading) { prevMatch = getWordAt( current.offset().left + current.width() / 2, current.closest("tr").prevAll(":not(.corpus_info)").first() @@ -715,7 +713,7 @@ angular.module("korpApp").component("kwic", { function selectDown() { let nextMatch const current = selectionManager.selected - if (!$ctrl.readingMode) { + if (!$ctrl.isReading) { nextMatch = getWordAt( current.offset().left + current.width() / 2, current.closest("tr").nextAll(":not(.corpus_info)").first() diff --git a/app/scripts/controllers/kwic_controller.ts b/app/scripts/controllers/kwic_controller.ts index 48f7df1ef..0e4d37adb 100644 --- a/app/scripts/controllers/kwic_controller.ts +++ b/app/scripts/controllers/kwic_controller.ts @@ -140,11 +140,7 @@ export class KwicCtrl implements IController { s.reading_mode = $location.search().reading_mode s.toggleReading = function () { s.reading_mode = !s.reading_mode - if (s.reading_mode) { - $location.search("reading_mode", true) - } else { - $location.search("reading_mode", undefined) - } + $location.search("reading_mode", s.reading_mode || undefined) s.readingChange() } From 8562ea93a70dd9d6a17eb848e8f1ccbabde009e3 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 4 Sep 2024 13:15:03 +0200 Subject: [PATCH 73/99] refactor: loglike-meter component --- .../components/dynamic_tabs/compare-tabs.js | 16 ++++-- app/scripts/components/loglike-meter.ts | 48 +++++++++++++++++ .../controllers/comparison_controller.ts | 4 +- app/scripts/directives/meter.ts | 53 ------------------- app/styles/styles.scss | 19 ++----- 5 files changed, 66 insertions(+), 74 deletions(-) create mode 100644 app/scripts/components/loglike-meter.ts delete mode 100644 app/scripts/directives/meter.ts diff --git a/app/scripts/components/dynamic_tabs/compare-tabs.js b/app/scripts/components/dynamic_tabs/compare-tabs.js index d80f359a0..50371a87c 100644 --- a/app/scripts/components/dynamic_tabs/compare-tabs.js +++ b/app/scripts/components/dynamic_tabs/compare-tabs.js @@ -2,8 +2,8 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" +import "@/components/loglike-meter" import "@/controllers/comparison_controller" -import "@/directives/meter" import "@/directives/tab-spinner" angular.module("korpApp").directive("compareTabs", () => ({ @@ -22,7 +22,12 @@ angular.module("korpApp").directive("compareTabs", () => ({

      {{'compare_distinctive' | loc:$root.lang}} {{cmp1.label}}

      • -
        +
      @@ -30,7 +35,12 @@ angular.module("korpApp").directive("compareTabs", () => ({

      {{'compare_distinctive' | loc:$root.lang}} {{cmp2.label}}

      • -
        +
      diff --git a/app/scripts/components/loglike-meter.ts b/app/scripts/components/loglike-meter.ts new file mode 100644 index 000000000..e79211e22 --- /dev/null +++ b/app/scripts/components/loglike-meter.ts @@ -0,0 +1,48 @@ +/** @format */ +import _ from "lodash" +import angular, { IController, IScope } from "angular" +import { loc } from "@/i18n" +import { html } from "@/util" +import { CompareItem } from "@/services/backend" + +type MeterController = IController & { + item: CompareItem + max: number + stringify: (x: string) => string +} + +type MeterScope = IScope & { + display: string + abs: number + tooltipHtml: string + barWidth: string +} + +angular.module("korpApp").component("loglikeMeter", { + template: html`
      +
      {{display}}
      +
      {{abs}}
      +
      `, + bindings: { + item: "<", + max: "<", + stringify: "<", + }, + controller: [ + "$scope", + function ($scope: MeterScope) { + const $ctrl = this as MeterController + const stringify = (token: string): string => (token === "|" || token === "" ? "–" : $ctrl.stringify(token)) + + $ctrl.$onInit = () => { + $scope.display = $ctrl.item.tokenLists.map((tokens) => tokens.map(stringify).join(" ")).join(";") + $scope.abs = $ctrl.item.abs + $scope.tooltipHtml = html`${loc("statstable_absfreq")}: ${$ctrl.item.abs}
      + loglike: ${Math.abs($ctrl.item.loglike)}` + + const ratio = Math.abs($ctrl.item.loglike / $ctrl.max) + $scope.barWidth = `${ratio * 100}%` + } + }, + ], +}) diff --git a/app/scripts/controllers/comparison_controller.ts b/app/scripts/controllers/comparison_controller.ts index 75641c5dd..c4b762519 100644 --- a/app/scripts/controllers/comparison_controller.ts +++ b/app/scripts/controllers/comparison_controller.ts @@ -19,7 +19,7 @@ type CompareCtrlScope = IScope & { resultOrder: (item: CompareItem) => number reduce: string[] rowClick: (row: CompareItem, cmp_index: number) => void - stringify: ((x: string) => string)[] + stringify: (x: string) => string tables: CompareTables newDynamicTab: any // TODO Defined in tabHash (services.js) closeDynamicTab: any // TODO Defined in tabHash (services.js) @@ -65,7 +65,7 @@ angular.module("korpApp").directive("compareCtrl", () => ({ locAttribute(attributes[reduceAttrName].translation, value, $rootScope.lang) } } - s.stringify = [stringify] + s.stringify = stringify s.max = max diff --git a/app/scripts/directives/meter.ts b/app/scripts/directives/meter.ts deleted file mode 100644 index aee981f3d..000000000 --- a/app/scripts/directives/meter.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** @format */ -import _ from "lodash" -import angular, { IScope } from "angular" -import { loc } from "@/i18n" -import { html } from "@/util" -import { CompareItem } from "@/services/backend" - -type MeterScope = IScope & { - meter: CompareItem - max: number - stringify: ((x: string) => string)[] - displayWd: string - loglike: number - tooltipHTML: string -} - -angular.module("korpApp").directive("meter", () => ({ - template: html`
      -
      -
      {{meter.abs}}
      -
      `, - replace: true, - scope: { - meter: "=", - max: "=", - stringify: "=", - }, - link(scope: MeterScope, elem, attr) { - const zipped = _.zip(scope.meter.tokenLists, scope.stringify) - scope.displayWd = _.map(zipped, function (...args) { - const [tokens, stringify] = args[0] - return _.map(tokens, function (token) { - if (token === "|" || token === "") { - return "—" - } else { - return stringify(token) - } - }).join(" ") - }).join(";") - - scope.loglike = Math.abs(scope.meter.loglike) - - scope.tooltipHTML = html`${loc("statstable_absfreq")}: ${scope.meter.abs} -
      - loglike: ${scope.loglike}` - - const w = 394 - const part = scope.loglike / Math.abs(scope.max) - - const bkg = elem.find(".background") - bkg.width(Math.round(part * w)) - }, -})) diff --git a/app/styles/styles.scss b/app/styles/styles.scss index 34357732d..15b6a3ccb 100644 --- a/app/styles/styles.scss +++ b/app/styles/styles.scss @@ -1787,23 +1787,17 @@ line.tick { vertical-align: top; } .meter { + display: block; cursor: pointer; position: relative; border: 1px solid rgba($primaryColor, 0); transition: all 200ms ease; + white-space : nowrap; &:hover { color : navy; border: 1px solid rgba(darken($primaryColor, 10%), 1); } } - .badge { - position: absolute; - right : 0; - top : 4px; - } - .background { - padding: 5px 0; - } .column_1 ul { background-color: lighten($primaryColor, 4%); border : 1px solid $primaryColor; @@ -1823,9 +1817,8 @@ line.tick { margin-right : 25px; width : 400px; font-size : 1.1em; - li { + li:not(:last-child) { margin-bottom : 0.5em; - white-space : nowrap; } } } @@ -1870,12 +1863,6 @@ line.tick { display : none; } - -.badge { - padding-right: 6px; - padding-left: 6px; -} - .tab-content { overflow: hidden; } From 22297f5a2fc260549358f62894705570c08adc9d Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 4 Sep 2024 13:33:56 +0200 Subject: [PATCH 74/99] refactor: search-submit component --- app/scripts/components/advanced-search.js | 2 +- .../components/extended/extended-standard.js | 2 +- app/scripts/components/search-submit.ts | 132 ++++++++++++++++++ app/scripts/components/simple-search.js | 6 +- app/scripts/directives/search-submit.ts | 124 ---------------- 5 files changed, 137 insertions(+), 129 deletions(-) create mode 100644 app/scripts/components/search-submit.ts delete mode 100644 app/scripts/directives/search-submit.ts diff --git a/app/scripts/components/advanced-search.js b/app/scripts/components/advanced-search.js index fc8d6c2e5..fce930bda 100644 --- a/app/scripts/components/advanced-search.js +++ b/app/scripts/components/advanced-search.js @@ -2,7 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/services/compare-searches" -import "@/directives/search-submit" +import "@/components/search-submit" angular.module("korpApp").component("advancedSearch", { template: html`
      diff --git a/app/scripts/components/extended/extended-standard.js b/app/scripts/components/extended/extended-standard.js index 95137a46e..f049683a0 100644 --- a/app/scripts/components/extended/extended-standard.js +++ b/app/scripts/components/extended/extended-standard.js @@ -7,7 +7,7 @@ import { expandOperators, mergeCqpExprs, parse, stringify, supportsInOrder } fro import { html } from "@/util" import "@/services/compare-searches" import "@/components/extended/tokens" -import "@/directives/search-submit" +import "@/components/search-submit" angular.module("korpApp").component("extendedStandard", { template: html` diff --git a/app/scripts/components/search-submit.ts b/app/scripts/components/search-submit.ts new file mode 100644 index 000000000..e2b21ab86 --- /dev/null +++ b/app/scripts/components/search-submit.ts @@ -0,0 +1,132 @@ +/** @format */ +import _ from "lodash" +import angular, { IController, IRootElementService, IScope } from "angular" +import { html } from "@/util" + +type SearchSubmitController = IController & { + onSearch: () => void + onSearchSave: (params: { name: string }) => void + disabled: boolean + pos: string +} + +type SearchSubmitScope = IScope & { + disabled: boolean + pos: string + name: string + isPopoverVisible: boolean + togglePopover: (event: Event) => void + popHide: () => void + popShow: () => void + onSubmit: () => void + onSendClick: (event: Event) => void + onPopoverClick: (event: Event) => void +} + +angular.module("korpApp").component("searchSubmit", { + template: html`
      +
      + + +
      +
      +
      +

      {{'compare_save_header' | loc:$root.lang}}

      +
      +
      + +
      +
      + +
      + +
      +
      `, + bindings: { + onSearch: "&", + onSearchSave: "&", + disabled: "<", + pos: "@", + }, + controller: [ + "$element", + "$rootElement", + "$scope", + function ($element: IRootElementService, $rootElement: IRootElementService, $scope: SearchSubmitScope) { + const $ctrl = this as SearchSubmitController + + $ctrl.$onInit = () => { + $scope.disabled = angular.isDefined($ctrl.disabled) ? $ctrl.disabled : false + $scope.pos = $ctrl.pos || "bottom" + } + + const popover = $element.find(".popover") + const opposites = { + bottom: "top", + top: "bottom", + right: "left", + left: "right", + } + + $scope.togglePopover = function (event) { + if ($scope.isPopoverVisible) { + $scope.popHide() + } else { + $scope.popShow() + } + event.preventDefault() + event.stopPropagation() + } + + $scope.onPopoverClick = function (event) { + if (event.target !== popover.find(".btn")[0]) { + event.preventDefault() + event.stopPropagation() + } + } + $scope.isPopoverVisible = false + + const onEscape = (event: JQueryEventObject) => { + if (event.key != "Escape") { + $scope.popHide() + return false + } + } + + $scope.popShow = function () { + $scope.isPopoverVisible = true + const horizontal = ["top", "bottom"].includes($scope.pos) + const my = horizontal ? `center ${opposites[$scope.pos]}` : `${opposites[$scope.pos]} center` + const at = horizontal ? `center ${$scope.pos}+10` : `${$scope.pos}+10 center` + popover + .fadeIn("fast") + .focus() + .position({ my, at, of: $element.find(".opener") }) + + $rootElement.on("keydown", onEscape) + $rootElement.on("click", $scope.popHide) + } + + $scope.popHide = function () { + $scope.isPopoverVisible = false + popover.fadeOut("fast") + $rootElement.off("keydown", onEscape) + $rootElement.off("click", $scope.popHide) + } + + $scope.onSubmit = function () { + $scope.popHide() + $ctrl.onSearchSave({ name: $scope.name }) + } + + $scope.onSendClick = () => $ctrl.onSearch() + }, + ], +}) diff --git a/app/scripts/components/simple-search.js b/app/scripts/components/simple-search.js index 8a75bc630..bcef4971e 100644 --- a/app/scripts/components/simple-search.js +++ b/app/scripts/components/simple-search.js @@ -9,14 +9,14 @@ import "@/services/compare-searches" import "@/services/lexicons" import "@/services/searches" import "@/components/autoc" -import "@/directives/search-submit" +import "@/components/search-submit" angular.module("korpApp").component("simpleSearch", { template: html`
      -
      +
      - +
      - -
      -
      -
      -

      {{'compare_save_header' | loc:$root.lang}}

      -
      -
      - -
      -
      - -
      - -
      -
      `, - restrict: "E", - replace: true, - scope: { - onSearch: "&", - onSearchSave: "&", - disabled: "<", - }, - link(scope: SearchSubmitScope, elem, attr) { - const s = scope - - s.disabled = angular.isDefined(s.disabled) ? s.disabled : false - - s.pos = attr.pos || "bottom" - s.togglePopover = function (event) { - if (s.isPopoverVisible) { - s.popHide() - } else { - s.popShow() - } - event.preventDefault() - event.stopPropagation() - } - - const popover = elem.find(".popover") - s.onPopoverClick = function (event) { - if (event.target !== popover.find(".btn")[0]) { - event.preventDefault() - event.stopPropagation() - } - } - s.isPopoverVisible = false - const trans = { - bottom: "top", - top: "bottom", - right: "left", - left: "right", - } - const horizontal = ["top", "bottom"].includes(s.pos) - const my = horizontal ? `center ${trans[s.pos]}` : `${trans[s.pos]} center` - const at = horizontal ? `center ${s.pos}+10` : `${s.pos}+10 center` - - const onEscape = function (event) { - if (event.which === 27) { - // escape - s.popHide() - return false - } - } - - s.popShow = function () { - s.isPopoverVisible = true - popover - .fadeIn("fast") - .focus() - .position({ my, at, of: elem.find(".opener") }) - - $rootElement.on("keydown", onEscape) - $rootElement.on("click", s.popHide) - } - - s.popHide = function () { - s.isPopoverVisible = false - popover.fadeOut("fast") - $rootElement.off("keydown", onEscape) - $rootElement.off("click", s.popHide) - } - - s.onSubmit = function () { - s.popHide() - s.onSearchSave({ name: s.name }) - } - - s.onSendClick = () => s.onSearch() - }, - }), -]) From f951e1869592823b29e340f39a12d207c446de40 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 5 Sep 2024 15:30:35 +0200 Subject: [PATCH 75/99] docs: "word research platform" --- README.md | 5 ++--- doc/user_manual_eng.md | 2 +- doc/user_manual_swe.md | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7385dac2e..3e8af073c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -This repo contains the frontend for [Korp](https://spraakbanken.gu.se/korp), -a tool using the IMS Open Corpus Workbench (CWB). Korp is a great -tool for searching and visualising natural language corpus data. +This repo contains the frontend for [Korp](https://spraakbanken.gu.se/korp), Språkbanken's word research platform using the IMS Open Corpus Workbench (CWB). +Korp is a great tool for searching and visualising natural language corpus data. Korp is mainly developed by [Språkbanken](https://spraakbanken.gu.se) at the University of Gothenburg, Sweden. Contributions are also made from other diff --git a/doc/user_manual_eng.md b/doc/user_manual_eng.md index 9d2ef0f64..39ecf3b3a 100644 --- a/doc/user_manual_eng.md +++ b/doc/user_manual_eng.md @@ -1,6 +1,6 @@ ## Introduction -This is a user manual for the corpus search tool [Korp](http://spraakbanken.gu.se/korp/). Before you continue reading +This is a user manual for the word research platform [Korp](http://spraakbanken.gu.se/korp/). Before you continue reading we recommend that you visit the Korp page and do some test searches in order to get a rough picture of how the interface works. diff --git a/doc/user_manual_swe.md b/doc/user_manual_swe.md index e7c3d44e4..1b1419144 100644 --- a/doc/user_manual_swe.md +++ b/doc/user_manual_swe.md @@ -1,6 +1,6 @@ ## Introduktion -Detta är en användarhandledning för korpussökningsverktyget [Korp](https://spraakbanken.gu.se/korp/). Prova gärna att +Detta är en användarhandledning för ordforskningsplattformen [Korp](https://spraakbanken.gu.se/korp/). Prova gärna att besöka sidan och göra ett par testsökningar innan du läser vidare, så att du får en bild av hur gränssnittet ser ut. Det finns även ett par övningsuppgifter som du kan ladda hem From 9368642d778b8a0fa5d8a40cc650808a25d5901c Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 5 Sep 2024 15:37:01 +0200 Subject: [PATCH 76/99] docs: more "word research platform" --- app/translations/locale-eng.json | 2 +- app/translations/locale-swe.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/translations/locale-eng.json b/app/translations/locale-eng.json index 3bb388238..abea4074d 100644 --- a/app/translations/locale-eng.json +++ b/app/translations/locale-eng.json @@ -38,7 +38,7 @@ "negate_explanation": "Negates the search expression, giving only sentences that do not match.", "about": "About Korp / Cite Korp", "about_short": "About Korp", - "about_body": "Korp is the concordance search tool of Språkbanken (The Swedish Language Bank). For more information, comments or suggestions, contact us.", + "about_body": "Korp is Språkbanken's word research platform. For more information, comments or suggestions, contact us.", "about_cite_header": "Cite Korp", "about_cite": "Lars Borin, Markus Forsberg and Johan Roxendal. 2012. Korp – the corpus infrastructure of Språkbanken. Proceedings of LREC 2012. Istanbul: ELRA, pages 474–478.
      BibTeX", "about_man": "Project Managers", diff --git a/app/translations/locale-swe.json b/app/translations/locale-swe.json index 12af4da5a..9ae27bf3e 100644 --- a/app/translations/locale-swe.json +++ b/app/translations/locale-swe.json @@ -38,7 +38,7 @@ "negate_explanation": "Ger alla meningar som inte matchar sökuttrycket.", "about": "Om Korp / Referera till Korp", "about_short": "Om Korp", - "about_body": "Korp är Språkbankens konkordansverktyg. För mer information, kommentarer eller förslag, kontakta Språkbanken.", + "about_body": "Korp är Språkbankens ordforskningsplattform. För mer information, kommentarer eller förslag, kontakta Språkbanken.", "about_cite_header": "Referera till Korp", "about_cite": "Lars Borin, Markus Forsberg and Johan Roxendal. 2012. Korp – the corpus infrastructure of Språkbanken. Proceedings of LREC 2012. Istanbul: ELRA, pages 474–478.
      BibTeX", "about_man": "Projektledning", From 6df4fc6e9b76373203c2b1583ed4d9e9e4155118 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 5 Sep 2024 15:58:58 +0200 Subject: [PATCH 77/99] docs: link Staffan --- app/markup/about.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/markup/about.html b/app/markup/about.html index f1fde115d..be4454eb0 100644 --- a/app/markup/about.html +++ b/app/markup/about.html @@ -48,7 +48,9 @@

      Korp version 9.6.0

    • Roland Schäfer
    • - {{'about_logo' | loc:lang}} Staffan Melin. + + {{'about_logo' | loc:lang}} + Staffan Melin.
      From d926054548490a426777258b8ee4b7cd4efbce25 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 9 Sep 2024 15:56:05 +0200 Subject: [PATCH 78/99] refactor: not array for utils.setupHash config arg --- CHANGELOG.md | 1 + app/scripts/components/header.js | 26 +++++----- app/scripts/controllers/kwic_controller.ts | 2 +- app/scripts/directives/tab-hash.ts | 18 +++---- app/scripts/services/utils.ts | 58 ++++++++++------------ 5 files changed, 49 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59352f57b..ecc6c6c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Removed globals `CSV` and `moment`, import the libraries instead - Converted the "radioList" JQuery widget to a component - Using Karp 7 backend instead of Karp 4 [#388](https://github.com/spraakbanken/korp-frontend/pull/388) +- For the `utils.setupHash()` function, the `config` argument is no longer an array. To sync multiple parameters, call it once for each. ### Fixed diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 0a6749848..241e9e0fc 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -183,22 +183,20 @@ angular.module("korpApp").component("header", { $rootScope.show_modal = false let modal = null - utils.setupHash($rootScope, [ - { - key: "display", - scope_name: "show_modal", - post_change(val) { - if (val) { - showAbout() - } else { - if (modal != null) { - modal.close() - } - modal = null + utils.setupHash($rootScope, { + key: "display", + scope_name: "show_modal", + post_change(val) { + if (val) { + showAbout() + } else { + if (modal != null) { + modal.close() } - }, + modal = null + } }, - ]) + }) const closeModals = function () { $rootScope.show_modal = false diff --git a/app/scripts/controllers/kwic_controller.ts b/app/scripts/controllers/kwic_controller.ts index 0e4d37adb..71c4bc50c 100644 --- a/app/scripts/controllers/kwic_controller.ts +++ b/app/scripts/controllers/kwic_controller.ts @@ -64,7 +64,7 @@ export class KwicCtrl implements IController { setupHash() { // Sync url param for page number - return this.utils.setupHash(this.scope, [{ key: "page", val_in: Number }]) + return this.utils.setupHash(this.scope, { key: "page", val_in: Number }) } initPage() { diff --git a/app/scripts/directives/tab-hash.ts b/app/scripts/directives/tab-hash.ts index 0f3d9952e..11fa58737 100644 --- a/app/scripts/directives/tab-hash.ts +++ b/app/scripts/directives/tab-hash.ts @@ -24,17 +24,15 @@ angular.module("korpApp").directive("tabHash", [ const contentScope = elem.find(".tab-content").scope() as any const watchHash = () => - utils.setupHash(s, [ - { - expr: "activeTab", - val_in(val) { - s.setSelected(Number(val)) - return s.activeTab - }, - key: attr.tabHash, - default: "0", + utils.setupHash(s, { + expr: "activeTab", + val_in(val) { + s.setSelected(Number(val)) + return s.activeTab }, - ]) + key: attr.tabHash, + default: "0", + }) s.setSelected = function (index, ignoreCheck) { if (!ignoreCheck && !(index in s.fixedTabs)) { diff --git a/app/scripts/services/utils.ts b/app/scripts/services/utils.ts index f4882b2d1..7070b9312 100644 --- a/app/scripts/services/utils.ts +++ b/app/scripts/services/utils.ts @@ -4,7 +4,7 @@ import angular, { IScope } from "angular" export type UtilsService = { /** Set up sync between a url param and a scope variable. */ - setupHash: (scope: IScope, config: SetupHashConfigItem[]) => void + setupHash: (scope: IScope, config: SetupHashConfigItem) => void } type SetupHashConfigItem = { @@ -31,43 +31,39 @@ angular.module("korpApp").factory("utils", [ ($location: LocationService): UtilsService => ({ setupHash(scope, config) { // Sync from url to scope - const onWatch = () => - config.forEach((obj) => { - let val = $location.search()[obj.key] - if (val == null) { - if ("default" in obj) { - val = obj.default - } else { - if (obj.post_change) obj.post_change(val) - return - } - } - - val = obj.val_in ? obj.val_in(val) : val - - if ("scope_name" in obj) { - scope[obj.scope_name] = val - } else if ("scope_func" in obj) { - scope[obj.scope_func](val) + const onWatch = () => { + let val = $location.search()[config.key] + if (val == null) { + if ("default" in config) { + val = config.default } else { - scope[obj.key] = val + if (config.post_change) config.post_change(val) + return } - }) + } + val = config.val_in ? config.val_in(val) : val + + if ("scope_name" in config) { + scope[config.scope_name] = val + } else if ("scope_func" in config) { + scope[config.scope_func](val) + } else { + scope[config.key] = val + } + } onWatch() scope.$watch(() => $location.search(), onWatch) // Sync from scope to url - config.forEach((obj) => - scope.$watch(obj.expr || obj.scope_name || obj.key, (val: any) => { - val = obj.val_out ? obj.val_out(val) : val - if (val === obj.default) { - val = null - } - $location.search(obj.key, val || null) - if (obj.post_change) obj.post_change(val) - }) - ) + scope.$watch(config.expr || config.scope_name || config.key, (val: any) => { + val = config.val_out ? config.val_out(val) : val + if (val === config.default) { + val = null + } + $location.search(config.key, val || null) + if (config.post_change) config.post_change(val) + }) }, }), ]) From 0c6e186b67bb39efc0db54b405f317f287c074fe Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 10 Sep 2024 10:46:06 +0200 Subject: [PATCH 79/99] refactor: tab-hash component Dynamic tabs broken --- app/scripts/components/results.js | 197 ++++++++++++++------------- app/scripts/components/searchtabs.js | 50 +++---- app/scripts/components/tab-hash.ts | 93 +++++++++++++ app/scripts/directives/tab-hash.ts | 77 ----------- app/scripts/services/utils.ts | 4 +- app/scripts/urlparams.ts | 2 + 6 files changed, 224 insertions(+), 199 deletions(-) create mode 100644 app/scripts/components/tab-hash.ts delete mode 100644 app/scripts/directives/tab-hash.ts diff --git a/app/scripts/components/results.js b/app/scripts/components/results.js index 9ef766a7f..b0b9f2659 100644 --- a/app/scripts/components/results.js +++ b/app/scripts/components/results.js @@ -11,8 +11,8 @@ import "@/components/korp-error" import "@/components/kwic" import "@/components/statistics" import "@/components/sidebar" +import "@/components/tab-hash" import "@/components/word-picture" -import "@/directives/tab-hash" import "@/directives/tab-preloader" angular.module("korpApp").component("results", { @@ -21,105 +21,110 @@ angular.module("korpApp").component("results", {
      - - - KWIC - -
      + + + + KWIC + +
      + + +
      +
      + + {{'statistics' | loc:$root.lang}} + + - -
      -
      - - {{'statistics' | loc:$root.lang}} - - - - - - - - {{'word_picture' | loc:$root.lang}} - - -
      - -
      - -
      - - - - - -
      + no-hits="no_hits" + prev-params="proxy.prevParams" + search-params="searchParams" + show-statistics="showStatistics" + > + + + + {{'word_picture' | loc:$root.lang}} + + +
      + +
      + +
      + + + + + + + diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index 1d2126c7b..2c72543c5 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -10,37 +10,39 @@ import "@/components/extended/extended-standard" import "@/components/extended/extended-parallel" import "@/components/advanced-search" import "@/components/compare-search" +import "@/components/tab-hash" import "@/directives/click-cover" import "@/directives/reduce-select" -import "@/directives/tab-hash" angular.module("korpApp").component("searchtabs", { template: html`
      - - - - - -
      - - + + + + + + +
      + + +
      +
      + + + + + + {{'compare' | loc:$root.lang}} + {{$ctrl.savedSearches.length}} + + + +
      +
      - - - - - - - {{'compare' | loc:$root.lang}} - {{$ctrl.savedSearches.length}} - - - -
      - -
      -
      + +
      + maxTab: number + setSelected: (index: number, ignoreCheck?: boolean) => void + newDynamicTab: () => void + closeDynamicTab: () => void +} + +/** Surround a `` element with this to sync selected tab number to url parameter `key`. */ +angular.module("korpApp").component("tabHash", { + template: html`
      `, + transclude: true, + bindings: { + key: "@", + }, + controller: [ + "utils", + "$element", + "$scope", + "$timeout", + function (utils: UtilsService, $element: IRootElementService, $scope: TabHashScope, $timeout: ITimeoutService) { + const $ctrl = this as TabHashController + + let tabsetScope + let contentScope + + $ctrl.$onInit = () => { + // Timeout needed to find elements created by uib-tabset + $timeout(function () { + tabsetScope = $element.find(".tabbable").scope() as any + contentScope = $element.find(".tab-content").scope() as any + + $scope.fixedTabs = {} + $scope.maxTab = -1 + for (let tab of contentScope.tabset.tabs) { + $scope.fixedTabs[tab.index] = tab + if (tab.index > $scope.maxTab) { + $scope.maxTab = tab.index + } + } + watchHash() + }, 0) + } + + const watchHash = () => + utils.setupHash($scope, { + expr: () => tabsetScope.activeTab, + val_in(val) { + $scope.setSelected(Number(val)) + return tabsetScope.activeTab + }, + key: $ctrl.key, + default: "0", + }) + + $scope.setSelected = function (index, ignoreCheck) { + if (!ignoreCheck && !(index in $scope.fixedTabs)) { + index = $scope.maxTab + } + tabsetScope.activeTab = index + } + + $scope.newDynamicTab = function () { + $timeout(function () { + $scope.setSelected($scope.maxTab + 1, true) + $scope.maxTab += 1 + }, 0) + } + + $scope.closeDynamicTab = function () { + $timeout(function () { + $scope.maxTab = -1 + for (let tab of contentScope.tabset.tabs) { + if (tab.index > $scope.maxTab) { + $scope.maxTab = tab.index + } + } + }, 0) + } + }, + ], +}) diff --git a/app/scripts/directives/tab-hash.ts b/app/scripts/directives/tab-hash.ts deleted file mode 100644 index 11fa58737..000000000 --- a/app/scripts/directives/tab-hash.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** @format */ -import _ from "lodash" -import angular, { IScope, ITimeoutService } from "angular" -import { UtilsService } from "@/services/utils" -import { LocationService } from "@/urlparams" -import "@/services/utils" - -type TabHashScope = IScope & { - activeTab: number - fixedTabs: Record - maxTab: number - setSelected: (index: number, ignoreCheck?: boolean) => void - newDynamicTab: () => void - closeDynamicTab: () => void -} - -angular.module("korpApp").directive("tabHash", [ - "utils", - "$location", - "$timeout", - (utils: UtilsService, $location: LocationService, $timeout: ITimeoutService) => ({ - link(scope, elem, attr) { - const s = scope as TabHashScope - const contentScope = elem.find(".tab-content").scope() as any - - const watchHash = () => - utils.setupHash(s, { - expr: "activeTab", - val_in(val) { - s.setSelected(Number(val)) - return s.activeTab - }, - key: attr.tabHash, - default: "0", - }) - - s.setSelected = function (index, ignoreCheck) { - if (!ignoreCheck && !(index in s.fixedTabs)) { - index = s.maxTab - } - s.activeTab = index - } - - const initTab = parseInt($location.search()[attr.tabHash]) || 0 - $timeout(function () { - s.fixedTabs = {} - s.maxTab = -1 - for (let tab of contentScope.tabset.tabs) { - s.fixedTabs[tab.index] = tab - if (tab.index > s.maxTab) { - s.maxTab = tab.index - } - } - s.setSelected(initTab) - watchHash() - }, 0) - - s.newDynamicTab = function () { - $timeout(function () { - s.setSelected(s.maxTab + 1, true) - s.maxTab += 1 - }, 0) - } - - s.closeDynamicTab = function () { - $timeout(function () { - s.maxTab = -1 - for (let tab of contentScope.tabset.tabs) { - if (tab.index > s.maxTab) { - s.maxTab = tab.index - } - } - }, 0) - } - }, - }), -]) diff --git a/app/scripts/services/utils.ts b/app/scripts/services/utils.ts index 7070b9312..b0d3a644b 100644 --- a/app/scripts/services/utils.ts +++ b/app/scripts/services/utils.ts @@ -15,7 +15,7 @@ type SetupHashConfigItem /** A function on the scope to pass value to, instead of setting `scope_name` */ scope_func?: string /** Expression to watch for changes; defaults to `scope_name` */ - expr?: string + expr?: string | (() => HashParams[K]) /** Default value of the scope variable, corresponding to the url param being empty */ default?: HashParams[K] /** Runs when the value is changed in scope or url */ @@ -56,7 +56,7 @@ angular.module("korpApp").factory("utils", [ scope.$watch(() => $location.search(), onWatch) // Sync from scope to url - scope.$watch(config.expr || config.scope_name || config.key, (val: any) => { + scope.$watch((config.expr as any) || config.scope_name || config.key, (val: any) => { val = config.val_out ? config.val_out(val) : val if (val === config.default) { val = null diff --git a/app/scripts/urlparams.ts b/app/scripts/urlparams.ts index 285b76429..032864124 100644 --- a/app/scripts/urlparams.ts +++ b/app/scripts/urlparams.ts @@ -37,6 +37,8 @@ export type HashParams = { random_seed?: `${number}` /** Whether the reading mode is enabled */ reading_mode?: boolean + /** Current tab of results */ + result_tab?: `${number}` /** * Search query for Simple or Advanced search: `|` * where `mode` can be: From c4935a915847e827ca93ae27f2323ebd2fae5036 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 10:35:38 +0200 Subject: [PATCH 80/99] refactor: absorb geokorp --- app/index.ts | 2 +- app/scripts/geo/geokorp-templates.js | 14 + app/scripts/geo/geokorp.css | 133 +++++ app/scripts/geo/geokorp.js | 750 +++++++++++++++++++++++++++ app/scripts/korp.module.ts | 4 +- package.json | 1 - 6 files changed, 900 insertions(+), 4 deletions(-) create mode 100644 app/scripts/geo/geokorp-templates.js create mode 100644 app/scripts/geo/geokorp.css create mode 100644 app/scripts/geo/geokorp.js diff --git a/app/index.ts b/app/index.ts index 94334eaab..e02a8bdce 100644 --- a/app/index.ts +++ b/app/index.ts @@ -27,7 +27,7 @@ require("rickshaw/rickshaw.css") require("leaflet/dist/leaflet.css") require("leaflet.markercluster/dist/MarkerCluster.css") -require("geokorp/dist/styles/geokorp.css") +require("./scripts/geo/geokorp.css") require("components-jqueryui/themes/smoothness/jquery-ui.min.css") require("./styles/_bootstrap-custom.scss") diff --git a/app/scripts/geo/geokorp-templates.js b/app/scripts/geo/geokorp-templates.js new file mode 100644 index 000000000..3bdb1ad99 --- /dev/null +++ b/app/scripts/geo/geokorp-templates.js @@ -0,0 +1,14 @@ +angular.module("sbMapTemplate", ["template/sb_map.html"]); + +angular.module("template/sb_map.html", []).run(["$templateCache", function($templateCache) { + $templateCache.put("template/sb_map.html", + "
      \n" + + "
      \n" + + "
      \n" + + "
      \n" + + " \n" + + "
      \n" + + "\n" + + "\n" + + ""); +}]); diff --git a/app/scripts/geo/geokorp.css b/app/scripts/geo/geokorp.css new file mode 100644 index 000000000..a326d6a50 --- /dev/null +++ b/app/scripts/geo/geokorp.css @@ -0,0 +1,133 @@ +sb-map { + padding: 8px; +} + +sb-map .leaflet-div-icon { + background: none; + border: none; +} + +sb-map .cluster-geokorp-marker-group { + position: absolute; + bottom: 0; + width: 40px; +} + +sb-map .cluster-geokorp-marker { + width: 10px; + border-radius: 1px; + display: inline-block; +} + +sb-map .marker-top .geokorp-multi-marker { + vertical-align: top; +} + +sb-map .marker-middle .geokorp-multi-marker { + vertical-align: middle; +} + +sb-map .marker-bottom .geokorp-multi-marker { + vertical-align: bottom; +} + +sb-map .geokorp-multi-marker { + opacity: 0.85; + display: inline-block; +} + +sb-map .geokorp-marker { + opacity: 0.93; +} + +sb-map .cluster-text { + font-weight: bold; +} + +sb-map .cluster-icon { + border-radius: 15px; + width: 30px !important; + height: 30px !important; + z-index: 400; + position: absolute; + padding-top: 5px; + padding-left: 1px; +} + +sb-map .leaflet-marker-icon.leaflet-div-icon.leaflet-clickable { + border: none; + background-color: transparent; +} + +sb-map .leaflet-popup-content-wrapper { + border-radius: 4px; + background-color: #f6f6f6; + background-image: linear-gradient(to bottom, #fff, #e6e6e6); + background-repeat: repeat-x; +} + +sb-map .leaflet-popup-tip { + background-color: #e6e6e6; +} + +sb-map .leaflet-bar a:link, sb-map .leaflet-bar a:visited { + color: black; +} + +sb-map .swatch { + width: 10px; + height: 10px; + display: inline-block; + margin-right: 5px; +} + +sb-map .marker-cluster-small { + background-color: rgba(136, 220, 168, 0.6); +} + +sb-map .marker-cluster-small div { + background-color: rgba(136, 220, 168, 0.6); +} + +sb-map .map { + border: 1px solid black; + position: relative; + height: 522px; +} + +sb-map .map-container { + height: 520px; +} + +sb-map .map-outer-container { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +sb-map .hover-info-container { + margin: 5px; + border-radius: 5px; + width: 200px; + height: 500px; + overflow: auto; + position: absolute; + top: 0; + right: 0; + z-index: 800; + opacity: 1; + transition: opacity 500ms; +} + +sb-map .hover-info { + z-index: 10; + background-color: #f6f6f6; + background-image: linear-gradient(to bottom, #fff, #e6e6e6); + background-repeat: repeat-x; + padding: 10px; + margin: 10px; + border-radius: 4px; + cursor: pointer; +} diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js new file mode 100644 index 000000000..93fecd25c --- /dev/null +++ b/app/scripts/geo/geokorp.js @@ -0,0 +1,750 @@ +(function() { + 'use strict'; + var c; + + c = console; + + angular.module('sbMap', ['sbMapTemplate']).filter("trust", function($sce) { + return function(input) { + return $sce.trustAsHtml(input); + }; + }).directive('sbMap', [ + '$compile', + '$timeout', + '$rootScope', + function($compile, + $timeout, + $rootScope) { + var link; + link = function(scope, + element, + attrs) { + var createCircleMarker, + createClusterIcon, + createFeatureLayer, + createMarkerCluster, + createMarkerIcon, + createMultiMarkerIcon, + map, + mergeMarkers, + mouseOut, + mouseOver, + openStreetMap, + shouldZooomToBounds, + updateMarkerSizes, + updateMarkers; + scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true; + scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false; + scope.showMap = false; + scope.hoverTemplate = `
      +
      +
      +
      {{ 'map_name' | loc }}: {{point.name}}
      +
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      +
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      +
      `; + map = angular.element(element.find(".map-container")); + scope.map = L.map(map[0], + { + minZoom: 1, + maxZoom: 13 + }).setView([51.505, + -0.09], + 13); + scope.selectedMarkers = []; + scope.$on("update_map", + function() { + return $timeout((function() { + return scope.map.invalidateSize(); + }), + 0); + }); + createCircleMarker = function(color, + diameter, + borderRadius) { + return L.divIcon({ + html: '
      ', + iconSize: new L.Point(diameter, + diameter) + }); + }; + createMarkerIcon = function(color, + cluster) { + // TODO use scope.maxRel, but scope.maxRel is not set when markers are created + // diameter = ((relSize / scope.maxRel) * 45) + 5 + return createCircleMarker(color, + 10, + cluster ? 1 : 5); + }; + createMultiMarkerIcon = function(markerData) { + var center, + circle, + color, + diameter, + elements, + grid, + gridSize, + height, + i, + id, + idx, + j, + marker, + markerClass, + neg, + ref, + ref1, + row, + something, + stop, + width, + x, + xOp, + y, + yOp; + elements = (function() { + var i, + len, + results; + results = []; + for (i = 0, len = markerData.length; i < len; i++) { + marker = markerData[i]; + color = marker.color; + diameter = ((marker.point.rel / scope.maxRel) * 40) + 10; + results.push([diameter, + '
      ']); + } + return results; + })(); + elements.sort(function(element1, + element2) { + return element1[0] - element2[0]; + }); + gridSize = (Math.ceil(Math.sqrt(elements.length))) + 1; + gridSize = gridSize % 2 === 0 ? gridSize + 1 : gridSize; + center = Math.floor(gridSize / 2); + grid = (function() { + var i, + ref, + results; + results = []; + for (x = i = 0, ref = gridSize - 1; (0 <= ref ? i <= ref : i >= ref); x = 0 <= ref ? ++i : --i) { + results.push([]); + } + return results; + })(); + id = function(x) { + return x; + }; + neg = function(x) { + return -x; + }; + for (idx = i = 0, ref = center; (0 <= ref ? i <= ref : i >= ref); idx = 0 <= ref ? ++i : --i) { + x = -1; + y = -1; + xOp = neg; + yOp = neg; + stop = idx === 0 ? 0 : idx * 4 - 1; + for (something = j = 0, ref1 = stop; (0 <= ref1 ? j <= ref1 : j >= ref1); something = 0 <= ref1 ? ++j : --j) { + if (x === -1) { + x = center + idx; + } else { + x = x + xOp(1); + } + if (y === -1) { + y = center; + } else { + y = y + yOp(1); + } + if (x === center - idx) { + xOp = id; + } + if (y === center - idx) { + yOp = id; + } + if (x === center + idx) { + xOp = neg; + } + if (y === center + idx) { + yOp = neg; + } + circle = elements.pop(); + if (circle) { + grid[y][x] = circle; + } else { + break; + } + } + } + // remove all empty arrays and elements + // TODO don't create empty stuff?? + grid = _.filter(grid, + function(row) { + return row.length > 0; + }); + grid = _.map(grid, + function(row) { + return row = _.filter(row, + function(elem) { + return elem; + }); + }); + //# take largest element from each row and add to height + height = 0; + width = 0; + center = Math.floor(grid.length / 2); + grid = (function() { + var k, + len, + results; + results = []; + for (idx = k = 0, len = grid.length; k < len; idx = ++k) { + row = grid[idx]; + height = height + _.reduce(row, + (function(memo, + val) { + if (val[0] > memo) { + return val[0]; + } else { + return memo; + } + }), + 0); + if (idx < center) { + markerClass = 'marker-bottom'; + } + if (idx === center) { + width = _.reduce(grid[center], + (function(memo, + val) { + return memo + val[0]; + }), + 0); + markerClass = 'marker-middle'; + } + if (idx > center) { + markerClass = 'marker-top'; + } + results.push('
      ' + _.map(row, + function(elem) { + return elem[1]; + }).join('') + '
      '); + } + return results; + })(); + return L.divIcon({ + html: grid.join(''), + iconSize: new L.Point(width, + height) + }); + }; + // use the previously calculated "scope.maxRel" to decide the sizes of the bars + // in the cluster icon that is returned (between 5px and 50px) + createClusterIcon = function(clusterGroups, + restColor) { + var allGroups, + visibleGroups; + allGroups = _.keys(clusterGroups); + visibleGroups = allGroups.sort(function(group1, + group2) { + return clusterGroups[group1].order - clusterGroups[group2].order; + }); + if (allGroups.length > 4) { + visibleGroups = visibleGroups.splice(0, + 3); + visibleGroups.push(restColor); + } + return function(cluster) { + var child, + color, + diameter, + divWidth, + elements, + group, + groupSize, + i, + j, + k, + len, + len1, + len2, + ref, + ref1, + rel, + sizes; + sizes = {}; + for (i = 0, len = visibleGroups.length; i < len; i++) { + group = visibleGroups[i]; + sizes[group] = 0; + } + ref = cluster.getAllChildMarkers(); + for (j = 0, len1 = ref.length; j < len1; j++) { + child = ref[j]; + color = child.markerData.color; + if (!(color in sizes)) { + color = restColor; + } + rel = child.markerData.point.rel; + sizes[color] = sizes[color] + rel; + } + if (allGroups.length === 1) { + color = _.keys(sizes)[0]; + groupSize = sizes[color]; + diameter = ((groupSize / scope.maxRel) * 45) + 5; + return createCircleMarker(color, + diameter, + diameter); + } else { + elements = ""; + ref1 = _.keys(sizes); + for (k = 0, len2 = ref1.length; k < len2; k++) { + color = ref1[k]; + groupSize = sizes[color]; + divWidth = ((groupSize / scope.maxRel) * 45) + 5; + elements = elements + '
      '; + } + return L.divIcon({ + html: '
      ' + elements + '
      ', + iconSize: new L.Point(40, + 50) + }); + } + }; + }; + // check if the cluster with split into several clusters / markers + // on zooom + // TODO: does not work in some cases + shouldZooomToBounds = function(cluster) { + var boundsZoom, + childCluster, + childClusters, + i, + len, + newClusters, + zoom; + childClusters = cluster._childClusters.slice(); + map = cluster._group._map; + boundsZoom = map.getBoundsZoom(cluster._bounds); + zoom = cluster._zoom + 1; + while (childClusters.length > 0 && boundsZoom > zoom) { + zoom = zoom + 1; + newClusters = []; + for (i = 0, len = childClusters.length; i < len; i++) { + childCluster = childClusters[i]; + newClusters = newClusters.concat(childCluster._childClusters); + } + childClusters = newClusters; + } + return childClusters.length > 1; + }; + // check all current clusters and sum up the sizes of its childen + // this is the max relative value of any cluster and can be used to + // calculate marker sizes + // TODO this needs to use the "rest" group when doing calcuations!! + updateMarkerSizes = function() { + var bounds; + bounds = scope.map.getBounds(); + scope.maxRel = 0; + if (scope.useClustering && scope.markerCluster) { + scope.map.eachLayer(function(layer) { + var child, + color, + i, + j, + len, + len1, + ref, + ref1, + rel, + results, + sumRel, + sumRels; + if (layer.getChildCount) { + sumRels = {}; + ref = layer.getAllChildMarkers(); + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i]; + color = child.markerData.color; + if (!sumRels[color]) { + sumRels[color] = 0; + } + sumRels[color] = sumRels[color] + child.markerData.point.rel; + } + ref1 = _.values(sumRels); + results = []; + for (j = 0, len1 = ref1.length; j < len1; j++) { + sumRel = ref1[j]; + if (sumRel > scope.maxRel) { + results.push(scope.maxRel = sumRel); + } else { + results.push(void 0); + } + } + return results; + } else if (layer.markerData) { + rel = layer.markerData.point.rel; + if (rel > scope.maxRel) { + return scope.maxRel = rel; + } + } + }); + return scope.markerCluster.refreshClusters(); + } + }; + // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this + + // create normal layer (and all listeners) to be used when clustering is not enabled + createFeatureLayer = function() { + var featureLayer; + featureLayer = L.featureGroup(); + featureLayer.on('click', + function(e) { + if (e.layer.markerData instanceof Array) { + scope.selectedMarkers = e.layer.markerData; + } else { + scope.selectedMarkers = [e.layer.markerData]; + } + return mouseOver(scope.selectedMarkers); + }); + featureLayer.on('mouseover', + function(e) { + if (e.layer.markerData instanceof Array) { + return mouseOver(e.layer.markerData); + } else { + return mouseOver([e.layer.markerData]); + } + }); + featureLayer.on('mouseout', + function(e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers); + } else { + return mouseOut(); + } + }); + return featureLayer; + }; + // create marker cluster layer and all listeners + createMarkerCluster = function(clusterGroups, + restColor) { + var markerCluster; + markerCluster = L.markerClusterGroup({ + spiderfyOnMaxZoom: false, + showCoverageOnHover: false, + maxClusterRadius: 40, + zoomToBoundsOnClick: false, + iconCreateFunction: createClusterIcon(clusterGroups, + restColor) + }); + markerCluster.on('clustermouseover', + function(e) { + return mouseOver(_.map(e.layer.getAllChildMarkers(), + function(layer) { + return layer.markerData; + })); + }); + markerCluster.on('clustermouseout', + function(e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers); + } else { + return mouseOut(); + } + }); + markerCluster.on('clusterclick', + function(e) { + scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), + function(layer) { + return layer.markerData; + }); + mouseOver(scope.selectedMarkers); + if (shouldZooomToBounds(e.layer)) { + return e.layer.zoomToBounds(); + } + }); + markerCluster.on('click', + function(e) { + scope.selectedMarkers = [e.layer.markerData]; + return mouseOver(scope.selectedMarkers); + }); + markerCluster.on('mouseover', + function(e) { + return mouseOver([e.layer.markerData]); + }); + markerCluster.on('mouseout', + function(e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers); + } else { + return mouseOut(); + } + }); + markerCluster.on('animationend', + function(e) { + return updateMarkerSizes(); + }); + return markerCluster; + }; + // takes a list of markers and displays clickable (callback determined by directive user) info boxes + mouseOver = function(markerData) { + return $timeout((function() { + return scope.$apply(function() { + var compiled, + content, + hoverInfoElem, + i, + len, + marker, + markerDiv, + msgScope, + name, + oldMap, + selectedMarkers; + content = []; + // support for "old" map + oldMap = false; + if (markerData[0].names) { + oldMap = true; + selectedMarkers = (function() { + var i, + len, + ref, + results; + ref = _.keys(markerData[0].names); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + name = ref[i]; + results.push({ + color: markerData[0].color, + searchCqp: markerData[0].searchCqp, + point: { + name: name, + abs: markerData[0].names[name].abs_occurrences, + rel: markerData[0].names[name].rel_occurrences + } + }); + } + return results; + })(); + } else { + markerData.sort(function(markerData1, + markerData2) { + return markerData2.point.rel - markerData1.point.rel; + }); + selectedMarkers = markerData; + } + for (i = 0, len = selectedMarkers.length; i < len; i++) { + marker = selectedMarkers[i]; + msgScope = $rootScope.$new(true); + msgScope.showLabel = !oldMap; + msgScope.point = marker.point; + msgScope.label = marker.label; + msgScope.color = marker.color; + compiled = $compile(scope.hoverTemplate); + markerDiv = compiled(msgScope); + (function(marker) { + return markerDiv.bind('click', + function() { + return scope.markerCallback(marker); + }); + })(marker); + content.push(markerDiv); + } + hoverInfoElem = angular.element(element.find(".hover-info-container")); + hoverInfoElem.empty(); + hoverInfoElem.append(content); + hoverInfoElem[0].scrollTop = 0; + hoverInfoElem.css('opacity', + '1'); + return hoverInfoElem.css('display', + 'block'); + }); + }), + 0); + }; + mouseOut = function() { + var hoverInfoElem; + hoverInfoElem = angular.element(element.find(".hover-info-container")); + hoverInfoElem.css('opacity', + '0'); + return hoverInfoElem.css('display', + 'none'); + }; + scope.showHoverInfo = false; + scope.map.on('click', + function(e) { + scope.selectedMarkers = []; + return mouseOut(); + }); + scope.$watchCollection("selectedGroups", + function(selectedGroups) { + return updateMarkers(); + }); + scope.$watch('useClustering', + function(newVal, + oldVal) { + if (newVal === !oldVal) { + return updateMarkers(); + } + }); + updateMarkers = function() { + var clusterGroups, + color, + group, + groupData, + i, + j, + k, + l, + len, + len1, + len2, + len3, + marker, + markerData, + markerGroup, + markerGroupId, + marker_id, + markers, + ref, + ref1, + selectedGroups; + selectedGroups = scope.selectedGroups; + markers = scope.markers; + if (scope.markerCluster) { + scope.map.removeLayer(scope.markerCluster); + } + if (scope.featureLayer) { + scope.map.removeLayer(scope.featureLayer); + } + if (scope.useClustering) { + clusterGroups = {}; + for (i = 0, len = selectedGroups.length; i < len; i++) { + group = selectedGroups[i]; + groupData = markers[group]; + clusterGroups[groupData.color] = { + order: groupData.order + }; + } + scope.markerCluster = createMarkerCluster(clusterGroups, + scope.restColor); + scope.map.addLayer(scope.markerCluster); + } else { + scope.featureLayer = createFeatureLayer(); + scope.map.addLayer(scope.featureLayer); + } + if (scope.useClustering || scope.oldMap) { + for (j = 0, len1 = selectedGroups.length; j < len1; j++) { + markerGroupId = selectedGroups[j]; + markerGroup = markers[markerGroupId]; + color = markerGroup.color; + scope.maxRel = 0; + ref = _.keys(markerGroup.markers); + for (k = 0, len2 = ref.length; k < len2; k++) { + marker_id = ref[k]; + markerData = markerGroup.markers[marker_id]; + markerData.color = color; + marker = L.marker([markerData.lat, + markerData.lng], + { + icon: createMarkerIcon(color, + !scope.oldMap && selectedGroups.length !== 1) + }); + marker.markerData = markerData; + if (scope.useClustering) { + scope.markerCluster.addLayer(marker); + } else { + scope.featureLayer.addLayer(marker); + } + } + } + } else { + markers = (function() { + var l, + len3, + results; + results = []; + for (l = 0, len3 = selectedGroups.length; l < len3; l++) { + markerGroupId = selectedGroups[l]; + results.push(markers[markerGroupId]); + } + return results; + })(); + ref1 = mergeMarkers(_.values(markers)); + for (l = 0, len3 = ref1.length; l < len3; l++) { + markerData = ref1[l]; + marker = L.marker([markerData.lat, + markerData.lng], + { + icon: createMultiMarkerIcon(markerData.markerData) + }); + marker.markerData = markerData.markerData; + scope.featureLayer.addLayer(marker); + } + } + return updateMarkerSizes(); + }; + // merge lists of markers into one list with several hits in one marker + // also calculate maxRel + mergeMarkers = function(markerLists) { + var val; + scope.maxRel = 0; + val = _.reduce(markerLists, + (function(memo, + val) { + var latLng, + markerData, + markerId, + ref; + ref = val.markers; + for (markerId in ref) { + markerData = ref[markerId]; + markerData.color = val.color; + latLng = markerData.lat + ',' + markerData.lng; + if (markerData.point.rel > scope.maxRel) { + scope.maxRel = markerData.point.rel; + } + if (latLng in memo) { + memo[latLng].markerData.push(markerData); + } else { + memo[latLng] = { + markerData: [markerData] + }; + memo[latLng].lat = markerData.lat; + memo[latLng].lng = markerData.lng; + } + } + return memo; + }), + {}); + return _.values(val); + }; + // Load map layer with leaflet-providers + openStreetMap = L.tileLayer.provider("OpenStreetMap"); + openStreetMap.addTo(scope.map); + scope.map.setView([scope.center.lat, + scope.center.lng], + scope.center.zoom); + return scope.showMap = true; + }; + return { + restrict: 'E', + scope: { + markers: '=sbMarkers', + center: '=sbCenter', + baseLayer: '=sbBaseLayer', + markerCallback: '=sbMarkerCallback', + selectedGroups: '=sbSelectedGroups', + useClustering: '=?sbUseClustering', + restColor: '=?sbRestColor', // free color to use for grouping etc + oldMap: '=?sbOldMap' + }, + link: link, + templateUrl: 'template/sb_map.html' + }; + } + ]); + +}).call(this); + +//# sourceMappingURL=sb_map.js.map diff --git a/app/scripts/korp.module.ts b/app/scripts/korp.module.ts index 69746980c..94b151f6a 100644 --- a/app/scripts/korp.module.ts +++ b/app/scripts/korp.module.ts @@ -3,8 +3,8 @@ import angular from "angular" import "angular-ui-bootstrap" import "angular-spinner" import "angular-ui-sortable" -import "geokorp/dist/scripts/geokorp" -import "geokorp/dist/scripts/geokorp-templates" +import "@/geo/geokorp" +import "@/geo/geokorp-templates" import "angular-dynamic-locale" import "angular-filter" diff --git a/package.json b/package.json index 7ef3169ce..abcb21e44 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "chart.js": "^4.4.3", "comma-separated-values": "3.6.4", "components-jqueryui": "1.12.1", - "geokorp": "spraakbanken/geokorp#1.5.0", "jquery": "3.6.3", "js-yaml": "^4.1.0", "leaflet": "^1.9.3", From e5aa6a0b9a25b6f18427e57d4640f8898dd3445f Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 10:45:10 +0200 Subject: [PATCH 81/99] style: format code --- app/scripts/geo/geokorp.js | 1404 +++++++++++++++++------------------- 1 file changed, 675 insertions(+), 729 deletions(-) diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index 93fecd25c..bbfff1980 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -1,750 +1,696 @@ -(function() { - 'use strict'; - var c; +/** @format */ +import { html } from "@/util" +import angular from "angular" - c = console; +const sbMap = angular.module("sbMap", ["sbMapTemplate"]) - angular.module('sbMap', ['sbMapTemplate']).filter("trust", function($sce) { - return function(input) { - return $sce.trustAsHtml(input); - }; - }).directive('sbMap', [ - '$compile', - '$timeout', - '$rootScope', - function($compile, - $timeout, - $rootScope) { - var link; - link = function(scope, - element, - attrs) { - var createCircleMarker, - createClusterIcon, - createFeatureLayer, - createMarkerCluster, - createMarkerIcon, - createMultiMarkerIcon, - map, - mergeMarkers, - mouseOut, - mouseOver, - openStreetMap, - shouldZooomToBounds, - updateMarkerSizes, - updateMarkers; - scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true; - scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false; - scope.showMap = false; - scope.hoverTemplate = `
      -
      -
      -
      {{ 'map_name' | loc }}: {{point.name}}
      -
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      -
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      -
      `; - map = angular.element(element.find(".map-container")); - scope.map = L.map(map[0], - { - minZoom: 1, - maxZoom: 13 - }).setView([51.505, - -0.09], - 13); - scope.selectedMarkers = []; - scope.$on("update_map", - function() { - return $timeout((function() { - return scope.map.invalidateSize(); - }), - 0); - }); - createCircleMarker = function(color, - diameter, - borderRadius) { - return L.divIcon({ - html: '
      ', - iconSize: new L.Point(diameter, - diameter) - }); - }; - createMarkerIcon = function(color, - cluster) { - // TODO use scope.maxRel, but scope.maxRel is not set when markers are created - // diameter = ((relSize / scope.maxRel) * 45) + 5 - return createCircleMarker(color, - 10, - cluster ? 1 : 5); - }; - createMultiMarkerIcon = function(markerData) { - var center, - circle, - color, - diameter, - elements, - grid, - gridSize, - height, - i, - id, - idx, - j, - marker, - markerClass, - neg, - ref, - ref1, - row, - something, - stop, - width, - x, - xOp, - y, - yOp; - elements = (function() { - var i, - len, - results; - results = []; - for (i = 0, len = markerData.length; i < len; i++) { - marker = markerData[i]; - color = marker.color; - diameter = ((marker.point.rel / scope.maxRel) * 40) + 10; - results.push([diameter, - '
      ']); - } - return results; - })(); - elements.sort(function(element1, - element2) { - return element1[0] - element2[0]; - }); - gridSize = (Math.ceil(Math.sqrt(elements.length))) + 1; - gridSize = gridSize % 2 === 0 ? gridSize + 1 : gridSize; - center = Math.floor(gridSize / 2); - grid = (function() { - var i, - ref, - results; - results = []; - for (x = i = 0, ref = gridSize - 1; (0 <= ref ? i <= ref : i >= ref); x = 0 <= ref ? ++i : --i) { - results.push([]); +sbMap.filter("trust", function ($sce) { + return function (input) { + return $sce.trustAsHtml(input) + } +}) + +sbMap.directive("sbMap", [ + "$compile", + "$timeout", + "$rootScope", + ($compile, $timeout, $rootScope) => ({ + templateUrl: "template/sb_map.html", + restrict: "E", + scope: { + markers: "=sbMarkers", + center: "=sbCenter", + baseLayer: "=sbBaseLayer", + markerCallback: "=sbMarkerCallback", + selectedGroups: "=sbSelectedGroups", + useClustering: "=?sbUseClustering", + restColor: "=?sbRestColor", // free color to use for grouping etc + oldMap: "=?sbOldMap", + }, + link(scope, element, attrs) { + var createCircleMarker, + createClusterIcon, + createFeatureLayer, + createMarkerCluster, + createMarkerIcon, + createMultiMarkerIcon, + map, + mergeMarkers, + mouseOut, + mouseOver, + openStreetMap, + shouldZooomToBounds, + updateMarkerSizes, + updateMarkers + scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true + scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false + scope.showMap = false + scope.hoverTemplate = html`
      +
      +
      +
      {{ 'map_name' | loc }}: {{point.name}}
      +
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      +
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      +
      ` + map = angular.element(element.find(".map-container")) + scope.map = L.map(map[0], { + minZoom: 1, + maxZoom: 13, + }).setView([51.505, -0.09], 13) + scope.selectedMarkers = [] + scope.$on("update_map", function () { + return $timeout(function () { + return scope.map.invalidateSize() + }, 0) + }) + createCircleMarker = function (color, diameter, borderRadius) { + return L.divIcon({ + html: + '
      ', + iconSize: new L.Point(diameter, diameter), + }) } - return results; - })(); - id = function(x) { - return x; - }; - neg = function(x) { - return -x; - }; - for (idx = i = 0, ref = center; (0 <= ref ? i <= ref : i >= ref); idx = 0 <= ref ? ++i : --i) { - x = -1; - y = -1; - xOp = neg; - yOp = neg; - stop = idx === 0 ? 0 : idx * 4 - 1; - for (something = j = 0, ref1 = stop; (0 <= ref1 ? j <= ref1 : j >= ref1); something = 0 <= ref1 ? ++j : --j) { - if (x === -1) { - x = center + idx; - } else { - x = x + xOp(1); - } - if (y === -1) { - y = center; - } else { - y = y + yOp(1); - } - if (x === center - idx) { - xOp = id; - } - if (y === center - idx) { - yOp = id; - } - if (x === center + idx) { - xOp = neg; - } - if (y === center + idx) { - yOp = neg; - } - circle = elements.pop(); - if (circle) { - grid[y][x] = circle; - } else { - break; - } + createMarkerIcon = function (color, cluster) { + // TODO use scope.maxRel, but scope.maxRel is not set when markers are created + // diameter = ((relSize / scope.maxRel) * 45) + 5 + return createCircleMarker(color, 10, cluster ? 1 : 5) } - } - // remove all empty arrays and elements - // TODO don't create empty stuff?? - grid = _.filter(grid, - function(row) { - return row.length > 0; - }); - grid = _.map(grid, - function(row) { - return row = _.filter(row, - function(elem) { - return elem; - }); - }); - //# take largest element from each row and add to height - height = 0; - width = 0; - center = Math.floor(grid.length / 2); - grid = (function() { - var k, - len, - results; - results = []; - for (idx = k = 0, len = grid.length; k < len; idx = ++k) { - row = grid[idx]; - height = height + _.reduce(row, - (function(memo, - val) { - if (val[0] > memo) { - return val[0]; - } else { - return memo; + createMultiMarkerIcon = function (markerData) { + var center, + circle, + color, + diameter, + elements, + grid, + gridSize, + height, + i, + id, + idx, + j, + marker, + markerClass, + neg, + ref, + ref1, + row, + something, + stop, + width, + x, + xOp, + y, + yOp + elements = (function () { + var i, len, results + results = [] + for (i = 0, len = markerData.length; i < len; i++) { + marker = markerData[i] + color = marker.color + diameter = (marker.point.rel / scope.maxRel) * 40 + 10 + results.push([ + diameter, + '
      ', + ]) + } + return results + })() + elements.sort(function (element1, element2) { + return element1[0] - element2[0] + }) + gridSize = Math.ceil(Math.sqrt(elements.length)) + 1 + gridSize = gridSize % 2 === 0 ? gridSize + 1 : gridSize + center = Math.floor(gridSize / 2) + grid = (function () { + var i, ref, results + results = [] + for (x = i = 0, ref = gridSize - 1; 0 <= ref ? i <= ref : i >= ref; x = 0 <= ref ? ++i : --i) { + results.push([]) + } + return results + })() + id = function (x) { + return x } - }), - 0); - if (idx < center) { - markerClass = 'marker-bottom'; - } - if (idx === center) { - width = _.reduce(grid[center], - (function(memo, - val) { - return memo + val[0]; - }), - 0); - markerClass = 'marker-middle'; - } - if (idx > center) { - markerClass = 'marker-top'; - } - results.push('
      ' + _.map(row, - function(elem) { - return elem[1]; - }).join('') + '
      '); - } - return results; - })(); - return L.divIcon({ - html: grid.join(''), - iconSize: new L.Point(width, - height) - }); - }; - // use the previously calculated "scope.maxRel" to decide the sizes of the bars - // in the cluster icon that is returned (between 5px and 50px) - createClusterIcon = function(clusterGroups, - restColor) { - var allGroups, - visibleGroups; - allGroups = _.keys(clusterGroups); - visibleGroups = allGroups.sort(function(group1, - group2) { - return clusterGroups[group1].order - clusterGroups[group2].order; - }); - if (allGroups.length > 4) { - visibleGroups = visibleGroups.splice(0, - 3); - visibleGroups.push(restColor); - } - return function(cluster) { - var child, - color, - diameter, - divWidth, - elements, - group, - groupSize, - i, - j, - k, - len, - len1, - len2, - ref, - ref1, - rel, - sizes; - sizes = {}; - for (i = 0, len = visibleGroups.length; i < len; i++) { - group = visibleGroups[i]; - sizes[group] = 0; - } - ref = cluster.getAllChildMarkers(); - for (j = 0, len1 = ref.length; j < len1; j++) { - child = ref[j]; - color = child.markerData.color; - if (!(color in sizes)) { - color = restColor; - } - rel = child.markerData.point.rel; - sizes[color] = sizes[color] + rel; - } - if (allGroups.length === 1) { - color = _.keys(sizes)[0]; - groupSize = sizes[color]; - diameter = ((groupSize / scope.maxRel) * 45) + 5; - return createCircleMarker(color, - diameter, - diameter); - } else { - elements = ""; - ref1 = _.keys(sizes); - for (k = 0, len2 = ref1.length; k < len2; k++) { - color = ref1[k]; - groupSize = sizes[color]; - divWidth = ((groupSize / scope.maxRel) * 45) + 5; - elements = elements + '
      '; - } - return L.divIcon({ - html: '
      ' + elements + '
      ', - iconSize: new L.Point(40, - 50) - }); - } - }; - }; - // check if the cluster with split into several clusters / markers - // on zooom - // TODO: does not work in some cases - shouldZooomToBounds = function(cluster) { - var boundsZoom, - childCluster, - childClusters, - i, - len, - newClusters, - zoom; - childClusters = cluster._childClusters.slice(); - map = cluster._group._map; - boundsZoom = map.getBoundsZoom(cluster._bounds); - zoom = cluster._zoom + 1; - while (childClusters.length > 0 && boundsZoom > zoom) { - zoom = zoom + 1; - newClusters = []; - for (i = 0, len = childClusters.length; i < len; i++) { - childCluster = childClusters[i]; - newClusters = newClusters.concat(childCluster._childClusters); - } - childClusters = newClusters; - } - return childClusters.length > 1; - }; - // check all current clusters and sum up the sizes of its childen - // this is the max relative value of any cluster and can be used to - // calculate marker sizes - // TODO this needs to use the "rest" group when doing calcuations!! - updateMarkerSizes = function() { - var bounds; - bounds = scope.map.getBounds(); - scope.maxRel = 0; - if (scope.useClustering && scope.markerCluster) { - scope.map.eachLayer(function(layer) { - var child, - color, - i, - j, - len, - len1, - ref, - ref1, - rel, - results, - sumRel, - sumRels; - if (layer.getChildCount) { - sumRels = {}; - ref = layer.getAllChildMarkers(); - for (i = 0, len = ref.length; i < len; i++) { - child = ref[i]; - color = child.markerData.color; - if (!sumRels[color]) { - sumRels[color] = 0; - } - sumRels[color] = sumRels[color] + child.markerData.point.rel; + neg = function (x) { + return -x } - ref1 = _.values(sumRels); - results = []; - for (j = 0, len1 = ref1.length; j < len1; j++) { - sumRel = ref1[j]; - if (sumRel > scope.maxRel) { - results.push(scope.maxRel = sumRel); - } else { - results.push(void 0); - } + for (idx = i = 0, ref = center; 0 <= ref ? i <= ref : i >= ref; idx = 0 <= ref ? ++i : --i) { + x = -1 + y = -1 + xOp = neg + yOp = neg + stop = idx === 0 ? 0 : idx * 4 - 1 + for ( + something = j = 0, ref1 = stop; + 0 <= ref1 ? j <= ref1 : j >= ref1; + something = 0 <= ref1 ? ++j : --j + ) { + if (x === -1) { + x = center + idx + } else { + x = x + xOp(1) + } + if (y === -1) { + y = center + } else { + y = y + yOp(1) + } + if (x === center - idx) { + xOp = id + } + if (y === center - idx) { + yOp = id + } + if (x === center + idx) { + xOp = neg + } + if (y === center + idx) { + yOp = neg + } + circle = elements.pop() + if (circle) { + grid[y][x] = circle + } else { + break + } + } } - return results; - } else if (layer.markerData) { - rel = layer.markerData.point.rel; - if (rel > scope.maxRel) { - return scope.maxRel = rel; + // remove all empty arrays and elements + // TODO don't create empty stuff?? + grid = _.filter(grid, function (row) { + return row.length > 0 + }) + grid = _.map(grid, function (row) { + return (row = _.filter(row, function (elem) { + return elem + })) + }) + //# take largest element from each row and add to height + height = 0 + width = 0 + center = Math.floor(grid.length / 2) + grid = (function () { + var k, len, results + results = [] + for (idx = k = 0, len = grid.length; k < len; idx = ++k) { + row = grid[idx] + height = + height + + _.reduce( + row, + function (memo, val) { + if (val[0] > memo) { + return val[0] + } else { + return memo + } + }, + 0 + ) + if (idx < center) { + markerClass = "marker-bottom" + } + if (idx === center) { + width = _.reduce( + grid[center], + function (memo, val) { + return memo + val[0] + }, + 0 + ) + markerClass = "marker-middle" + } + if (idx > center) { + markerClass = "marker-top" + } + results.push( + '
      ' + + _.map(row, function (elem) { + return elem[1] + }).join("") + + "
      " + ) + } + return results + })() + return L.divIcon({ + html: grid.join(""), + iconSize: new L.Point(width, height), + }) + } + // use the previously calculated "scope.maxRel" to decide the sizes of the bars + // in the cluster icon that is returned (between 5px and 50px) + createClusterIcon = function (clusterGroups, restColor) { + var allGroups, visibleGroups + allGroups = _.keys(clusterGroups) + visibleGroups = allGroups.sort(function (group1, group2) { + return clusterGroups[group1].order - clusterGroups[group2].order + }) + if (allGroups.length > 4) { + visibleGroups = visibleGroups.splice(0, 3) + visibleGroups.push(restColor) + } + return function (cluster) { + var child, + color, + diameter, + divWidth, + elements, + group, + groupSize, + i, + j, + k, + len, + len1, + len2, + ref, + ref1, + rel, + sizes + sizes = {} + for (i = 0, len = visibleGroups.length; i < len; i++) { + group = visibleGroups[i] + sizes[group] = 0 + } + ref = cluster.getAllChildMarkers() + for (j = 0, len1 = ref.length; j < len1; j++) { + child = ref[j] + color = child.markerData.color + if (!(color in sizes)) { + color = restColor + } + rel = child.markerData.point.rel + sizes[color] = sizes[color] + rel + } + if (allGroups.length === 1) { + color = _.keys(sizes)[0] + groupSize = sizes[color] + diameter = (groupSize / scope.maxRel) * 45 + 5 + return createCircleMarker(color, diameter, diameter) + } else { + elements = "" + ref1 = _.keys(sizes) + for (k = 0, len2 = ref1.length; k < len2; k++) { + color = ref1[k] + groupSize = sizes[color] + divWidth = (groupSize / scope.maxRel) * 45 + 5 + elements = + elements + + '
      ' + } + return L.divIcon({ + html: '
      ' + elements + "
      ", + iconSize: new L.Point(40, 50), + }) + } } - } - }); - return scope.markerCluster.refreshClusters(); - } - }; - // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this - - // create normal layer (and all listeners) to be used when clustering is not enabled - createFeatureLayer = function() { - var featureLayer; - featureLayer = L.featureGroup(); - featureLayer.on('click', - function(e) { - if (e.layer.markerData instanceof Array) { - scope.selectedMarkers = e.layer.markerData; - } else { - scope.selectedMarkers = [e.layer.markerData]; } - return mouseOver(scope.selectedMarkers); - }); - featureLayer.on('mouseover', - function(e) { - if (e.layer.markerData instanceof Array) { - return mouseOver(e.layer.markerData); - } else { - return mouseOver([e.layer.markerData]); + // check if the cluster with split into several clusters / markers + // on zooom + // TODO: does not work in some cases + shouldZooomToBounds = function (cluster) { + var boundsZoom, childCluster, childClusters, i, len, newClusters, zoom + childClusters = cluster._childClusters.slice() + map = cluster._group._map + boundsZoom = map.getBoundsZoom(cluster._bounds) + zoom = cluster._zoom + 1 + while (childClusters.length > 0 && boundsZoom > zoom) { + zoom = zoom + 1 + newClusters = [] + for (i = 0, len = childClusters.length; i < len; i++) { + childCluster = childClusters[i] + newClusters = newClusters.concat(childCluster._childClusters) + } + childClusters = newClusters + } + return childClusters.length > 1 } - }); - featureLayer.on('mouseout', - function(e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers); - } else { - return mouseOut(); + // check all current clusters and sum up the sizes of its childen + // this is the max relative value of any cluster and can be used to + // calculate marker sizes + // TODO this needs to use the "rest" group when doing calcuations!! + updateMarkerSizes = function () { + var bounds + bounds = scope.map.getBounds() + scope.maxRel = 0 + if (scope.useClustering && scope.markerCluster) { + scope.map.eachLayer(function (layer) { + var child, color, i, j, len, len1, ref, ref1, rel, results, sumRel, sumRels + if (layer.getChildCount) { + sumRels = {} + ref = layer.getAllChildMarkers() + for (i = 0, len = ref.length; i < len; i++) { + child = ref[i] + color = child.markerData.color + if (!sumRels[color]) { + sumRels[color] = 0 + } + sumRels[color] = sumRels[color] + child.markerData.point.rel + } + ref1 = _.values(sumRels) + results = [] + for (j = 0, len1 = ref1.length; j < len1; j++) { + sumRel = ref1[j] + if (sumRel > scope.maxRel) { + results.push((scope.maxRel = sumRel)) + } else { + results.push(void 0) + } + } + return results + } else if (layer.markerData) { + rel = layer.markerData.point.rel + if (rel > scope.maxRel) { + return (scope.maxRel = rel) + } + } + }) + return scope.markerCluster.refreshClusters() + } } - }); - return featureLayer; - }; - // create marker cluster layer and all listeners - createMarkerCluster = function(clusterGroups, - restColor) { - var markerCluster; - markerCluster = L.markerClusterGroup({ - spiderfyOnMaxZoom: false, - showCoverageOnHover: false, - maxClusterRadius: 40, - zoomToBoundsOnClick: false, - iconCreateFunction: createClusterIcon(clusterGroups, - restColor) - }); - markerCluster.on('clustermouseover', - function(e) { - return mouseOver(_.map(e.layer.getAllChildMarkers(), - function(layer) { - return layer.markerData; - })); - }); - markerCluster.on('clustermouseout', - function(e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers); - } else { - return mouseOut(); + // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this + + // create normal layer (and all listeners) to be used when clustering is not enabled + createFeatureLayer = function () { + var featureLayer + featureLayer = L.featureGroup() + featureLayer.on("click", function (e) { + if (e.layer.markerData instanceof Array) { + scope.selectedMarkers = e.layer.markerData + } else { + scope.selectedMarkers = [e.layer.markerData] + } + return mouseOver(scope.selectedMarkers) + }) + featureLayer.on("mouseover", function (e) { + if (e.layer.markerData instanceof Array) { + return mouseOver(e.layer.markerData) + } else { + return mouseOver([e.layer.markerData]) + } + }) + featureLayer.on("mouseout", function (e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers) + } else { + return mouseOut() + } + }) + return featureLayer } - }); - markerCluster.on('clusterclick', - function(e) { - scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), - function(layer) { - return layer.markerData; - }); - mouseOver(scope.selectedMarkers); - if (shouldZooomToBounds(e.layer)) { - return e.layer.zoomToBounds(); + // create marker cluster layer and all listeners + createMarkerCluster = function (clusterGroups, restColor) { + var markerCluster + markerCluster = L.markerClusterGroup({ + spiderfyOnMaxZoom: false, + showCoverageOnHover: false, + maxClusterRadius: 40, + zoomToBoundsOnClick: false, + iconCreateFunction: createClusterIcon(clusterGroups, restColor), + }) + markerCluster.on("clustermouseover", function (e) { + return mouseOver( + _.map(e.layer.getAllChildMarkers(), function (layer) { + return layer.markerData + }) + ) + }) + markerCluster.on("clustermouseout", function (e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers) + } else { + return mouseOut() + } + }) + markerCluster.on("clusterclick", function (e) { + scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), function (layer) { + return layer.markerData + }) + mouseOver(scope.selectedMarkers) + if (shouldZooomToBounds(e.layer)) { + return e.layer.zoomToBounds() + } + }) + markerCluster.on("click", function (e) { + scope.selectedMarkers = [e.layer.markerData] + return mouseOver(scope.selectedMarkers) + }) + markerCluster.on("mouseover", function (e) { + return mouseOver([e.layer.markerData]) + }) + markerCluster.on("mouseout", function (e) { + if (scope.selectedMarkers.length > 0) { + return mouseOver(scope.selectedMarkers) + } else { + return mouseOut() + } + }) + markerCluster.on("animationend", function (e) { + return updateMarkerSizes() + }) + return markerCluster } - }); - markerCluster.on('click', - function(e) { - scope.selectedMarkers = [e.layer.markerData]; - return mouseOver(scope.selectedMarkers); - }); - markerCluster.on('mouseover', - function(e) { - return mouseOver([e.layer.markerData]); - }); - markerCluster.on('mouseout', - function(e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers); - } else { - return mouseOut(); + // takes a list of markers and displays clickable (callback determined by directive user) info boxes + mouseOver = function (markerData) { + return $timeout(function () { + return scope.$apply(function () { + var compiled, + content, + hoverInfoElem, + i, + len, + marker, + markerDiv, + msgScope, + name, + oldMap, + selectedMarkers + content = [] + // support for "old" map + oldMap = false + if (markerData[0].names) { + oldMap = true + selectedMarkers = (function () { + var i, len, ref, results + ref = _.keys(markerData[0].names) + results = [] + for (i = 0, len = ref.length; i < len; i++) { + name = ref[i] + results.push({ + color: markerData[0].color, + searchCqp: markerData[0].searchCqp, + point: { + name: name, + abs: markerData[0].names[name].abs_occurrences, + rel: markerData[0].names[name].rel_occurrences, + }, + }) + } + return results + })() + } else { + markerData.sort(function (markerData1, markerData2) { + return markerData2.point.rel - markerData1.point.rel + }) + selectedMarkers = markerData + } + for (i = 0, len = selectedMarkers.length; i < len; i++) { + marker = selectedMarkers[i] + msgScope = $rootScope.$new(true) + msgScope.showLabel = !oldMap + msgScope.point = marker.point + msgScope.label = marker.label + msgScope.color = marker.color + compiled = $compile(scope.hoverTemplate) + markerDiv = compiled(msgScope) + ;(function (marker) { + return markerDiv.bind("click", function () { + return scope.markerCallback(marker) + }) + })(marker) + content.push(markerDiv) + } + hoverInfoElem = angular.element(element.find(".hover-info-container")) + hoverInfoElem.empty() + hoverInfoElem.append(content) + hoverInfoElem[0].scrollTop = 0 + hoverInfoElem.css("opacity", "1") + return hoverInfoElem.css("display", "block") + }) + }, 0) } - }); - markerCluster.on('animationend', - function(e) { - return updateMarkerSizes(); - }); - return markerCluster; - }; - // takes a list of markers and displays clickable (callback determined by directive user) info boxes - mouseOver = function(markerData) { - return $timeout((function() { - return scope.$apply(function() { - var compiled, - content, - hoverInfoElem, - i, - len, - marker, - markerDiv, - msgScope, - name, - oldMap, - selectedMarkers; - content = []; - // support for "old" map - oldMap = false; - if (markerData[0].names) { - oldMap = true; - selectedMarkers = (function() { - var i, - len, - ref, - results; - ref = _.keys(markerData[0].names); - results = []; - for (i = 0, len = ref.length; i < len; i++) { - name = ref[i]; - results.push({ - color: markerData[0].color, - searchCqp: markerData[0].searchCqp, - point: { - name: name, - abs: markerData[0].names[name].abs_occurrences, - rel: markerData[0].names[name].rel_occurrences - } - }); - } - return results; - })(); - } else { - markerData.sort(function(markerData1, - markerData2) { - return markerData2.point.rel - markerData1.point.rel; - }); - selectedMarkers = markerData; - } - for (i = 0, len = selectedMarkers.length; i < len; i++) { - marker = selectedMarkers[i]; - msgScope = $rootScope.$new(true); - msgScope.showLabel = !oldMap; - msgScope.point = marker.point; - msgScope.label = marker.label; - msgScope.color = marker.color; - compiled = $compile(scope.hoverTemplate); - markerDiv = compiled(msgScope); - (function(marker) { - return markerDiv.bind('click', - function() { - return scope.markerCallback(marker); - }); - })(marker); - content.push(markerDiv); - } - hoverInfoElem = angular.element(element.find(".hover-info-container")); - hoverInfoElem.empty(); - hoverInfoElem.append(content); - hoverInfoElem[0].scrollTop = 0; - hoverInfoElem.css('opacity', - '1'); - return hoverInfoElem.css('display', - 'block'); - }); - }), - 0); - }; - mouseOut = function() { - var hoverInfoElem; - hoverInfoElem = angular.element(element.find(".hover-info-container")); - hoverInfoElem.css('opacity', - '0'); - return hoverInfoElem.css('display', - 'none'); - }; - scope.showHoverInfo = false; - scope.map.on('click', - function(e) { - scope.selectedMarkers = []; - return mouseOut(); - }); - scope.$watchCollection("selectedGroups", - function(selectedGroups) { - return updateMarkers(); - }); - scope.$watch('useClustering', - function(newVal, - oldVal) { - if (newVal === !oldVal) { - return updateMarkers(); - } - }); - updateMarkers = function() { - var clusterGroups, - color, - group, - groupData, - i, - j, - k, - l, - len, - len1, - len2, - len3, - marker, - markerData, - markerGroup, - markerGroupId, - marker_id, - markers, - ref, - ref1, - selectedGroups; - selectedGroups = scope.selectedGroups; - markers = scope.markers; - if (scope.markerCluster) { - scope.map.removeLayer(scope.markerCluster); - } - if (scope.featureLayer) { - scope.map.removeLayer(scope.featureLayer); - } - if (scope.useClustering) { - clusterGroups = {}; - for (i = 0, len = selectedGroups.length; i < len; i++) { - group = selectedGroups[i]; - groupData = markers[group]; - clusterGroups[groupData.color] = { - order: groupData.order - }; + mouseOut = function () { + var hoverInfoElem + hoverInfoElem = angular.element(element.find(".hover-info-container")) + hoverInfoElem.css("opacity", "0") + return hoverInfoElem.css("display", "none") } - scope.markerCluster = createMarkerCluster(clusterGroups, - scope.restColor); - scope.map.addLayer(scope.markerCluster); - } else { - scope.featureLayer = createFeatureLayer(); - scope.map.addLayer(scope.featureLayer); - } - if (scope.useClustering || scope.oldMap) { - for (j = 0, len1 = selectedGroups.length; j < len1; j++) { - markerGroupId = selectedGroups[j]; - markerGroup = markers[markerGroupId]; - color = markerGroup.color; - scope.maxRel = 0; - ref = _.keys(markerGroup.markers); - for (k = 0, len2 = ref.length; k < len2; k++) { - marker_id = ref[k]; - markerData = markerGroup.markers[marker_id]; - markerData.color = color; - marker = L.marker([markerData.lat, - markerData.lng], - { - icon: createMarkerIcon(color, - !scope.oldMap && selectedGroups.length !== 1) - }); - marker.markerData = markerData; + scope.showHoverInfo = false + scope.map.on("click", function (e) { + scope.selectedMarkers = [] + return mouseOut() + }) + scope.$watchCollection("selectedGroups", function (selectedGroups) { + return updateMarkers() + }) + scope.$watch("useClustering", function (newVal, oldVal) { + if (newVal === !oldVal) { + return updateMarkers() + } + }) + updateMarkers = function () { + var clusterGroups, + color, + group, + groupData, + i, + j, + k, + l, + len, + len1, + len2, + len3, + marker, + markerData, + markerGroup, + markerGroupId, + marker_id, + markers, + ref, + ref1, + selectedGroups + selectedGroups = scope.selectedGroups + markers = scope.markers + if (scope.markerCluster) { + scope.map.removeLayer(scope.markerCluster) + } + if (scope.featureLayer) { + scope.map.removeLayer(scope.featureLayer) + } if (scope.useClustering) { - scope.markerCluster.addLayer(marker); + clusterGroups = {} + for (i = 0, len = selectedGroups.length; i < len; i++) { + group = selectedGroups[i] + groupData = markers[group] + clusterGroups[groupData.color] = { + order: groupData.order, + } + } + scope.markerCluster = createMarkerCluster(clusterGroups, scope.restColor) + scope.map.addLayer(scope.markerCluster) } else { - scope.featureLayer.addLayer(marker); + scope.featureLayer = createFeatureLayer() + scope.map.addLayer(scope.featureLayer) } - } - } - } else { - markers = (function() { - var l, - len3, - results; - results = []; - for (l = 0, len3 = selectedGroups.length; l < len3; l++) { - markerGroupId = selectedGroups[l]; - results.push(markers[markerGroupId]); - } - return results; - })(); - ref1 = mergeMarkers(_.values(markers)); - for (l = 0, len3 = ref1.length; l < len3; l++) { - markerData = ref1[l]; - marker = L.marker([markerData.lat, - markerData.lng], - { - icon: createMultiMarkerIcon(markerData.markerData) - }); - marker.markerData = markerData.markerData; - scope.featureLayer.addLayer(marker); + if (scope.useClustering || scope.oldMap) { + for (j = 0, len1 = selectedGroups.length; j < len1; j++) { + markerGroupId = selectedGroups[j] + markerGroup = markers[markerGroupId] + color = markerGroup.color + scope.maxRel = 0 + ref = _.keys(markerGroup.markers) + for (k = 0, len2 = ref.length; k < len2; k++) { + marker_id = ref[k] + markerData = markerGroup.markers[marker_id] + markerData.color = color + marker = L.marker([markerData.lat, markerData.lng], { + icon: createMarkerIcon(color, !scope.oldMap && selectedGroups.length !== 1), + }) + marker.markerData = markerData + if (scope.useClustering) { + scope.markerCluster.addLayer(marker) + } else { + scope.featureLayer.addLayer(marker) + } + } + } + } else { + markers = (function () { + var l, len3, results + results = [] + for (l = 0, len3 = selectedGroups.length; l < len3; l++) { + markerGroupId = selectedGroups[l] + results.push(markers[markerGroupId]) + } + return results + })() + ref1 = mergeMarkers(_.values(markers)) + for (l = 0, len3 = ref1.length; l < len3; l++) { + markerData = ref1[l] + marker = L.marker([markerData.lat, markerData.lng], { + icon: createMultiMarkerIcon(markerData.markerData), + }) + marker.markerData = markerData.markerData + scope.featureLayer.addLayer(marker) + } + } + return updateMarkerSizes() } - } - return updateMarkerSizes(); - }; - // merge lists of markers into one list with several hits in one marker - // also calculate maxRel - mergeMarkers = function(markerLists) { - var val; - scope.maxRel = 0; - val = _.reduce(markerLists, - (function(memo, - val) { - var latLng, - markerData, - markerId, - ref; - ref = val.markers; - for (markerId in ref) { - markerData = ref[markerId]; - markerData.color = val.color; - latLng = markerData.lat + ',' + markerData.lng; - if (markerData.point.rel > scope.maxRel) { - scope.maxRel = markerData.point.rel; - } - if (latLng in memo) { - memo[latLng].markerData.push(markerData); - } else { - memo[latLng] = { - markerData: [markerData] - }; - memo[latLng].lat = markerData.lat; - memo[latLng].lng = markerData.lng; - } + // merge lists of markers into one list with several hits in one marker + // also calculate maxRel + mergeMarkers = function (markerLists) { + var val + scope.maxRel = 0 + val = _.reduce( + markerLists, + function (memo, val) { + var latLng, markerData, markerId, ref + ref = val.markers + for (markerId in ref) { + markerData = ref[markerId] + markerData.color = val.color + latLng = markerData.lat + "," + markerData.lng + if (markerData.point.rel > scope.maxRel) { + scope.maxRel = markerData.point.rel + } + if (latLng in memo) { + memo[latLng].markerData.push(markerData) + } else { + memo[latLng] = { + markerData: [markerData], + } + memo[latLng].lat = markerData.lat + memo[latLng].lng = markerData.lng + } + } + return memo + }, + {} + ) + return _.values(val) } - return memo; - }), - {}); - return _.values(val); - }; - // Load map layer with leaflet-providers - openStreetMap = L.tileLayer.provider("OpenStreetMap"); - openStreetMap.addTo(scope.map); - scope.map.setView([scope.center.lat, - scope.center.lng], - scope.center.zoom); - return scope.showMap = true; - }; - return { - restrict: 'E', - scope: { - markers: '=sbMarkers', - center: '=sbCenter', - baseLayer: '=sbBaseLayer', - markerCallback: '=sbMarkerCallback', - selectedGroups: '=sbSelectedGroups', - useClustering: '=?sbUseClustering', - restColor: '=?sbRestColor', // free color to use for grouping etc - oldMap: '=?sbOldMap' + // Load map layer with leaflet-providers + openStreetMap = L.tileLayer.provider("OpenStreetMap") + openStreetMap.addTo(scope.map) + scope.map.setView([scope.center.lat, scope.center.lng], scope.center.zoom) + return (scope.showMap = true) }, - link: link, - templateUrl: 'template/sb_map.html' - }; - } - ]); - -}).call(this); - -//# sourceMappingURL=sb_map.js.map + }), +]) From 918500b18f73de8049c7eabcf3ec0b53e6e0eee9 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 12:09:33 +0200 Subject: [PATCH 82/99] refactor: simplify code --- app/scripts/geo/geokorp-templates.js | 14 - app/scripts/geo/geokorp.js | 423 +++++++++------------------ app/scripts/korp.module.ts | 1 - 3 files changed, 133 insertions(+), 305 deletions(-) delete mode 100644 app/scripts/geo/geokorp-templates.js diff --git a/app/scripts/geo/geokorp-templates.js b/app/scripts/geo/geokorp-templates.js deleted file mode 100644 index 3bdb1ad99..000000000 --- a/app/scripts/geo/geokorp-templates.js +++ /dev/null @@ -1,14 +0,0 @@ -angular.module("sbMapTemplate", ["template/sb_map.html"]); - -angular.module("template/sb_map.html", []).run(["$templateCache", function($templateCache) { - $templateCache.put("template/sb_map.html", - "
      \n" + - "
      \n" + - "
      \n" + - "
      \n" + - " \n" + - "
      \n" + - "\n" + - "\n" + - ""); -}]); diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index bbfff1980..59ea09573 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -2,20 +2,21 @@ import { html } from "@/util" import angular from "angular" -const sbMap = angular.module("sbMap", ["sbMapTemplate"]) +const sbMap = angular.module("sbMap", []) -sbMap.filter("trust", function ($sce) { - return function (input) { - return $sce.trustAsHtml(input) - } -}) +sbMap.filter("trust", ($sce) => (input) => $sce.trustAsHtml(input)) sbMap.directive("sbMap", [ "$compile", "$timeout", "$rootScope", ($compile, $timeout, $rootScope) => ({ - templateUrl: "template/sb_map.html", + template: html`
      +
      +
      +
      + +
      `, restrict: "E", scope: { markers: "=sbMarkers", @@ -69,14 +70,10 @@ sbMap.directive("sbMap", [ }) createCircleMarker = function (color, diameter, borderRadius) { return L.divIcon({ - html: - '
      ', + html: html`
      `, iconSize: new L.Point(diameter, diameter), }) } @@ -86,107 +83,48 @@ sbMap.directive("sbMap", [ return createCircleMarker(color, 10, cluster ? 1 : 5) } createMultiMarkerIcon = function (markerData) { - var center, - circle, - color, - diameter, - elements, - grid, - gridSize, - height, - i, - id, - idx, - j, - marker, - markerClass, - neg, - ref, - ref1, - row, - something, - stop, - width, - x, - xOp, - y, - yOp - elements = (function () { - var i, len, results - results = [] - for (i = 0, len = markerData.length; i < len; i++) { - marker = markerData[i] - color = marker.color - diameter = (marker.point.rel / scope.maxRel) * 40 + 10 - results.push([ - diameter, - '
      ', - ]) - } - return results - })() - elements.sort(function (element1, element2) { - return element1[0] - element2[0] - }) - gridSize = Math.ceil(Math.sqrt(elements.length)) + 1 - gridSize = gridSize % 2 === 0 ? gridSize + 1 : gridSize - center = Math.floor(gridSize / 2) - grid = (function () { - var i, ref, results - results = [] - for (x = i = 0, ref = gridSize - 1; 0 <= ref ? i <= ref : i >= ref; x = 0 <= ref ? ++i : --i) { - results.push([]) - } - return results - })() - id = function (x) { - return x - } - neg = function (x) { - return -x + var idx, markerClass, row + + /** @type {[number, string][]} */ + const elements = [] + for (let i = 0; i < markerData.length; i++) { + const marker = markerData[i] + const diameter = (marker.point.rel / scope.maxRel) * 40 + 10 + elements.push([ + diameter, + html`
      `, + ]) } - for (idx = i = 0, ref = center; 0 <= ref ? i <= ref : i >= ref; idx = 0 <= ref ? ++i : --i) { - x = -1 - y = -1 - xOp = neg - yOp = neg - stop = idx === 0 ? 0 : idx * 4 - 1 - for ( - something = j = 0, ref1 = stop; - 0 <= ref1 ? j <= ref1 : j >= ref1; - something = 0 <= ref1 ? ++j : --j - ) { - if (x === -1) { - x = center + idx - } else { - x = x + xOp(1) - } - if (y === -1) { - y = center - } else { - y = y + yOp(1) - } - if (x === center - idx) { - xOp = id - } - if (y === center - idx) { - yOp = id - } - if (x === center + idx) { - xOp = neg - } - if (y === center + idx) { - yOp = neg - } - circle = elements.pop() + + elements.sort((element1, element2) => element1[0] - element2[0]) + + const gridSizeRaw = Math.ceil(Math.sqrt(elements.length)) + 1 + const gridSize = gridSizeRaw % 2 === 0 ? gridSizeRaw + 1 : gridSizeRaw + const center = Math.floor(gridSize / 2) + + /** @type {([number, string][] | [])[]} */ + let grid = [] + for (let i = 0; i <= gridSize - 1; i++) grid.push([]) + + const id = (x) => x + const neg = (x) => -x + for (let idx = 0; idx <= center; idx++) { + let x = -1 + let y = -1 + let xOp = neg + let yOp = neg + const stop = idx === 0 ? 0 : idx * 4 - 1 + for (let j = 0; j <= stop; ++j) { + x = x === -1 ? center + idx : x + xOp(1) + y = y === -1 ? center : y + yOp(1) + if (x === center - idx) xOp = id + if (y === center - idx) yOp = id + if (x === center + idx) xOp = neg + if (y === center + idx) yOp = neg + const circle = elements.pop() if (circle) { grid[y][x] = circle } else { @@ -196,139 +134,76 @@ sbMap.directive("sbMap", [ } // remove all empty arrays and elements // TODO don't create empty stuff?? - grid = _.filter(grid, function (row) { - return row.length > 0 - }) - grid = _.map(grid, function (row) { - return (row = _.filter(row, function (elem) { - return elem - })) - }) + grid = grid.filter((row) => row.length > 0) + grid = grid.map((row) => row.filter((elem) => elem)) + //# take largest element from each row and add to height - height = 0 - width = 0 - center = Math.floor(grid.length / 2) - grid = (function () { - var k, len, results - results = [] - for (idx = k = 0, len = grid.length; k < len; idx = ++k) { - row = grid[idx] - height = - height + - _.reduce( - row, - function (memo, val) { - if (val[0] > memo) { - return val[0] - } else { - return memo - } - }, - 0 - ) - if (idx < center) { - markerClass = "marker-bottom" - } - if (idx === center) { - width = _.reduce( - grid[center], - function (memo, val) { - return memo + val[0] - }, - 0 - ) - markerClass = "marker-middle" - } - if (idx > center) { - markerClass = "marker-top" - } - results.push( - '
      ' + - _.map(row, function (elem) { - return elem[1] - }).join("") + - "
      " - ) - } - return results - })() + let height = 0 + let width = 0 + const gridCenter = Math.floor(grid.length / 2) + + /** @type {string[]} */ + const grid2 = [] + for (let idx = 0; idx < grid.length; ++idx) { + row = grid[idx] + height += row.reduce((memo, val) => (val[0] > memo ? val[0] : memo), 0) + if (idx === gridCenter) { + width = grid[gridCenter].reduce((memo, val) => memo + val[0], 0) + markerClass = "marker-middle" + } else markerClass = idx > gridCenter ? "marker-top" : "marker-bottom" + grid2.push( + html`
      + ${row.map((elem) => elem[1]).join("")} +
      ` + ) + } return L.divIcon({ - html: grid.join(""), + html: grid2.join(""), iconSize: new L.Point(width, height), }) } + // use the previously calculated "scope.maxRel" to decide the sizes of the bars // in the cluster icon that is returned (between 5px and 50px) + /** + * @param clusterGroups {Record} + * @param restColor {string} + */ createClusterIcon = function (clusterGroups, restColor) { - var allGroups, visibleGroups - allGroups = _.keys(clusterGroups) - visibleGroups = allGroups.sort(function (group1, group2) { - return clusterGroups[group1].order - clusterGroups[group2].order - }) - if (allGroups.length > 4) { - visibleGroups = visibleGroups.splice(0, 3) - visibleGroups.push(restColor) + const groups = _.keys(clusterGroups) + groups.sort((group1, group2) => clusterGroups[group1].order - clusterGroups[group2].order) + if (groups.length > 4) { + groups.splice(3) + groups.push(restColor) } return function (cluster) { - var child, - color, - diameter, - divWidth, - elements, - group, - groupSize, - i, - j, - k, - len, - len1, - len2, - ref, - ref1, - rel, - sizes - sizes = {} - for (i = 0, len = visibleGroups.length; i < len; i++) { - group = visibleGroups[i] - sizes[group] = 0 - } - ref = cluster.getAllChildMarkers() - for (j = 0, len1 = ref.length; j < len1; j++) { - child = ref[j] - color = child.markerData.color - if (!(color in sizes)) { - color = restColor - } - rel = child.markerData.point.rel - sizes[color] = sizes[color] + rel - } - if (allGroups.length === 1) { - color = _.keys(sizes)[0] - groupSize = sizes[color] - diameter = (groupSize / scope.maxRel) * 45 + 5 + /** @type {Record} */ + const sizes = groups.reduce((map, color) => ({ ...map, [color]: 0 }), {}) + cluster.getAllChildMarkers().forEach((childMarker) => { + let color = childMarker.markerData.color + if (!(color in sizes)) color = restColor + sizes[color] += childMarker.markerData.point.rel + }) + + if (groups.length === 1) { + const color = groups[0] + const groupSize = sizes[color] + const diameter = (groupSize / scope.maxRel) * 45 + 5 return createCircleMarker(color, diameter, diameter) - } else { - elements = "" - ref1 = _.keys(sizes) - for (k = 0, len2 = ref1.length; k < len2; k++) { - color = ref1[k] - groupSize = sizes[color] - divWidth = (groupSize / scope.maxRel) * 45 + 5 - elements = - elements + - '
      ' - } - return L.divIcon({ - html: '
      ' + elements + "
      ", - iconSize: new L.Point(40, 50), - }) } + + const elements = Object.keys(sizes).map((color) => { + const groupSize = sizes[color] + const divWidth = (groupSize / scope.maxRel) * 45 + 5 + return html`
      ` + }) + return L.divIcon({ + html: html`
      ${elements.join("")}
      `, + iconSize: new L.Point(40, 50), + }) } } // check if the cluster with split into several clusters / markers @@ -408,20 +283,14 @@ sbMap.directive("sbMap", [ } return mouseOver(scope.selectedMarkers) }) - featureLayer.on("mouseover", function (e) { - if (e.layer.markerData instanceof Array) { - return mouseOver(e.layer.markerData) - } else { - return mouseOver([e.layer.markerData]) - } - }) - featureLayer.on("mouseout", function (e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers) - } else { - return mouseOut() - } - }) + featureLayer.on("mouseover", (e) => + e.layer.markerData instanceof Array + ? mouseOver(e.layer.markerData) + : mouseOver([e.layer.markerData]) + ) + featureLayer.on("mouseout", (e) => + scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + ) return featureLayer } // create marker cluster layer and all listeners @@ -434,20 +303,12 @@ sbMap.directive("sbMap", [ zoomToBoundsOnClick: false, iconCreateFunction: createClusterIcon(clusterGroups, restColor), }) - markerCluster.on("clustermouseover", function (e) { - return mouseOver( - _.map(e.layer.getAllChildMarkers(), function (layer) { - return layer.markerData - }) - ) - }) - markerCluster.on("clustermouseout", function (e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers) - } else { - return mouseOut() - } - }) + markerCluster.on("clustermouseover", (e) => + mouseOver(_.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData)) + ) + markerCluster.on("clustermouseout", (e) => + scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + ) markerCluster.on("clusterclick", function (e) { scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), function (layer) { return layer.markerData @@ -461,19 +322,11 @@ sbMap.directive("sbMap", [ scope.selectedMarkers = [e.layer.markerData] return mouseOver(scope.selectedMarkers) }) - markerCluster.on("mouseover", function (e) { - return mouseOver([e.layer.markerData]) - }) - markerCluster.on("mouseout", function (e) { - if (scope.selectedMarkers.length > 0) { - return mouseOver(scope.selectedMarkers) - } else { - return mouseOut() - } - }) - markerCluster.on("animationend", function (e) { - return updateMarkerSizes() - }) + markerCluster.on("mouseover", (e) => mouseOver([e.layer.markerData])) + markerCluster.on("mouseout", (e) => + scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + ) + markerCluster.on("animationend", (e) => updateMarkerSizes()) return markerCluster } // takes a list of markers and displays clickable (callback determined by directive user) info boxes @@ -529,11 +382,7 @@ sbMap.directive("sbMap", [ msgScope.color = marker.color compiled = $compile(scope.hoverTemplate) markerDiv = compiled(msgScope) - ;(function (marker) { - return markerDiv.bind("click", function () { - return scope.markerCallback(marker) - }) - })(marker) + markerDiv.bind("click", () => scope.markerCallback(marker)) content.push(markerDiv) } hoverInfoElem = angular.element(element.find(".hover-info-container")) @@ -556,14 +405,8 @@ sbMap.directive("sbMap", [ scope.selectedMarkers = [] return mouseOut() }) - scope.$watchCollection("selectedGroups", function (selectedGroups) { - return updateMarkers() - }) - scope.$watch("useClustering", function (newVal, oldVal) { - if (newVal === !oldVal) { - return updateMarkers() - } - }) + scope.$watchCollection("selectedGroups", () => updateMarkers()) + scope.$watch("useClustering", (newVal, oldVal) => newVal === !oldVal && updateMarkers()) updateMarkers = function () { var clusterGroups, color, diff --git a/app/scripts/korp.module.ts b/app/scripts/korp.module.ts index 94b151f6a..173a3f7d2 100644 --- a/app/scripts/korp.module.ts +++ b/app/scripts/korp.module.ts @@ -4,7 +4,6 @@ import "angular-ui-bootstrap" import "angular-spinner" import "angular-ui-sortable" import "@/geo/geokorp" -import "@/geo/geokorp-templates" import "angular-dynamic-locale" import "angular-filter" From c5e467fedb6817b9ec2d44f7e73a2fe746aa2588 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 15:37:32 +0200 Subject: [PATCH 83/99] refactor: simplify more --- app/scripts/geo/geokorp.js | 80 +++++++++++++------------------------- 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index 59ea09573..6d559cb64 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -29,20 +29,6 @@ sbMap.directive("sbMap", [ oldMap: "=?sbOldMap", }, link(scope, element, attrs) { - var createCircleMarker, - createClusterIcon, - createFeatureLayer, - createMarkerCluster, - createMarkerIcon, - createMultiMarkerIcon, - map, - mergeMarkers, - mouseOut, - mouseOver, - openStreetMap, - shouldZooomToBounds, - updateMarkerSizes, - updateMarkers scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false scope.showMap = false @@ -57,8 +43,8 @@ sbMap.directive("sbMap", [
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      ` - map = angular.element(element.find(".map-container")) - scope.map = L.map(map[0], { + const container = angular.element(element.find(".map-container")).first() + scope.map = L.map(container, { minZoom: 1, maxZoom: 13, }).setView([51.505, -0.09], 13) @@ -68,7 +54,7 @@ sbMap.directive("sbMap", [ return scope.map.invalidateSize() }, 0) }) - createCircleMarker = function (color, diameter, borderRadius) { + function createCircleMarker(color, diameter, borderRadius) { return L.divIcon({ html: html`
      (val[0] > memo ? val[0] : memo), 0) if (idx === gridCenter) { width = grid[gridCenter].reduce((memo, val) => memo + val[0], 0) - markerClass = "marker-middle" - } else markerClass = idx > gridCenter ? "marker-top" : "marker-bottom" + } + const markerClass = + idx === gridCenter ? "marker-middle" : idx > gridCenter ? "marker-top" : "marker-bottom" grid2.push( html`
      ${row.map((elem) => elem[1]).join("")} @@ -169,7 +154,7 @@ sbMap.directive("sbMap", [ * @param clusterGroups {Record} * @param restColor {string} */ - createClusterIcon = function (clusterGroups, restColor) { + function createClusterIcon(clusterGroups, restColor) { const groups = _.keys(clusterGroups) groups.sort((group1, group2) => clusterGroups[group1].order - clusterGroups[group2].order) if (groups.length > 4) { @@ -209,19 +194,14 @@ sbMap.directive("sbMap", [ // check if the cluster with split into several clusters / markers // on zooom // TODO: does not work in some cases - shouldZooomToBounds = function (cluster) { - var boundsZoom, childCluster, childClusters, i, len, newClusters, zoom - childClusters = cluster._childClusters.slice() - map = cluster._group._map - boundsZoom = map.getBoundsZoom(cluster._bounds) - zoom = cluster._zoom + 1 + function shouldZooomToBounds(cluster) { + let childClusters = cluster._childClusters.slice() + const map = cluster._group._map + const boundsZoom = map.getBoundsZoom(cluster._bounds) + let zoom = cluster._zoom + 1 while (childClusters.length > 0 && boundsZoom > zoom) { - zoom = zoom + 1 - newClusters = [] - for (i = 0, len = childClusters.length; i < len; i++) { - childCluster = childClusters[i] - newClusters = newClusters.concat(childCluster._childClusters) - } + zoom += 1 + const newClusters = childClusters.flatMap((childCluster) => childCluster._childClusters) childClusters = newClusters } return childClusters.length > 1 @@ -230,9 +210,8 @@ sbMap.directive("sbMap", [ // this is the max relative value of any cluster and can be used to // calculate marker sizes // TODO this needs to use the "rest" group when doing calcuations!! - updateMarkerSizes = function () { - var bounds - bounds = scope.map.getBounds() + function updateMarkerSizes() { + const bounds = scope.map.getBounds() scope.maxRel = 0 if (scope.useClustering && scope.markerCluster) { scope.map.eachLayer(function (layer) { @@ -272,9 +251,8 @@ sbMap.directive("sbMap", [ // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this // create normal layer (and all listeners) to be used when clustering is not enabled - createFeatureLayer = function () { - var featureLayer - featureLayer = L.featureGroup() + function createFeatureLayer() { + const featureLayer = L.featureGroup() featureLayer.on("click", function (e) { if (e.layer.markerData instanceof Array) { scope.selectedMarkers = e.layer.markerData @@ -294,9 +272,8 @@ sbMap.directive("sbMap", [ return featureLayer } // create marker cluster layer and all listeners - createMarkerCluster = function (clusterGroups, restColor) { - var markerCluster - markerCluster = L.markerClusterGroup({ + function createMarkerCluster(clusterGroups, restColor) { + const markerCluster = L.markerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, maxClusterRadius: 40, @@ -330,7 +307,7 @@ sbMap.directive("sbMap", [ return markerCluster } // takes a list of markers and displays clickable (callback determined by directive user) info boxes - mouseOver = function (markerData) { + function mouseOver(markerData) { return $timeout(function () { return scope.$apply(function () { var compiled, @@ -394,7 +371,7 @@ sbMap.directive("sbMap", [ }) }, 0) } - mouseOut = function () { + function mouseOut() { var hoverInfoElem hoverInfoElem = angular.element(element.find(".hover-info-container")) hoverInfoElem.css("opacity", "0") @@ -407,7 +384,7 @@ sbMap.directive("sbMap", [ }) scope.$watchCollection("selectedGroups", () => updateMarkers()) scope.$watch("useClustering", (newVal, oldVal) => newVal === !oldVal && updateMarkers()) - updateMarkers = function () { + function updateMarkers() { var clusterGroups, color, group, @@ -498,7 +475,7 @@ sbMap.directive("sbMap", [ } // merge lists of markers into one list with several hits in one marker // also calculate maxRel - mergeMarkers = function (markerLists) { + function mergeMarkers(markerLists) { var val scope.maxRel = 0 val = _.reduce( @@ -530,8 +507,7 @@ sbMap.directive("sbMap", [ return _.values(val) } // Load map layer with leaflet-providers - openStreetMap = L.tileLayer.provider("OpenStreetMap") - openStreetMap.addTo(scope.map) + L.tileLayer.provider("OpenStreetMap").addTo(scope.map) scope.map.setView([scope.center.lat, scope.center.lng], scope.center.zoom) return (scope.showMap = true) }, From 088d72bc7a7a38b3f629db5754d19d8b0aca012f Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 15:37:38 +0200 Subject: [PATCH 84/99] revert: "refactor: tab-hash component" This reverts commit 0c6e186b67bb39efc0db54b405f317f287c074fe. It left dynamic tabs broken. --- app/scripts/components/results.js | 197 +++++++++++++-------------- app/scripts/components/searchtabs.js | 50 ++++--- app/scripts/components/tab-hash.ts | 93 ------------- app/scripts/directives/tab-hash.ts | 77 +++++++++++ app/scripts/services/utils.ts | 4 +- app/scripts/urlparams.ts | 2 - 6 files changed, 199 insertions(+), 224 deletions(-) delete mode 100644 app/scripts/components/tab-hash.ts create mode 100644 app/scripts/directives/tab-hash.ts diff --git a/app/scripts/components/results.js b/app/scripts/components/results.js index b0b9f2659..9ef766a7f 100644 --- a/app/scripts/components/results.js +++ b/app/scripts/components/results.js @@ -11,8 +11,8 @@ import "@/components/korp-error" import "@/components/kwic" import "@/components/statistics" import "@/components/sidebar" -import "@/components/tab-hash" import "@/components/word-picture" +import "@/directives/tab-hash" import "@/directives/tab-preloader" angular.module("korpApp").component("results", { @@ -21,110 +21,105 @@ angular.module("korpApp").component("results", {
      - - - - KWIC - -
      - - -
      -
      - - {{'statistics' | loc:$root.lang}} - - + + + KWIC + +
      - - - - - {{'word_picture' | loc:$root.lang}} - - -
      - -
      - -
      - - - - - - - + prev-request="proxy.prevRequest" + corpus-order="corpusOrder" + > +
      +
      + + {{'statistics' | loc:$root.lang}} + + + + + + + + {{'word_picture' | loc:$root.lang}} + + +
      + +
      + +
      + + + + + +
      diff --git a/app/scripts/components/searchtabs.js b/app/scripts/components/searchtabs.js index 2c72543c5..1d2126c7b 100644 --- a/app/scripts/components/searchtabs.js +++ b/app/scripts/components/searchtabs.js @@ -10,39 +10,37 @@ import "@/components/extended/extended-standard" import "@/components/extended/extended-parallel" import "@/components/advanced-search" import "@/components/compare-search" -import "@/components/tab-hash" import "@/directives/click-cover" import "@/directives/reduce-select" +import "@/directives/tab-hash" angular.module("korpApp").component("searchtabs", { template: html`
      - - - - - - -
      - - -
      -
      - - - - - - {{'compare' | loc:$root.lang}} - {{$ctrl.savedSearches.length}} - - - -
      - + + + + + +
      + +
      -
      - + + + + + + + {{'compare' | loc:$root.lang}} + {{$ctrl.savedSearches.length}} + + + +
      + +
      +
      - maxTab: number - setSelected: (index: number, ignoreCheck?: boolean) => void - newDynamicTab: () => void - closeDynamicTab: () => void -} - -/** Surround a `` element with this to sync selected tab number to url parameter `key`. */ -angular.module("korpApp").component("tabHash", { - template: html`
      `, - transclude: true, - bindings: { - key: "@", - }, - controller: [ - "utils", - "$element", - "$scope", - "$timeout", - function (utils: UtilsService, $element: IRootElementService, $scope: TabHashScope, $timeout: ITimeoutService) { - const $ctrl = this as TabHashController - - let tabsetScope - let contentScope - - $ctrl.$onInit = () => { - // Timeout needed to find elements created by uib-tabset - $timeout(function () { - tabsetScope = $element.find(".tabbable").scope() as any - contentScope = $element.find(".tab-content").scope() as any - - $scope.fixedTabs = {} - $scope.maxTab = -1 - for (let tab of contentScope.tabset.tabs) { - $scope.fixedTabs[tab.index] = tab - if (tab.index > $scope.maxTab) { - $scope.maxTab = tab.index - } - } - watchHash() - }, 0) - } - - const watchHash = () => - utils.setupHash($scope, { - expr: () => tabsetScope.activeTab, - val_in(val) { - $scope.setSelected(Number(val)) - return tabsetScope.activeTab - }, - key: $ctrl.key, - default: "0", - }) - - $scope.setSelected = function (index, ignoreCheck) { - if (!ignoreCheck && !(index in $scope.fixedTabs)) { - index = $scope.maxTab - } - tabsetScope.activeTab = index - } - - $scope.newDynamicTab = function () { - $timeout(function () { - $scope.setSelected($scope.maxTab + 1, true) - $scope.maxTab += 1 - }, 0) - } - - $scope.closeDynamicTab = function () { - $timeout(function () { - $scope.maxTab = -1 - for (let tab of contentScope.tabset.tabs) { - if (tab.index > $scope.maxTab) { - $scope.maxTab = tab.index - } - } - }, 0) - } - }, - ], -}) diff --git a/app/scripts/directives/tab-hash.ts b/app/scripts/directives/tab-hash.ts new file mode 100644 index 000000000..11fa58737 --- /dev/null +++ b/app/scripts/directives/tab-hash.ts @@ -0,0 +1,77 @@ +/** @format */ +import _ from "lodash" +import angular, { IScope, ITimeoutService } from "angular" +import { UtilsService } from "@/services/utils" +import { LocationService } from "@/urlparams" +import "@/services/utils" + +type TabHashScope = IScope & { + activeTab: number + fixedTabs: Record + maxTab: number + setSelected: (index: number, ignoreCheck?: boolean) => void + newDynamicTab: () => void + closeDynamicTab: () => void +} + +angular.module("korpApp").directive("tabHash", [ + "utils", + "$location", + "$timeout", + (utils: UtilsService, $location: LocationService, $timeout: ITimeoutService) => ({ + link(scope, elem, attr) { + const s = scope as TabHashScope + const contentScope = elem.find(".tab-content").scope() as any + + const watchHash = () => + utils.setupHash(s, { + expr: "activeTab", + val_in(val) { + s.setSelected(Number(val)) + return s.activeTab + }, + key: attr.tabHash, + default: "0", + }) + + s.setSelected = function (index, ignoreCheck) { + if (!ignoreCheck && !(index in s.fixedTabs)) { + index = s.maxTab + } + s.activeTab = index + } + + const initTab = parseInt($location.search()[attr.tabHash]) || 0 + $timeout(function () { + s.fixedTabs = {} + s.maxTab = -1 + for (let tab of contentScope.tabset.tabs) { + s.fixedTabs[tab.index] = tab + if (tab.index > s.maxTab) { + s.maxTab = tab.index + } + } + s.setSelected(initTab) + watchHash() + }, 0) + + s.newDynamicTab = function () { + $timeout(function () { + s.setSelected(s.maxTab + 1, true) + s.maxTab += 1 + }, 0) + } + + s.closeDynamicTab = function () { + $timeout(function () { + s.maxTab = -1 + for (let tab of contentScope.tabset.tabs) { + if (tab.index > s.maxTab) { + s.maxTab = tab.index + } + } + }, 0) + } + }, + }), +]) diff --git a/app/scripts/services/utils.ts b/app/scripts/services/utils.ts index b0d3a644b..7070b9312 100644 --- a/app/scripts/services/utils.ts +++ b/app/scripts/services/utils.ts @@ -15,7 +15,7 @@ type SetupHashConfigItem /** A function on the scope to pass value to, instead of setting `scope_name` */ scope_func?: string /** Expression to watch for changes; defaults to `scope_name` */ - expr?: string | (() => HashParams[K]) + expr?: string /** Default value of the scope variable, corresponding to the url param being empty */ default?: HashParams[K] /** Runs when the value is changed in scope or url */ @@ -56,7 +56,7 @@ angular.module("korpApp").factory("utils", [ scope.$watch(() => $location.search(), onWatch) // Sync from scope to url - scope.$watch((config.expr as any) || config.scope_name || config.key, (val: any) => { + scope.$watch(config.expr || config.scope_name || config.key, (val: any) => { val = config.val_out ? config.val_out(val) : val if (val === config.default) { val = null diff --git a/app/scripts/urlparams.ts b/app/scripts/urlparams.ts index 032864124..285b76429 100644 --- a/app/scripts/urlparams.ts +++ b/app/scripts/urlparams.ts @@ -37,8 +37,6 @@ export type HashParams = { random_seed?: `${number}` /** Whether the reading mode is enabled */ reading_mode?: boolean - /** Current tab of results */ - result_tab?: `${number}` /** * Search query for Simple or Advanced search: `|` * where `mode` can be: From 7e9f03f57a2600cb48705d29e6392c13e3644d65 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Wed, 11 Sep 2024 19:32:55 +0200 Subject: [PATCH 85/99] refactor: simplify more --- app/scripts/geo/geokorp.js | 281 ++++++++++++++----------------------- package.json | 3 + yarn.lock | 30 +++- 3 files changed, 134 insertions(+), 180 deletions(-) diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index 6d559cb64..a6b6029c3 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -1,11 +1,26 @@ /** @format */ -import { html } from "@/util" import angular from "angular" +import L, { Map } from "leaflet" +import { html } from "@/util" const sbMap = angular.module("sbMap", []) sbMap.filter("trust", ($sce) => (input) => $sce.trustAsHtml(input)) +/** + * @typedef SbMapScope + * @prop {Map} map + * @prop {string[]} selectedGroups + * @prop {number} maxRel - Maximum frequency in current result + * @prop {Record} markers + * @prop {string} restColor + */ + +/** + * @typedef Marker + * @prop {string} color + */ + sbMap.directive("sbMap", [ "$compile", "$timeout", @@ -28,6 +43,10 @@ sbMap.directive("sbMap", [ restColor: "=?sbRestColor", // free color to use for grouping etc oldMap: "=?sbOldMap", }, + /** + * @param {SbMapScope} scope + * @returns + */ link(scope, element, attrs) { scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false @@ -43,7 +62,7 @@ sbMap.directive("sbMap", [
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      ` - const container = angular.element(element.find(".map-container")).first() + const container = element.find(".map-container")[0] scope.map = L.map(container, { minZoom: 1, maxZoom: 13, @@ -155,7 +174,7 @@ sbMap.directive("sbMap", [ * @param restColor {string} */ function createClusterIcon(clusterGroups, restColor) { - const groups = _.keys(clusterGroups) + const groups = Object.keys(clusterGroups) groups.sort((group1, group2) => clusterGroups[group1].order - clusterGroups[group2].order) if (groups.length > 4) { groups.splice(3) @@ -163,7 +182,7 @@ sbMap.directive("sbMap", [ } return function (cluster) { /** @type {Record} */ - const sizes = groups.reduce((map, color) => ({ ...map, [color]: 0 }), {}) + const sizes = groups.reduce((sizes, color) => ({ ...sizes, [color]: 0 }), {}) cluster.getAllChildMarkers().forEach((childMarker) => { let color = childMarker.markerData.color if (!(color in sizes)) color = restColor @@ -214,36 +233,17 @@ sbMap.directive("sbMap", [ const bounds = scope.map.getBounds() scope.maxRel = 0 if (scope.useClustering && scope.markerCluster) { - scope.map.eachLayer(function (layer) { - var child, color, i, j, len, len1, ref, ref1, rel, results, sumRel, sumRels + scope.map.eachLayer((layer) => { if (layer.getChildCount) { - sumRels = {} - ref = layer.getAllChildMarkers() - for (i = 0, len = ref.length; i < len; i++) { - child = ref[i] - color = child.markerData.color - if (!sumRels[color]) { - sumRels[color] = 0 - } - sumRels[color] = sumRels[color] + child.markerData.point.rel + /** @type {Record} */ + const sumRels = {} + for (const child of layer.getAllChildMarkers()) { + const color = child.markerData.color + if (!sumRels[color]) sumRels[color] = 0 + sumRels[color] += child.markerData.point.rel } - ref1 = _.values(sumRels) - results = [] - for (j = 0, len1 = ref1.length; j < len1; j++) { - sumRel = ref1[j] - if (sumRel > scope.maxRel) { - results.push((scope.maxRel = sumRel)) - } else { - results.push(void 0) - } - } - return results - } else if (layer.markerData) { - rel = layer.markerData.point.rel - if (rel > scope.maxRel) { - return (scope.maxRel = rel) - } - } + scope.maxRel = Math.max(scope.maxRel, ...Object.values(sumRels)) + } else if (layer.markerData?.point.rel > scope.maxRel) scope.maxRel = layer.markerData.point.rel }) return scope.markerCluster.refreshClusters() } @@ -253,12 +253,9 @@ sbMap.directive("sbMap", [ // create normal layer (and all listeners) to be used when clustering is not enabled function createFeatureLayer() { const featureLayer = L.featureGroup() - featureLayer.on("click", function (e) { - if (e.layer.markerData instanceof Array) { - scope.selectedMarkers = e.layer.markerData - } else { - scope.selectedMarkers = [e.layer.markerData] - } + featureLayer.on("click", (e) => { + scope.selectedMarkers = + e.layer.markerData instanceof Array ? e.layer.markerData : [e.layer.markerData] return mouseOver(scope.selectedMarkers) }) featureLayer.on("mouseover", (e) => @@ -271,7 +268,12 @@ sbMap.directive("sbMap", [ ) return featureLayer } - // create marker cluster layer and all listeners + + /** + * create marker cluster layer and all listeners + * @param {Record} clusterGroups + * @param {string} restColor + */ function createMarkerCluster(clusterGroups, restColor) { const markerCluster = L.markerClusterGroup({ spiderfyOnMaxZoom: false, @@ -286,16 +288,14 @@ sbMap.directive("sbMap", [ markerCluster.on("clustermouseout", (e) => scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() ) - markerCluster.on("clusterclick", function (e) { - scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), function (layer) { - return layer.markerData - }) + markerCluster.on("clusterclick", (e) => { + scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData) mouseOver(scope.selectedMarkers) if (shouldZooomToBounds(e.layer)) { return e.layer.zoomToBounds() } }) - markerCluster.on("click", function (e) { + markerCluster.on("click", (e) => { scope.selectedMarkers = [e.layer.markerData] return mouseOver(scope.selectedMarkers) }) @@ -308,61 +308,41 @@ sbMap.directive("sbMap", [ } // takes a list of markers and displays clickable (callback determined by directive user) info boxes function mouseOver(markerData) { - return $timeout(function () { - return scope.$apply(function () { - var compiled, - content, - hoverInfoElem, - i, - len, - marker, - markerDiv, - msgScope, - name, - oldMap, - selectedMarkers - content = [] + return $timeout(() => { + return scope.$apply(() => { // support for "old" map - oldMap = false - if (markerData[0].names) { - oldMap = true - selectedMarkers = (function () { - var i, len, ref, results - ref = _.keys(markerData[0].names) - results = [] - for (i = 0, len = ref.length; i < len; i++) { - name = ref[i] - results.push({ - color: markerData[0].color, - searchCqp: markerData[0].searchCqp, - point: { - name: name, - abs: markerData[0].names[name].abs_occurrences, - rel: markerData[0].names[name].rel_occurrences, - }, - }) - } - return results - })() + // TODO Usage was removed in 2020 (39b34962), remove support? + const oldMap = !!markerData[0].names + + let selectedMarkers + if (oldMap) { + selectedMarkers = _.map(markerData[0].names).map((thing, name) => ({ + color: markerData[0].color, + searchCqp: markerData[0].searchCqp, + point: { + name, + abs: thing.abs_occurrences, + rel: thing.rel_occurrences, + }, + })) } else { - markerData.sort(function (markerData1, markerData2) { - return markerData2.point.rel - markerData1.point.rel - }) + markerData.sort((a, b) => b.point.rel - a.point.rel) selectedMarkers = markerData } - for (i = 0, len = selectedMarkers.length; i < len; i++) { - marker = selectedMarkers[i] - msgScope = $rootScope.$new(true) + + const content = selectedMarkers.map((marker) => { + const msgScope = $rootScope.$new(true) msgScope.showLabel = !oldMap msgScope.point = marker.point msgScope.label = marker.label msgScope.color = marker.color - compiled = $compile(scope.hoverTemplate) - markerDiv = compiled(msgScope) + const compiled = $compile(scope.hoverTemplate) + const markerDiv = compiled(msgScope) markerDiv.bind("click", () => scope.markerCallback(marker)) - content.push(markerDiv) - } - hoverInfoElem = angular.element(element.find(".hover-info-container")) + return markerDiv + }) + + const hoverInfoElem = angular.element(element.find(".hover-info-container")) hoverInfoElem.empty() hoverInfoElem.append(content) hoverInfoElem[0].scrollTop = 0 @@ -372,42 +352,21 @@ sbMap.directive("sbMap", [ }, 0) } function mouseOut() { - var hoverInfoElem - hoverInfoElem = angular.element(element.find(".hover-info-container")) + const hoverInfoElem = angular.element(element.find(".hover-info-container")) hoverInfoElem.css("opacity", "0") return hoverInfoElem.css("display", "none") } scope.showHoverInfo = false - scope.map.on("click", function (e) { + scope.map.on("click", (e) => { scope.selectedMarkers = [] return mouseOut() }) + scope.$watchCollection("selectedGroups", () => updateMarkers()) scope.$watch("useClustering", (newVal, oldVal) => newVal === !oldVal && updateMarkers()) + function updateMarkers() { - var clusterGroups, - color, - group, - groupData, - i, - j, - k, - l, - len, - len1, - len2, - len3, - marker, - markerData, - markerGroup, - markerGroupId, - marker_id, - markers, - ref, - ref1, - selectedGroups - selectedGroups = scope.selectedGroups - markers = scope.markers + const selectedGroups = scope.selectedGroups if (scope.markerCluster) { scope.map.removeLayer(scope.markerCluster) } @@ -415,14 +374,8 @@ sbMap.directive("sbMap", [ scope.map.removeLayer(scope.featureLayer) } if (scope.useClustering) { - clusterGroups = {} - for (i = 0, len = selectedGroups.length; i < len; i++) { - group = selectedGroups[i] - groupData = markers[group] - clusterGroups[groupData.color] = { - order: groupData.order, - } - } + const selectedMarkers = selectedGroups.map((group) => scope.markers[group]) + const clusterGroups = _.groupBy(selectedMarkers, "color") scope.markerCluster = createMarkerCluster(clusterGroups, scope.restColor) scope.map.addLayer(scope.markerCluster) } else { @@ -430,18 +383,14 @@ sbMap.directive("sbMap", [ scope.map.addLayer(scope.featureLayer) } if (scope.useClustering || scope.oldMap) { - for (j = 0, len1 = selectedGroups.length; j < len1; j++) { - markerGroupId = selectedGroups[j] - markerGroup = markers[markerGroupId] - color = markerGroup.color + for (const group of selectedGroups) { + const markerGroup = scope.markers[group] scope.maxRel = 0 - ref = _.keys(markerGroup.markers) - for (k = 0, len2 = ref.length; k < len2; k++) { - marker_id = ref[k] - markerData = markerGroup.markers[marker_id] - markerData.color = color - marker = L.marker([markerData.lat, markerData.lng], { - icon: createMarkerIcon(color, !scope.oldMap && selectedGroups.length !== 1), + for (const markerId in markerGroup.markers) { + const markerData = markerGroup.markers[markerId] + markerData.color = markerGroup.color + const marker = L.marker([markerData.lat, markerData.lng], { + icon: createMarkerIcon(markerGroup.color, !scope.oldMap && selectedGroups.length !== 1), }) marker.markerData = markerData if (scope.useClustering) { @@ -452,19 +401,10 @@ sbMap.directive("sbMap", [ } } } else { - markers = (function () { - var l, len3, results - results = [] - for (l = 0, len3 = selectedGroups.length; l < len3; l++) { - markerGroupId = selectedGroups[l] - results.push(markers[markerGroupId]) - } - return results - })() - ref1 = mergeMarkers(_.values(markers)) - for (l = 0, len3 = ref1.length; l < len3; l++) { - markerData = ref1[l] - marker = L.marker([markerData.lat, markerData.lng], { + const markers = selectedGroups.map((group) => scope.markers[group]) + const markersMerged = mergeMarkers(markers) + for (const markerData of markersMerged) { + const marker = L.marker([markerData.lat, markerData.lng], { icon: createMultiMarkerIcon(markerData.markerData), }) marker.markerData = markerData.markerData @@ -476,40 +416,29 @@ sbMap.directive("sbMap", [ // merge lists of markers into one list with several hits in one marker // also calculate maxRel function mergeMarkers(markerLists) { - var val scope.maxRel = 0 - val = _.reduce( - markerLists, - function (memo, val) { - var latLng, markerData, markerId, ref - ref = val.markers - for (markerId in ref) { - markerData = ref[markerId] - markerData.color = val.color - latLng = markerData.lat + "," + markerData.lng - if (markerData.point.rel > scope.maxRel) { - scope.maxRel = markerData.point.rel - } - if (latLng in memo) { - memo[latLng].markerData.push(markerData) - } else { - memo[latLng] = { - markerData: [markerData], - } - memo[latLng].lat = markerData.lat - memo[latLng].lng = markerData.lng + const val = markerLists.reduce((memo, parent) => { + for (const child of Object.values(parent.markers)) { + child.color = parent.color + const latLng = child.lat + "," + child.lng + if (child.point.rel > scope.maxRel) scope.maxRel = child.point.rel + if (latLng in memo) memo[latLng].markerData.push(child) + else + memo[latLng] = { + markerData: [child], + lat: child.lat, + lng: child.lng, } - } - return memo - }, - {} - ) - return _.values(val) + } + return memo + }, {}) + return Object.values(val) } + // Load map layer with leaflet-providers L.tileLayer.provider("OpenStreetMap").addTo(scope.map) scope.map.setView([scope.center.lat, scope.center.lng], scope.center.zoom) - return (scope.showMap = true) + scope.showMap = true }, }), ]) diff --git a/package.json b/package.json index abcb21e44..622043b43 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "@types/angular-dynamic-locale": "^0.1.35", "@types/angular-ui-bootstrap": "^1.0.7", "@types/jqueryui": "^1.12.23", + "@types/leaflet": "^1.9.12", + "@types/leaflet-providers": "^1.2.4", + "@types/leaflet.markercluster": "^1.5.4", "angular": "1.8.3", "angular-dynamic-locale": "0.1.38", "angular-filter": "0.5.17", diff --git a/yarn.lock b/yarn.lock index 728ad6889..4f92d30e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,6 +255,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/geojson@*": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + "@types/html-minifier-terser@^6.0.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" @@ -303,6 +308,27 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/leaflet-providers@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/leaflet-providers/-/leaflet-providers-1.2.4.tgz#284e8a197d4c2dbfeda436842e03b2adb502be16" + integrity sha512-4wYEpreixp+G5t510s202eQ5eubOmxHevIfCNrzUZbzp50XQsC9lSetX7MnPl3ANRJnUBbCWOzZ9EQFiH0Jm+g== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet.markercluster@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.5.4.tgz#2ab43417cf3f6a42d0f1baf4e1c8f659cf1dc3a1" + integrity sha512-tfMP8J62+wfsVLDLGh5Zh1JZxijCaBmVsMAX78MkLPwvPitmZZtSin5aWOVRhZrCS+pEOZwNzexbfWXlY+7yjg== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet@*", "@types/leaflet@^1.9.12": + version "1.9.12" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.12.tgz#a6626a0b3fba36fd34723d6e95b22e8024781ad6" + integrity sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg== + dependencies: + "@types/geojson" "*" + "@types/lodash@^4.14.118": version "4.14.191" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" @@ -1946,10 +1972,6 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -geokorp@spraakbanken/geokorp#1.5.0: - version "1.5.0" - resolved "https://codeload.github.com/spraakbanken/geokorp/tar.gz/fa2e6b802d60723cbf5ebb10fb45f320c4b13893" - get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" From a6d8bb5ebe32cfc38073c110f577aadc560bbc6a Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Sep 2024 12:50:10 +0200 Subject: [PATCH 86/99] refactor: move styles --- app/index.ts | 1 - app/scripts/geo/geokorp.css | 133 ---------------------------------- app/scripts/geo/geokorp.js | 1 + app/scripts/geo/geokorp.scss | 134 +++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 134 deletions(-) delete mode 100644 app/scripts/geo/geokorp.css create mode 100644 app/scripts/geo/geokorp.scss diff --git a/app/index.ts b/app/index.ts index e02a8bdce..6cc810662 100644 --- a/app/index.ts +++ b/app/index.ts @@ -27,7 +27,6 @@ require("rickshaw/rickshaw.css") require("leaflet/dist/leaflet.css") require("leaflet.markercluster/dist/MarkerCluster.css") -require("./scripts/geo/geokorp.css") require("components-jqueryui/themes/smoothness/jquery-ui.min.css") require("./styles/_bootstrap-custom.scss") diff --git a/app/scripts/geo/geokorp.css b/app/scripts/geo/geokorp.css deleted file mode 100644 index a326d6a50..000000000 --- a/app/scripts/geo/geokorp.css +++ /dev/null @@ -1,133 +0,0 @@ -sb-map { - padding: 8px; -} - -sb-map .leaflet-div-icon { - background: none; - border: none; -} - -sb-map .cluster-geokorp-marker-group { - position: absolute; - bottom: 0; - width: 40px; -} - -sb-map .cluster-geokorp-marker { - width: 10px; - border-radius: 1px; - display: inline-block; -} - -sb-map .marker-top .geokorp-multi-marker { - vertical-align: top; -} - -sb-map .marker-middle .geokorp-multi-marker { - vertical-align: middle; -} - -sb-map .marker-bottom .geokorp-multi-marker { - vertical-align: bottom; -} - -sb-map .geokorp-multi-marker { - opacity: 0.85; - display: inline-block; -} - -sb-map .geokorp-marker { - opacity: 0.93; -} - -sb-map .cluster-text { - font-weight: bold; -} - -sb-map .cluster-icon { - border-radius: 15px; - width: 30px !important; - height: 30px !important; - z-index: 400; - position: absolute; - padding-top: 5px; - padding-left: 1px; -} - -sb-map .leaflet-marker-icon.leaflet-div-icon.leaflet-clickable { - border: none; - background-color: transparent; -} - -sb-map .leaflet-popup-content-wrapper { - border-radius: 4px; - background-color: #f6f6f6; - background-image: linear-gradient(to bottom, #fff, #e6e6e6); - background-repeat: repeat-x; -} - -sb-map .leaflet-popup-tip { - background-color: #e6e6e6; -} - -sb-map .leaflet-bar a:link, sb-map .leaflet-bar a:visited { - color: black; -} - -sb-map .swatch { - width: 10px; - height: 10px; - display: inline-block; - margin-right: 5px; -} - -sb-map .marker-cluster-small { - background-color: rgba(136, 220, 168, 0.6); -} - -sb-map .marker-cluster-small div { - background-color: rgba(136, 220, 168, 0.6); -} - -sb-map .map { - border: 1px solid black; - position: relative; - height: 522px; -} - -sb-map .map-container { - height: 520px; -} - -sb-map .map-outer-container { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; -} - -sb-map .hover-info-container { - margin: 5px; - border-radius: 5px; - width: 200px; - height: 500px; - overflow: auto; - position: absolute; - top: 0; - right: 0; - z-index: 800; - opacity: 1; - transition: opacity 500ms; -} - -sb-map .hover-info { - z-index: 10; - background-color: #f6f6f6; - background-image: linear-gradient(to bottom, #fff, #e6e6e6); - background-repeat: repeat-x; - padding: 10px; - margin: 10px; - border-radius: 4px; - cursor: pointer; -} diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index a6b6029c3..9784a3984 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -2,6 +2,7 @@ import angular from "angular" import L, { Map } from "leaflet" import { html } from "@/util" +import "./geokorp.scss" const sbMap = angular.module("sbMap", []) diff --git a/app/scripts/geo/geokorp.scss b/app/scripts/geo/geokorp.scss new file mode 100644 index 000000000..071251113 --- /dev/null +++ b/app/scripts/geo/geokorp.scss @@ -0,0 +1,134 @@ +sb-map { + padding: 8px; + + .leaflet-div-icon { + background: none; + border: none; + } + + .cluster-geokorp-marker-group { + position: absolute; + bottom: 0; + width: 40px; + } + + .cluster-geokorp-marker { + width: 10px; + border-radius: 1px; + display: inline-block; + } + + .marker-top .geokorp-multi-marker { + vertical-align: top; + } + + .marker-middle .geokorp-multi-marker { + vertical-align: middle; + } + + .marker-bottom .geokorp-multi-marker { + vertical-align: bottom; + } + + .geokorp-multi-marker { + opacity: 0.85; + display: inline-block; + } + + .geokorp-marker { + opacity: 0.93; + } + + .cluster-text { + font-weight: bold; + } + + .cluster-icon { + border-radius: 15px; + width: 30px !important; + height: 30px !important; + z-index: 400; + position: absolute; + padding-top: 5px; + padding-left: 1px; + } + + .leaflet-marker-icon.leaflet-div-icon.leaflet-clickable { + border: none; + background-color: transparent; + } + + .leaflet-popup-content-wrapper { + border-radius: 4px; + background-color: #f6f6f6; + background-image: linear-gradient(to bottom, #fff, #e6e6e6); + background-repeat: repeat-x; + } + + .leaflet-popup-tip { + background-color: #e6e6e6; + } + + .leaflet-bar a:link, + .leaflet-bar a:visited { + color: black; + } + + .swatch { + width: 10px; + height: 10px; + display: inline-block; + margin-right: 5px; + } + + .marker-cluster-small { + background-color: rgba(136, 220, 168, 0.6); + } + + .marker-cluster-small div { + background-color: rgba(136, 220, 168, 0.6); + } + + .map { + border: 1px solid black; + position: relative; + height: 522px; + } + + .map-container { + height: 520px; + } + + .map-outer-container { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + } + + .hover-info-container { + margin: 5px; + border-radius: 5px; + width: 200px; + height: 500px; + overflow: auto; + position: absolute; + top: 0; + right: 0; + z-index: 800; + opacity: 1; + transition: opacity 500ms; + } + + .hover-info { + z-index: 10; + background-color: #f6f6f6; + background-image: linear-gradient(to bottom, #fff, #e6e6e6); + background-repeat: repeat-x; + padding: 10px; + margin: 10px; + border-radius: 4px; + cursor: pointer; + } +} \ No newline at end of file From 21b5168876088e217ae6fe1df2f6a14831b1df62 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Sep 2024 14:22:51 +0200 Subject: [PATCH 87/99] refactor: map as component --- .../components/dynamic_tabs/map-tabs.js | 14 +- app/scripts/geo/geokorp.js | 242 ++++++++++-------- 2 files changed, 139 insertions(+), 117 deletions(-) diff --git a/app/scripts/components/dynamic_tabs/map-tabs.js b/app/scripts/components/dynamic_tabs/map-tabs.js index 597200275..a2fc856fa 100644 --- a/app/scripts/components/dynamic_tabs/map-tabs.js +++ b/app/scripts/components/dynamic_tabs/map-tabs.js @@ -37,14 +37,12 @@ angular.module("korpApp").directive("mapTabs", () => ({
      diff --git a/app/scripts/geo/geokorp.js b/app/scripts/geo/geokorp.js index 9784a3984..c2228a635 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/geo/geokorp.js @@ -22,37 +22,40 @@ sbMap.filter("trust", ($sce) => (input) => $sce.trustAsHtml(input)) * @prop {string} color */ -sbMap.directive("sbMap", [ - "$compile", - "$timeout", - "$rootScope", - ($compile, $timeout, $rootScope) => ({ - template: html`
      -
      -
      -
      - -
      `, - restrict: "E", - scope: { - markers: "=sbMarkers", - center: "=sbCenter", - baseLayer: "=sbBaseLayer", - markerCallback: "=sbMarkerCallback", - selectedGroups: "=sbSelectedGroups", - useClustering: "=?sbUseClustering", - restColor: "=?sbRestColor", // free color to use for grouping etc - oldMap: "=?sbOldMap", - }, +sbMap.component("sbMap", { + template: html`
      +
      +
      +
      + +
      `, + bindings: { + center: "<", + markers: "<", + markerCallback: "<", + selectedGroups: "<", + restColor: "<", // free color to use for grouping etc + useClustering: "<", + // TODO Usage was removed in 2020 (39b34962), remove support? + oldMap: "<", + }, + controller: [ + "$compile", + "$element", + "$scope", + "$timeout", + "$rootScope", /** - * @param {SbMapScope} scope + * @param {SbMapScope} $scope * @returns */ - link(scope, element, attrs) { - scope.useClustering = angular.isDefined(scope.useClustering) ? scope.useClustering : true - scope.oldMap = angular.isDefined(scope.oldMap) ? scope.oldMap : false - scope.showMap = false - scope.hoverTemplate = html`
      + function ($compile, $element, $scope, $timeout, $rootScope) { + const $ctrl = this + const useClustering = () => (angular.isDefined($ctrl.useClustering) ? $ctrl.useClustering : true) + const isOldMap = () => (angular.isDefined($ctrl.oldMap) ? $ctrl.oldMap : false) + $scope.showMap = false + $scope.selectedMarkers = [] + $scope.hoverTemplate = html`
      {{ 'map_abs_occurrences' | loc }}: {{point.abs}}
      {{ 'map_rel_occurrences' | loc }}: {{point.rel | number:2}}
      ` - const container = element.find(".map-container")[0] - scope.map = L.map(container, { + + $ctrl.$onChanges = () => { + updateMarkers() + } + + const container = $element.find(".map-container")[0] + $scope.map = L.map(container, { minZoom: 1, maxZoom: 13, }).setView([51.505, -0.09], 13) - scope.selectedMarkers = [] - scope.$on("update_map", function () { - return $timeout(function () { - return scope.map.invalidateSize() - }, 0) - }) + + // Load map layer with leaflet-providers + L.tileLayer.provider("OpenStreetMap").addTo($scope.map) + + $ctrl.$onInit = () => { + $scope.map.setView([$ctrl.center.lat, $ctrl.center.lng], $ctrl.center.zoom) + $scope.showMap = true + } + + $scope.$on("update_map", () => $timeout(() => $scope.map.invalidateSize())) + function createCircleMarker(color, diameter, borderRadius) { return L.divIcon({ html: html`
      element1[0] - element2[0]) + elements.sort((a, b) => a[0] - b[0]) const gridSizeRaw = Math.ceil(Math.sqrt(elements.length)) + 1 const gridSize = gridSizeRaw % 2 === 0 ? gridSizeRaw + 1 : gridSizeRaw @@ -168,9 +183,9 @@ sbMap.directive("sbMap", [ }) } - // use the previously calculated "scope.maxRel" to decide the sizes of the bars - // in the cluster icon that is returned (between 5px and 50px) /** + * use the previously calculated "scope.maxRel" to decide the sizes of the bars + * in the cluster icon that is returned (between 5px and 50px) * @param clusterGroups {Record} * @param restColor {string} */ @@ -193,13 +208,13 @@ sbMap.directive("sbMap", [ if (groups.length === 1) { const color = groups[0] const groupSize = sizes[color] - const diameter = (groupSize / scope.maxRel) * 45 + 5 + const diameter = (groupSize / $scope.maxRel) * 45 + 5 return createCircleMarker(color, diameter, diameter) } const elements = Object.keys(sizes).map((color) => { const groupSize = sizes[color] - const divWidth = (groupSize / scope.maxRel) * 45 + 5 + const divWidth = (groupSize / $scope.maxRel) * 45 + 5 return html`
      1 } - // check all current clusters and sum up the sizes of its childen - // this is the max relative value of any cluster and can be used to - // calculate marker sizes - // TODO this needs to use the "rest" group when doing calcuations!! + + /** + * check all current clusters and sum up the sizes of its childen + * this is the max relative value of any cluster and can be used to + * calculate marker sizes + * TODO this needs to use the "rest" group when doing calcuations!! + */ function updateMarkerSizes() { - const bounds = scope.map.getBounds() - scope.maxRel = 0 - if (scope.useClustering && scope.markerCluster) { - scope.map.eachLayer((layer) => { + const bounds = $scope.map.getBounds() + $scope.maxRel = 0 + if (useClustering() && $scope.markerCluster) { + $scope.map.eachLayer((layer) => { if (layer.getChildCount) { /** @type {Record} */ const sumRels = {} @@ -243,21 +263,24 @@ sbMap.directive("sbMap", [ if (!sumRels[color]) sumRels[color] = 0 sumRels[color] += child.markerData.point.rel } - scope.maxRel = Math.max(scope.maxRel, ...Object.values(sumRels)) - } else if (layer.markerData?.point.rel > scope.maxRel) scope.maxRel = layer.markerData.point.rel + $scope.maxRel = Math.max($scope.maxRel, ...Object.values(sumRels)) + } else if (layer.markerData?.point.rel > $scope.maxRel) + $scope.maxRel = layer.markerData.point.rel }) - return scope.markerCluster.refreshClusters() + return $scope.markerCluster.refreshClusters() } } // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this - // create normal layer (and all listeners) to be used when clustering is not enabled + /** + * create normal layer (and all listeners) to be used when clustering is not enabled + */ function createFeatureLayer() { const featureLayer = L.featureGroup() featureLayer.on("click", (e) => { - scope.selectedMarkers = + $scope.selectedMarkers = e.layer.markerData instanceof Array ? e.layer.markerData : [e.layer.markerData] - return mouseOver(scope.selectedMarkers) + return mouseOver($scope.selectedMarkers) }) featureLayer.on("mouseover", (e) => e.layer.markerData instanceof Array @@ -265,7 +288,7 @@ sbMap.directive("sbMap", [ : mouseOver([e.layer.markerData]) ) featureLayer.on("mouseout", (e) => - scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() ) return featureLayer } @@ -287,30 +310,33 @@ sbMap.directive("sbMap", [ mouseOver(_.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData)) ) markerCluster.on("clustermouseout", (e) => - scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() ) markerCluster.on("clusterclick", (e) => { - scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData) - mouseOver(scope.selectedMarkers) + $scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData) + mouseOver($scope.selectedMarkers) if (shouldZooomToBounds(e.layer)) { return e.layer.zoomToBounds() } }) markerCluster.on("click", (e) => { - scope.selectedMarkers = [e.layer.markerData] - return mouseOver(scope.selectedMarkers) + $scope.selectedMarkers = [e.layer.markerData] + return mouseOver($scope.selectedMarkers) }) markerCluster.on("mouseover", (e) => mouseOver([e.layer.markerData])) markerCluster.on("mouseout", (e) => - scope.selectedMarkers.length > 0 ? mouseOver(scope.selectedMarkers) : mouseOut() + $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() ) markerCluster.on("animationend", (e) => updateMarkerSizes()) return markerCluster } - // takes a list of markers and displays clickable (callback determined by directive user) info boxes + + /** + * takes a list of markers and displays clickable (callback determined by directive user) info boxes + */ function mouseOver(markerData) { return $timeout(() => { - return scope.$apply(() => { + return $scope.$apply(() => { // support for "old" map // TODO Usage was removed in 2020 (39b34962), remove support? const oldMap = !!markerData[0].names @@ -337,13 +363,13 @@ sbMap.directive("sbMap", [ msgScope.point = marker.point msgScope.label = marker.label msgScope.color = marker.color - const compiled = $compile(scope.hoverTemplate) + const compiled = $compile($scope.hoverTemplate) const markerDiv = compiled(msgScope) - markerDiv.bind("click", () => scope.markerCallback(marker)) + markerDiv.bind("click", () => $ctrl.markerCallback(marker)) return markerDiv }) - const hoverInfoElem = angular.element(element.find(".hover-info-container")) + const hoverInfoElem = $element.find(".hover-info-container") hoverInfoElem.empty() hoverInfoElem.append(content) hoverInfoElem[0].scrollTop = 0 @@ -352,77 +378,80 @@ sbMap.directive("sbMap", [ }) }, 0) } + function mouseOut() { - const hoverInfoElem = angular.element(element.find(".hover-info-container")) + const hoverInfoElem = $element.find(".hover-info-container") hoverInfoElem.css("opacity", "0") return hoverInfoElem.css("display", "none") } - scope.showHoverInfo = false - scope.map.on("click", (e) => { - scope.selectedMarkers = [] + + $scope.showHoverInfo = false + + $scope.map.on("click", (e) => { + $scope.selectedMarkers = [] return mouseOut() }) - scope.$watchCollection("selectedGroups", () => updateMarkers()) - scope.$watch("useClustering", (newVal, oldVal) => newVal === !oldVal && updateMarkers()) - function updateMarkers() { - const selectedGroups = scope.selectedGroups - if (scope.markerCluster) { - scope.map.removeLayer(scope.markerCluster) + const selectedGroups = $ctrl.selectedGroups + if ($scope.markerCluster) { + $scope.map.removeLayer($scope.markerCluster) } - if (scope.featureLayer) { - scope.map.removeLayer(scope.featureLayer) + if ($scope.featureLayer) { + $scope.map.removeLayer($scope.featureLayer) } - if (scope.useClustering) { - const selectedMarkers = selectedGroups.map((group) => scope.markers[group]) + if (useClustering()) { + const selectedMarkers = selectedGroups.map((group) => $ctrl.markers[group]) const clusterGroups = _.groupBy(selectedMarkers, "color") - scope.markerCluster = createMarkerCluster(clusterGroups, scope.restColor) - scope.map.addLayer(scope.markerCluster) + $scope.markerCluster = createMarkerCluster(clusterGroups, $ctrl.restColor) + $scope.map.addLayer($scope.markerCluster) } else { - scope.featureLayer = createFeatureLayer() - scope.map.addLayer(scope.featureLayer) + $scope.featureLayer = createFeatureLayer() + $scope.map.addLayer($scope.featureLayer) } - if (scope.useClustering || scope.oldMap) { + if (useClustering() || isOldMap()) { for (const group of selectedGroups) { - const markerGroup = scope.markers[group] - scope.maxRel = 0 + const markerGroup = $ctrl.markers[group] + $scope.maxRel = 0 for (const markerId in markerGroup.markers) { const markerData = markerGroup.markers[markerId] markerData.color = markerGroup.color const marker = L.marker([markerData.lat, markerData.lng], { - icon: createMarkerIcon(markerGroup.color, !scope.oldMap && selectedGroups.length !== 1), + icon: createMarkerIcon(markerGroup.color, !isOldMap() && selectedGroups.length !== 1), }) marker.markerData = markerData - if (scope.useClustering) { - scope.markerCluster.addLayer(marker) + if (useClustering()) { + $scope.markerCluster.addLayer(marker) } else { - scope.featureLayer.addLayer(marker) + $scope.featureLayer.addLayer(marker) } } } } else { - const markers = selectedGroups.map((group) => scope.markers[group]) + const markers = selectedGroups.map((group) => $ctrl.markers[group]) const markersMerged = mergeMarkers(markers) for (const markerData of markersMerged) { const marker = L.marker([markerData.lat, markerData.lng], { icon: createMultiMarkerIcon(markerData.markerData), }) marker.markerData = markerData.markerData - scope.featureLayer.addLayer(marker) + $scope.featureLayer.addLayer(marker) } } return updateMarkerSizes() } - // merge lists of markers into one list with several hits in one marker - // also calculate maxRel + + /** + * merge lists of markers into one list with several hits in one marker + * also calculate maxRel + */ function mergeMarkers(markerLists) { - scope.maxRel = 0 + $scope.maxRel = 0 const val = markerLists.reduce((memo, parent) => { for (const child of Object.values(parent.markers)) { child.color = parent.color const latLng = child.lat + "," + child.lng - if (child.point.rel > scope.maxRel) scope.maxRel = child.point.rel + if (child.point.rel > $scope.maxRel) $scope.maxRel = child.point.rel if (latLng in memo) memo[latLng].markerData.push(child) else memo[latLng] = { @@ -435,11 +464,6 @@ sbMap.directive("sbMap", [ }, {}) return Object.values(val) } - - // Load map layer with leaflet-providers - L.tileLayer.provider("OpenStreetMap").addTo(scope.map) - scope.map.setView([scope.center.lat, scope.center.lng], scope.center.zoom) - scope.showMap = true }, - }), -]) + ], +}) From d05456e436c1ba5f75803fdc8ad7fe415b24d89e Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Sep 2024 14:32:03 +0200 Subject: [PATCH 88/99] refactor: move files, merge module --- app/scripts/components/dynamic_tabs/map-tabs.js | 5 +++-- app/scripts/{geo/geokorp.js => components/result-map.js} | 8 ++------ app/scripts/korp.module.ts | 2 -- app/{scripts/geo/geokorp.scss => styles/map.scss} | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) rename app/scripts/{geo/geokorp.js => components/result-map.js} (99%) rename app/{scripts/geo/geokorp.scss => styles/map.scss} (99%) diff --git a/app/scripts/components/dynamic_tabs/map-tabs.js b/app/scripts/components/dynamic_tabs/map-tabs.js index a2fc856fa..3023a6cdf 100644 --- a/app/scripts/components/dynamic_tabs/map-tabs.js +++ b/app/scripts/components/dynamic_tabs/map-tabs.js @@ -2,6 +2,7 @@ import angular from "angular" import { html } from "@/util" import "@/components/korp-error" +import "@/components/result-map" import "@/directives/tab-spinner" angular.module("korpApp").directive("mapTabs", () => ({ @@ -36,14 +37,14 @@ angular.module("korpApp").directive("mapTabs", () => ({ >
      - + >
      diff --git a/app/scripts/geo/geokorp.js b/app/scripts/components/result-map.js similarity index 99% rename from app/scripts/geo/geokorp.js rename to app/scripts/components/result-map.js index c2228a635..dabf15a0e 100644 --- a/app/scripts/geo/geokorp.js +++ b/app/scripts/components/result-map.js @@ -2,11 +2,7 @@ import angular from "angular" import L, { Map } from "leaflet" import { html } from "@/util" -import "./geokorp.scss" - -const sbMap = angular.module("sbMap", []) - -sbMap.filter("trust", ($sce) => (input) => $sce.trustAsHtml(input)) +import "@/../styles/map.scss" /** * @typedef SbMapScope @@ -22,7 +18,7 @@ sbMap.filter("trust", ($sce) => (input) => $sce.trustAsHtml(input)) * @prop {string} color */ -sbMap.component("sbMap", { +angular.module("korpApp").component("resultMap", { template: html`
      diff --git a/app/scripts/korp.module.ts b/app/scripts/korp.module.ts index 173a3f7d2..3370828e5 100644 --- a/app/scripts/korp.module.ts +++ b/app/scripts/korp.module.ts @@ -3,7 +3,6 @@ import angular from "angular" import "angular-ui-bootstrap" import "angular-spinner" import "angular-ui-sortable" -import "@/geo/geokorp" import "angular-dynamic-locale" import "angular-filter" @@ -35,7 +34,6 @@ const korpApp = angular.module("korpApp", [ "uib/template/popover/popover-template.html", "angularSpinner", "ui.sortable", - "sbMap", "tmh.dynamicLocale", "angular.filter", ]) diff --git a/app/scripts/geo/geokorp.scss b/app/styles/map.scss similarity index 99% rename from app/scripts/geo/geokorp.scss rename to app/styles/map.scss index 071251113..e51c09360 100644 --- a/app/scripts/geo/geokorp.scss +++ b/app/styles/map.scss @@ -1,4 +1,4 @@ -sb-map { +result-map { padding: 8px; .leaflet-div-icon { From 74deabdcb17e2b2396510bfe229aee00e482654b Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Thu, 12 Sep 2024 16:44:14 +0200 Subject: [PATCH 89/99] docs: changelog absorb geo --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc6c6c6e..dce3c6b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - `$rootScope` - Auth module - services (`backend`, `compare-searches`, `lexicons`, `searches`, `utils`) +- Map code from `korp-geo` has moved into this codebase [#359](https://github.com/spraakbanken/korp-frontend/issues/359) ### Changed From 2e62503de73d13a00cb057cc0e195e87fc3cf43f Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 13 Sep 2024 12:00:19 +0200 Subject: [PATCH 90/99] refactor(ts): map_controller --- app/index.ts | 2 +- app/scripts/backend/types.ts | 5 + app/scripts/controllers/map_controller.ts | 186 ++++++++++++++++++++++ app/scripts/corpus_listing.ts | 3 +- app/scripts/index.d.ts | 5 + app/scripts/map_controllers.js | 147 ----------------- app/scripts/map_services.ts | 2 +- app/scripts/parallel/corpus_listing.ts | 3 +- app/scripts/services/backend.ts | 6 +- package.json | 1 - yarn.lock | 5 - 11 files changed, 205 insertions(+), 160 deletions(-) create mode 100644 app/scripts/controllers/map_controller.ts delete mode 100644 app/scripts/map_controllers.js diff --git a/app/index.ts b/app/index.ts index 6cc810662..8ddc49e23 100644 --- a/app/index.ts +++ b/app/index.ts @@ -72,8 +72,8 @@ require("./scripts/controllers/example_controller") require("./scripts/controllers/statistics_controller") require("./scripts/controllers/trend_diagram_controller") require("./scripts/controllers/word_picture_controller") +require("./scripts/controllers/map_controller") -require("./scripts/map_controllers.js") require("./scripts/text_reader_controller.js") require("./scripts/video_controllers.js") require("./scripts/extended.js") diff --git a/app/scripts/backend/types.ts b/app/scripts/backend/types.ts index 6bee008c4..9ae58439a 100644 --- a/app/scripts/backend/types.ts +++ b/app/scripts/backend/types.ts @@ -58,3 +58,8 @@ export type Histogram = { /** Frequency of items at unknown time */ ""?: number } + +export type WithinParameters = { + default_within: string + within: string +} diff --git a/app/scripts/controllers/map_controller.ts b/app/scripts/controllers/map_controller.ts new file mode 100644 index 000000000..77a309c9b --- /dev/null +++ b/app/scripts/controllers/map_controller.ts @@ -0,0 +1,186 @@ +/** @format */ +import _ from "lodash" +import angular, { IScope, ITimeoutService } from "angular" +import settings from "@/settings" +import { regescape } from "@/util" +import { RootScope } from "@/root-scope.types" +import { AppSettings } from "@/settings/app-settings.types" +import { MapRequestResult } from "@/services/backend" +import { MapResult, Point } from "@/map_services" +import { WithinParameters } from "@/backend/types" + +type MapControllerScope = IScope & { + center: AppSettings["map_center"] + error: boolean + selectedGroups: string[] + markerGroups: Record + loading: boolean + numResults: number + useClustering: boolean + promise: Promise + restColor: string + newDynamicTab: any // TODO Defined in tabHash (services.js) + closeDynamicTab: any // TODO Defined in tabHash (services.js) + closeTab: (idx: number, e: Event) => void + newKWICSearch: (marker: Marker) => void + toggleMarkerGroup: (groupName: string) => void + onentry: () => void + onexit: () => void +} + +type MarkerGroup = { + selected: boolean + order: number + color: string + markers: Record +} + +type Marker = { + lat: number + lng: number + label: string + point: Point + queryData: MarkerQueryData +} + +type MarkerQueryData = { + searchCqp: string + subCqp: string + label: string + corpora: string[] + within: WithinParameters +} + +angular.module("korpApp").directive("mapCtrl", [ + "$timeout", + ($timeout: ITimeoutService) => ({ + controller: [ + "$scope", + "$rootScope", + ($scope: MapControllerScope, $rootScope: RootScope) => { + $scope.onentry = () => $scope.$broadcast("update_map") + $scope.loading = true + $scope.newDynamicTab() + $scope.center = settings["map_center"] + $scope.selectedGroups = [] + $scope.markerGroups = {} + $scope.numResults = 0 + $scope.useClustering = false + + const rickshawPromise = import(/* webpackChunkName: "rickshaw" */ "rickshaw") + + Promise.all([rickshawPromise, $scope.promise]).then( + ([Rickshaw, result]) => { + $scope.$apply(($scope: MapControllerScope) => { + $scope.loading = false + $scope.numResults = 20 + $scope.markerGroups = result && getMarkerGroups(Rickshaw, result) + $scope.selectedGroups = _.keys($scope.markerGroups) + }) + }, + (err) => { + console.error("Map data parsing failed:", err) + $scope.$apply(($scope: MapControllerScope) => { + $scope.loading = false + $scope.error = true + }) + } + ) + + $scope.toggleMarkerGroup = function (groupName: string) { + $scope.markerGroups[groupName].selected = !$scope.markerGroups[groupName].selected + if ($scope.selectedGroups.includes(groupName)) { + $scope.selectedGroups.splice($scope.selectedGroups.indexOf(groupName), 1) + } else { + $scope.selectedGroups.push(groupName) + } + } + + function getMarkerGroups(Rickshaw: any, result: MapRequestResult): Record { + const palette: { color: () => string } = new Rickshaw.Color.Palette({ scheme: "colorwheel" }) // spectrum2000 + const groups = result.data.reduce((groups, res, idx) => { + const markers = getMarkers( + result.attribute.label, + result.cqp, + result.corpora, + result.within, + res, + idx + ) + const group = { + selected: true, + order: idx, + color: palette.color(), + markers, + } + return { ...groups, [res.label]: group } + }, {} as Record) + $scope.restColor = "#9b9fa5" + return groups + } + + function getMarkers( + label: string, + cqp: string, + corpora: string[], + within: WithinParameters, + res: MapResult, + idx: number + ): Record { + return _.fromPairs( + res.points.map((point, pointIdx) => { + // Include point index in the key, so that multiple + // places with the same name but different coordinates + // each get their own markers + const id = [point.name.replace(/-/g, ""), pointIdx.toString(), idx].join(":") + const marker = { + lat: point.lat, + lng: point.lng, + queryData: { + searchCqp: cqp, + subCqp: res.cqp, + label, + corpora, + within, + }, + label: res.label, + point, + } + return [id, marker] + }) + ) + } + + /** Open the occurrences at a selected location */ + $scope.newKWICSearch = (marker: Marker) => { + const { point, queryData } = marker + const cl = settings.corpusListing.subsetFactory(queryData.corpora) + const numberOfTokens = queryData.subCqp.split("[").length - 1 + const opts = { + start: 0, + end: 24, + ajaxParams: { + cqp: queryData.searchCqp, + cqp2: `[_.${queryData.label} contains "${regescape( + [point.name, point.countryCode, point.lat, point.lng].join(";") + )}"]{${numberOfTokens}}`, + cqp3: queryData.subCqp, + corpus: cl.stringifySelected(), + show_struct: _.keys(cl.getStructAttrs()).join(","), + expand_prequeries: false, + ...queryData.within, + }, + } + const readingMode = queryData.label === "paragraph__geocontext" + $timeout(() => $rootScope.kwicTabs.push({ queryParams: opts, readingMode }), 0) + } + + $scope.closeTab = function (idx: number, e: Event) { + e.preventDefault() + $rootScope.mapTabs.splice(idx, 1) + $scope.closeDynamicTab() + } + }, + ], + }), +]) diff --git a/app/scripts/corpus_listing.ts b/app/scripts/corpus_listing.ts index 3f2ac99c4..28e0cea6c 100644 --- a/app/scripts/corpus_listing.ts +++ b/app/scripts/corpus_listing.ts @@ -7,6 +7,7 @@ import { locObj } from "@/i18n" import { Attribute } from "./settings/config.types" import { CorpusTransformed } from "./settings/config-transformed.types" import { LangString } from "./i18n/types" +import { WithinParameters } from "./backend/types" export type Filter = { settings: Attribute @@ -252,7 +253,7 @@ export class CorpusListing { return _(output).compact().join() } - getWithinParameters(): { default_within: string; within: string } { + getWithinParameters(): WithinParameters { const defaultWithin = locationSearchGet("within") || _.keys(settings.default_within)[0] const output: string[] = [] diff --git a/app/scripts/index.d.ts b/app/scripts/index.d.ts index 71c2b0d5f..7c2a6d16e 100644 --- a/app/scripts/index.d.ts +++ b/app/scripts/index.d.ts @@ -14,3 +14,8 @@ declare module "*.png" { const content: any export default content } + +declare module "rickshaw" { + const Rickshaw: any + export default Rickshaw +} diff --git a/app/scripts/map_controllers.js b/app/scripts/map_controllers.js deleted file mode 100644 index 0971f5216..000000000 --- a/app/scripts/map_controllers.js +++ /dev/null @@ -1,147 +0,0 @@ -/** @format */ -import _ from "lodash" -import settings from "@/settings" -import { regescape } from "@/util" - -const korpApp = angular.module("korpApp") - -korpApp.directive("mapCtrl", [ - "$timeout", - ($timeout) => ({ - controller: [ - "$scope", - "$rootScope", - ($scope, $rootScope) => { - const s = $scope - const r = $rootScope - - s.onentry = () => s.$broadcast("update_map") - - s.loading = true - s.newDynamicTab() - s.center = settings["map_center"] - s.markers = {} - s.selectedGroups = [] - s.markerGroups = [] - s.mapSettings = { baseLayer: "OpenStreetMap" } - s.numResults = 0 - s.useClustering = false - - const rickshawPromise = import(/* webpackChunkName: "rickshaw" */ "rickshaw") - - Promise.all([rickshawPromise, s.promise]).then( - ([Rickshaw, result]) => { - s.$apply(($scope) => { - $scope.loading = false - $scope.numResults = 20 - $scope.markerGroups = getMarkerGroups(Rickshaw, result) - $scope.selectedGroups = _.keys($scope.markerGroups) - }) - }, - (err) => { - console.error("Map data parsing failed:", err) - this.s.$apply(($scope) => { - $scope.loading = false - $scope.error = true - }) - } - ) - - s.toggleMarkerGroup = function (groupName) { - s.markerGroups[groupName].selected = !s.markerGroups[groupName].selected - if (s.selectedGroups.includes(groupName)) { - return s.selectedGroups.splice(s.selectedGroups.indexOf(groupName), 1) - } else { - return s.selectedGroups.push(groupName) - } - } - - var getMarkerGroups = function (Rickshaw, result) { - const palette = new Rickshaw.Color.Palette({ scheme: "colorwheel" }) // spectrum2000 - const groups = {} - _.map( - result.data, - (res, idx) => - (groups[res.label] = { - selected: true, - order: idx, - color: palette.color(), - markers: getMarkers( - result.attribute.label, - result.cqp, - result.corpora, - result.within, - res, - idx - ), - }) - ) - s.restColor = "#9b9fa5" - return groups - } - - var getMarkers = function (label, cqp, corpora, within, res, idx) { - const markers = {} - - for (let [pointIdx, point] of res.points.entries()) { - // Include point index in the key, so that multiple - // places with the same name but different coordinates - // each get their own markers - const id = [point.name.replace(/-/g, ""), pointIdx.toString(), idx].join(":") - markers[id] = { - lat: point.lat, - lng: point.lng, - queryData: { - searchCqp: cqp, - subCqp: res.cqp, - label, - corpora, - within, - }, - label: res.label, - point, - } - } - - return markers - } - - s.newKWICSearch = function (marker) { - const { queryData } = marker - const { point } = marker - const cl = settings.corpusListing.subsetFactory(queryData.corpora) - const numberOfTokens = queryData.subCqp.split("[").length - 1 - const opts = { - start: 0, - end: 24, - ajaxParams: { - cqp: queryData.searchCqp, - cqp2: `[_.${queryData.label} contains "${regescape( - [point.name, point.countryCode, point.lat, point.lng].join(";") - )}"]{${numberOfTokens}}`, - cqp3: queryData.subCqp, - corpus: cl.stringifySelected(), - show_struct: _.keys(cl.getStructAttrs()), - expand_prequeries: false, - }, - } - _.extend(opts.ajaxParams, queryData.within) - $timeout( - () => - $rootScope.kwicTabs.push({ - readingMode: queryData.label === "paragraph__geocontext", - queryParams: opts, - }), - 0 - ) - } - - s.closeTab = function (idx, e) { - e.preventDefault() - r.mapTabs.splice(idx, 1) - s.closeDynamicTab() - } - }, - ], - }), -]) diff --git a/app/scripts/map_services.ts b/app/scripts/map_services.ts index 4adcafc96..44baa0fca 100644 --- a/app/scripts/map_services.ts +++ b/app/scripts/map_services.ts @@ -2,7 +2,7 @@ import _ from "lodash" import { StatsData, InnerData } from "./interfaces/stats" -interface Point { +export interface Point { abs: number rel: number name: string diff --git a/app/scripts/parallel/corpus_listing.ts b/app/scripts/parallel/corpus_listing.ts index 55730ccab..31fb7aaef 100644 --- a/app/scripts/parallel/corpus_listing.ts +++ b/app/scripts/parallel/corpus_listing.ts @@ -6,6 +6,7 @@ import { getUrlHash, locationSearchGet } from "@/util" import { CorpusTransformed } from "@/settings/config-transformed.types" import { Attribute } from "@/settings/config.types" import { LangString } from "@/i18n/types" +import { WithinParameters } from "@/backend/types" export class ParallelCorpusListing extends CorpusListing { activeLangs: string[] @@ -127,7 +128,7 @@ export class ParallelCorpusListing extends CorpusListing { return this.getAttributeQuery("context") } - getWithinParameters(): { default_within: string; within: string } { + getWithinParameters(): WithinParameters { const defaultWithin = locationSearchGet("within") || _.keys(settings["default_within"])[0] const within = this.getAttributeQuery("within") return { default_within: defaultWithin, within } diff --git a/app/scripts/services/backend.ts b/app/scripts/services/backend.ts index 51202e6d7..6790611de 100644 --- a/app/scripts/services/backend.ts +++ b/app/scripts/services/backend.ts @@ -2,7 +2,7 @@ import _ from "lodash" import angular, { IDeferred, IHttpService, IPromise, IQService } from "angular" import { getAuthorizationHeader } from "@/components/auth/auth" -import { KorpResponse } from "@/backend/types" +import { KorpResponse, WithinParameters } from "@/backend/types" import { SavedSearch } from "@/local-storage" import settings from "@/settings" import { httpConfAddMethod, httpConfAddMethodAngular } from "@/util" @@ -16,7 +16,7 @@ export type BackendService = { requestMapData: ( cqp: string, cqpExprs: Record, - within: { default_within: string; within: string }, + within: WithinParameters, attribute: MapAttribute, relative: boolean ) => IPromise @@ -55,7 +55,7 @@ export type CompareItem = { export type MapRequestResult = { corpora: string[] cqp: string - within: { default_within: string; within: string } + within: WithinParameters data: MapResult[] attribute: MapAttribute } diff --git a/package.json b/package.json index 622043b43..668f85b8a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@types/jquery": "^3.5.29", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.14.118", - "@types/rickshaw": "^0.0.31", "autoprefixer": "^10.2.4", "chromedriver": "^122.0.4", "compression-webpack-plugin": "^10.0.0", diff --git a/yarn.lock b/yarn.lock index 4f92d30e9..7ec655a5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -369,11 +369,6 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/rickshaw@^0.0.31": - version "0.0.31" - resolved "https://registry.yarnpkg.com/@types/rickshaw/-/rickshaw-0.0.31.tgz#023ed58a6b8a61ac8ada3f96cbd586852047fd83" - integrity sha512-wrcKrD9b6Z+1Ff3TRoMGVnmR9BdS0HLrRTm63sZTOb+J86XNlbRT60TCHxVJy4x3J2wllau9XEqk18k0OgjydA== - "@types/selenium-webdriver@^3.0.0": version "3.0.20" resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.20.tgz#448771a0608ebf1c86cb5885914da6311e323c3a" From 95f6543fdf6c418132ea4c71abd5244b1f4144a0 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 13 Sep 2024 14:11:02 +0200 Subject: [PATCH 91/99] refactor(ts): result-map --- .../{result-map.js => result-map.ts} | 383 ++++++++++-------- app/scripts/controllers/map_controller.ts | 14 +- 2 files changed, 228 insertions(+), 169 deletions(-) rename app/scripts/components/{result-map.js => result-map.ts} (55%) diff --git a/app/scripts/components/result-map.js b/app/scripts/components/result-map.ts similarity index 55% rename from app/scripts/components/result-map.js rename to app/scripts/components/result-map.ts index dabf15a0e..143ce21f8 100644 --- a/app/scripts/components/result-map.js +++ b/app/scripts/components/result-map.ts @@ -1,22 +1,75 @@ /** @format */ -import angular from "angular" -import L, { Map } from "leaflet" +import _ from "lodash" +import angular, { ICompileService, IController, IRootElementService, IScope, ITimeoutService } from "angular" +import L from "leaflet" import { html } from "@/util" +import { RootScope } from "@/root-scope.types" +import { MarkerEvent, MarkerGroup } from "@/controllers/map_controller" import "@/../styles/map.scss" - -/** - * @typedef SbMapScope - * @prop {Map} map - * @prop {string[]} selectedGroups - * @prop {number} maxRel - Maximum frequency in current result - * @prop {Record} markers - * @prop {string} restColor - */ - -/** - * @typedef Marker - * @prop {string} color - */ +import { AppSettings } from "@/settings/app-settings.types" + +type ResultMapController = IController & { + center: AppSettings["map_center"] + markers: Record + markerCallback: (marker: MarkerEvent) => void + selectedGroups: string[] + restColor: string + useClustering: boolean + /** @deprecated */ + oldMap: boolean +} + +type ResultMapScope = IScope & { + showMap: boolean +} + +type CustomMarker = L.Marker & { + markerData: MarkerData +} + +type CustomMarkerMany = L.Marker & { + markerData: MarkerData[] +} + +type MarkerData = MarkerEvent & { + label: string + color: string +} + +type MergedMarker = { + markerData: MarkerData[] + lat: number + lng: number +} + +type OldMapMarkerData = { + names: Record + searchCqp: string +} + +type OldMapCounts = { abs_occurrences: number; rel_occurrences: number } + +type MessageScope = IScope & { + showLabel?: boolean + point?: MarkerData["point"] + label?: string + color?: string +} + +class MarkerClusterGroup extends L.MarkerClusterGroup { + getAllChildMarkers: () => CustomMarker[] +} + +class MarkerCluster extends L.MarkerCluster { + getAllChildMarkers: () => CustomMarker[] +} + +/** Determine if a given layer is a single marker */ +const isMarker = (layer: T | CustomMarker): layer is CustomMarker => "markerData" in layer + +/** Determine if a given layer is a cluster marker */ +const isMarkerCluster = (layer: T | MarkerClusterGroup): layer is MarkerClusterGroup => + "getChildCount" in layer angular.module("korpApp").component("resultMap", { template: html`
      @@ -41,17 +94,26 @@ angular.module("korpApp").component("resultMap", { "$scope", "$timeout", "$rootScope", - /** - * @param {SbMapScope} $scope - * @returns - */ - function ($compile, $element, $scope, $timeout, $rootScope) { - const $ctrl = this + function ( + $compile: ICompileService, + $element: IRootElementService, + $scope: ResultMapScope, + $timeout: ITimeoutService, + $rootScope: RootScope + ) { + const $ctrl = this as ResultMapController + + $scope.showMap = false + let selectedMarkers: MarkerData[] = [] + let featureLayer: L.FeatureGroup + let markerCluster: MarkerClusterGroup + /** Maximum frequency in current result */ + let maxRel = 0 + const useClustering = () => (angular.isDefined($ctrl.useClustering) ? $ctrl.useClustering : true) const isOldMap = () => (angular.isDefined($ctrl.oldMap) ? $ctrl.oldMap : false) - $scope.showMap = false - $scope.selectedMarkers = [] - $scope.hoverTemplate = html`
      + + const hoverTemplate = html`
      { - $scope.map.setView([$ctrl.center.lat, $ctrl.center.lng], $ctrl.center.zoom) + map.setView([$ctrl.center.lat, $ctrl.center.lng], $ctrl.center.zoom) $scope.showMap = true } - $scope.$on("update_map", () => $timeout(() => $scope.map.invalidateSize())) + $scope.$on("update_map", () => $timeout(() => map.invalidateSize())) - function createCircleMarker(color, diameter, borderRadius) { + function createCircleMarker(color: string, diameter: number, borderRadius: number) { return L.divIcon({ html: html`
      { + const diameter = (marker.point.rel / maxRel) * 40 + 10 + return [ diameter, html`
      `, - ]) - } + ] as [number, string] + }) elements.sort((a, b) => a[0] - b[0]) @@ -120,12 +185,11 @@ angular.module("korpApp").component("resultMap", { const gridSize = gridSizeRaw % 2 === 0 ? gridSizeRaw + 1 : gridSizeRaw const center = Math.floor(gridSize / 2) - /** @type {([number, string][] | [])[]} */ - let grid = [] - for (let i = 0; i <= gridSize - 1; i++) grid.push([]) + const gridRaw: ([number, string][] | [])[] = [] + for (let i = 0; i <= gridSize - 1; i++) gridRaw.push([]) - const id = (x) => x - const neg = (x) => -x + const id = (x: number) => x + const neg = (x: number) => -x for (let idx = 0; idx <= center; idx++) { let x = -1 let y = -1 @@ -141,7 +205,7 @@ angular.module("korpApp").component("resultMap", { if (y === center + idx) yOp = neg const circle = elements.pop() if (circle) { - grid[y][x] = circle + gridRaw[y][x] = circle } else { break } @@ -149,32 +213,26 @@ angular.module("korpApp").component("resultMap", { } // remove all empty arrays and elements // TODO don't create empty stuff?? - grid = grid.filter((row) => row.length > 0) - grid = grid.map((row) => row.filter((elem) => elem)) + const grid = gridRaw.filter((row) => row.length > 0).map((row) => row.filter((elem) => elem)) //# take largest element from each row and add to height let height = 0 let width = 0 const gridCenter = Math.floor(grid.length / 2) - /** @type {string[]} */ - const grid2 = [] - for (let idx = 0; idx < grid.length; ++idx) { - const row = grid[idx] + const rows = grid.map((row, idx) => { height += row.reduce((memo, val) => (val[0] > memo ? val[0] : memo), 0) if (idx === gridCenter) { width = grid[gridCenter].reduce((memo, val) => memo + val[0], 0) } const markerClass = idx === gridCenter ? "marker-middle" : idx > gridCenter ? "marker-top" : "marker-bottom" - grid2.push( - html`
      - ${row.map((elem) => elem[1]).join("")} -
      ` - ) - } + return html`
      + ${row.map((elem) => elem[1]).join("")} +
      ` + }) return L.divIcon({ - html: grid2.join(""), + html: rows.join(""), iconSize: new L.Point(width, height), }) } @@ -182,20 +240,17 @@ angular.module("korpApp").component("resultMap", { /** * use the previously calculated "scope.maxRel" to decide the sizes of the bars * in the cluster icon that is returned (between 5px and 50px) - * @param clusterGroups {Record} - * @param restColor {string} */ - function createClusterIcon(clusterGroups, restColor) { + function createClusterIcon(clusterGroups: Record, restColor: string) { const groups = Object.keys(clusterGroups) groups.sort((group1, group2) => clusterGroups[group1].order - clusterGroups[group2].order) if (groups.length > 4) { groups.splice(3) groups.push(restColor) } - return function (cluster) { - /** @type {Record} */ - const sizes = groups.reduce((sizes, color) => ({ ...sizes, [color]: 0 }), {}) - cluster.getAllChildMarkers().forEach((childMarker) => { + return function (cluster: MarkerClusterGroup) { + const sizes = _.fromPairs(groups.map((color) => [color, 0])) + cluster.getAllChildMarkers().forEach((childMarker: CustomMarker) => { let color = childMarker.markerData.color if (!(color in sizes)) color = restColor sizes[color] += childMarker.markerData.point.rel @@ -204,18 +259,18 @@ angular.module("korpApp").component("resultMap", { if (groups.length === 1) { const color = groups[0] const groupSize = sizes[color] - const diameter = (groupSize / $scope.maxRel) * 45 + 5 + const diameter = (groupSize / maxRel) * 45 + 5 return createCircleMarker(color, diameter, diameter) } - const elements = Object.keys(sizes).map((color) => { - const groupSize = sizes[color] - const divWidth = (groupSize / $scope.maxRel) * 45 + 5 - return html`
      ` - }) + const elements = _.map( + sizes, + (size, color) => + html`
      ` + ) return L.divIcon({ html: html`
      ${elements.join("")}
      `, iconSize: new L.Point(40, 50), @@ -247,23 +302,20 @@ angular.module("korpApp").component("resultMap", { * TODO this needs to use the "rest" group when doing calcuations!! */ function updateMarkerSizes() { - const bounds = $scope.map.getBounds() - $scope.maxRel = 0 - if (useClustering() && $scope.markerCluster) { - $scope.map.eachLayer((layer) => { - if (layer.getChildCount) { - /** @type {Record} */ - const sumRels = {} + maxRel = 0 + if (useClustering() && markerCluster) { + map.eachLayer((layer) => { + if (isMarkerCluster(layer)) { + const sumRels: Record = {} for (const child of layer.getAllChildMarkers()) { const color = child.markerData.color if (!sumRels[color]) sumRels[color] = 0 sumRels[color] += child.markerData.point.rel } - $scope.maxRel = Math.max($scope.maxRel, ...Object.values(sumRels)) - } else if (layer.markerData?.point.rel > $scope.maxRel) - $scope.maxRel = layer.markerData.point.rel + maxRel = Math.max(maxRel, ...Object.values(sumRels)) + } else if (isMarker(layer)) maxRel = Math.max(maxRel, layer.markerData.point.rel) }) - return $scope.markerCluster.refreshClusters() + return markerCluster.refreshClusters() } } // TODO when scope.maxRel is set, we should redraw all non-cluster markers using this @@ -274,54 +326,54 @@ angular.module("korpApp").component("resultMap", { function createFeatureLayer() { const featureLayer = L.featureGroup() featureLayer.on("click", (e) => { - $scope.selectedMarkers = - e.layer.markerData instanceof Array ? e.layer.markerData : [e.layer.markerData] - return mouseOver($scope.selectedMarkers) + const marker = e.propagatedFrom as CustomMarker | CustomMarkerMany + selectedMarkers = marker.markerData instanceof Array ? marker.markerData : [marker.markerData] + mouseOver(selectedMarkers) }) - featureLayer.on("mouseover", (e) => - e.layer.markerData instanceof Array - ? mouseOver(e.layer.markerData) - : mouseOver([e.layer.markerData]) - ) - featureLayer.on("mouseout", (e) => - $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() + featureLayer.on("mouseover", (e) => { + const marker = e.propagatedFrom as CustomMarker | CustomMarkerMany + marker.markerData instanceof Array ? mouseOver(marker.markerData) : mouseOver([marker.markerData]) + }) + featureLayer.on("mouseout", () => + selectedMarkers.length > 0 ? mouseOver(selectedMarkers) : mouseOut() ) return featureLayer } /** * create marker cluster layer and all listeners - * @param {Record} clusterGroups - * @param {string} restColor */ - function createMarkerCluster(clusterGroups, restColor) { + function createMarkerCluster( + clusterGroups: Record, + restColor: string + ): MarkerClusterGroup { const markerCluster = L.markerClusterGroup({ spiderfyOnMaxZoom: false, showCoverageOnHover: false, maxClusterRadius: 40, zoomToBoundsOnClick: false, - iconCreateFunction: createClusterIcon(clusterGroups, restColor), - }) - markerCluster.on("clustermouseover", (e) => - mouseOver(_.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData)) + iconCreateFunction: createClusterIcon(clusterGroups, restColor) as any, + }) as MarkerClusterGroup + markerCluster.on("clustermouseover", (e: { propagatedFrom: MarkerCluster }) => + mouseOver(e.propagatedFrom.getAllChildMarkers().map((layer) => layer.markerData)) ) - markerCluster.on("clustermouseout", (e) => - $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() + markerCluster.on("clustermouseout", () => + selectedMarkers.length > 0 ? mouseOver(selectedMarkers) : mouseOut() ) - markerCluster.on("clusterclick", (e) => { - $scope.selectedMarkers = _.map(e.layer.getAllChildMarkers(), (layer) => layer.markerData) - mouseOver($scope.selectedMarkers) - if (shouldZooomToBounds(e.layer)) { - return e.layer.zoomToBounds() + markerCluster.on("clusterclick", (e: { propagatedFrom: MarkerCluster }) => { + selectedMarkers = e.propagatedFrom.getAllChildMarkers().map((layer) => layer.markerData) + mouseOver(selectedMarkers) + if (shouldZooomToBounds(e.propagatedFrom)) { + return e.propagatedFrom.zoomToBounds() } }) - markerCluster.on("click", (e) => { - $scope.selectedMarkers = [e.layer.markerData] - return mouseOver($scope.selectedMarkers) + markerCluster.on("click", (e: { propagatedFrom: CustomMarker }) => { + selectedMarkers = [e.propagatedFrom.markerData] + return mouseOver(selectedMarkers) }) - markerCluster.on("mouseover", (e) => mouseOver([e.layer.markerData])) + markerCluster.on("mouseover", (e) => mouseOver([e.propagatedFrom.markerData])) markerCluster.on("mouseout", (e) => - $scope.selectedMarkers.length > 0 ? mouseOver($scope.selectedMarkers) : mouseOut() + selectedMarkers.length > 0 ? mouseOver(selectedMarkers) : mouseOut() ) markerCluster.on("animationend", (e) => updateMarkerSizes()) return markerCluster @@ -330,23 +382,28 @@ angular.module("korpApp").component("resultMap", { /** * takes a list of markers and displays clickable (callback determined by directive user) info boxes */ - function mouseOver(markerData) { + function mouseOver(markerData: MarkerData[]) { return $timeout(() => { return $scope.$apply(() => { // support for "old" map // TODO Usage was removed in 2020 (39b34962), remove support? - const oldMap = !!markerData[0].names + const oldMap = "names" in markerData[0] - let selectedMarkers + let selectedMarkers: MarkerData[] if (oldMap) { - selectedMarkers = _.map(markerData[0].names).map((thing, name) => ({ + const oldMarkerData: OldMapMarkerData = markerData[0] as any + selectedMarkers = _.map(oldMarkerData.names, (thing, name) => ({ + label: "", color: markerData[0].color, - searchCqp: markerData[0].searchCqp, + // TODO Missing some of MarkerQueryData and Point, things that probably weren't used in old map? + queryData: { + searchCqp: oldMarkerData.searchCqp, + } as any, point: { name, abs: thing.abs_occurrences, rel: thing.rel_occurrences, - }, + } as any, })) } else { markerData.sort((a, b) => b.point.rel - a.point.rel) @@ -354,15 +411,13 @@ angular.module("korpApp").component("resultMap", { } const content = selectedMarkers.map((marker) => { - const msgScope = $rootScope.$new(true) + const msgScope: MessageScope = $rootScope.$new(true) msgScope.showLabel = !oldMap msgScope.point = marker.point msgScope.label = marker.label msgScope.color = marker.color - const compiled = $compile($scope.hoverTemplate) - const markerDiv = compiled(msgScope) - markerDiv.bind("click", () => $ctrl.markerCallback(marker)) - return markerDiv + const markerDiv = $compile(hoverTemplate)(msgScope) + return markerDiv.bind("click", () => $ctrl.markerCallback(marker)) }) const hoverInfoElem = $element.find(".hover-info-container") @@ -381,83 +436,83 @@ angular.module("korpApp").component("resultMap", { return hoverInfoElem.css("display", "none") } - $scope.showHoverInfo = false - - $scope.map.on("click", (e) => { - $scope.selectedMarkers = [] + map.on("click", (e) => { + selectedMarkers = [] return mouseOut() }) function updateMarkers() { const selectedGroups = $ctrl.selectedGroups - if ($scope.markerCluster) { - $scope.map.removeLayer($scope.markerCluster) + if (markerCluster) { + map.removeLayer(markerCluster) } - if ($scope.featureLayer) { - $scope.map.removeLayer($scope.featureLayer) + if (featureLayer) { + map.removeLayer(featureLayer) } if (useClustering()) { const selectedMarkers = selectedGroups.map((group) => $ctrl.markers[group]) - const clusterGroups = _.groupBy(selectedMarkers, "color") - $scope.markerCluster = createMarkerCluster(clusterGroups, $ctrl.restColor) - $scope.map.addLayer($scope.markerCluster) + const clusterGroups = _.keyBy(selectedMarkers, "color") + markerCluster = createMarkerCluster(clusterGroups, $ctrl.restColor) + map.addLayer(markerCluster) } else { - $scope.featureLayer = createFeatureLayer() - $scope.map.addLayer($scope.featureLayer) + featureLayer = createFeatureLayer() + map.addLayer(featureLayer) } if (useClustering() || isOldMap()) { + const isCluster = !isOldMap() && selectedGroups.length !== 1 for (const group of selectedGroups) { const markerGroup = $ctrl.markers[group] - $scope.maxRel = 0 - for (const markerId in markerGroup.markers) { - const markerData = markerGroup.markers[markerId] - markerData.color = markerGroup.color - const marker = L.marker([markerData.lat, markerData.lng], { - icon: createMarkerIcon(markerGroup.color, !isOldMap() && selectedGroups.length !== 1), - }) - marker.markerData = markerData + maxRel = 0 + Object.values(markerGroup.markers).map((markerOrig) => { + const icon = createMarkerIcon(markerGroup.color, isCluster) + const marker = L.marker([markerOrig.lat, markerOrig.lng], { icon }) as CustomMarker + marker.markerData = { + label: markerOrig.label, + color: markerGroup.color, + point: markerOrig.point, + queryData: markerOrig.queryData, + } if (useClustering()) { - $scope.markerCluster.addLayer(marker) + markerCluster.addLayer(marker) } else { - $scope.featureLayer.addLayer(marker) + featureLayer.addLayer(marker) } - } + }) } } else { const markers = selectedGroups.map((group) => $ctrl.markers[group]) const markersMerged = mergeMarkers(markers) for (const markerData of markersMerged) { - const marker = L.marker([markerData.lat, markerData.lng], { - icon: createMultiMarkerIcon(markerData.markerData), - }) + const icon = createMultiMarkerIcon(markerData.markerData) + const marker = L.marker([markerData.lat, markerData.lng], { icon }) as CustomMarkerMany marker.markerData = markerData.markerData - $scope.featureLayer.addLayer(marker) + featureLayer.addLayer(marker) } } - return updateMarkerSizes() + updateMarkerSizes() } /** * merge lists of markers into one list with several hits in one marker * also calculate maxRel */ - function mergeMarkers(markerLists) { - $scope.maxRel = 0 + function mergeMarkers(markerLists: MarkerGroup[]): MergedMarker[] { + maxRel = 0 const val = markerLists.reduce((memo, parent) => { - for (const child of Object.values(parent.markers)) { - child.color = parent.color - const latLng = child.lat + "," + child.lng - if (child.point.rel > $scope.maxRel) $scope.maxRel = child.point.rel + for (const child1 of Object.values(parent.markers)) { + const child: MarkerData = { ...child1, color: parent.color } + const latLng = child1.lat + "," + child1.lng + if (child.point.rel > maxRel) maxRel = child.point.rel if (latLng in memo) memo[latLng].markerData.push(child) else memo[latLng] = { markerData: [child], - lat: child.lat, - lng: child.lng, + lat: child1.lat, + lng: child1.lng, } } return memo - }, {}) + }, {} as Record) return Object.values(val) } }, diff --git a/app/scripts/controllers/map_controller.ts b/app/scripts/controllers/map_controller.ts index 77a309c9b..4fc1b43d4 100644 --- a/app/scripts/controllers/map_controller.ts +++ b/app/scripts/controllers/map_controller.ts @@ -22,28 +22,32 @@ type MapControllerScope = IScope & { newDynamicTab: any // TODO Defined in tabHash (services.js) closeDynamicTab: any // TODO Defined in tabHash (services.js) closeTab: (idx: number, e: Event) => void - newKWICSearch: (marker: Marker) => void + newKWICSearch: (marker: MarkerEvent) => void toggleMarkerGroup: (groupName: string) => void onentry: () => void onexit: () => void } -type MarkerGroup = { +export type MarkerGroup = { selected: boolean order: number color: string markers: Record } -type Marker = { +export type Marker = MarkerEvent & { lat: number lng: number label: string +} + +export type MarkerEvent = { point: Point queryData: MarkerQueryData } -type MarkerQueryData = { +/** Needed for making a sub-search */ +export type MarkerQueryData = { searchCqp: string subCqp: string label: string @@ -152,7 +156,7 @@ angular.module("korpApp").directive("mapCtrl", [ } /** Open the occurrences at a selected location */ - $scope.newKWICSearch = (marker: Marker) => { + $scope.newKWICSearch = (marker: MarkerEvent) => { const { point, queryData } = marker const cl = settings.corpusListing.subsetFactory(queryData.corpora) const numberOfTokens = queryData.subCqp.split("[").length - 1 From 0686a3387075fd7344b04613741b761aaad5e4d4 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Fri, 13 Sep 2024 15:09:30 +0200 Subject: [PATCH 92/99] refactor: remove old map support --- CHANGELOG.md | 1 + app/scripts/components/result-map.ts | 51 +++++----------------------- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce3c6b44..2d09fe817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Converted the "radioList" JQuery widget to a component - Using Karp 7 backend instead of Karp 4 [#388](https://github.com/spraakbanken/korp-frontend/pull/388) - For the `utils.setupHash()` function, the `config` argument is no longer an array. To sync multiple parameters, call it once for each. +- Dopped support for old map usage (`sb-old-map="true"`) ### Fixed diff --git a/app/scripts/components/result-map.ts b/app/scripts/components/result-map.ts index 143ce21f8..09eb047e4 100644 --- a/app/scripts/components/result-map.ts +++ b/app/scripts/components/result-map.ts @@ -15,8 +15,6 @@ type ResultMapController = IController & { selectedGroups: string[] restColor: string useClustering: boolean - /** @deprecated */ - oldMap: boolean } type ResultMapScope = IScope & { @@ -42,13 +40,6 @@ type MergedMarker = { lng: number } -type OldMapMarkerData = { - names: Record - searchCqp: string -} - -type OldMapCounts = { abs_occurrences: number; rel_occurrences: number } - type MessageScope = IScope & { showLabel?: boolean point?: MarkerData["point"] @@ -85,8 +76,6 @@ angular.module("korpApp").component("resultMap", { selectedGroups: "<", restColor: "<", // free color to use for grouping etc useClustering: "<", - // TODO Usage was removed in 2020 (39b34962), remove support? - oldMap: "<", }, controller: [ "$compile", @@ -111,7 +100,6 @@ angular.module("korpApp").component("resultMap", { let maxRel = 0 const useClustering = () => (angular.isDefined($ctrl.useClustering) ? $ctrl.useClustering : true) - const isOldMap = () => (angular.isDefined($ctrl.oldMap) ? $ctrl.oldMap : false) const hoverTemplate = html`
      @@ -385,34 +373,12 @@ angular.module("korpApp").component("resultMap", { function mouseOver(markerData: MarkerData[]) { return $timeout(() => { return $scope.$apply(() => { - // support for "old" map - // TODO Usage was removed in 2020 (39b34962), remove support? - const oldMap = "names" in markerData[0] - - let selectedMarkers: MarkerData[] - if (oldMap) { - const oldMarkerData: OldMapMarkerData = markerData[0] as any - selectedMarkers = _.map(oldMarkerData.names, (thing, name) => ({ - label: "", - color: markerData[0].color, - // TODO Missing some of MarkerQueryData and Point, things that probably weren't used in old map? - queryData: { - searchCqp: oldMarkerData.searchCqp, - } as any, - point: { - name, - abs: thing.abs_occurrences, - rel: thing.rel_occurrences, - } as any, - })) - } else { - markerData.sort((a, b) => b.point.rel - a.point.rel) - selectedMarkers = markerData - } + markerData.sort((a, b) => b.point.rel - a.point.rel) + const selectedMarkers = markerData const content = selectedMarkers.map((marker) => { const msgScope: MessageScope = $rootScope.$new(true) - msgScope.showLabel = !oldMap + msgScope.showLabel = true msgScope.point = marker.point msgScope.label = marker.label msgScope.color = marker.color @@ -442,7 +408,6 @@ angular.module("korpApp").component("resultMap", { }) function updateMarkers() { - const selectedGroups = $ctrl.selectedGroups if (markerCluster) { map.removeLayer(markerCluster) } @@ -450,7 +415,7 @@ angular.module("korpApp").component("resultMap", { map.removeLayer(featureLayer) } if (useClustering()) { - const selectedMarkers = selectedGroups.map((group) => $ctrl.markers[group]) + const selectedMarkers = $ctrl.selectedGroups.map((group) => $ctrl.markers[group]) const clusterGroups = _.keyBy(selectedMarkers, "color") markerCluster = createMarkerCluster(clusterGroups, $ctrl.restColor) map.addLayer(markerCluster) @@ -458,9 +423,9 @@ angular.module("korpApp").component("resultMap", { featureLayer = createFeatureLayer() map.addLayer(featureLayer) } - if (useClustering() || isOldMap()) { - const isCluster = !isOldMap() && selectedGroups.length !== 1 - for (const group of selectedGroups) { + if (useClustering()) { + const isCluster = $ctrl.selectedGroups.length !== 1 + for (const group of $ctrl.selectedGroups) { const markerGroup = $ctrl.markers[group] maxRel = 0 Object.values(markerGroup.markers).map((markerOrig) => { @@ -480,7 +445,7 @@ angular.module("korpApp").component("resultMap", { }) } } else { - const markers = selectedGroups.map((group) => $ctrl.markers[group]) + const markers = $ctrl.selectedGroups.map((group) => $ctrl.markers[group]) const markersMerged = mergeMarkers(markers) for (const markerData of markersMerged) { const icon = createMultiMarkerIcon(markerData.markerData) From 54cef8c6be9a726549216b7b6646f80e008c85a9 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 10:42:22 +0200 Subject: [PATCH 93/99] refactor: replace popper with uib-popover and uib-dropdown --- CHANGELOG.md | 1 + app/scripts/components/datetime-picker.ts | 43 ++++---- app/scripts/components/extended/cqp-value.js | 4 +- app/scripts/components/extended/token.js | 26 +++-- app/scripts/components/header.js | 105 ++++++++----------- app/scripts/directives/popper.ts | 48 --------- app/scripts/extended.js | 33 ++++-- app/styles/styles.scss | 37 ++++--- 8 files changed, 131 insertions(+), 166 deletions(-) delete mode 100644 app/scripts/directives/popper.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d09fe817..cfd5c867c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Replaced jStorage library with native `localStorage`, and added TypeScript typings - In the `ParallelCorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures +- Replaced custom `popper` directive with `uib-popover` and `uib-dropdown` () - Removed the `mapper` template filter; change `x | mapper:f` to `f(x)` - Removed the global `c` alias for `console` - Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) diff --git a/app/scripts/components/datetime-picker.ts b/app/scripts/components/datetime-picker.ts index 7c2c47eeb..33852773e 100644 --- a/app/scripts/components/datetime-picker.ts +++ b/app/scripts/components/datetime-picker.ts @@ -2,7 +2,6 @@ import angular, { type ui, type IComponentController, type IScope } from "angular" import { html } from "@/util" import moment, { type Moment } from "moment" -import "@/directives/popper" angular.module("korpApp").component("datetimePicker", { template: html` @@ -12,30 +11,38 @@ angular.module("korpApp").component("datetimePicker", {
      - - `, diff --git a/app/scripts/components/extended/cqp-value.js b/app/scripts/components/extended/cqp-value.js index 09fb25a50..964d9c8f0 100644 --- a/app/scripts/components/extended/cqp-value.js +++ b/app/scripts/components/extended/cqp-value.js @@ -80,9 +80,9 @@ angular.module("korpApp").component("extendedCqpValue", { } else { let tmplObj if (ctrl.attributeDefinition.value === "word") { - tmplObj = { maybe_placeholder: "placeholder='<{{\"any\" | loc:$root.lang}}>'" } + tmplObj = { placeholder: "<{{'any' | loc:$root.lang}}>" } } else { - tmplObj = { maybe_placeholder: "" } + tmplObj = { placeholder: "" } } template = extendedComponents.default.template(tmplObj) diff --git a/app/scripts/components/extended/token.js b/app/scripts/components/extended/token.js index 54edd45c2..852a57254 100644 --- a/app/scripts/components/extended/token.js +++ b/app/scripts/components/extended/token.js @@ -2,7 +2,6 @@ import angular from "angular" import { html } from "@/util" import "@/components/extended/and-token" -import "@/directives/popper" angular.module("korpApp").component("extendedToken", { template: html` @@ -34,16 +33,25 @@ angular.module("korpApp").component("extendedToken", { {{"and" | loc:$root.lang}} - - + +
      {{'repeat' | loc:$root.lang}} diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 241e9e0fc..612c7df18 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -12,7 +12,6 @@ import { collatorSort, html } from "@/util" import "@/services/utils" import "@/components/corpus_chooser/corpus-chooser" import "@/components/radio-list" -import "@/directives/popper" angular.module("korpApp").component("header", { template: html` @@ -22,16 +21,13 @@ angular.module("korpApp").component("header", {
    • {{mode.label | locObj:lang}}
    • -
      diff --git a/app/scripts/directives/popper.ts b/app/scripts/directives/popper.ts deleted file mode 100644 index 85c7d6d29..000000000 --- a/app/scripts/directives/popper.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** @format */ -import _ from "lodash" -import angular, { IRootElementService } from "angular" - -angular.module("korpApp").directive("popper", [ - "$rootElement", - ($rootElement: IRootElementService) => ({ - scope: {}, - link(scope, elem, attrs) { - const popup = elem.next() - popup.appendTo("body").hide() - - if (attrs.noCloseOnClick == null) { - popup.on("click", function () { - popup.hide() - return false - }) - } - - elem.on("click", function () { - // Hide other popper menus on the page - const other = $(".popper_menu:visible").not(popup) - if (other.length) { - other.hide() - } - - // Close this menu if visible, show if hidden - if (popup.is(":visible")) { - popup.hide() - } else { - popup.show() - } - - // See https://api.jqueryui.com/position/ - popup.position({ - my: attrs.my || "right top", - at: attrs.at || "bottom right", - of: elem, - }) - - return false - }) - - // Hide menu if any other part of the page is clicked - $rootElement.on("click", () => popup.hide()) - }, - }), -]) diff --git a/app/scripts/extended.js b/app/scripts/extended.js index 69e452926..c8ac9a776 100644 --- a/app/scripts/extended.js +++ b/app/scripts/extended.js @@ -7,7 +7,6 @@ import { loc, locAttribute } from "@/i18n" import "@/components/autoc" import "@/components/datetime-picker" import "@/directives/escaper" -import "@/directives/popper" let customExtendedTemplates = {} @@ -178,17 +177,29 @@ export default _.merge( ], }, default: { - template: _.template(`\ - > - Aa - - `), + placeholder="<%= placeholder %>" + /> + + + + Aa + + + + `), controller: [ "$scope", function ($scope) { diff --git a/app/styles/styles.scss b/app/styles/styles.scss index 15b6a3ccb..0b4b72f06 100644 --- a/app/styles/styles.scss +++ b/app/styles/styles.scss @@ -291,7 +291,7 @@ label.placeholder { text-align : left; } } -.date_interval.popper_menu { +.date_interval { width: auto; padding : 1em; } @@ -931,15 +931,6 @@ a { .dropdown-toggle { background-color : transparent; } - &.menu_more { - margin-left: -3px; - a { - background: rgba(0,0,0,0); - } - i { - padding-left: 5px; - } - } } } @@ -1205,9 +1196,6 @@ body.modal-open { } } -.popover { - display : none; -} .search_submit { display : inline; margin-right : 5px; @@ -1824,10 +1812,31 @@ line.tick { } .popover { + // Override rules from Bootstrap + font-family: inherit; + font-size: inherit; + max-width: 90vw; + border-radius: 2px; + max-height : 575px; + .popover-content { padding : 5px; + overflow-y: auto; + } + + li { + &.selected a { + color : black; + } + a { + color: rgb(67 56 202 / var(--tw-text-opacity)); + } + a:hover { + color: indianred; + } } + .btn_container { margin-top : 1em; text-align : right; @@ -1909,6 +1918,7 @@ line.tick { border-radius: 2px; max-height : 575px; overflow-y: auto; + li { &.selected a { color : black; @@ -1920,7 +1930,6 @@ line.tick { color: indianred; } } - } /* ----------- Autocompletion ----------- */ From 92b5ea5d9c1bc17aba4d388910228c7fa0f1dcfd Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 10:57:24 +0200 Subject: [PATCH 94/99] docs: comment on shouldZoomToBounds origin --- CHANGELOG.md | 2 +- app/scripts/components/result-map.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd5c867c..2d84dfd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Replaced Raphael library with Chart.js, used in the pie chart over corpus distribution in statistics - Replaced jStorage library with native `localStorage`, and added TypeScript typings - In the `ParallelCorpusListing` class, the methods `getLinked` and `getEnabledByLang` have new parameter signatures -- Replaced custom `popper` directive with `uib-popover` and `uib-dropdown` () +- Replaced custom `popper` directive with `uib-popover` and `uib-dropdown` ([docs](https://angular-ui.github.io/bootstrap/)) - Removed the `mapper` template filter; change `x | mapper:f` to `f(x)` - Removed the global `c` alias for `console` - Removed global `lang`, use `$rootScope["lang"]` instead (outside Angular: `getService("$rootScope")["lang"]`) diff --git a/app/scripts/components/result-map.ts b/app/scripts/components/result-map.ts index 09eb047e4..1ebad4218 100644 --- a/app/scripts/components/result-map.ts +++ b/app/scripts/components/result-map.ts @@ -270,7 +270,9 @@ angular.module("korpApp").component("resultMap", { * check if the cluster with split into several clusters / markers on zooom * TODO: does not work in some cases */ - function shouldZooomToBounds(cluster) { + function shouldZooomToBounds(cluster: any) { + // This code is a modification of MarkerCluster.zoomToBounds() + // See https://github.com/Leaflet/Leaflet.markercluster/blob/master/src/MarkerCluster.js let childClusters = cluster._childClusters.slice() const map = cluster._group._map const boundsZoom = map.getBoundsZoom(cluster._bounds) From b6ae9e25bfe02bc7753b9f85fa3202ecaaaab052 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 11:17:28 +0200 Subject: [PATCH 95/99] refactor: simplify control flow in updateMarkers --- app/scripts/components/result-map.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/app/scripts/components/result-map.ts b/app/scripts/components/result-map.ts index 1ebad4218..1967472a2 100644 --- a/app/scripts/components/result-map.ts +++ b/app/scripts/components/result-map.ts @@ -410,22 +410,15 @@ angular.module("korpApp").component("resultMap", { }) function updateMarkers() { - if (markerCluster) { - map.removeLayer(markerCluster) - } - if (featureLayer) { - map.removeLayer(featureLayer) - } + if (markerCluster) map.removeLayer(markerCluster) + if (featureLayer) map.removeLayer(featureLayer) + if (useClustering()) { const selectedMarkers = $ctrl.selectedGroups.map((group) => $ctrl.markers[group]) const clusterGroups = _.keyBy(selectedMarkers, "color") markerCluster = createMarkerCluster(clusterGroups, $ctrl.restColor) map.addLayer(markerCluster) - } else { - featureLayer = createFeatureLayer() - map.addLayer(featureLayer) - } - if (useClustering()) { + const isCluster = $ctrl.selectedGroups.length !== 1 for (const group of $ctrl.selectedGroups) { const markerGroup = $ctrl.markers[group] @@ -439,14 +432,13 @@ angular.module("korpApp").component("resultMap", { point: markerOrig.point, queryData: markerOrig.queryData, } - if (useClustering()) { - markerCluster.addLayer(marker) - } else { - featureLayer.addLayer(marker) - } + markerCluster.addLayer(marker) }) } } else { + featureLayer = createFeatureLayer() + map.addLayer(featureLayer) + const markers = $ctrl.selectedGroups.map((group) => $ctrl.markers[group]) const markersMerged = mergeMarkers(markers) for (const markerData of markersMerged) { @@ -456,6 +448,7 @@ angular.module("korpApp").component("resultMap", { featureLayer.addLayer(marker) } } + updateMarkerSizes() } From 1db0c37abd4fc1c622f064eb54f6fddd34ad5951 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 11:18:15 +0200 Subject: [PATCH 96/99] fix: trigger map update when group selection changes --- app/scripts/controllers/map_controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scripts/controllers/map_controller.ts b/app/scripts/controllers/map_controller.ts index 4fc1b43d4..6e5197c94 100644 --- a/app/scripts/controllers/map_controller.ts +++ b/app/scripts/controllers/map_controller.ts @@ -93,10 +93,11 @@ angular.module("korpApp").directive("mapCtrl", [ $scope.toggleMarkerGroup = function (groupName: string) { $scope.markerGroups[groupName].selected = !$scope.markerGroups[groupName].selected + // It is important to replace the array, not modify it, to trigger a watcher in the result-map component. if ($scope.selectedGroups.includes(groupName)) { - $scope.selectedGroups.splice($scope.selectedGroups.indexOf(groupName), 1) + $scope.selectedGroups = $scope.selectedGroups.filter((group) => group != groupName) } else { - $scope.selectedGroups.push(groupName) + $scope.selectedGroups = [...$scope.selectedGroups, groupName] } } From 4597a780bf025ba040e17f77acb53c3ea89a99f0 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 15:32:10 +0200 Subject: [PATCH 97/99] fix: simplify and fix kwic arrow key navigation Fixes #368 --- CHANGELOG.md | 1 + app/scripts/components/kwic.js | 155 ++++++++++++--------------------- 2 files changed, 59 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d84dfd93..cb443959c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Paging broken in word picture example search [#383](https://github.com/spraakbanken/korp-frontend/issues/383) - Incoherent style change to corpus heading when switching between KWIC and context view [#389](https://github.com/spraakbanken/korp-frontend/issues/389) - Context view broken in example search [#386](https://github.com/spraakbanken/korp-frontend/issues/386) +- Can't navigate between tokens in KWIC using arrow keys [#368](https://github.com/spraakbanken/korp-frontend/issues/368) ## [9.6.0] - 2024-05-27 diff --git a/app/scripts/components/kwic.js b/app/scripts/components/kwic.js index 037c15a99..d66921bf5 100644 --- a/app/scripts/components/kwic.js +++ b/app/scripts/components/kwic.js @@ -172,6 +172,11 @@ angular.module("korpApp").component("kwic", { "$location", "$element", "$timeout", + /** + * @param {import("angular").ILocationService} $location + * @param {JQLite} $element + * @param {import("angular").ITimeoutService} $timeout + */ function ($location, $element, $timeout) { let $ctrl = this @@ -645,111 +650,66 @@ angular.module("korpApp").component("kwic", { } if (next) { - scrollToShowWord($(next)) + next.trigger("click") + scrollToShowWord(next) return false } }) } function selectNext() { - let next - if (!$ctrl.isReading) { - const i = getCurrentRow().index($element.find(".token_selected").get(0)) - next = getCurrentRow().get(i + 1) - if (next == null) { - return - } - $(next).click() - } else { - next = $element.find(".token_selected").next().click() - } - return next + return stepWord(1) } function selectPrev() { - let prev - if (!$ctrl.isReading) { - const i = getCurrentRow().index($element.find(".token_selected").get(0)) - if (i === 0) { - return - } - prev = getCurrentRow().get(i - 1) - $(prev).click() - } else { - prev = $element.find(".token_selected").prev().click() - } - return prev + return stepWord(-1) + } + + function stepWord(diff) { + const $words = $element.find(".word") + const $current = $element.find(".token_selected").first() + const currentIndex = $words.index($current) + const wouldWrap = (diff < 0 && currentIndex == 0) || (diff > 0 && currentIndex == $words.length - 1) + if (wouldWrap) return + const next = $words.get(currentIndex + diff) + return $(next) } function selectUp() { - let prevMatch const current = selectionManager.selected + const $prevSentence = current.closest(".sentence").prev(":not(.corpus_info)") + if (!$ctrl.isReading) { - prevMatch = getWordAt( - current.offset().left + current.width() / 2, - current.closest("tr").prevAll(":not(.corpus_info)").first() - ) - prevMatch.click() - } else { - const searchwords = current - .prevAll(".word") - .get() - .concat( - current - .closest(":not(.corpus_info)") - .prevAll(":not(.corpus_info)") - .first() - .find(".word") - .get() - .reverse() - ) - const def = current.parent().prev().find(".word:last") - prevMatch = getFirstAtCoor(current.offset().left + current.width() / 2, $(searchwords), def).click() + return getWordAt(current.offset().left + current.width() / 2, $prevSentence) } - return prevMatch + const searchwords = current.prevAll(".word").get().concat($prevSentence.find(".word").get().reverse()) + const def = $prevSentence.find(".word:last") + return getFirstAtCoor(current.offset().left + current.width() / 2, $(searchwords), def) } function selectDown() { - let nextMatch const current = selectionManager.selected + const $nextSentence = current.closest(".sentence").next(":not(.corpus_info)") + if (!$ctrl.isReading) { - nextMatch = getWordAt( - current.offset().left + current.width() / 2, - current.closest("tr").nextAll(":not(.corpus_info)").first() - ) - nextMatch.click() - } else { - const searchwords = current - .nextAll(".word") - .add(current.closest(":not(.corpus_info)").nextAll(":not(.corpus_info)").first().find(".word")) - const def = current.parent().next().find(".word:first") - nextMatch = getFirstAtCoor(current.offset().left + current.width() / 2, searchwords, def).click() + return getWordAt(current.offset().left + current.width() / 2, $nextSentence) } - return nextMatch - } - function getCurrentRow() { - const tr = $element.find(".token_selected").closest("tr") - if ($element.find(".token_selected").parent().is("td")) { - return tr.find("td > .word") - } else { - return tr.find("div > .word") - } + const searchwords = current.nextAll(".word").add($nextSentence.find(".word")) + const def = $nextSentence.find(".word:last") + return getFirstAtCoor(current.offset().left + current.width() / 2, searchwords, def) } - function getFirstAtCoor(xCoor, wds, default_word) { - let output = null - wds.each(function (i, item) { - const thisLeft = $(this).offset().left - const thisRight = $(this).offset().left + $(this).width() - if (xCoor > thisLeft && xCoor < thisRight) { - output = $(this) - return false - } - }) - - return output || default_word + /** + * @param {number} x + * @param {JQLite} wds + * @param {JQLite} default_word + */ + function getFirstAtCoor(x, wds, default_word) { + const isHit = (word) => x > $(word).offset().left && x < $(word).offset().left + $(word).width() + const hit = wds.get().find(isHit) + return hit ? $(hit) : default_word } function getWordAt(xCoor, $row) { @@ -766,28 +726,29 @@ angular.module("korpApp").component("kwic", { return output } + /** + * @param {JQLite} word + */ function scrollToShowWord(word) { - if (!word.length) { - return - } + if (!word.length) return const offset = 200 - const wordTop = word.offset().top - let newY = window.scrollY - if (wordTop > $(window).height() + window.scrollY) { - newY += offset$r - } else if (wordTop < window.scrollY) { - newY -= offset + + if (word.offset().top + word.height() > window.scrollY + $(window).height()) { + $("html, body") + .stop(true, true) + .animate({ scrollTop: window.scrollY + offset }) + } else if (word.offset().top < window.scrollY) { + $("html, body") + .stop(true, true) + .animate({ scrollTop: window.scrollY - offset }) } - $("html, body").stop(true, true).animate({ scrollTop: newY }) - const wordLeft = word.offset().left + const area = $element.find(".table_scrollarea") - let newX = Number(area.scrollLeft()) - if (wordLeft > area.offset().left + area.width()) { - newX += offset - } else if (wordLeft < area.offset().left) { - newX -= offset + if (word.offset().left + word.width() > area.offset().left + area.width()) { + area.stop(true, true).animate({ scrollLeft: area.scrollLeft() + offset }) + } else if (word.offset().left < area.offset().left) { + area.stop(true, true).animate({ scrollLeft: area.scrollLeft() - offset }) } - area.stop(true, true).animate({ scrollLeft: newX }) } }, ], From 4efb290482599d98e0712e0893655399e75f3d60 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Mon, 16 Sep 2024 16:48:55 +0200 Subject: [PATCH 98/99] fix: localization before modal shown Fixes #391 --- app/scripts/components/header.js | 34 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/app/scripts/components/header.js b/app/scripts/components/header.js index 612c7df18..1dc31e989 100644 --- a/app/scripts/components/header.js +++ b/app/scripts/components/header.js @@ -129,8 +129,9 @@ angular.module("korpApp").component("header", { "$uibModal", "$rootScope", "$scope", + "$timeout", "utils", - function ($location, $uibModal, $rootScope, $scope, utils) { + function ($location, $uibModal, $rootScope, $scope, $timeout, utils) { const $ctrl = this $scope.lang = $rootScope.lang @@ -160,16 +161,14 @@ angular.module("korpApp").component("header", { $rootScope.show_modal = false let modal = null + utils.setupHash($rootScope, { key: "display", scope_name: "show_modal", post_change(val) { - if (val) { - showAbout() - } else { - if (modal != null) { - modal.close() - } + if (val) showAbout() + else { + modal?.close() modal = null } }, @@ -181,18 +180,17 @@ angular.module("korpApp").component("header", { const modalScope = $rootScope.$new(true) modalScope.clickX = () => closeModals() - var showAbout = function () { - const params = { - template: require("../../markup/about.html"), - scope: modalScope, - windowClass: "about", - } - modal = $uibModal.open(params) - modal.result.then( - () => closeModals(), - () => closeModals() - ) + function showAbout() { + // $timeout is used to let localization happen before modal is shown (if loaded with "display=about") + $timeout(() => { + modal = $uibModal.open({ + template: require("../../markup/about.html"), + scope: modalScope, + windowClass: "about", + }) + modal.result.catch(() => closeModals()) + }) } const N_VISIBLE = settings["visible_modes"] From 7d723b9b6a43e2f84949065488be9ccb705a7bc2 Mon Sep 17 00:00:00 2001 From: Arild Matsson Date: Tue, 17 Sep 2024 14:25:01 +0200 Subject: [PATCH 99/99] chore: bump to 9.7.0 --- CHANGELOG.md | 3 +++ app/markup/about.html | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb443959c..8978c7a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [9.7.0] - 2024-09-17 + ### Added - TypeScript typings for: @@ -255,6 +257,7 @@ - Lots of bug fixes for the sidebar [unreleased]: https://github.com/spraakbanken/korp-frontend/compare/master...dev +[9.7.0]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.7.0 [9.6.0]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.6.0 [9.5.3]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.3 [9.5.2]: https://github.com/spraakbanken/korp-frontend/releases/tag/v9.5.2 diff --git a/app/markup/about.html b/app/markup/about.html index be4454eb0..48d4519e5 100644 --- a/app/markup/about.html +++ b/app/markup/about.html @@ -1,5 +1,5 @@ diff --git a/package.json b/package.json index 668f85b8a..118c31f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "korp-frontend", - "version": "9.6.0", + "version": "9.7.0", "dependencies": { "@fortawesome/fontawesome-free": "6.2.1", "@types/angular-dynamic-locale": "^0.1.35",