diff --git a/package-lock.json b/package-lock.json index 175f9adb..b45cc0b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@subugoe/tido", "version": "3.3.0", "license": "AGPL-3.0-or-later", + "dependencies": { + "pinia": "^2.1.7" + }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", "@vueuse/core": "^10.9.0", @@ -25,7 +28,6 @@ "standard-version": "^9.5.0", "start-server-and-test": "^2.0.3", "tailwindcss": "^3.4.1", - "typescript": "^5.4.5", "vite": "^5.2.6", "vue": "^3.4.20", "vue-i18n": "^9.2.0-beta.35", @@ -169,7 +171,6 @@ "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -891,8 +892,7 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1224,7 +1224,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz", "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", - "dev": true, "dependencies": { "@babel/parser": "^7.23.9", "@vue/shared": "3.4.21", @@ -1237,7 +1236,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", - "dev": true, "dependencies": { "@vue/compiler-core": "3.4.21", "@vue/shared": "3.4.21" @@ -1247,7 +1245,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", - "dev": true, "dependencies": { "@babel/parser": "^7.23.9", "@vue/compiler-core": "3.4.21", @@ -1264,7 +1261,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", - "dev": true, "dependencies": { "@vue/compiler-dom": "3.4.21", "@vue/shared": "3.4.21" @@ -1273,14 +1269,12 @@ "node_modules/@vue/devtools-api": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", - "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==", - "dev": true + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" }, "node_modules/@vue/reactivity": { "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz", "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", - "dev": true, "dependencies": { "@vue/shared": "3.4.21" } @@ -1289,7 +1283,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz", "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", - "dev": true, "dependencies": { "@vue/reactivity": "3.4.21", "@vue/shared": "3.4.21" @@ -1299,7 +1292,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", - "dev": true, "dependencies": { "@vue/runtime-core": "3.4.21", "@vue/shared": "3.4.21", @@ -1310,7 +1302,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz", "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", - "dev": true, "dependencies": { "@vue/compiler-ssr": "3.4.21", "@vue/shared": "3.4.21" @@ -1322,8 +1313,7 @@ "node_modules/@vue/shared": { "version": "3.4.21", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", - "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==", - "dev": true + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" }, "node_modules/@vueuse/core": { "version": "10.9.0", @@ -2676,8 +2666,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { "version": "13.7.0", @@ -3111,7 +3100,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -3612,8 +3600,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -5475,7 +5462,6 @@ "version": "0.30.8", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, @@ -5850,7 +5836,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -6312,8 +6297,7 @@ "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6336,6 +6320,56 @@ "node": ">=0.10.0" } }, + "node_modules/pinia": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz", + "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -6390,7 +6424,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7234,7 +7267,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8055,7 +8087,8 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, + "optional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8282,7 +8315,6 @@ "version": "3.4.21", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", - "dev": true, "dependencies": { "@vue/compiler-dom": "3.4.21", "@vue/compiler-sfc": "3.4.21", diff --git a/package.json b/package.json index 2a52e281..90622cfb 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "http-server": "^14.1.1", "ncp": "^2.0.0", "openseadragon": "^3.1.0", + "pinia": "^2.1.7", "primevue": "^3.49.1", "sass": "^1.71.1", "standard-version": "^9.5.0", diff --git a/src/App.vue b/src/App.vue index b88de9f0..f653905a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -26,17 +26,19 @@ - - + \ No newline at end of file diff --git a/src/components/ContentView.vue b/src/components/ContentView.vue index b3dde870..8beac3fb 100644 --- a/src/components/ContentView.vue +++ b/src/components/ContentView.vue @@ -22,6 +22,7 @@ import { computed, readonly, ref, watch, } from 'vue'; import { useStore } from 'vuex'; +import { useConfigStore } from '@/stores/config'; import Notification from '@/components/Notification.vue'; import { request } from '@/utils/http'; import { domParser, delay } from '@/utils'; @@ -34,12 +35,13 @@ const props = defineProps({ const emit = defineEmits(['loading']); const store = useStore(); +const configStore = useConfigStore() const content = ref(''); const errorTextMessage = ref(null); const notificationMessage = readonly(errorTextMessage); -const config = computed(() => store.getters['config/config']); +const config = computed(() => configStore.config); const contentStyle = computed(() => ({ fontSize: `${props.fontSize}px`, })); @@ -118,4 +120,4 @@ function isValidTextContent(text) { .rtl { direction: rtl; } - + \ No newline at end of file diff --git a/src/components/Notification.vue b/src/components/Notification.vue index 69cd8cd9..742cd92f 100644 --- a/src/components/Notification.vue +++ b/src/components/Notification.vue @@ -17,7 +17,7 @@ + \ No newline at end of file diff --git a/src/components/TreeView.vue b/src/components/TreeView.vue index 88d99583..efe96551 100644 --- a/src/components/TreeView.vue +++ b/src/components/TreeView.vue @@ -48,6 +48,7 @@ import { computed, nextTick, onMounted, ref, watch, } from 'vue'; import { useStore } from 'vuex'; +import { useConfigStore } from '@/stores/config'; import { useI18n } from 'vue-i18n'; import { request } from '@/utils/http'; import { isElementVisible } from '@/utils'; @@ -55,6 +56,7 @@ import { isElementVisible } from '@/utils'; const emit = defineEmits(['loading']); const store = useStore(); +const configStore = useConfigStore() const { t } = useI18n(); const expanded = ref({}); @@ -62,7 +64,7 @@ const selected = ref(null); const tree = ref([]); const containerRef = ref(null); -const config = computed(() => store.getters['config/config']); +const config = computed(() => configStore.config); const collectionTitle = computed(() => store.getters['contents/collectionTitle']); const collection = computed(() => store.getters['contents/collection']); const labels = computed(() => (config.value && config.value.labels) || {}); @@ -162,10 +164,11 @@ async function onNodeExpand(node) { } async function onNodeSelect(node) { + const configStore = useConfigStore() if (currentManifest.value.id !== node.parent) { // If we selected an item from a different manifest await store.dispatch('contents/initManifest', node.parent); - await store.dispatch('config/setDefaultActiveViews'); + await configStore.setDefaultActiveViews() } await store.dispatch('contents/initItem', node.key); @@ -204,4 +207,4 @@ function getNodeByKey(key, root) { return root.children.find((child) => !!(getNodeByKey(key, child))); } - + \ No newline at end of file diff --git a/src/components/annotations/AnnotationIcon.vue b/src/components/annotations/AnnotationIcon.vue index 8680e892..8a7e3990 100644 --- a/src/components/annotations/AnnotationIcon.vue +++ b/src/components/annotations/AnnotationIcon.vue @@ -12,6 +12,7 @@ import BaseIcon from '@/components/base/BaseIcon.vue'; interface Props { name: string } + const props = defineProps() diff --git a/src/components/annotations/AnnotationsView.vue b/src/components/annotations/AnnotationsView.vue index 54c11513..6aae6de8 100644 --- a/src/components/annotations/AnnotationsView.vue +++ b/src/components/annotations/AnnotationsView.vue @@ -28,6 +28,10 @@ import AnnotationsList from '@/components/annotations/AnnotationsList.vue'; import Notification from '@/components/Notification.vue'; import * as AnnotationUtils from '@/utils/annotations'; +import { useConfigStore } from '@/stores/config'; + +const configStore = useConfigStore() + const props = defineProps({ url: String, types: Array, @@ -36,7 +40,7 @@ const props = defineProps({ const store = useStore(); const message = ref('no_annotations_in_view'); -const config = computed(() => store.getters['config/config']); +const config = computed(() => configStore.config); const annotations = computed(() => store.getters['annotations/annotations']); const activeAnnotations = computed(() => store.getters['annotations/activeAnnotations']); const filteredAnnotations = computed(() => store.getters['annotations/filteredAnnotations']); @@ -95,4 +99,4 @@ function highlightTargetsLevel0() { + \ No newline at end of file diff --git a/src/components/base/BaseCheckbox.vue b/src/components/base/BaseCheckbox.vue index 59116499..654dbff3 100644 --- a/src/components/base/BaseCheckbox.vue +++ b/src/components/base/BaseCheckbox.vue @@ -1,6 +1,5 @@ + \ No newline at end of file diff --git a/src/components/header/GlobalHeader.vue b/src/components/header/GlobalHeader.vue index dcd98324..fa410852 100644 --- a/src/components/header/GlobalHeader.vue +++ b/src/components/header/GlobalHeader.vue @@ -19,6 +19,7 @@ + \ No newline at end of file diff --git a/src/components/header/Language.vue b/src/components/header/Language.vue index 99a65423..57caa7fc 100644 --- a/src/components/header/Language.vue +++ b/src/components/header/Language.vue @@ -21,7 +21,7 @@ import { computed, onMounted, ref, watch, } from 'vue'; -import { useStore } from 'vuex'; +import { useConfigStore } from '@/stores/config'; import { useI18n } from 'vue-i18n'; import BaseDropdown from '@/components/base/BaseDropdown.vue'; @@ -30,7 +30,7 @@ interface Language { value: string } -const store = useStore(); +const configStore = useConfigStore() const { locale: i18nLocale } = useI18n(); const langs = ref([ @@ -39,7 +39,7 @@ const langs = ref([ ]); const selectedLang = ref(langs.value[0]); const showDropdown = ref(false); -const config = computed(() => store.getters['config/config']); +const config = computed(() => configStore.config); watch( selectedLang, diff --git a/src/components/header/Navbar.vue b/src/components/header/Navbar.vue index 6846a2ca..c294288b 100644 --- a/src/components/header/Navbar.vue +++ b/src/components/header/Navbar.vue @@ -26,10 +26,12 @@ + \ No newline at end of file diff --git a/src/components/header/PanelsToggle.vue b/src/components/header/PanelsToggle.vue index 425aefe1..5acf0041 100644 --- a/src/components/header/PanelsToggle.vue +++ b/src/components/header/PanelsToggle.vue @@ -57,20 +57,19 @@ + \ No newline at end of file diff --git a/src/components/header/TitleBar.vue b/src/components/header/TitleBar.vue index 53204dfe..70e8d972 100644 --- a/src/components/header/TitleBar.vue +++ b/src/components/header/TitleBar.vue @@ -36,6 +36,7 @@ diff --git a/src/components/panels/Panel.vue b/src/components/panels/Panel.vue index 104bffc0..bed3c1eb 100644 --- a/src/components/panels/Panel.vue +++ b/src/components/panels/Panel.vue @@ -95,6 +95,7 @@ import { computed, nextTick, ref, watch, } from 'vue'; import { useStore } from 'vuex'; +import { useConfigStore } from '@/stores/config'; import { useI18n } from 'vue-i18n'; import TabView from 'primevue/tabview'; import TabPanel from 'primevue/tabpanel'; @@ -136,6 +137,7 @@ export default { }, setup(props, { emit }) { const store = useStore(); + const configStore = useConfigStore() const { t } = useI18n(); const tabs = ref([]); @@ -317,7 +319,7 @@ export default { [contentItem] = item.value.content; // TODO: this should be moved to loading time in order dynamically recognize all content types // instead of only the first one - store.dispatch('config/setContentType', contentItem.type.split('type=')[1]); + configStore.setContentType(contentItem.type.split('type=')[1]); } contentItem = item.value.content.find((c) => c.type.split('type=')[1] === type); diff --git a/src/components/panels/PanelsWrapper.vue b/src/components/panels/PanelsWrapper.vue index 3c164a4d..49645dd5 100644 --- a/src/components/panels/PanelsWrapper.vue +++ b/src/components/panels/PanelsWrapper.vue @@ -15,23 +15,25 @@ + \ No newline at end of file diff --git a/src/main.js b/src/main.js index 1ae38cf1..0914c20d 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { createApp, h } from 'vue'; import PrimeVue from 'primevue/config'; +import { createPinia } from 'pinia' import store from './store'; import { i18n } from './i18n'; import App from './App.vue'; @@ -8,6 +9,8 @@ import './css/style.css'; import './css/style.scss'; import { getRGBColor } from '@/utils/color'; +const pinia = createPinia() + function generateId() { return Math.random().toString(36).slice(2, 16); } @@ -23,6 +26,7 @@ window.Tido = function Tido(config = {}) { this.app.provide('config', this.config); this.app.use(PrimeVue); + this.app.use(pinia); this.app.use(store); this.app.use(i18n); @@ -63,4 +67,4 @@ window.Tido = function Tido(config = {}) { this.mount(container); }; -export default window.Tido; +export default window.Tido; \ No newline at end of file diff --git a/src/store/annotations/actions.js b/src/store/annotations/actions.js index c73cf3ea..66e4915a 100644 --- a/src/store/annotations/actions.js +++ b/src/store/annotations/actions.js @@ -4,7 +4,10 @@ import * as Utils from '@/utils'; import { scrollIntoViewIfNeeded } from '@/utils'; import { getAnnotationListElement } from '@/utils/annotations'; +import { useConfigStore } from '@/stores/config'; + export const addActiveAnnotation = ({ getters, rootGetters, dispatch }, id) => { + const configStore = useConfigStore() const { activeAnnotations, annotations } = getters; const newActiveAnnotation = annotations.find((annotation) => annotation.id === id); @@ -12,7 +15,7 @@ export const addActiveAnnotation = ({ getters, rootGetters, dispatch }, id) => { return; } - const iconName = rootGetters['config/getIconByType'](newActiveAnnotation.body['x-content-type']); + const iconName = configStore.getIconByType(newActiveAnnotation.body['x-content-type']); const activeAnnotationsList = { ...activeAnnotations }; @@ -37,7 +40,8 @@ export const setActiveAnnotations = ({ commit }, activeAnnotations) => { export const setFilteredAnnotations = ({ commit, getters, rootGetters }, types) => { const { annotations } = getters; - const activeContentType = rootGetters['config/activeContentType']; + const configStore = useConfigStore() + const activeContentType = configStore.activeContentType let filteredAnnotations = []; if (annotations !== null) { @@ -143,6 +147,7 @@ export const addHighlightHoverListeners = ({ getters, rootGetters }) => { const annotationElements = Array.from(document.querySelectorAll('[data-annotation]')); const tooltipEl = null; + const configStore = useConfigStore() // Annotations can be nested, so we filter out all outer elements from this selection and // iterate over the deepest elements @@ -161,7 +166,7 @@ export const addHighlightHoverListeners = ({ getters, rootGetters }) => { const { filteredAnnotations } = getters; const annotationTooltipModels = filteredAnnotations.reduce((acc, curr) => { const { id } = curr; - const name = rootGetters['config/getIconByType'](curr.body['x-content-type']); + const name = configStore.getIconByType(curr.body['x-content-type']) acc[id] = { value: curr.body.value, name, @@ -283,4 +288,4 @@ function discoverChildAnnotationIds(el, annotationIds = {}) { } }); return annotationIds; -} +} \ No newline at end of file diff --git a/src/store/config/actions.js b/src/store/config/actions.js deleted file mode 100644 index a5771986..00000000 --- a/src/store/config/actions.js +++ /dev/null @@ -1,376 +0,0 @@ -import messages from 'src/i18n'; -import { isUrl } from '@/utils'; -import BookmarkService from '@/services/bookmark'; -import { i18n } from '@/i18n'; - -const defaultPanel = { - label: 'Panel', - show: true, - toggle: true, - views: [], -}; - -const defaultView = { - id: 'view', - name: 'View', - default: false, - connector: { - id: 1, - options: {}, - }, -}; - -function validateCollection(value) { - return !!(value); -} - -function validateManifest(value) { - return !!(value); -} - -function validateItem(value) { - return !!(value); -} - -function validateTranslations(value) { - return !!(value) && Object.keys(value).every((key) => key === 'en' || key === 'de'); -} - -function validatePanels(value) { - return !!(value) && Array.isArray(value); -} - -function validateLang(value) { - return !!(value); -} - -function validateColors(value) { - return !!(value); -} - -function validateContainer(value) { - return !!(value); -} - -function validateHeader(value, defaultValue) { - if (!value) return false; - - const defaultKeys = Object.keys(defaultValue); - const invalidKeys = Object.keys(value) - .filter((key) => defaultKeys.findIndex((defaultKey) => defaultKey === key) === -1); - return invalidKeys.length === 0; -} - -function validateLabels(labels, validLabels) { - // valid labels are the labels from the default config - // we consider the custom labels, in the case when all the keys have a value, otherwise we would have the button with empty text i.e for the following scenario - // when the item is '' - if (!labels || !validLabels) return false; - - let isValid = true; - Object.keys(labels).forEach((key) => { - if (!(key in validLabels) || labels[key] === '') { - isValid = false; - } - }); - - return isValid; -} - -function createDefaultActiveViews(panelsConfig) { - return panelsConfig - .filter((p) => p.views && p.views.length > 0) - .map((panel) => { - const defaultIndex = panel.views.findIndex((view) => view.default === true); - return defaultIndex > -1 ? defaultIndex : 0; - }) - .reduce((acc, cur, i) => { - acc[i] = cur; - return acc; - }, {}); -} - -// URL Config -function splitUrlParts(urlQuery, attributes) { - if (urlQuery === '') { - return [undefined, undefined, undefined, undefined]; - } - const arrayAttributes = urlQuery.split('_'); - const manifestPart = arrayAttributes.find((element) => element[0].includes(attributes[0])); // index of manifest part in the splitted array: element[0] is 'm' the first letter of the part ? - const itemPart = arrayAttributes.find((element) => element[0].includes(attributes[1])); - const panelsPart = arrayAttributes.find((element) => element[0].includes(attributes[2])); - const showPart = arrayAttributes.find((element) => element[0].includes(attributes[3])); - return [manifestPart, itemPart, panelsPart, showPart]; -} - -function isManifestPartValid(manifestPart) { - const regexManifest = /m\d+$/; - return regexManifest.exec(manifestPart) !== null; -} - -function isItemPartValid(itemPart) { - const regexItem = /i\d+$/; - return regexItem.exec(itemPart) !== null; -} - -function isPanelsPartValid(panelsPart, panelsValue, numberPanels) { - const numbersPartArray = panelsValue.split('-'); - const regexNumber = /^\d+$/; - if (panelsPart[0] !== 'p' || numbersPartArray.length !== numberPanels) { - return false; - } - - for (let i = 0; i < numbersPartArray.length; i++) { - const panelTabPair = numbersPartArray[i]; - if (panelTabPair.length !== 3 - || regexNumber.test(panelTabPair[0]) === false - || regexNumber.test(panelTabPair[2]) === false - || panelTabPair[1] !== '.') { - return false; - } - } - return true; -} - -function isShowPartValid(showValue, numberPanels) { - const showValueAsArray = showValue.split('-'); - const regexNumbersPart = /\d\-/; - if (showValueAsArray.length > numberPanels) { - return false; - } - for (let i = 0; i < showValueAsArray.length - 1; i++) { - // if s0-2 is given and there are in total 4 panels, then it is still fine, since we can show less number of panels than the total one - // match the couples of (d-) -> a digit followed by a "-" character. In total there are (s.length - 1) - so number of panels we want to open - 1 - const groupMatch = showValue.slice(i * 2, i * 2 + 2).match(regexNumbersPart); - if (groupMatch === null) { - return false; - } - } - const lastNumberString = showValue.slice(-1)[0]; - const lastNumberInt = parseInt(lastNumberString, 10); - // last character must have only digits and not be greater than number of max panels - if (/^\d+$/.test(lastNumberString) === false || (lastNumberInt >= numberPanels || lastNumberInt < 0)) { - return false; - } - return true; -} - -function createDefaultPanelValue(numberPanels) { - // get the number of panels and then create as many couples of (panel_index.0) until n_panels-1, the last couple need not have the '-' symbol - let p = ''; - for (let j = 0; j < numberPanels; j++) { - if (j !== numberPanels - 1) { - p += `${j}.0-`; - } else { - p += `${j}.0`; - } - } - return p; -} - -function createActiveViewsFromPanelsArray(panelsArray) { - // converts 'panelsArray' to an object with key, value: 'panel index: visible tab index' - return panelsArray.reduce((acc, cur) => { - // eslint-disable-next-line no-shadow - const [panelIndex, viewIndex] = cur.split('.').map((i) => parseInt(i, 10)); - acc[panelIndex] = viewIndex; - return acc; - }, {}); -} - -function discoverCustomConfig(customConfig, defaultConfig) { - const { - container, translations, collection, manifest, item, panels, lang, colors, header, labels - } = customConfig; - - return { - ...(validateContainer(container) && { container }), - ...(validateCollection(collection) && { collection }), - ...(validateManifest(manifest) && { manifest }), - ...(validateItem(item) && { item }), - ...(validateTranslations(translations) && { translations }), - ...(validatePanels(panels) && { panels }), - ...(validateLang(lang) && { lang }), - ...(validateColors(colors) && { colors }), - ...(validateHeader(header, defaultConfig.header) && { header }), - ...(validateLabels(labels, defaultConfig.labels) && { labels }), - }; -} - -function discoverUrlConfig(config) { - // split the url based on '_' - // get the part of attribute: get the attribute name and the value based on the type of attribute - // add each attribute to UrlConfig as key value - let urlConfig = {}; - const urlQuery = BookmarkService.getQuery(); - const attributes = ['m', 'i', 'p', 's']; - // values of manifest, item Indices ... - let [m, i, p, s] = [undefined, undefined, undefined, undefined]; - const numberPanels = config.panels ? config.panels.length : 0; - const [manifestPart, itemPart, panelsPart, showPart] = splitUrlParts(urlQuery, attributes); - /* - if (isUrl(item)) urlConfig.item = item; - if (isUrl(manifest)) urlConfig.manifest = manifest; - if (isUrl(collection)) urlConfig.collection = collection; - */ - // here we will validate for the structure of each component:, not their value range - if (manifestPart !== undefined) { - if (!isManifestPartValid(manifestPart)) { - throw new Error(i18n.global.t('error_manifestpart_tido_url')); - } else { - urlConfig.m = parseInt(manifestPart.slice(1), 10); - } - } - if (itemPart !== undefined) { - if (!isItemPartValid(itemPart)) { - throw new Error(i18n.global.t('error_itempart_tido_url')); - } else { - urlConfig.i = parseInt(itemPart.slice(1), 10); - } - } - if (panelsPart !== undefined) { - const panelsValue = panelsPart.slice(1); - if (!isPanelsPartValid(panelsPart, panelsValue, numberPanels)) { - throw new Error(i18n.global.t('error_panelspart_tido_url')); - } else { - p = panelsValue; - } - } else { - p = createDefaultPanelValue(numberPanels); - } - const panelsArray = p !== '' ? p.split('-') : []; - urlConfig.activeViews = createActiveViewsFromPanelsArray(panelsArray); - - if (showPart !== undefined) { - const showValue = showPart.slice(1); - if (!isShowPartValid(showValue, numberPanels)) { - throw new Error(i18n.global.t('error_showpart_tido_url')); - } else { - // showValue needs to be an array of opened panel indices (Integers) - s = showValue.split('-').map(Number); - } - } - if (s === undefined) { - // If 's' is not given in URL, then we open all the panels which are given in config - urlConfig.show = Array.from({ length: numberPanels }, (value, index) => index); - } else { - urlConfig.show = s; - } - return urlConfig; -} - -function discoverDefaultConfig(config) { - return { - ...JSON.parse(JSON.stringify(config)), - activeViews: createDefaultActiveViews(config.panels), - }; -} - -export const load = ({ commit, getters, dispatch }, config) => { - const customConfig = discoverCustomConfig(config, getters.config); - const urlConfig = discoverUrlConfig(config); - const defaultConfig = discoverDefaultConfig(getters.config); - - const header = { - ...defaultConfig.header, - ...customConfig.header, - }; - - if (customConfig.panels) { - // If the custom config provide panels config, we still need to check if it's valid. - // Here we enhance the potentially missing parts with default panel/view config. - // Hint: Not to confuse with the "defaultConfig" which provides an out-of-the-box panels setup - - customConfig.panels = customConfig.panels.map((panel) => { - if (panel.views) { - panel.views = panel.views.map((view) => ({ - ...defaultView, - ...view, - })); - } - - return { - ...defaultPanel, - ...panel, - }; - }); - } - - const resultConfig = { - ...defaultConfig, - ...customConfig, - ...urlConfig, - header, - }; - - const activeViews = urlConfig.activeViews || defaultConfig.activeViews; - commit('setActiveViews', activeViews); - - if (resultConfig.show && resultConfig.show.length > 0) { - // Set visible panels - // First hide all - resultConfig.panels.map((panel, i) => resultConfig.panels[i].show = false); - - // Next show configured - resultConfig.show.forEach((panelIndex) => { - if (!Number.isInteger(panelIndex)) return; - const panel = resultConfig.panels[panelIndex]; - if (!panel) return; - - resultConfig.panels[panelIndex].show = true; - }); - } - - if (resultConfig.translations) { - const locales = Object.keys(resultConfig.translations); - - locales.forEach((locale) => { - i18n.global.setLocaleMessage(locale, { ...(messages[locale] ? messages[locale] : {}), ...resultConfig.translations[locale] }); - }); - } - commit('setConfig', resultConfig); - - if (urlConfig.activeViews) commit('setActiveViews', activeViews); - else dispatch('setDefaultActiveViews', false); -}; - -export const setActivePanelView = async ({ commit, getters }, { panelIndex, viewIndex }) => { - commit('setActivePanelView', { panelIndex, viewIndex }); - await BookmarkService.updatePanels(getters.activeViews); -}; - -export const setShowPanel = ({ commit, getters }, { index, show }) => { - commit('setShowPanel', { index, show }); - - let panelIndexes = getters.config.panels.reduce((acc, cur, i) => (cur.show ? [...acc, i] : acc), []); - if (panelIndexes.length === getters.config.panels.length) panelIndexes = []; - - BookmarkService.updateShow(panelIndexes); -}; - -export const setContentType = ({ commit, getters }, type) => { - const { config } = getters; - const newConfig = { ...config }; - - newConfig.panels[3].views[0].connector.options = { type }; - commit('setConfig', newConfig); -}; - -export const setDefaultActiveViews = async ({ commit, getters }, bookmark = true) => { - const { config } = getters; - const activeViews = []; - - config.panels.forEach(({ views }, panelIndex) => { - let defaultViewIndex = views.findIndex((view) => !!(view.default)); - if (defaultViewIndex === -1) defaultViewIndex = 0; - activeViews[panelIndex] = defaultViewIndex; - }); - - if (bookmark) await BookmarkService.updatePanels(activeViews); - - commit('config/setActiveViews', activeViews, { root: true }); -}; - -export const setInstanceId = ({ commit }, id) => { - commit('config/setInstanceId', id); -}; diff --git a/src/store/config/getters.js b/src/store/config/getters.js deleted file mode 100644 index 229ad806..00000000 --- a/src/store/config/getters.js +++ /dev/null @@ -1,26 +0,0 @@ -export const config = (state) => state.config; - -export const activeViews = (state) => state.activeViews; - -// eslint-disable-next-line no-shadow -export const activeContentType = ({ config, activeViews }) => { - const contentConnectorId = 4; - const panelIndex = config.panels.findIndex(({ views }) => views.find(({ connector }) => contentConnectorId === connector.id)); - - if (panelIndex === -1) return -1; - - const viewIndex = activeViews[panelIndex]; - return config.panels[panelIndex].views[viewIndex].connector.options.type; -}; - -// eslint-disable-next-line no-shadow -export const getIconByType = ({ config, activeViews }) => (type) => { - const annotationsConnectorId = 5; - const panelIndex = config.panels.findIndex(({ views }) => views.find(({ connector }) => annotationsConnectorId === connector.id)); - - if (panelIndex === -1) return -1; - - const viewIndex = activeViews[panelIndex]; - const types = config.panels[panelIndex].views[viewIndex].connector.options?.types; - return types.find(({ name }) => name === type)?.icon || 'biPencilSquare'; -}; diff --git a/src/store/config/index.js b/src/store/config/index.js deleted file mode 100644 index b0beb1b9..00000000 --- a/src/store/config/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import state from './state'; -import * as getters from './getters'; -import * as mutations from './mutations'; -import * as actions from './actions'; - -export default { - namespaced: true, - state, - getters, - mutations, - actions, -}; diff --git a/src/store/config/mutations.js b/src/store/config/mutations.js deleted file mode 100644 index 3119ddc3..00000000 --- a/src/store/config/mutations.js +++ /dev/null @@ -1,31 +0,0 @@ -export const setConfig = (state, payload) => { - state.config = payload; -}; - -export const setActivePanelView = (state, { panelIndex, viewIndex }) => { - const { activeViews } = state; - if (activeViews[panelIndex] !== undefined) { - activeViews[panelIndex] = viewIndex; - } -}; - -export const setPanels = (state, panels) => { - state.config.panels = panels; -}; - -export const setShowPanel = (state, { index, show }) => { - state.config.panels[index].show = show; -}; - -export const loadConfig = (state, { config, isValid }) => { - state.config = config; - state.isValid = isValid; -}; - -export const setActiveViews = (state, payload) => { - state.activeViews = payload; -}; - -export const setInstanceId = (state, payload) => { - state.instanceId = payload; -}; diff --git a/src/store/config/state.js b/src/store/config/state.js deleted file mode 100644 index 96900b5b..00000000 --- a/src/store/config/state.js +++ /dev/null @@ -1,137 +0,0 @@ -export default function ConfigState() { - return { - instanceId: null, - config: { - container: '#app', - collection: '', - manifest: '', - item: '', - panels: [ - { - label: 'contents', - toggle: true, - show: true, - views: [ - { - id: 'tree', - label: 'contents', - connector: { - id: 1, - options: { - labels: { - item: 'Sheet', - manifest: 'Manuscript', - }, - }, - }, - }, - ], - }, - { - label: 'metadata', - show: true, - toggle: true, - views: [ - { - id: 'metadata', - label: 'metadata', - connector: { - id: 2, - options: { - collection: { - all: true, - }, - manifest: { - all: true, - }, - item: { - all: true, - }, - }, - }, - }, - ], - }, - { - label: 'image', - show: true, - toggle: true, - views: [ - { - id: 'image', - label: 'Image', - connector: { - id: 3, - }, - }], - }, - { - label: 'text', - show: true, - toggle: true, - views: [ - { - id: 'text1', - label: 'Transcription', - default: true, - connector: { - id: 4, - }, - }, - ], - }, - { - label: 'annotations', - show: true, - toggle: true, - views: [ - { - id: 'annotations1', - label: 'annotations', - connector: { - id: 5, - options: { - types: [], - }, - }, - }, - ], - }, - ], - colors: { - forceMode: 'none', - primary: '', - secondary: '', - accent: '', - }, - header: { - show: true, - navigation: true, - panelsToggle: true, - languageSwitch: false, - }, - labels: { - item: 'Sheet', - manifest: 'Manuscript', - }, - lang: 'en', - meta: { - collection: { - all: true, - }, - manifest: { - all: true, - }, - item: { - all: true, - }, - }, - notificationColors: { - info: 'blue-400', - warning: 'red-400', - }, - }, - activeViews: [], - isValid: false, - }; -} diff --git a/src/store/contents/actions.js b/src/store/contents/actions.js index 59136ef9..a3497882 100644 --- a/src/store/contents/actions.js +++ b/src/store/contents/actions.js @@ -3,6 +3,9 @@ import { i18n } from '@/i18n'; import BookmarkService from '@/services/bookmark'; import { loadCss, loadFont } from '../../utils'; +import { useConfigStore } from '@/stores/config'; + + export const getItemIndex = async ({ getters }, itemUrl) => { const { manifest } = getters; if (!manifest) return -1; @@ -44,9 +47,10 @@ function isItemPartInsideRangeValue(i, numberItems) { export const initCollection = async ({ commit, dispatch, getters, rootGetters, }, url) => { - const { item } = getters; - const resultConfig = rootGetters['config/config']; - let { item: itemUrl } = rootGetters['config/config']; + const configStore = useConfigStore() + const resultConfig = configStore.config; + let item = resultConfig.item; + let itemUrl; let collection = ''; let activeManifest = ''; let manifestIndex; @@ -105,22 +109,20 @@ export const initCollection = async ({ activeManifest = manifests[manifestIndex]; itemUrl = activeManifest.sequence[itemIndex].id; - if ('p' in resultConfig) commit('setPanels', resultConfig.p); - if ('s' in resultConfig) commit('setShow', resultConfig.s); const { support } = activeManifest; if (support && support.length > 0) { await dispatch('getSupport', support); } commit('setManifest', activeManifest); - - if (!item) dispatch('initItem', itemUrl); + dispatch('initItem', itemUrl); } }; export const initManifest = async ({ commit, dispatch, rootGetters, }, url) => { + const configStore = useConfigStore() let manifest = ''; try { manifest = await request(url); @@ -133,7 +135,7 @@ export const initManifest = async ({ const numberItems = manifest.sequence.length; commit('setManifest', manifest); - const resultConfig = rootGetters['config/config']; + const resultConfig = configStore.config; const { item } = resultConfig; let itemIndex; @@ -195,6 +197,7 @@ export const initItem = async ({ commit, dispatch, getters }, url) => { m, i, } : { i }; + await BookmarkService.updateQuery(query); }; @@ -208,11 +211,12 @@ export const initAnnotations = async ({ commit }, url) => { }; export const getSupport = ({ rootGetters }, support) => { - const { container } = rootGetters['config/config']; + const configStore = useConfigStore() + const { container } = configStore.config; support.forEach((s) => { const hasElement = document.getElementById(s.url); if (s.type === 'font' && !hasElement) loadFont(s.url, container); if (s.type !== 'font' && !hasElement) loadCss(s.url); }); -}; +}; \ No newline at end of file diff --git a/src/store/contents/getters.js b/src/store/contents/getters.js index a8120db0..69489916 100644 --- a/src/store/contents/getters.js +++ b/src/store/contents/getters.js @@ -21,37 +21,6 @@ export const itemUrl = (state) => state.itemUrl; export const manifests = (state) => state.manifests; -export const selectedItemIndex = (state) => { - const selectedItemUrl = encodeURI(decodeURI(state.itemUrl)); - const index = state.itemUrls.findIndex((item) => encodeURI(decodeURI(item)) === selectedItemUrl); - return index > -1 ? index : 0; -}; - -export const selectedManifest = (state) => state.manifests.find((manifest) => { - manifest = { ...manifest }; - const selectedItemUrl = encodeURI(decodeURI(state.itemUrl)); - if (!Array.isArray(manifest.sequence)) { - manifest.sequence = [manifest.sequence]; - } - return manifest.sequence.find((manifestItem) => encodeURI(decodeURI(manifestItem.id)) === selectedItemUrl); -}); - -export const selectedSequenceIndex = (state, getters) => { - const item = getters.selectedManifest; - if (!item) { - return null; - } - - const { label } = item; - let index = null; - state.manifests.forEach((manifest, idx) => { - if (manifest.label === label) { - index = idx; - } - }); - return index; -}; - export const item = (state) => state.item; export const manifest = (state) => state.manifest; diff --git a/src/store/contents/mutations.js b/src/store/contents/mutations.js index 692eeb7f..02ec2133 100644 --- a/src/store/contents/mutations.js +++ b/src/store/contents/mutations.js @@ -2,14 +2,6 @@ export const setCollection = (state, payload) => { state.collection = { ...payload }; }; -export const setCollectionTitle = (state, title) => { - state.collectionTitle = title; -}; - -export const setConnectorValues = (state, payload) => { - state.connectorValues = payload; -}; - export const setItem = (state, payload) => { state.item = payload; }; @@ -22,10 +14,6 @@ export const setManifests = (state, payload) => { state.manifests = payload; }; -export const setPanels = (state, payload) => { - state.panels = payload; -}; - export const setManifest = (state, payload) => { if (!Array.isArray(payload.sequence)) payload.sequence = [payload.sequence]; state.manifest = { ...payload }; diff --git a/src/store/index.js b/src/store/index.js index ba610719..eb35387f 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,13 +1,11 @@ import { createStore } from 'vuex'; import annotations from './annotations'; -import config from './config'; import contents from './contents'; export default createStore({ modules: { annotations, - config, contents, }, }); diff --git a/src/stores/config.ts b/src/stores/config.ts new file mode 100644 index 00000000..8f01433e --- /dev/null +++ b/src/stores/config.ts @@ -0,0 +1,595 @@ +import { defineStore } from 'pinia' +import { + computed, ref, + } from 'vue'; + +import messages from 'src/i18n'; +import BookmarkService from '@/services/bookmark'; +import { i18n } from '@/i18n'; + + export const useConfigStore = defineStore('config', () => { + // States ('Setup Pinia': refs) + const instanceId = ref(null) + const config = ref({ + container: '#app', + collection: '', + manifest: '', + item: '', + panels: [ + { + label: 'contents', + toggle: true, + show: true, + views: [ + { + id: 'tree', + label: 'contents', + connector: { + id: 1, + options: { + labels: { + item: 'Sheet', + manifest: 'Manuscript', + }, + }, + }, + }, + ], + }, + { + label: 'metadata', + show: true, + toggle: true, + views: [ + { + id: 'metadata', + label: 'metadata', + connector: { + id: 2, + options: { + collection: { + all: true, + }, + manifest: { + all: true, + }, + item: { + all: true, + }, + }, + }, + }, + ], + }, + { + label: 'image', + show: true, + toggle: true, + views: [ + { + id: 'image', + label: 'Image', + connector: { + id: 3, + }, + }], + }, + { + label: 'text', + show: true, + toggle: true, + views: [ + { + id: 'text1', + label: 'Transcription', + default: true, + connector: { + id: 4, + }, + }, + ], + }, + { + label: 'annotations', + show: true, + toggle: true, + views: [ + { + id: 'annotations1', + label: 'annotations', + connector: { + id: 5, + options: { + types: [], + }, + }, + }, + ], + }, + ], + colors: { + forceMode: 'none', + primary: '', + secondary: '', + accent: '', + }, + header: { + show: true, + navigation: true, + panelsToggle: true, + languageSwitch: false, + }, + labels: { + item: 'Sheet', + manifest: 'Manuscript', + }, + lang: 'en', + meta: { + collection: { + all: true, + }, + manifest: { + all: true, + }, + item: { + all: true, + }, + }, + notificationColors: { + info: 'blue-400', + warning: 'red-400', + }, + }) + const activeViews = ref([]) + const isValid = ref(false) + + + const defaultPanel = { + label: 'Panel', + show: true, + toggle: true, + views: [], + }; + + const defaultView = { + id: 'view', + name: 'View', + default: false, + connector: { + id: 1, + options: {}, + }, + }; + + // Getters ('Setup Pinia' computed()) + const activeContentType = computed(() => { + const contentConnectorId = 4; + const panelIndex = config.value.panels.findIndex(({ views }) => views.find(({ connector }) => contentConnectorId === connector.id)); + + if (panelIndex === -1) return -1; + + const viewIndex = activeViews.value[panelIndex]; + return config.value.panels[panelIndex].views[viewIndex].connector.options.type; + }) + + // I think it doesn't matter whether getIconByType is a function or a computed property, since its only being called by actions in config + function getIconByType(type) { + const annotationsConnectorId = 5; + const panelIndex = config.value.panels.findIndex(({ views }) => views.find(({ connector }) => annotationsConnectorId === connector.id)); + + if (panelIndex === -1) return -1; + + const viewIndex = activeViews.value[panelIndex]; + const types = config.value.panels[panelIndex].views[viewIndex].connector.options?.types; + return types.find(({ name }) => name === type)?.icon || 'biPencilSquare'; + } + + + + // Functions (mutators and actions in Vuex are now converted to functions in Pinia) + + function setConfig(payload) { + config.value = payload; + } + + async function setActivePanelView(viewIndex, panelIndex) { + if(activeViews.value[panelIndex] !== undefined) { + activeViews.value[panelIndex] = viewIndex; + } + await BookmarkService.updatePanels(activeViews.value); + } + + function setPanels(panels) { + config.value.panels = panels; + } + + function setShowPanelSetter(index, show) { + config.value.panels[index].show = show; + } + + function loadConfig(config, isValid) { + config.value = config; + isValid.value = isValid; + } + + function setActiveViews(payload) { + activeViews.value = payload; + } + + function setInstanceId(payload) { + instanceId.value = payload; + } + + // Actions to functions + + + function validateCollection(value) { + return !!(value); + } + + function validateManifest(value) { + return !!(value); + } + + function validateItem(value) { + return !!(value); + } + + function validateTranslations(value) { + return !!(value) && Object.keys(value).every((key) => key === 'en' || key === 'de'); + } + + function validatePanels(value) { + return !!(value) && Array.isArray(value); + } + + function validateLang(value) { + return !!(value); + } + + function validateColors(value) { + return !!(value); + } + + function validateContainer(value) { + return !!(value); + } + + function validateHeader(value, defaultValue) { + if (!value) return false; + + const defaultKeys = Object.keys(defaultValue); + const invalidKeys = Object.keys(value) + .filter((key) => defaultKeys.findIndex((defaultKey) => defaultKey === key) === -1); + return invalidKeys.length === 0; + } + + function validateLabels(labels, validLabels: Labels) { + // valid labels are the labels from the default config + // we consider the custom labels, in the case when all the keys have a value, otherwise we would have the button with empty text i.e for the following scenario + // when the item is '' + if (!labels || !validLabels) return false; + + let isValid = true; + Object.keys(labels).forEach((key) => { + if (!(key in validLabels) || labels[key] === '') { + isValid = false; + } + }); + + return isValid; + } + + function createDefaultActiveViews(panelsConfig) { + return panelsConfig + .filter((p) => p.views && p.views.length > 0) + .map((panel) => { + const defaultIndex = panel.views.findIndex((view) => view.default === true); + return defaultIndex > -1 ? defaultIndex : 0; + }) + .reduce((acc, cur, i) => { + acc[i] = cur; + return acc; + }, {}); + } + + + // URL Config + + function splitUrlParts(urlQuery, attributes) { + if (urlQuery === '') { + return [undefined, undefined, undefined, undefined]; + } + const arrayAttributes = urlQuery.split('_'); + const manifestPart = arrayAttributes.find((element) => element[0].includes(attributes[0])); // index of manifest part in the splitted array: element[0] is 'm' the first letter of the part ? + const itemPart = arrayAttributes.find((element) => element[0].includes(attributes[1])); + const panelsPart = arrayAttributes.find((element) => element[0].includes(attributes[2])); + const showPart = arrayAttributes.find((element) => element[0].includes(attributes[3])); + return [manifestPart, itemPart, panelsPart, showPart]; + } + + function isManifestPartValid(manifestPart) { + const regexManifest = /m\d+$/; + return regexManifest.exec(manifestPart) !== null; + } + + function isItemPartValid(itemPart) { + const regexItem = /i\d+$/; + return regexItem.exec(itemPart) !== null; + } + + function isPanelsPartValid(panelsPart, panelsValue, numberPanels) { + const numbersPartArray = panelsValue.split('-'); + const regexNumber = /^\d+$/; + if (panelsPart[0] !== 'p' || numbersPartArray.length !== numberPanels) { + return false; + } + + for (let i = 0; i < numbersPartArray.length; i++) { + const panelTabPair = numbersPartArray[i]; + if (panelTabPair.length !== 3 + || regexNumber.test(panelTabPair[0]) === false + || regexNumber.test(panelTabPair[2]) === false + || panelTabPair[1] !== '.') { + return false; + } + } + return true; + } + + + function isShowPartValid(showValue, numberPanels) { + const showValueAsArray = showValue.split('-'); + const regexNumbersPart = /\d\-/; + if (showValueAsArray.length > numberPanels) { + return false; + } + for (let i = 0; i < showValueAsArray.length - 1; i++) { + // if s0-2 is given and there are in total 4 panels, then it is still fine, since we can show less number of panels than the total one + // match the couples of (d-) -> a digit followed by a "-" character. In total there are (s.length - 1) - so number of panels we want to open - 1 + const groupMatch = showValue.slice(i * 2, i * 2 + 2).match(regexNumbersPart); + if (groupMatch === null) { + return false; + } + } + const lastNumberString = showValue.slice(-1)[0]; + const lastNumberInt = parseInt(lastNumberString, 10); + // last character must have only digits and not be greater than number of max panels + if (/^\d+$/.test(lastNumberString) === false || (lastNumberInt >= numberPanels || lastNumberInt < 0)) { + return false; + } + return true; + } + + function createDefaultPanelValue(numberPanels) { + // get the number of panels and then create as many couples of (panel_index.0) until n_panels-1, the last couple need not have the '-' symbol + let p = ''; + for (let j = 0; j < numberPanels; j++) { + if (j !== numberPanels - 1) { + p += `${j}.0-`; + } else { + p += `${j}.0`; + } + } + return p; + } + + function createActiveViewsFromPanelsArray(panelsArray) { + // converts 'panelsArray' to an object with key, value: 'panel index: visible tab index' + return panelsArray.reduce((acc, cur) => { + // eslint-disable-next-line no-shadow + const [panelIndex, viewIndex] = cur.split('.').map((i) => parseInt(i, 10)); + acc[panelIndex] = viewIndex; + return acc; + }, {}); + } + + function discoverCustomConfig(customConfig, defaultConfig) { + const { + container, translations, collection, manifest, item, panels, lang, colors, header, labels + } = customConfig; + + return { + ...(validateContainer(container) && { container }), + ...(validateCollection(collection) && { collection }), + ...(validateManifest(manifest) && { manifest }), + ...(validateItem(item) && { item }), + ...(validateTranslations(translations) && { translations }), + ...(validatePanels(panels) && { panels }), + ...(validateLang(lang) && { lang }), + ...(validateColors(colors) && { colors }), + ...(validateHeader(header, defaultConfig.header) && { header }), + ...(validateLabels(labels, defaultConfig.labels) && { labels }), + }; + } + + function discoverUrlConfig(config) { + // split the url based on '_' + // get the part of attribute: get the attribute name and the value based on the type of attribute + // add each attribute to UrlConfig as key value + let urlConfig = {}; + const urlQuery = BookmarkService.getQuery(); + const attributes = ['m', 'i', 'p', 's']; + // values of manifest, item Indices ... + let [m, i, p, s] = [undefined, undefined, undefined, undefined]; + const numberPanels = config.panels ? config.panels.length : 0; + const [manifestPart, itemPart, panelsPart, showPart] = splitUrlParts(urlQuery, attributes); + /* + if (isUrl(item)) urlConfig.item = item; + if (isUrl(manifest)) urlConfig.manifest = manifest; + if (isUrl(collection)) urlConfig.collection = collection; + */ + // here we will validate for the structure of each component:, not their value range + if (manifestPart !== undefined) { + if (!isManifestPartValid(manifestPart)) { + throw new Error(i18n.global.t('error_manifestpart_tido_url')); + } else { + urlConfig.m = parseInt(manifestPart.slice(1), 10); + } + } + if (itemPart !== undefined) { + if (!isItemPartValid(itemPart)) { + throw new Error(i18n.global.t('error_itempart_tido_url')); + } else { + urlConfig.i = parseInt(itemPart.slice(1), 10); + } + } + if (panelsPart !== undefined) { + const panelsValue = panelsPart.slice(1); + if (!isPanelsPartValid(panelsPart, panelsValue, numberPanels)) { + throw new Error(i18n.global.t('error_panelspart_tido_url')); + } else { + p = panelsValue; + } + } else { + p = createDefaultPanelValue(numberPanels); + } + const panelsArray = p !== '' ? p.split('-') : []; + urlConfig.activeViews = createActiveViewsFromPanelsArray(panelsArray); + + if (showPart !== undefined) { + const showValue = showPart.slice(1); + if (!isShowPartValid(showValue, numberPanels)) { + throw new Error(i18n.global.t('error_showpart_tido_url')); + } else { + // showValue needs to be an array of opened panel indices (Integers) + s = showValue.split('-').map(Number); + } + } + if (s === undefined) { + // If 's' is not given in URL, then we open all the panels which are given in config + urlConfig.show = Array.from({ length: numberPanels }, (value, index) => index); + } else { + urlConfig.show = s; + } + return urlConfig; + } + + function discoverDefaultConfig(config) { + return { + ...JSON.parse(JSON.stringify(config)), + activeViews: createDefaultActiveViews(config.panels), + }; + } + + function load(custConfig) { + const customConfig = discoverCustomConfig(custConfig, config.value); + const urlConfig = discoverUrlConfig(custConfig); + const defaultConfig = discoverDefaultConfig(config.value); + + const header: Header = { + ...defaultConfig.header, + ...customConfig.header, + }; + + if (customConfig.panels) { + // If the custom config provide panels config, we still need to check if it's valid. + // Here we enhance the potentially missing parts with default panel/view config. + // Hint: Not to confuse with the "defaultConfig" which provides an out-of-the-box panels setup + + customConfig.panels = customConfig.panels.map((panel) => { + if (panel.views) { + panel.views = panel.views.map((view) => ({ + ...defaultView, + ...view, + })); + } + + return { + ...defaultPanel, + ...panel, + }; + }); + } + + const resultConfig = { + ...defaultConfig, + ...customConfig, + ...urlConfig, + header, + }; + + const activeViews = urlConfig.activeViews || defaultConfig.activeViews; + setActiveViews(activeViews) + + if (resultConfig.show && resultConfig.show.length > 0) { + // Set visible panels + // First hide all + resultConfig.panels.map((panel, i) => resultConfig.panels[i].show = false); + + // Next show configured + resultConfig.show.forEach((panelIndex) => { + if (!Number.isInteger(panelIndex)) return; + const panel = resultConfig.panels[panelIndex]; + if (!panel) return; + + resultConfig.panels[panelIndex].show = true; + }); + } + + if (resultConfig.translations) { + const locales = Object.keys(resultConfig.translations); + + locales.forEach((locale) => { + i18n.global.setLocaleMessage(locale, { ...(messages[locale] ? messages[locale] : {}), ...resultConfig.translations[locale] }); + }); + } + setConfig(resultConfig) + + if (urlConfig.activeViews) setActiveViews(activeViews) + else setDefaultActiveViews(false) //dispatch('setDefaultActiveViews', false); + } + + + function setShowPanel( {index, show} ) { + setShowPanelSetter(index, show) + + let panelIndexes = config.value.panels.reduce((acc, cur, i) => (cur.show ? [...acc, i] : acc), []); + if (panelIndexes.length === config.value.panels.length) panelIndexes = []; + + BookmarkService.updateShow(panelIndexes); + }; + + function setContentType( type) { + const newConfig = { ...config.value }; + + newConfig.panels[3].views[0].connector.options = { type }; + setConfig(newConfig) + }; + + + async function setDefaultActiveViews (bookmark = true){ + const activeViews = []; + + config.value.panels.forEach(({ views }, panelIndex) => { + let defaultViewIndex = views.findIndex((view) => !!(view.default)); + if (defaultViewIndex === -1) defaultViewIndex = 0; + activeViews[panelIndex] = defaultViewIndex; + }); + + if (bookmark) await BookmarkService.updatePanels(activeViews); + setActiveViews(activeViews) + } + + return { + instanceId, config, activeViews, isValid, + activeContentType, getIconByType, + setConfig, setActivePanelView, setPanels, setShowPanelSetter, loadConfig, setActiveViews, setInstanceId, + validateCollection, validateManifest, validateItem, validateTranslations, validatePanels, validateLang, validateColors, validateContainer, validateLabels, validateHeader, + createDefaultActiveViews, splitUrlParts, isManifestPartValid, isItemPartValid, isShowPartValid, isPanelsPartValid, + createDefaultPanelValue, createActiveViewsFromPanelsArray, discoverCustomConfig, discoverUrlConfig, discoverDefaultConfig, + load, setShowPanel, setContentType, setDefaultActiveViews + } + }) + + + + + + + + + + + + \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts index 66a51234..c6fd36ac 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -7,6 +7,7 @@ declare global { id: string } + interface ActiveAnnotation { [key: string]: Annotation } @@ -48,9 +49,16 @@ declare global { total?: number, annotationCollection?: string, modules?: Module[] - } + interface Colors { + forceMode: string, + primary: string, + secondary: string, + accent: string + } + + interface Content { '@context': string, url: string, @@ -69,6 +77,13 @@ declare global { value: string } + interface Header { + show: boolean, + navigation: boolean, + panelsToggle: boolean, + languageSwitch: boolean + } + interface Idref { '@context': string, base?: string, @@ -82,6 +97,7 @@ declare global { manifest?: string, license: License } + interface Item { '@context': string, @@ -136,11 +152,18 @@ declare global { metadata?: Metadata[] } + interface Module { editionManuscripts?: boolean, editionPrints?: boolean } + interface NotificationColors { + info: string, + warning: string + } + + type RangeSelector = { type: 'RangeSelector', startSelector: CssSelector, @@ -178,7 +201,6 @@ declare global { type: TitleType } type TitleType = 'main' | 'sub'; - } export {} \ No newline at end of file