@projektemacher/storymapjs 0.10.7 → 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG +350 -19
- package/README.md +43 -9
- package/dist/assets/demo-DBpeF31K.js +1 -0
- package/dist/assets/harness-Cr0rYpvt.js +1 -0
- package/dist/assets/harness-multi-CYHgI0a9.js +1 -0
- package/dist/assets/harness-support-Do7sIp2j.js +1 -0
- package/dist/assets/index-wHPqGrBS.js +1 -0
- package/dist/assets/main-DXalctuR.js +17 -0
- package/dist/context.json +12 -9
- package/dist/css/fonts/files/amatic-sc-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-hebrew-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-hebrew-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/amatic-sc-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-100-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-100-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-200-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-600-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-800-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/bitter-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-arabic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-200-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-600-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-800-normal.woff2 +0 -0
- package/dist/css/fonts/files/cairo-latin-ext-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/dancing-script-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/eb-garamond-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-greek-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/gentium-book-plus-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/old-standard-tt-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-hebrew-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-hebrew-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/open-sans-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-cyrillic-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-sc-vietnamese-900-normal.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/playfair-display-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-sans-narrow-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/pt-serif-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/raleway-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/raleway-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-greek-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/roboto-slab-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-cyrillic-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-300-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-300-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-400-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-500-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-500-normal.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-700-italic.woff2 +0 -0
- package/dist/css/fonts/files/ubuntu-greek-ext-700-normal.woff2 +0 -0
- package/dist/css/fonts/files/unica-one-vietnamese-400-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-cyrillic-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-greek-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-greek-wght-normal.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-vietnamese-wght-italic.woff2 +0 -0
- package/dist/css/fonts/files/vollkorn-vietnamese-wght-normal.woff2 +0 -0
- package/dist/css/fonts/font.abril-droidsans.css +3 -0
- package/dist/css/fonts/font.amatic-andika.css +57 -0
- package/dist/css/fonts/font.bitter-raleway.css +111 -0
- package/dist/css/fonts/font.cairo.css +312 -0
- package/dist/css/fonts/font.clicker-garamond.css +93 -0
- package/dist/css/fonts/font.dancing-ledger.css +12 -0
- package/dist/css/fonts/font.default.css +183 -0
- package/dist/css/fonts/font.fjalla-average.css +3 -0
- package/dist/css/fonts/font.georgia-helvetica.css +3 -0
- package/dist/css/fonts/font.knightlab.css +642 -0
- package/dist/css/fonts/font.lustria-lato.css +3 -0
- package/dist/css/fonts/font.medula-lato.css +3 -0
- package/dist/css/fonts/font.oldstandard.css +84 -0
- package/dist/css/fonts/font.opensans-gentiumbook.css +183 -0
- package/dist/css/fonts/font.playfair-faunaone.css +39 -0
- package/dist/css/fonts/font.playfair.css +147 -0
- package/dist/css/fonts/font.pt.css +183 -0
- package/dist/css/fonts/font.roboto-megrim.css +3 -0
- package/dist/css/fonts/font.rufina-sintony.css +3 -0
- package/dist/css/fonts/font.ubuntu.css +291 -0
- package/dist/css/fonts/font.unicaone-vollkorn.css +84 -0
- package/dist/css/storymap.css +1 -1
- package/dist/demo.html +2 -2
- package/dist/docs/api/.nojekyll +1 -0
- package/dist/docs/api/assets/custom.css +47 -0
- package/dist/docs/api/assets/hierarchy.js +1 -0
- package/dist/docs/api/assets/highlight.css +57 -0
- package/dist/docs/api/assets/icons.js +18 -0
- package/dist/docs/api/assets/icons.svg +1 -0
- package/dist/docs/api/assets/main.js +60 -0
- package/dist/docs/api/assets/navigation.js +1 -0
- package/dist/docs/api/assets/search.js +1 -0
- package/dist/docs/api/assets/style.css +1648 -0
- package/dist/docs/api/classes/StoryMap.html +1604 -0
- package/dist/docs/api/functions/MediaType.html +51 -0
- package/dist/docs/api/functions/isPresentation3Collection.html +47 -0
- package/dist/docs/api/functions/isPresentation3Manifest.html +53 -0
- package/dist/docs/api/functions/loadCSS.html +63 -0
- package/dist/docs/api/functions/manifestToStorymapData.html +49 -0
- package/dist/docs/api/functions/setLanguage.html +54 -0
- package/dist/docs/api/functions/storymapToManifest.html +57 -0
- package/dist/docs/api/functions/validateStorymap.html +53 -0
- package/dist/docs/api/functions/validateStorymapAndReport.html +54 -0
- package/dist/docs/api/hierarchy.html +28 -0
- package/dist/docs/api/index.html +197 -0
- package/dist/docs/api/interfaces/AnimationHandle.html +60 -0
- package/dist/docs/api/interfaces/ImagereadyPayload.html +71 -0
- package/dist/docs/api/interfaces/LanguageEntry.html +102 -0
- package/dist/docs/api/interfaces/LatLngLiteral.html +73 -0
- package/dist/docs/api/interfaces/LoadOptions.html +59 -0
- package/dist/docs/api/interfaces/OlLayer.html +1400 -0
- package/dist/docs/api/interfaces/OlMap.html +1656 -0
- package/dist/docs/api/interfaces/OlProjection.html +332 -0
- package/dist/docs/api/interfaces/OlSource.html +784 -0
- package/dist/docs/api/interfaces/OlTileLayer.html +1497 -0
- package/dist/docs/api/interfaces/OlVectorLayer.html +1559 -0
- package/dist/docs/api/interfaces/OlView.html +1958 -0
- package/dist/docs/api/interfaces/StorymapData.html +71 -0
- package/dist/docs/api/interfaces/StorymapDataWrapper.html +60 -0
- package/dist/docs/api/interfaces/StorymapDocument.html +68 -0
- package/dist/docs/api/interfaces/StorymapError.html +63 -0
- package/dist/docs/api/interfaces/StorymapGeoreference.html +93 -0
- package/dist/docs/api/interfaces/StorymapGeoreferenceBody.html +74 -0
- package/dist/docs/api/interfaces/StorymapGroundControlPoint.html +69 -0
- package/dist/docs/api/interfaces/StorymapLanguageMap.html +57 -0
- package/dist/docs/api/interfaces/StorymapManifest.html +118 -0
- package/dist/docs/api/interfaces/StorymapManifestAnnotation.html +105 -0
- package/dist/docs/api/interfaces/StorymapManifestBackground.html +81 -0
- package/dist/docs/api/interfaces/StorymapManifestCanvas.html +140 -0
- package/dist/docs/api/interfaces/StorymapManifestContentResource.html +97 -0
- package/dist/docs/api/interfaces/StorymapManifestFeatureCollection.html +82 -0
- package/dist/docs/api/interfaces/StorymapManifestGeoreferencing.html +87 -0
- package/dist/docs/api/interfaces/StorymapManifestImageService.html +69 -0
- package/dist/docs/api/interfaces/StorymapManifestRange.html +75 -0
- package/dist/docs/api/interfaces/StorymapManifestService.html +77 -0
- package/dist/docs/api/interfaces/StorymapManifestSpecificResource.html +69 -0
- package/dist/docs/api/interfaces/StorymapManifestStatement.html +63 -0
- package/dist/docs/api/interfaces/StorymapMapOptions.html +92 -0
- package/dist/docs/api/interfaces/StorymapOptions.html +532 -0
- package/dist/docs/api/interfaces/StorymapOverlayLayer.html +126 -0
- package/dist/docs/api/interfaces/StorymapSlide.html +132 -0
- package/dist/docs/api/interfaces/StorymapSlideBackground.html +72 -0
- package/dist/docs/api/interfaces/StorymapSlideLocation.html +117 -0
- package/dist/docs/api/interfaces/StorymapSlideMarker.html +104 -0
- package/dist/docs/api/interfaces/StorymapSlideMedia.html +110 -0
- package/dist/docs/api/interfaces/StorymapSlideNarration.html +65 -0
- package/dist/docs/api/interfaces/StorymapSlideText.html +72 -0
- package/dist/docs/api/interfaces/StorymapTilejson.html +105 -0
- package/dist/docs/api/media/es.json +31 -0
- package/dist/docs/api/media/migration-from-knightlab.md +359 -0
- package/dist/docs/api/modules.html +50 -0
- package/dist/docs/api/types/ContentState.html +70 -0
- package/dist/docs/api/types/ImagereadyKind.html +34 -0
- package/dist/docs/api/types/MediaTypeMatch.html +78 -0
- package/dist/docs/api/types/TileSourceFactory.html +53 -0
- package/dist/docs/authoring.html +293 -0
- package/dist/docs/iiif.html +617 -62
- package/dist/docs/migration.html +313 -11
- package/dist/docs/readme.html +40 -9
- package/dist/embed/index.html +1 -1
- package/dist/examples/issue-242-nonlatin.json +55 -0
- package/dist/examples/issue-305-start-at-slide-data.json +96 -0
- package/dist/examples/issue-358-iframe.json +57 -0
- package/dist/examples/issue-iiif-geo.json +32 -0
- package/dist/examples/issue-image-alt.json +23 -0
- package/dist/examples/issue-image-region.json +42 -0
- package/dist/examples/issue-image-srcset.json +24 -0
- package/dist/examples/map-area-overlays.json +68 -0
- package/dist/examples/no-map.json +44 -0
- package/dist/examples-iiif/annotated-image.json +188 -0
- package/dist/examples-iiif/bob-boilen.json +11 -5
- package/dist/examples-iiif/courbet.json +9 -6
- package/dist/examples-iiif/empty.json +8 -4
- package/dist/examples-iiif/flickr-drewvigal.json +248 -24
- package/dist/examples-iiif/flickr-zach.wise.json +8 -4
- package/dist/examples-iiif/georeferenced-layer-unsupported.json +153 -0
- package/dist/examples-iiif/georeferenced-layer.json +318 -0
- package/dist/examples-iiif/iiif-wellcome.json +9 -6
- package/dist/examples-iiif/instagram-chenpamela.json +381 -36
- package/dist/examples-iiif/instagram_brentfoster.json +429 -40
- package/dist/examples-iiif/instagram_couch.json +261 -26
- package/dist/examples-iiif/instagram_joegermuska.json +441 -41
- package/dist/examples-iiif/instagram_matteich.json +261 -26
- package/dist/examples-iiif/instagram_mirandamulligan.json +261 -26
- package/dist/examples-iiif/instagram_raymondmccrea.json +261 -26
- package/dist/examples-iiif/instagram_test.json +261 -26
- package/dist/examples-iiif/issue-134-no-lines.json +9 -5
- package/dist/examples-iiif/issue-177-color-vars.json +9 -5
- package/dist/examples-iiif/issue-211-rtl.json +9 -5
- package/dist/examples-iiif/issue-242-nonlatin.json +239 -0
- package/dist/examples-iiif/issue-272-facebook.json +9 -5
- package/dist/examples-iiif/issue-305-start-at-slide-data.json +305 -0
- package/dist/examples-iiif/issue-305-start-at-slide.json +9 -5
- package/dist/examples-iiif/issue-341-distance.json +9 -5
- package/dist/examples-iiif/issue-358-iframe.json +190 -0
- package/dist/examples-iiif/issue-360-juxtapose.json +22 -7
- package/dist/examples-iiif/issue-368-null-fields.json +9 -5
- package/dist/examples-iiif/issue-375-code-lists.json +9 -5
- package/dist/examples-iiif/issue-381-dateline.json +9 -5
- package/dist/examples-iiif/issue-405-custom-icon.json +203 -0
- package/dist/examples-iiif/issue-425-many-slides.json +9 -5
- package/dist/examples-iiif/issue-428-background.json +20 -8
- package/dist/examples-iiif/issue-435-googlemaps.json +9 -5
- package/dist/examples-iiif/issue-437-documentcloud.json +22 -7
- package/dist/examples-iiif/issue-451-webp.json +9 -5
- package/dist/examples-iiif/issue-455-audio.json +9 -5
- package/dist/examples-iiif/issue-480-shorts.json +9 -5
- package/dist/examples-iiif/issue-506-marker-sync.json +9 -5
- package/dist/examples-iiif/issue-iiif-geo.json +200 -0
- package/dist/examples-iiif/issue-image-alt.json +90 -0
- package/dist/examples-iiif/issue-image-region.json +203 -0
- package/dist/examples-iiif/issue-image-srcset.json +87 -0
- package/dist/examples-iiif/jansteen.json +9 -6
- package/dist/examples-iiif/katrina.json +197 -41
- package/dist/examples-iiif/map-area-overlays.json +289 -0
- package/dist/examples-iiif/marktwain.json +255 -58
- package/dist/examples-iiif/no-map.json +148 -0
- package/dist/examples-iiif/population.json +440 -76
- package/dist/examples-iiif/president.json +456 -83
- package/dist/examples-iiif/scroll-hint.json +190 -0
- package/dist/examples-iiif/seurat.json +113 -22
- package/dist/examples-iiif/zoomify-clamp.json +153 -0
- package/dist/harness-multi.html +17 -0
- package/dist/harness.html +3 -2
- package/dist/index.html +16 -10
- package/dist/js/storymap.d.ts +1439 -114
- package/dist/js/storymap.js +24654 -21936
- package/dist/js/storymap.js.map +1 -1
- package/dist/navplace-properties.json +40 -0
- package/docs/migration-from-knightlab.md +229 -9
- package/docs/storymap-as-iiif-manifest.md +559 -135
- package/package.json +11 -8
- package/schema/storymap.schema.json +431 -78
- package/src/animation/easings.ts +8 -1
- package/src/animation/tween.ts +186 -0
- package/src/core/Browser.ts +6 -5
- package/src/core/Load.ts +54 -2
- package/src/core/Util.ts +255 -193
- package/src/core/mixins.ts +24 -7
- package/src/core/viewers.ts +94 -0
- package/src/dom/DomEvent.ts +27 -8
- package/src/globals.d.ts +8 -0
- package/src/language/Language.ts +199 -25
- package/src/language/locale/.expected-gaps.json +86 -0
- package/src/language/locale/be.json +15 -2
- package/src/language/locale/bg.json +15 -2
- package/src/language/locale/cs.json +15 -2
- package/src/language/locale/de.json +12 -1
- package/src/language/locale/el.json +15 -2
- package/src/language/locale/en.json +6 -2
- package/src/language/locale/es.json +15 -2
- package/src/language/locale/et.json +5 -1
- package/src/language/locale/fr.json +15 -2
- package/src/language/locale/he.json +16 -3
- package/src/language/locale/hu.json +15 -2
- package/src/language/locale/is.json +5 -1
- package/src/language/locale/it.json +15 -2
- package/src/language/locale/jp.json +15 -2
- package/src/language/locale/ko.json +15 -2
- package/src/language/locale/nl.json +15 -2
- package/src/language/locale/nn.json +5 -1
- package/src/language/locale/no.json +15 -2
- package/src/language/locale/pl.json +15 -2
- package/src/language/locale/pt.json +15 -2
- package/src/language/locale/ru.json +15 -2
- package/src/language/locale/sk.json +15 -2
- package/src/language/locale/sr.json +5 -1
- package/src/language/locale/sv.json +15 -2
- package/src/language/locale/tr.json +15 -2
- package/src/language/locale/uk.json +15 -2
- package/src/language/locale/ur.json +5 -1
- package/src/language/locale/zh-cn.json +15 -2
- package/src/language/locale/zh-tw.json +15 -2
- package/src/main.ts +92 -0
- package/src/map/Map.ts +183 -106
- package/src/map/MapMarker.ts +28 -5
- package/src/map/georeference.ts +325 -0
- package/src/map/openlayers/Map.OpenLayers.ts +850 -237
- package/src/map/openlayers/MapMarker.OpenLayers.ts +248 -24
- package/src/map/openlayers/zoomifyTiles.ts +1 -4
- package/src/media/EmbedUtil.ts +308 -24
- package/src/media/Media.ts +243 -55
- package/src/media/embedId.ts +104 -0
- package/src/media/types/Audio.ts +9 -82
- package/src/media/types/Blockquote.ts +2 -3
- package/src/media/types/DailyMotion.ts +5 -10
- package/src/media/types/DocumentCloud.ts +6 -35
- package/src/media/types/Facebook.ts +3 -4
- package/src/media/types/Flickr.ts +9 -9
- package/src/media/types/GoogleDoc.ts +3 -4
- package/src/media/types/HtmlMedia.ts +167 -0
- package/src/media/types/IFrame.ts +3 -4
- package/src/media/types/Image.ts +72 -6
- package/src/media/types/Juxtapose.ts +14 -5
- package/src/media/types/SoundCloud.ts +28 -6
- package/src/media/types/Text.ts +19 -13
- package/src/media/types/Twitter.ts +27 -14
- package/src/media/types/Video.ts +9 -79
- package/src/media/types/Vimeo.ts +10 -10
- package/src/media/types/Website.ts +23 -13
- package/src/media/types/Wikipedia.ts +18 -8
- package/src/media/types/YouTube.ts +71 -39
- package/src/scss/Typography.scss +1 -3
- package/src/scss/VCO.StoryMap.Dark.scss +18 -6
- package/src/scss/VCO.StoryMap.scss +37 -6
- package/src/scss/Variables.Dark.scss +3 -2
- package/src/scss/Variables.scss +3 -2
- package/src/scss/core/Mixins.scss +5 -63
- package/src/scss/fonts/font.amatic-andika.scss +3 -0
- package/src/scss/fonts/font.bitter-raleway.scss +6 -0
- package/src/scss/fonts/font.cairo.scss +79 -0
- package/src/scss/fonts/font.clicker-garamond.scss +5 -0
- package/src/scss/fonts/font.dancing-ledger.scss +1 -0
- package/src/scss/fonts/font.knightlab.scss +14 -0
- package/src/scss/fonts/font.oldstandard.scss +6 -0
- package/src/scss/fonts/font.opensans-gentiumbook.scss +5 -0
- package/src/scss/fonts/font.playfair-faunaone.scss +2 -0
- package/src/scss/fonts/font.playfair.scss +4 -0
- package/src/scss/fonts/font.pt.scss +6 -0
- package/src/scss/fonts/font.ubuntu.scss +4 -0
- package/src/scss/fonts/font.unicaone-vollkorn.scss +5 -0
- package/src/scss/map/VCO.MapMarker.scss +98 -3
- package/src/scss/media/VCO.Media.scss +7 -10
- package/src/scss/media/types/VCO.Media.Text.scss +8 -0
- package/src/scss/media/types/VCO.Media.Wikipedia.scss +2 -1
- package/src/scss/slider/VCO.Slide.scss +13 -3
- package/src/scss/slider/VCO.SlideNav.scss +11 -17
- package/src/scss/slider/VCO.StorySlider.scss +4 -2
- package/src/scss/ui/VCO.Consent.scss +44 -0
- package/src/scss/ui/VCO.MenuBar.Button.scss +8 -0
- package/src/scss/ui/VCO.Message.scss +2 -5
- package/src/site/site.ts +2 -1
- package/src/slider/Slide.ts +121 -27
- package/src/slider/SlideNav.ts +39 -5
- package/src/slider/StorySlider.ts +153 -42
- package/src/storymap/Consent.ts +407 -82
- package/src/storymap/StoryMap.ts +1192 -182
- package/src/storymap/content-state.ts +185 -0
- package/src/storymap/iiif.ts +1526 -68
- package/src/storymap/to-iiif.ts +921 -0
- package/src/storymap/validate.ts +80 -7
- package/src/types.ts +223 -26
- package/src/ui/MenuBar.ts +98 -45
- package/src/ui/Message.ts +9 -0
- package/src/ui/Swipable.ts +145 -78
- package/dist/assets/demo-D6cTmzn6.js +0 -1
- package/dist/assets/harness-BPFlzutp.js +0 -1
- package/dist/assets/index-DDsPBPZA.js +0 -1
- package/dist/assets/main-Y-MYUSJ4.js +0 -17
- package/src/animation/morpheus.d.ts +0 -41
- package/src/scss/media/types/VCO.Media.Instagram.scss +0 -7
- package/src/scss/media/types/VCO.Media.Profile.scss +0 -5
- package/src/ui/Draggable.ts +0 -374
package/src/storymap/StoryMap.ts
CHANGED
|
@@ -1,19 +1,63 @@
|
|
|
1
|
-
import { mergeData, slideTransitionDuration, updateData } from "../core/Util";
|
|
1
|
+
import { mergeData, slideTransitionDuration, updateData, prefersReducedMotion } from "../core/Util";
|
|
2
2
|
import { loadCSS } from "../core/Load";
|
|
3
3
|
import { validateStorymapAndReport } from "./validate";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
CONTENT_STATE_PARAM,
|
|
6
|
+
formatContentState,
|
|
7
|
+
parseContentState,
|
|
8
|
+
type ContentState,
|
|
9
|
+
} from "./content-state";
|
|
10
|
+
import {
|
|
11
|
+
isPresentation3Manifest,
|
|
12
|
+
isPresentation3Collection,
|
|
13
|
+
loadSeeAlso,
|
|
14
|
+
manifestToStorymapData,
|
|
15
|
+
} from "./iiif";
|
|
16
|
+
import {
|
|
17
|
+
ConsentManager,
|
|
18
|
+
consentManagerOf,
|
|
19
|
+
fontService,
|
|
20
|
+
mediaService,
|
|
21
|
+
narrationService,
|
|
22
|
+
tileService,
|
|
23
|
+
type ConsentService,
|
|
24
|
+
} from "./Consent";
|
|
6
25
|
import Dom from "../dom/Dom";
|
|
7
26
|
import { easeInOutQuint, easeOutStrong } from "../animation/easings";
|
|
8
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
setLanguage,
|
|
29
|
+
isRtl,
|
|
30
|
+
currentLanguageCode,
|
|
31
|
+
claimLanguage,
|
|
32
|
+
releaseLanguage,
|
|
33
|
+
isLanguageConflict,
|
|
34
|
+
} from "../language/Language";
|
|
35
|
+
import {
|
|
36
|
+
markInteraction,
|
|
37
|
+
ownsPageEvent,
|
|
38
|
+
registerParticipant,
|
|
39
|
+
unregisterParticipant,
|
|
40
|
+
} from "../core/viewers";
|
|
41
|
+
import MediaType from "../media/MediaType";
|
|
9
42
|
import { Evented, type EventedInstance } from "../core/mixins";
|
|
10
43
|
import OpenLayersMap from "../map/openlayers/Map.OpenLayers";
|
|
11
44
|
import MenuBar from "../ui/MenuBar";
|
|
12
45
|
import StorySlider from "../slider/StorySlider";
|
|
13
46
|
import { Browser } from "../core/Browser";
|
|
14
|
-
import Animate from "
|
|
47
|
+
import Animate from "../animation/tween";
|
|
15
48
|
import type { Map as OlMap } from "ol";
|
|
16
|
-
import type
|
|
49
|
+
import type OlLayer from "ol/layer/Layer";
|
|
50
|
+
import type OpenLayersMapMarker from "../map/openlayers/MapMarker.OpenLayers";
|
|
51
|
+
import type {
|
|
52
|
+
AnimationHandle,
|
|
53
|
+
StorymapData,
|
|
54
|
+
StorymapDataWrapper,
|
|
55
|
+
StorymapOptions,
|
|
56
|
+
StorymapSlide,
|
|
57
|
+
} from "../types";
|
|
58
|
+
|
|
59
|
+
/** Map height in pixels while the menubar has collapsed the map (portrait only). */
|
|
60
|
+
const COLLAPSED_MAP_HEIGHT = 1;
|
|
17
61
|
|
|
18
62
|
type StoryMapListener = (e: unknown) => void;
|
|
19
63
|
|
|
@@ -28,14 +72,44 @@ type StoryMapListener = (e: unknown) => void;
|
|
|
28
72
|
export function resolveFontCssUrl(font: string): string {
|
|
29
73
|
if (font.startsWith("stock:")) {
|
|
30
74
|
const font_name = font.split(":")[1] || "default";
|
|
75
|
+
// A crafted name ("stock:../../secret") would otherwise resolve to an
|
|
76
|
+
// arbitrary same-origin file next to the bundle, because the name is
|
|
77
|
+
// concatenated into a URL path. Only accept a bare theme name.
|
|
78
|
+
if (!/^[a-z0-9-]+$/i.test(font_name)) {
|
|
79
|
+
console.warn(
|
|
80
|
+
"StoryMapJS: ignoring font_css with an invalid stock theme name",
|
|
81
|
+
font_name,
|
|
82
|
+
);
|
|
83
|
+
return new URL(/* @vite-ignore */ "../css/fonts/font.default.css", import.meta.url)
|
|
84
|
+
.href;
|
|
85
|
+
}
|
|
31
86
|
// resolved against the library location: one directory up from
|
|
32
87
|
// src/main.ts (dev) and js/storymap.js (build) in both cases
|
|
33
88
|
return new URL("../css/fonts/font." + font_name + ".css", import.meta.url).href;
|
|
34
89
|
}
|
|
35
|
-
|
|
36
|
-
|
|
90
|
+
// Absolute URL, protocol-relative, or a data: URL. Note this must be a
|
|
91
|
+
// real URL test, not a `/^(http|https|\/\/)/` prefix check: the old prefix
|
|
92
|
+
// test false-positived on a relative path that merely *starts* with those
|
|
93
|
+
// letters (e.g. "httpfonts.css"), which raised a spurious consent prompt
|
|
94
|
+
// for a same-origin file.
|
|
95
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(font) || font.startsWith("//")) {
|
|
96
|
+
return font;
|
|
97
|
+
}
|
|
98
|
+
return new URL(font, document.baseURI).href;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True when `url` points off-origin (or at a data: URL) and needs a consent ask. */
|
|
102
|
+
export function isExternalUrl(url: string): boolean {
|
|
103
|
+
// data:/blob: are not off-origin but are not a theme stylesheet either
|
|
104
|
+
if (/^(data|blob):/i.test(url)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return new URL(url, document.baseURI).origin !== window.location.origin;
|
|
109
|
+
} catch {
|
|
110
|
+
// unparseable: treat as external, so we err towards asking
|
|
111
|
+
return true;
|
|
37
112
|
}
|
|
38
|
-
return font;
|
|
39
113
|
}
|
|
40
114
|
|
|
41
115
|
/**
|
|
@@ -46,7 +120,7 @@ export function resolveFontCssUrl(font: string): string {
|
|
|
46
120
|
*
|
|
47
121
|
* @example
|
|
48
122
|
* ```ts
|
|
49
|
-
* import { StoryMap } from "storymapjs";
|
|
123
|
+
* import { StoryMap } from "@projektemacher/storymapjs";
|
|
50
124
|
* // from an inline data object
|
|
51
125
|
* const map = new StoryMap("embed", { storymap: { ... } }, { start_at_slide: 2 });
|
|
52
126
|
* // or straight from a source file (storymap JSON or IIIF manifest)
|
|
@@ -61,25 +135,77 @@ class StoryMapBase {
|
|
|
61
135
|
declare "_el": {
|
|
62
136
|
container: HTMLElement;
|
|
63
137
|
menubar: HTMLElement;
|
|
64
|
-
map: HTMLElement;
|
|
138
|
+
map: HTMLElement | null;
|
|
65
139
|
storyslider: HTMLElement;
|
|
66
140
|
};
|
|
67
141
|
declare "_storyslider": StorySlider;
|
|
68
|
-
declare "_map": OpenLayersMap;
|
|
69
|
-
|
|
142
|
+
declare "_map": OpenLayersMap | null;
|
|
143
|
+
/** `map_type: "none"`: the story is text-and-media, with no map at all. */
|
|
144
|
+
declare "_map_disabled": boolean;
|
|
145
|
+
/**
|
|
146
|
+
* The raw OpenLayers map. `null` until the map is built (the
|
|
147
|
+
* constructor assigns it during data load) — check it, or use the
|
|
148
|
+
* accessors, which return `null` until then.
|
|
149
|
+
*/
|
|
150
|
+
declare "map": OlMap | null;
|
|
70
151
|
declare "_menubar": MenuBar;
|
|
71
152
|
declare "data": StorymapData;
|
|
72
153
|
declare "options": StorymapOptions;
|
|
73
154
|
declare "current_slide": number;
|
|
155
|
+
/**
|
|
156
|
+
* The manifest this viewer was built from, kept so `loadAnnotations()` can
|
|
157
|
+
* resolve its `seeAlso` targets. `null` for a storymap-JSON viewer, which
|
|
158
|
+
* has no `seeAlso`.
|
|
159
|
+
*/
|
|
160
|
+
declare "_raw_manifest": unknown | null;
|
|
161
|
+
/**
|
|
162
|
+
* The deep link the page was opened with, captured before anything can
|
|
163
|
+
* rewrite the URL.
|
|
164
|
+
*
|
|
165
|
+
* This has to be read *early*. The slider's opening `goTo()` can reach
|
|
166
|
+
* `_navigate()` before `_onLoaded()` runs, and `_navigate()` calls
|
|
167
|
+
* `_syncHash()` — which would overwrite the incoming link with the current
|
|
168
|
+
* slide before `_onLoaded()` ever looked at it. Whether that happens
|
|
169
|
+
* depends on the order the slider and map finish loading, which is exactly
|
|
170
|
+
* the kind of race that makes a deep link work most of the time.
|
|
171
|
+
*/
|
|
172
|
+
declare "_initial_deep_link": { state: ContentState | null; hash: string } | null;
|
|
74
173
|
declare "animator_map": AnimationHandle | null;
|
|
75
174
|
declare "animator_storyslider": AnimationHandle | null;
|
|
76
175
|
declare "_autoplay_timer": ReturnType<typeof setTimeout> | null;
|
|
77
176
|
declare "_transition_timer": ReturnType<typeof setTimeout> | null;
|
|
78
177
|
declare "_autoplay_stopped": boolean;
|
|
178
|
+
/** The narration player: one <audio> for the whole story, reused. */
|
|
179
|
+
declare "_narration_el": HTMLAudioElement | null;
|
|
180
|
+
/** Guards a stale play() from a slide change that already moved on. */
|
|
181
|
+
declare "_narration_token": number;
|
|
182
|
+
/** Whether narration consent was granted (asked once per story). */
|
|
183
|
+
declare "_narration_allowed": boolean;
|
|
184
|
+
/** True once the visitor has clicked/keyed: unmuted audio needs this. */
|
|
185
|
+
declare "_user_gestured": boolean;
|
|
186
|
+
/** The advance armed for the current slide (media-ended or the timer). */
|
|
187
|
+
declare "_autoplay_advance": (() => void) | null;
|
|
188
|
+
/** A narration that was skipped for want of a gesture, to retry later. */
|
|
189
|
+
declare "_replayNarrationAfterGesture": boolean;
|
|
79
190
|
declare "_hash_initialized": boolean;
|
|
191
|
+
declare "_collapsed": boolean;
|
|
80
192
|
/** the data source was a IIIF Presentation manifest (legacy zoomify options are ignored) */
|
|
81
193
|
declare "_data_from_manifest": boolean;
|
|
82
194
|
declare "_resize_observer": ResizeObserver | null;
|
|
195
|
+
/** Stored so `dispose()` can remove them again (see AGENTS: no leaks) */
|
|
196
|
+
declare "_on_resize": (() => void) | null;
|
|
197
|
+
declare "_on_keydown_global": ((e: KeyboardEvent) => void) | null;
|
|
198
|
+
declare "_on_fullscreen": (() => void) | null;
|
|
199
|
+
declare "_on_hashchange": (() => void) | null;
|
|
200
|
+
/** Set by `dispose()` so a second call returns early. */
|
|
201
|
+
declare "_disposed": boolean;
|
|
202
|
+
/** identity of this viewer's claim on the page-wide locale */
|
|
203
|
+
declare "_language_holder": symbol;
|
|
204
|
+
/** the locale the caller actually asked for, if any; see _loadLanguage */
|
|
205
|
+
declare "_language_requested": string | undefined;
|
|
206
|
+
/** bumped on interaction, so page-wide keys can pick one viewer */
|
|
207
|
+
declare "_interaction": number;
|
|
208
|
+
declare "_onInteraction": (() => void) | null;
|
|
83
209
|
declare "_resize_timer": ReturnType<typeof setTimeout> | null;
|
|
84
210
|
declare "fire": EventedInstance["fire"];
|
|
85
211
|
declare "hasEventListeners": EventedInstance["hasEventListeners"];
|
|
@@ -130,16 +256,21 @@ class StoryMapBase {
|
|
|
130
256
|
}
|
|
131
257
|
|
|
132
258
|
// Version
|
|
133
|
-
|
|
259
|
+
// build-time constant from package.json (see vite.config.ts); the
|
|
260
|
+
// typeof guard keeps a dev-server run without the define working
|
|
261
|
+
this.version =
|
|
262
|
+
typeof __STORYMAP_VERSION__ === "string" ? __STORYMAP_VERSION__ : "0.0.0-dev";
|
|
134
263
|
|
|
135
264
|
// Ready
|
|
136
265
|
this.ready = false;
|
|
137
266
|
|
|
138
|
-
// DOM ELEMENTS
|
|
267
|
+
// DOM ELEMENTS. Only map may legitimately stay null: a mapless story
|
|
268
|
+
// never builds the pane, so unlike its siblings it gets a real null
|
|
269
|
+
// rather than a stub that would read truthy before _initLayout runs.
|
|
139
270
|
this._el = {
|
|
140
271
|
container: {} as HTMLElement,
|
|
141
272
|
storyslider: {} as HTMLElement,
|
|
142
|
-
map:
|
|
273
|
+
map: null,
|
|
143
274
|
menubar: {} as HTMLElement,
|
|
144
275
|
};
|
|
145
276
|
|
|
@@ -147,15 +278,23 @@ class StoryMapBase {
|
|
|
147
278
|
if (typeof elem === "object") {
|
|
148
279
|
this._el.container = elem;
|
|
149
280
|
} else {
|
|
150
|
-
|
|
281
|
+
const found = Dom.get(elem);
|
|
282
|
+
if (!found) {
|
|
283
|
+
throw new Error("StoryMapJS: no element with id " + elem);
|
|
284
|
+
}
|
|
285
|
+
this._el.container = found;
|
|
151
286
|
}
|
|
152
287
|
|
|
153
288
|
// Slider
|
|
154
289
|
this._storyslider = {} as StorySlider;
|
|
155
290
|
|
|
156
|
-
// Map
|
|
157
|
-
|
|
158
|
-
this.
|
|
291
|
+
// Map. Genuinely null when the story has no map (map_type "none") or
|
|
292
|
+
// before the layout builds one: the accessor guards below read
|
|
293
|
+
// `!this._disposed && this._map`, which a `{}` stub would sail past
|
|
294
|
+
// and then call a method on.
|
|
295
|
+
this._map = null;
|
|
296
|
+
// direct access to the OpenLayers map; null until it is built
|
|
297
|
+
this.map = null;
|
|
159
298
|
|
|
160
299
|
// Menu Bar
|
|
161
300
|
this._menubar = {} as MenuBar;
|
|
@@ -169,6 +308,10 @@ class StoryMapBase {
|
|
|
169
308
|
|
|
170
309
|
this.options = {
|
|
171
310
|
script_path: StoryMap.SCRIPT_PATH,
|
|
311
|
+
// raw OpenLayers Map/View options passed through (see map_options
|
|
312
|
+
// in the README); empty by default, and the OL code reads it
|
|
313
|
+
// defensively because the option is also settable from JSON
|
|
314
|
+
map_options: {},
|
|
172
315
|
height: this._el.container.offsetHeight,
|
|
173
316
|
width: this._el.container.offsetWidth,
|
|
174
317
|
layout: "landscape", // portrait or landscape
|
|
@@ -176,7 +319,6 @@ class StoryMapBase {
|
|
|
176
319
|
default_bg_color: { r: 255, g: 255, b: 255 },
|
|
177
320
|
map_size_sticky: 2.5, // Set as division 1/3 etc
|
|
178
321
|
map_center_offset: null, // takes object {top:0,left:0}
|
|
179
|
-
less_bounce: false, // Less map bounce when calculating zoom, false is good when there are clusters of tightly grouped markers
|
|
180
322
|
start_at_slide: 0,
|
|
181
323
|
call_to_action: false,
|
|
182
324
|
call_to_action_text: "",
|
|
@@ -188,20 +330,19 @@ class StoryMapBase {
|
|
|
188
330
|
// animation
|
|
189
331
|
duration: 1000,
|
|
190
332
|
ease: easeInOutQuint,
|
|
191
|
-
// interaction
|
|
192
|
-
dragging: true,
|
|
193
333
|
trackResize: true,
|
|
194
334
|
keyboard: false,
|
|
195
335
|
nocache: false,
|
|
196
336
|
autoplay: 0,
|
|
337
|
+
autoplay_media: false,
|
|
197
338
|
show_progress: false,
|
|
198
339
|
marker_labels: false,
|
|
199
340
|
text_align: "left",
|
|
200
341
|
map_overview_center: null,
|
|
201
342
|
map_type: "", // "osm:standard",
|
|
343
|
+
tile_source_factory: null,
|
|
202
344
|
attribution: "",
|
|
203
345
|
map_mini: true,
|
|
204
|
-
map_subdomains: "",
|
|
205
346
|
map_as_image: false,
|
|
206
347
|
// no bundled credentials: pass map_access_token in the options
|
|
207
348
|
// if you use Mapbox/Stadia tiles, api_key_flickr for flickr API
|
|
@@ -210,6 +351,13 @@ class StoryMapBase {
|
|
|
210
351
|
map_background_color: "#d9d9d9",
|
|
211
352
|
map_area: "full", // "left" limits the map to the visible half (landscape)
|
|
212
353
|
map_bbox: null,
|
|
354
|
+
// options the map also declares: updateData only copies keys
|
|
355
|
+
// that already exist here, so storymap JSON could not reach them
|
|
356
|
+
overview_extent: null,
|
|
357
|
+
overlays: [],
|
|
358
|
+
// TileJSON 2.1 metadata for a tile-source basemap; a keyword
|
|
359
|
+
// basemap has none (interop §2.9)
|
|
360
|
+
tilejson: undefined,
|
|
213
361
|
consent_required: false,
|
|
214
362
|
zoomify: undefined,
|
|
215
363
|
text_color: "",
|
|
@@ -225,9 +373,6 @@ class StoryMapBase {
|
|
|
225
373
|
slide_padding_lr: 45, // padding on slide of slide
|
|
226
374
|
slide_default_fade: "0%", // landscape fade
|
|
227
375
|
menubar_default_y: 0,
|
|
228
|
-
path_gfx: "gfx",
|
|
229
|
-
map_popup: false,
|
|
230
|
-
zoom_distance: 100,
|
|
231
376
|
calculate_zoom: true, // Allow map to determine best zoom level between markers (recommended)
|
|
232
377
|
line_follows_path: true, // Map history path follows default line, if false it will connect previous and current only
|
|
233
378
|
line_color: "#c34528", //"#DA0000",
|
|
@@ -247,16 +392,46 @@ class StoryMapBase {
|
|
|
247
392
|
this.animator_map = null;
|
|
248
393
|
this.animator_storyslider = null;
|
|
249
394
|
this._resize_observer = null;
|
|
395
|
+
this._on_resize = null;
|
|
396
|
+
this._on_keydown_global = null;
|
|
397
|
+
this._on_fullscreen = null;
|
|
398
|
+
this._on_hashchange = null;
|
|
399
|
+
this._raw_manifest = null;
|
|
400
|
+
this._initial_deep_link = null;
|
|
401
|
+
this._disposed = false;
|
|
402
|
+
this._language_holder = Symbol("storymap");
|
|
403
|
+
this._language_requested = undefined;
|
|
404
|
+
this._interaction = 0;
|
|
405
|
+
this._onInteraction = null;
|
|
250
406
|
this._resize_timer = null;
|
|
251
407
|
this._autoplay_timer = null;
|
|
252
408
|
this._transition_timer = null;
|
|
253
409
|
this._autoplay_stopped = false;
|
|
410
|
+
this._narration_el = null;
|
|
411
|
+
this._narration_token = 0;
|
|
412
|
+
this._narration_allowed = true;
|
|
413
|
+
this._user_gestured = false;
|
|
414
|
+
this._replayNarrationAfterGesture = false;
|
|
415
|
+
this._autoplay_advance = null;
|
|
254
416
|
this._hash_initialized = false;
|
|
417
|
+
this._collapsed = false;
|
|
418
|
+
|
|
419
|
+
// The caller's own `language`, read before the merge: afterwards
|
|
420
|
+
// options.language is always set (the default is "en"), so "asked for
|
|
421
|
+
// German" and "never mentioned a language" are indistinguishable.
|
|
422
|
+
this._language_requested =
|
|
423
|
+
typeof options?.language === "string" && options.language !== ""
|
|
424
|
+
? options.language
|
|
425
|
+
: undefined;
|
|
255
426
|
|
|
256
427
|
// Merge Options -- legacy, in case people still need to pass in
|
|
257
428
|
mergeData(this.options, options);
|
|
258
429
|
|
|
259
|
-
// Current Slide
|
|
430
|
+
// Current Slide — the constructor options have been merged, but the
|
|
431
|
+
// storymap *data* has not (issue #305). _initData -> _initOptions
|
|
432
|
+
// merges the data in afterwards, and the data may carry a
|
|
433
|
+
// start_at_slide of its own, so re-read it there rather than
|
|
434
|
+
// freezing the constructor value here.
|
|
260
435
|
this.current_slide = this.options.start_at_slide;
|
|
261
436
|
|
|
262
437
|
this._initData(data);
|
|
@@ -278,8 +453,11 @@ class StoryMapBase {
|
|
|
278
453
|
: data;
|
|
279
454
|
void this._loadDataFromUrl(url, data);
|
|
280
455
|
} else if (typeof data === "object") {
|
|
281
|
-
|
|
456
|
+
// a Collection is a legitimate IIIF input too: its member
|
|
457
|
+
// Manifests' canvases concatenate into one linear story
|
|
458
|
+
if (isPresentation3Manifest(data) || isPresentation3Collection(data)) {
|
|
282
459
|
this._data_from_manifest = true;
|
|
460
|
+
this._raw_manifest = data;
|
|
283
461
|
this.data = manifestToStorymapData(data);
|
|
284
462
|
} else {
|
|
285
463
|
const wrapper = data as StorymapDataWrapper;
|
|
@@ -306,8 +484,16 @@ class StoryMapBase {
|
|
|
306
484
|
throw new Error("HTTP " + response.status + " " + response.statusText);
|
|
307
485
|
}
|
|
308
486
|
const result: unknown = await response.json();
|
|
309
|
-
|
|
487
|
+
// A teardown while the fetch was in flight must win: without
|
|
488
|
+
// this, a late-resolving response rebuilds the whole viewer into
|
|
489
|
+
// the container dispose() just emptied — menubar, slider, map,
|
|
490
|
+
// listeners, locale claim, autoplay — and sets ready = true.
|
|
491
|
+
if (this._disposed) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (isPresentation3Manifest(result) || isPresentation3Collection(result)) {
|
|
310
495
|
this._data_from_manifest = true;
|
|
496
|
+
this._raw_manifest = result;
|
|
311
497
|
this.data = manifestToStorymapData(result);
|
|
312
498
|
} else {
|
|
313
499
|
validateStorymapAndReport(result, source);
|
|
@@ -319,6 +505,14 @@ class StoryMapBase {
|
|
|
319
505
|
}
|
|
320
506
|
this._initOptions();
|
|
321
507
|
} catch (err: unknown) {
|
|
508
|
+
if (isLanguageConflict(err)) {
|
|
509
|
+
// a second viewer on this page asked for another locale. This
|
|
510
|
+
// is a configuration error, not a failed fetch, and reporting
|
|
511
|
+
// it as one would send the host looking for a 404.
|
|
512
|
+
console.error((err as Error).message);
|
|
513
|
+
this.fire("error", { message: (err as Error).message, source, conflict: true });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
322
516
|
console.error("StoryMapJS: could not load storymap data from " + source, err);
|
|
323
517
|
this.fire("error", { message: String(err), source });
|
|
324
518
|
}
|
|
@@ -330,6 +524,22 @@ class StoryMapBase {
|
|
|
330
524
|
// Grab options from storymap data
|
|
331
525
|
updateData(this.options, this.data);
|
|
332
526
|
|
|
527
|
+
// Capture the deep link now, before the slider and map exist and can
|
|
528
|
+
// navigate — and so rewrite the URL we would read it from later.
|
|
529
|
+
this._initial_deep_link = {
|
|
530
|
+
state: parseContentState(
|
|
531
|
+
new URLSearchParams(window.location.search).get(CONTENT_STATE_PARAM),
|
|
532
|
+
),
|
|
533
|
+
hash: window.location.hash,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// issue #305: the data may override start_at_slide. The constructor
|
|
537
|
+
// read it before this merge, and the slider's opening goTo() fires
|
|
538
|
+
// before _initEvents() attaches our change listener — so nothing
|
|
539
|
+
// else resynchronises the field, leaving the hash, the progress bar
|
|
540
|
+
// and current_slide disagreeing with the visible slide.
|
|
541
|
+
this.current_slide = this.options.start_at_slide;
|
|
542
|
+
|
|
333
543
|
// legacy zoomify options only work with storymap JSON sources — a
|
|
334
544
|
// IIIF Presentation manifest cannot carry them
|
|
335
545
|
if (this._data_from_manifest) {
|
|
@@ -373,6 +583,18 @@ class StoryMapBase {
|
|
|
373
583
|
}
|
|
374
584
|
}
|
|
375
585
|
|
|
586
|
+
// `map_type` is optional in the schema, and a hand-written document
|
|
587
|
+
// can carry an explicit null. Normalise rather than guard: leaving a
|
|
588
|
+
// null in place only moves the crash to the map's own
|
|
589
|
+
// `map_type.split(":")`. "" is the documented default and means OSM.
|
|
590
|
+
if (typeof this.options.map_type !== "string") {
|
|
591
|
+
this.options.map_type = "";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// "none" is the only way to say there is no map. An absent key or ""
|
|
595
|
+
// keeps meaning OSM, so no existing document changes meaning.
|
|
596
|
+
this._map_disabled = this.options.map_type === "none";
|
|
597
|
+
|
|
376
598
|
// handle Stamen change
|
|
377
599
|
if (this.options.map_type.startsWith("stamen")) {
|
|
378
600
|
const old_type = this.options.map_type;
|
|
@@ -391,24 +613,70 @@ class StoryMapBase {
|
|
|
391
613
|
================================================== */
|
|
392
614
|
|
|
393
615
|
_loadLanguage() {
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
|
|
616
|
+
// the locale chunk is fetched on demand, so the language is not
|
|
617
|
+
// available synchronously; layout proceeds with the English defaults
|
|
618
|
+
// and the labels are repainted once the locale arrives.
|
|
619
|
+
//
|
|
620
|
+
// The claim is what makes a second viewer on this page safe: the UI
|
|
621
|
+
// strings are one module-level binding, so a conflicting locale would
|
|
622
|
+
// otherwise repaint this viewer in the other viewer's language. It
|
|
623
|
+
// throws rather than warn, before any of this viewer's DOM exists.
|
|
624
|
+
//
|
|
625
|
+
// The document may carry its own `language` (updateData merges the
|
|
626
|
+
// document's keys into the options), so that counts as a request too.
|
|
627
|
+
const requested = this._requestedLanguage();
|
|
628
|
+
if (requested === undefined) {
|
|
629
|
+
// Nobody asked for a locale, so adopt whatever the page is already
|
|
630
|
+
// using. Claiming the "en" default instead would conflict with a
|
|
631
|
+
// sibling that did ask — a host that called setLanguage("de") and
|
|
632
|
+
// then built a viewer with no `language` option used to get
|
|
633
|
+
// English, silently repainting its German sibling's chrome.
|
|
634
|
+
claimLanguage(currentLanguageCode(), this._language_holder);
|
|
635
|
+
} else {
|
|
636
|
+
claimLanguage(requested, this._language_holder);
|
|
637
|
+
setLanguage(requested);
|
|
638
|
+
}
|
|
397
639
|
this._onDataLoaded();
|
|
398
640
|
}
|
|
399
641
|
|
|
642
|
+
/**
|
|
643
|
+
* The locale this viewer was actually asked for, or undefined when the
|
|
644
|
+
* caller and the document both stayed silent.
|
|
645
|
+
*/
|
|
646
|
+
_requestedLanguage(): string | undefined {
|
|
647
|
+
// the document wins, because updateData merges it over the options, so
|
|
648
|
+
// a document-level `language` is what options.language ends up holding
|
|
649
|
+
const fromDocument = (this.data as { language?: unknown } | null | undefined)?.language;
|
|
650
|
+
if (typeof fromDocument === "string" && fromDocument !== "") {
|
|
651
|
+
return fromDocument;
|
|
652
|
+
}
|
|
653
|
+
return this._language_requested;
|
|
654
|
+
}
|
|
655
|
+
|
|
400
656
|
/* Switch the UI language at runtime, without a page reload.
|
|
401
657
|
================================================== */
|
|
402
658
|
|
|
403
659
|
refreshLanguage(code: string): void {
|
|
660
|
+
if (this._disposed) return;
|
|
661
|
+
// throws if a sibling viewer is rendering in another language
|
|
662
|
+
claimLanguage(code, this._language_holder);
|
|
404
663
|
this.options.language = code;
|
|
405
664
|
setLanguage(code);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
665
|
+
this._applyLanguageLayout();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Repaint everything a runtime language switch affects: the menubar
|
|
670
|
+
* labels and the `vco-rtl` class.
|
|
671
|
+
*
|
|
672
|
+
* This deliberately does *not* call `updateDisplay()`. A full re-layout
|
|
673
|
+
* re-runs the slider's `goTo(current_slide)`, which goes through the
|
|
674
|
+
* navigation path that re-arms autoplay — so calling it here silently
|
|
675
|
+
* cancelled a pending autoplay tick and the story stopped advancing.
|
|
676
|
+
*/
|
|
677
|
+
_applyLanguageLayout(): void {
|
|
678
|
+
this._menubar?.refreshLabels?.();
|
|
679
|
+
this._el.container.classList.toggle("vco-rtl", isRtl());
|
|
412
680
|
}
|
|
413
681
|
|
|
414
682
|
/* Load the font theme stylesheet
|
|
@@ -424,11 +692,7 @@ class StoryMapBase {
|
|
|
424
692
|
// external font CSS is an external service — ask first
|
|
425
693
|
const host = new URL(font.startsWith("//") ? "https:" + font : font).host;
|
|
426
694
|
const container = this._el.map ?? (this._el.container as HTMLElement);
|
|
427
|
-
const allowed = await manager.request(
|
|
428
|
-
consentMessage("consent_service_fonts", "web fonts"),
|
|
429
|
-
host,
|
|
430
|
-
container,
|
|
431
|
-
);
|
|
695
|
+
const allowed = await manager.request(fontService(), host, container);
|
|
432
696
|
if (!allowed) {
|
|
433
697
|
return;
|
|
434
698
|
}
|
|
@@ -452,28 +716,71 @@ class StoryMapBase {
|
|
|
452
716
|
/* Navigation
|
|
453
717
|
================================================== */
|
|
454
718
|
/**
|
|
455
|
-
* Navigate to a slide by index.
|
|
719
|
+
* Navigate to a slide by index. Out-of-range indices are ignored, as is
|
|
720
|
+
* the current index.
|
|
456
721
|
*
|
|
457
722
|
* @param n - Zero-based slide index; the map animates to the slide's
|
|
458
|
-
* marker
|
|
723
|
+
* marker. The overview is selected by the slide's own
|
|
724
|
+
* `type: "overview"`, not by its index.
|
|
459
725
|
*/
|
|
460
726
|
goTo(n: number) {
|
|
727
|
+
if (this._disposed) return;
|
|
461
728
|
// out-of-range indices are ignored: they would desync the slider and
|
|
462
729
|
// map (no active slide) and write a broken #slide-N bookmark
|
|
463
730
|
if (n >= 0 && n < (this.data?.slides?.length ?? 0) && n !== this.current_slide) {
|
|
464
|
-
|
|
465
|
-
|
|
731
|
+
this._navigate(n);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The single navigation path shared by programmatic navigation, the
|
|
737
|
+
* slider's `change` event, the map's `change` event and "back to start".
|
|
738
|
+
*
|
|
739
|
+
* `navigate` names the single component that the bubble source already
|
|
740
|
+
* moved; the other component follows. `navigated` says whether the caller
|
|
741
|
+
* has *not* already reported the change outward, so the outward `change`
|
|
742
|
+
* event fires exactly once per navigation (the bubble paths would
|
|
743
|
+
* otherwise re-enter through the other component's handler).
|
|
744
|
+
*/
|
|
745
|
+
private _navigate(
|
|
746
|
+
n: number,
|
|
747
|
+
opts: {
|
|
748
|
+
navigate?: "slider" | "map";
|
|
749
|
+
navigated?: boolean;
|
|
750
|
+
animate?: boolean;
|
|
751
|
+
} = {},
|
|
752
|
+
): void {
|
|
753
|
+
const { navigate, navigated = false, animate = true } = opts;
|
|
754
|
+
const duration = animate ? slideTransitionDuration(this.current_slide, n) : 0;
|
|
755
|
+
|
|
756
|
+
this.current_slide = n;
|
|
757
|
+
if (navigate !== "slider") {
|
|
466
758
|
this._storyslider.goTo(this.current_slide);
|
|
467
|
-
|
|
468
|
-
|
|
759
|
+
}
|
|
760
|
+
if (navigate !== "map") {
|
|
761
|
+
this._map?.goTo(this.current_slide);
|
|
762
|
+
}
|
|
763
|
+
this._beginTransition(duration);
|
|
764
|
+
if (!navigated) {
|
|
469
765
|
// programmatic navigation reports outward like interaction does;
|
|
470
|
-
// the bubbled slider/map change events
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.
|
|
766
|
+
// the bubbled slider/map change events are dropped by their
|
|
767
|
+
// equality guards, so this fires exactly once
|
|
768
|
+
// `current_id` so a host can tell *which stop* it is on, not just
|
|
769
|
+
// how far along it is: an index moves when a slide is inserted, an
|
|
770
|
+
// id does not (§5.1)
|
|
771
|
+
this.fire(
|
|
772
|
+
"change",
|
|
773
|
+
{
|
|
774
|
+
current_slide: this.current_slide,
|
|
775
|
+
current_id: this._currentSlideId(),
|
|
776
|
+
},
|
|
777
|
+
this,
|
|
778
|
+
);
|
|
476
779
|
}
|
|
780
|
+
this._syncHash();
|
|
781
|
+
this._playNarration(this.data.slides?.[this.current_slide]);
|
|
782
|
+
this._scheduleAutoplay();
|
|
783
|
+
this._updateProgress();
|
|
477
784
|
}
|
|
478
785
|
|
|
479
786
|
/**
|
|
@@ -498,6 +805,9 @@ class StoryMapBase {
|
|
|
498
805
|
* Called automatically on resize (see the `trackResize` option).
|
|
499
806
|
*/
|
|
500
807
|
updateDisplay() {
|
|
808
|
+
if (this._disposed) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
501
811
|
if (this.ready) {
|
|
502
812
|
this._updateDisplay();
|
|
503
813
|
}
|
|
@@ -506,8 +816,10 @@ class StoryMapBase {
|
|
|
506
816
|
/**
|
|
507
817
|
* Change a single map option at runtime and apply its effect immediately.
|
|
508
818
|
*
|
|
509
|
-
* Runtime-changeable options: `map_type` (
|
|
510
|
-
* layers, keeping the overview fitted to
|
|
819
|
+
* Runtime-changeable options: `map_type` (swaps one basemap for another and
|
|
820
|
+
* rebuilds the main + minimap tile layers, keeping the overview fitted to
|
|
821
|
+
* the marker bounds; it cannot add or remove a map, and `"none"` and
|
|
822
|
+
* non-strings are declined), `overlays`
|
|
511
823
|
* (rebuilds the stacked overlay layers), `show_lines`, `line_color`,
|
|
512
824
|
* `line_color_inactive`, `line_weight`, `line_opacity`, `line_dash`,
|
|
513
825
|
* `line_join`, `line_follows_path`, `show_history_line` (restyled
|
|
@@ -516,64 +828,346 @@ class StoryMapBase {
|
|
|
516
828
|
* options only take effect on the next navigation or require re-creating
|
|
517
829
|
* the StoryMap.
|
|
518
830
|
*/
|
|
831
|
+
/**
|
|
832
|
+
* Sugar over {@link setMapOptions} — `setMapOption(name, value)` is the
|
|
833
|
+
* same call as `setMapOptions({ [name]: value })`.
|
|
834
|
+
*/
|
|
519
835
|
setMapOption(name: string, value: unknown) {
|
|
836
|
+
if (this._disposed) return;
|
|
520
837
|
this.setMapOptions({ [name]: value } as Partial<StorymapOptions>);
|
|
521
838
|
}
|
|
522
839
|
|
|
523
840
|
/**
|
|
524
841
|
* Show or hide a stacked overlay by index (see the `overlays` option).
|
|
842
|
+
* Prefer `getOverlayLayer(index).setVisible(v)` on the layer itself; this
|
|
843
|
+
* wrapper also re-syncs the attribution line and the overlay blend mode.
|
|
525
844
|
*/
|
|
526
845
|
setOverlayVisible(index: number, visible: boolean): void {
|
|
527
|
-
this.
|
|
846
|
+
if (this._disposed) return;
|
|
847
|
+
this._map?.setOverlayVisible(index, visible);
|
|
528
848
|
}
|
|
529
849
|
|
|
530
850
|
/**
|
|
531
851
|
* Set a stacked overlay's opacity by index (see the `overlays` option).
|
|
852
|
+
* Alias: `getOverlayLayer(index)?.setOpacity(opacity)`.
|
|
532
853
|
*/
|
|
533
854
|
setOverlayOpacity(index: number, opacity: number): void {
|
|
534
|
-
this.
|
|
855
|
+
if (this._disposed) return;
|
|
856
|
+
this._map?.setOverlayOpacity(index, opacity);
|
|
535
857
|
}
|
|
536
858
|
|
|
537
859
|
/**
|
|
538
860
|
* Change several map options at runtime (see setMapOption).
|
|
539
861
|
*/
|
|
540
862
|
setMapOptions(options: Partial<StorymapOptions>) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
863
|
+
if (this._disposed) return;
|
|
864
|
+
// `map_type` decides at construction time whether a map exists. The
|
|
865
|
+
// engine can swap one basemap for another, but it can neither grow a
|
|
866
|
+
// map onto a mapless story nor remove a built one — "none" has no
|
|
867
|
+
// basemap branch and would silently install OpenStreetMap, the exact
|
|
868
|
+
// inverse of its meaning — and a non-string crashes the layer
|
|
869
|
+
// builder. So both are declined here, loudly, rather than half-applied.
|
|
870
|
+
let effective = options;
|
|
871
|
+
if (options.map_type !== undefined) {
|
|
872
|
+
if (typeof options.map_type !== "string") {
|
|
873
|
+
console.warn(
|
|
874
|
+
`StoryMapJS: setMapOptions ignores a non-string map_type (${String(options.map_type)}); pass "" for the default basemap.`,
|
|
875
|
+
);
|
|
876
|
+
effective = { ...options, map_type: "" };
|
|
877
|
+
} else if (options.map_type === "none" || this._map_disabled) {
|
|
878
|
+
console.warn(
|
|
879
|
+
"StoryMapJS: setMapOptions cannot add or remove a map after construction; map_type is load-time only.",
|
|
880
|
+
);
|
|
881
|
+
effective = { ...options };
|
|
882
|
+
delete effective.map_type;
|
|
550
883
|
}
|
|
551
884
|
}
|
|
885
|
+
mergeData(this.options, effective);
|
|
886
|
+
if (this._map && this._map.options) {
|
|
887
|
+
mergeData(this._map.options, effective);
|
|
888
|
+
this._map.applyOptions(Object.keys(effective));
|
|
889
|
+
}
|
|
552
890
|
if (this.ready) {
|
|
553
891
|
// text color theming follows runtime option changes (issue #177)
|
|
554
|
-
|
|
555
|
-
|
|
892
|
+
this._applyTextColors();
|
|
893
|
+
this.updateDisplay();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* OpenLayers accessors
|
|
898
|
+
The map itself is public (`storymap.map`, the raw `ol/Map`). These hand
|
|
899
|
+
out the objects the viewer keeps private, so a consumer never has to reach
|
|
900
|
+
through an underscore field. `getBaseLayer()` returns `null` while the
|
|
901
|
+
base layer is deferred awaiting tile consent.
|
|
902
|
+
================================================= */
|
|
903
|
+
|
|
904
|
+
/** The base tile layer, or `null` when deferred (tile consent not granted). */
|
|
905
|
+
getBaseLayer(): OlLayer | null {
|
|
906
|
+
return !this._disposed && this._map ? this._map.getBaseLayer() : null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** The stacked `overlays[]` layers, in `overlays[]` order. */
|
|
910
|
+
getOverlayLayers(): OlLayer[] {
|
|
911
|
+
return !this._disposed && this._map ? this._map.getOverlayLayers() : [];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** One stacked overlay layer by its `overlays[]` index, or `null`. */
|
|
915
|
+
getOverlayLayer(index: number): OlLayer | null {
|
|
916
|
+
return !this._disposed && this._map ? this._map.getOverlayLayer(index) : null;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Number of stacked overlay layers (see the `overlays` option). */
|
|
920
|
+
getOverlayCount(): number {
|
|
921
|
+
return !this._disposed && this._map ? this._map.getOverlayCount() : 0;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** The minimap's OpenLayers map (the `OverviewMap` control's inner map). */
|
|
925
|
+
getMinimap(): OlMap | null {
|
|
926
|
+
return !this._disposed && this._map ? this._map.getMinimap() : null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** The full (inactive) route line layer. */
|
|
930
|
+
getLine(): OlLayer | null {
|
|
931
|
+
return !this._disposed && this._map ? this._map.getLine() : null;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* True when the map shows a IIIF image as a picture of the world
|
|
936
|
+
* (`map_as_image`) rather than as a georeferenced map. In that mode the
|
|
937
|
+
* view is `EPSG:4326` image space: `map.getView().getCenter()` is in
|
|
938
|
+
* degrees, the resolution is in pixels per degree, and marker locations
|
|
939
|
+
* are pixel offsets into the image. Ask this instead of checking
|
|
940
|
+
* `map_type` — georeferenced IIIF (`map_bbox` without `map_as_image`)
|
|
941
|
+
* is an ordinary mercator map.
|
|
942
|
+
*/
|
|
943
|
+
isImageSpace(): boolean {
|
|
944
|
+
return !this._disposed && this._map ? this._map.isImageSpace() : false;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* The `uniqueid` of a slide — the current one by default.
|
|
949
|
+
*
|
|
950
|
+
* A IIIF manifest supplies one per canvas (§2.3) and it is the only
|
|
951
|
+
* identity a link can be built on, since an index moves when a slide is
|
|
952
|
+
* inserted. A storymap-JSON slide usually has none, in which case this is
|
|
953
|
+
* `null` rather than a generated id: a random one would be stable to nobody.
|
|
954
|
+
*/
|
|
955
|
+
getSlideId(index?: number): string | null {
|
|
956
|
+
if (this._disposed) return null;
|
|
957
|
+
const at = index ?? this.current_slide;
|
|
958
|
+
const id = this.data?.slides?.[at]?.uniqueid;
|
|
959
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Read the annotation documents this manifest points at with `seeAlso` and
|
|
964
|
+
* add the tour stops they carry.
|
|
965
|
+
*
|
|
966
|
+
* Explicit and asynchronous on purpose (§5.2). The alternative — following
|
|
967
|
+
* `seeAlso` inside `manifestToStorymapData` — would mean one blocking
|
|
968
|
+
* network round trip per referenced document before the viewer could paint
|
|
969
|
+
* its first slide, against third-party servers the host may not want to
|
|
970
|
+
* contact at all. Here the story stands on its own immediately and the
|
|
971
|
+
* annotations arrive afterwards, behind a per-(id, type) cache, and
|
|
972
|
+
* `annotationsloaded` fires when they are in.
|
|
973
|
+
*
|
|
974
|
+
* The stops are **appended**, not placed after the canvas they annotate.
|
|
975
|
+
* A canvas's own annotations are inserted in position at parse time; doing
|
|
976
|
+
* the same to a live tour would mean rebuilding the slider and every piece
|
|
977
|
+
* of state that hangs off it, for an ordering nicety. A host that needs
|
|
978
|
+
* the stops placed exactly can read them off the resolved value and place
|
|
979
|
+
* them itself.
|
|
980
|
+
*
|
|
981
|
+
* @returns the stops that were added, keyed by canvas id, plus any
|
|
982
|
+
* `SearchService1` endpoint seen and any document that failed to load.
|
|
983
|
+
*/
|
|
984
|
+
async loadAnnotations(
|
|
985
|
+
options: { fetchImpl?: typeof fetch } = {},
|
|
986
|
+
): Promise<{ stops: StorymapSlide[]; searchService: string | null; failed: string[] }> {
|
|
987
|
+
const added: StorymapSlide[] = [];
|
|
988
|
+
if (this._disposed || this._raw_manifest === null) {
|
|
989
|
+
return { stops: added, searchService: null, failed: [] };
|
|
990
|
+
}
|
|
991
|
+
const loaded = await loadSeeAlso(this._raw_manifest, options);
|
|
992
|
+
if (this._disposed) {
|
|
993
|
+
// torn down while the requests were in flight
|
|
994
|
+
return { stops: added, searchService: loaded.searchService, failed: loaded.failed };
|
|
995
|
+
}
|
|
996
|
+
// canvas order, so the appended stops follow the story rather than the
|
|
997
|
+
// order the documents happened to resolve in
|
|
998
|
+
for (const slide of this.data.slides ?? []) {
|
|
999
|
+
const stops = loaded.stops.get(slide.uniqueid ?? "");
|
|
1000
|
+
if (stops === undefined) continue;
|
|
1001
|
+
for (const stop of stops) {
|
|
1002
|
+
stop.uniqueid = `${slide.uniqueid ?? "slide"}#seealso-${added.length}`;
|
|
1003
|
+
added.push(stop);
|
|
1004
|
+
this.data.slides.push(stop);
|
|
1005
|
+
this._storyslider.createSlide(stop);
|
|
556
1006
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1007
|
+
}
|
|
1008
|
+
if (added.length > 0) {
|
|
1009
|
+
this._updateProgress();
|
|
1010
|
+
this._updateDistance();
|
|
1011
|
+
}
|
|
1012
|
+
const result = { stops: added, searchService: loaded.searchService, failed: loaded.failed };
|
|
1013
|
+
this.fire("annotationsloaded", result, this);
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Release everything the viewer attached to the page: window/document
|
|
1019
|
+
* listeners, timers, the resize observer, running Web Animations, the
|
|
1020
|
+
* slider, the menubar, the map and the map markers. Use this when tearing
|
|
1021
|
+
* a storymap down (SPA route change, modal close).
|
|
1022
|
+
*
|
|
1023
|
+
* Teardown is terminal: the host container is emptied, `map` becomes
|
|
1024
|
+
* null, and the public methods (`goTo`, `setMapOptions`,
|
|
1025
|
+
* `createMiniMap`, ...) become no-ops rather than throwing. There is no
|
|
1026
|
+
* re-init path; build a new StoryMap on the same element instead.
|
|
1027
|
+
* Calling `dispose()` twice is a no-op.
|
|
1028
|
+
*/
|
|
1029
|
+
dispose(): void {
|
|
1030
|
+
if (this._disposed) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this._disposed = true;
|
|
1034
|
+
// `updateDisplay()` is gated on `ready`, not `_disposed`, so without
|
|
1035
|
+
// this a post-teardown resize listener (or a stray host call) re-runs
|
|
1036
|
+
// the whole layout — and reaches the map engine — against an emptied
|
|
1037
|
+
// container and a disposed ol/Map.
|
|
1038
|
+
this.ready = false;
|
|
1039
|
+
|
|
1040
|
+
for (const timer of [this._transition_timer, this._autoplay_timer, this._resize_timer]) {
|
|
1041
|
+
if (timer !== null && timer !== undefined) {
|
|
1042
|
+
clearTimeout(timer);
|
|
562
1043
|
}
|
|
563
|
-
this.updateDisplay();
|
|
564
1044
|
}
|
|
1045
|
+
this._transition_timer = null;
|
|
1046
|
+
this._autoplay_timer = null;
|
|
1047
|
+
this._resize_timer = null;
|
|
1048
|
+
|
|
1049
|
+
this._resize_observer?.disconnect();
|
|
1050
|
+
this._resize_observer = null;
|
|
1051
|
+
|
|
1052
|
+
if (this._on_resize) {
|
|
1053
|
+
window.removeEventListener("resize", this._on_resize);
|
|
1054
|
+
this._on_resize = null;
|
|
1055
|
+
}
|
|
1056
|
+
if (this._on_keydown_global) {
|
|
1057
|
+
window.removeEventListener("keydown", this._on_keydown_global);
|
|
1058
|
+
this._on_keydown_global = null;
|
|
1059
|
+
}
|
|
1060
|
+
if (this._on_fullscreen) {
|
|
1061
|
+
document.removeEventListener("fullscreenchange", this._on_fullscreen);
|
|
1062
|
+
this._on_fullscreen = null;
|
|
1063
|
+
}
|
|
1064
|
+
if (this._on_hashchange) {
|
|
1065
|
+
window.removeEventListener("hashchange", this._on_hashchange);
|
|
1066
|
+
this._on_hashchange = null;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// slide transitions in flight (Web Animations API)
|
|
1070
|
+
for (const el of [this._el?.container, this._el?.map]) {
|
|
1071
|
+
el?.getAnimations?.().forEach((a) => a.cancel());
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// an unanswered consent panel holds a promise that Media.loadMedia()
|
|
1075
|
+
// is awaiting; settle it before the children go away
|
|
1076
|
+
consentManagerOf(this.options)?.dispose();
|
|
1077
|
+
|
|
1078
|
+
this._stopNarration();
|
|
1079
|
+
if (this._narration_el) {
|
|
1080
|
+
this._narration_el.src = "";
|
|
1081
|
+
this._narration_el = null;
|
|
1082
|
+
}
|
|
1083
|
+
this._storyslider?.dispose?.();
|
|
1084
|
+
this._menubar?.dispose?.();
|
|
1085
|
+
this._map?.dispose?.();
|
|
1086
|
+
|
|
1087
|
+
// the public ol/Map handle. The viewer is terminal after dispose(), so
|
|
1088
|
+
// this is null rather than a disposed instance; the public methods
|
|
1089
|
+
// above are guarded and no-op instead of throwing.
|
|
1090
|
+
this.map = null;
|
|
1091
|
+
|
|
1092
|
+
// empty the host element: the children owned their own listeners and
|
|
1093
|
+
// players, but their nodes (and the ol canvas) would otherwise stay
|
|
1094
|
+
// in the document after teardown
|
|
1095
|
+
this._el?.container?.replaceChildren();
|
|
1096
|
+
|
|
1097
|
+
// let another viewer claim a different language, now that this one is
|
|
1098
|
+
// no longer reading the shared strings (refcounted, so a sibling in
|
|
1099
|
+
// the same language keeps its claim)
|
|
1100
|
+
releaseLanguage(this._language_holder);
|
|
1101
|
+
|
|
1102
|
+
if (this._onInteraction) {
|
|
1103
|
+
for (const type of ["pointerdown", "focusin", "wheel"] as const) {
|
|
1104
|
+
this._el.container.removeEventListener(type, this._onInteraction);
|
|
1105
|
+
}
|
|
1106
|
+
this._onInteraction = null;
|
|
1107
|
+
}
|
|
1108
|
+
unregisterParticipant(this);
|
|
565
1109
|
}
|
|
566
1110
|
|
|
567
|
-
|
|
568
|
-
|
|
1111
|
+
/**
|
|
1112
|
+
* This viewer's root element. Satisfies the `Participant` shape the
|
|
1113
|
+
* page-wide key registry (`src/core/viewers.ts`) arbitrates on.
|
|
1114
|
+
*
|
|
1115
|
+
* @internal
|
|
1116
|
+
*/
|
|
1117
|
+
get element(): HTMLElement | null {
|
|
1118
|
+
return this._el?.container ?? null;
|
|
1119
|
+
}
|
|
569
1120
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1121
|
+
/**
|
|
1122
|
+
* Monotonic stamp of the last interaction with this viewer; the highest
|
|
1123
|
+
* one owns a page-wide keypress when nothing is focused.
|
|
1124
|
+
*
|
|
1125
|
+
* @internal
|
|
1126
|
+
*/
|
|
1127
|
+
get interaction(): number {
|
|
1128
|
+
return this._interaction;
|
|
1129
|
+
}
|
|
574
1130
|
|
|
575
|
-
|
|
576
|
-
|
|
1131
|
+
/** The highlighted route line drawn up to the current slide. */
|
|
1132
|
+
getLineActive(): OlLayer | null {
|
|
1133
|
+
return !this._disposed && this._map ? this._map.getLineActive() : null;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/** The map markers, indexed by slide. */
|
|
1137
|
+
getMarkers(): OpenLayersMapMarker[] {
|
|
1138
|
+
return !this._disposed && this._map ? this._map.getMarkers() : [];
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** One map marker by slide index, or `null`. */
|
|
1142
|
+
getMarker(index: number): OpenLayersMapMarker | null {
|
|
1143
|
+
return !this._disposed && this._map ? this._map.getMarker(index) : null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Rebuild the minimap (`OverviewMap` control). The constructor already
|
|
1148
|
+
* builds it; this is for hosts that recreate it after a `map_type` swap
|
|
1149
|
+
* or a deferred tile-consent grant.
|
|
1150
|
+
*/
|
|
1151
|
+
createMiniMap(): void {
|
|
1152
|
+
if (this._disposed) return;
|
|
1153
|
+
this._map?.createMiniMap();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Append attribution fragments and refresh the credit line (e.g. for a
|
|
1158
|
+
* custom layer added through `tile_source_factory`).
|
|
1159
|
+
*/
|
|
1160
|
+
setExtraAttributions(parts: string[]): void {
|
|
1161
|
+
if (this._disposed) return;
|
|
1162
|
+
this._map?.setExtraAttributions(parts);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Publish the text colour custom properties. Called from both the initial
|
|
1167
|
+
* layout and every runtime option change (issue #177) so the two paths
|
|
1168
|
+
* cannot drift.
|
|
1169
|
+
*/
|
|
1170
|
+
_applyTextColors(): void {
|
|
577
1171
|
if (this.options.text_color) {
|
|
578
1172
|
this._el.container.style.setProperty("--vco-color-text", this.options.text_color);
|
|
579
1173
|
}
|
|
@@ -583,28 +1177,65 @@ class StoryMapBase {
|
|
|
583
1177
|
this.options.text_background_color,
|
|
584
1178
|
);
|
|
585
1179
|
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* Private Methods
|
|
1183
|
+
================================================== */
|
|
1184
|
+
|
|
1185
|
+
// Initialize the layout
|
|
1186
|
+
_initLayout() {
|
|
1187
|
+
if (this._disposed) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this._el.container.className += " vco-storymap";
|
|
1191
|
+
this.options.base_class = this._el.container.className;
|
|
1192
|
+
|
|
1193
|
+
// Text color theming (issue #177): expose the text colors as CSS
|
|
1194
|
+
// custom properties consumed by the slide typography
|
|
1195
|
+
this._applyTextColors();
|
|
586
1196
|
|
|
587
1197
|
// Create Layout
|
|
588
1198
|
this._el.menubar = Dom.create("div", "vco-menubar", this._el.container);
|
|
589
|
-
this._el.map = this.
|
|
590
|
-
|
|
591
|
-
this.
|
|
592
|
-
}
|
|
1199
|
+
this._el.map = this._map_disabled
|
|
1200
|
+
? null
|
|
1201
|
+
: (this._resolveMapElement() ?? Dom.create("div", "vco-map", this._el.container));
|
|
593
1202
|
this._el.storyslider = Dom.create("div", "vco-storyslider", this._el.container);
|
|
594
1203
|
|
|
595
1204
|
// Initial Default Layout
|
|
596
1205
|
this.options.width = this._el.container.offsetWidth;
|
|
597
1206
|
this.options.height = this._el.container.offsetHeight;
|
|
598
|
-
this._el.map
|
|
1207
|
+
if (this._el.map) {
|
|
1208
|
+
this._el.map.style.height = "1px";
|
|
1209
|
+
}
|
|
599
1210
|
this._el.storyslider.style.top = "1px";
|
|
600
1211
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1212
|
+
if (this._map_disabled) {
|
|
1213
|
+
// No map pane, so the map options have nothing to size. The
|
|
1214
|
+
// slider takes the whole height below the menubar, and every
|
|
1215
|
+
// map-derived option is set to something the layout code below
|
|
1216
|
+
// does not need. Nothing is constructed - no ol/Map, no tile
|
|
1217
|
+
// layer, no consent ask. storyslider_height is set by the
|
|
1218
|
+
// _updateDisplay call below, so assigning it here would be dead.
|
|
1219
|
+
this.options.map_height = 0;
|
|
1220
|
+
// the overview control is a map control: with no map there is
|
|
1221
|
+
// nothing for it to zoom out to, so it is hidden the same way
|
|
1222
|
+
// `show_overview: false` hides it
|
|
1223
|
+
this.options.show_overview = false;
|
|
1224
|
+
} else {
|
|
1225
|
+
// Create Map using preferred Map API
|
|
1226
|
+
this._map = new OpenLayersMap(this._map_el(), this.data, this.options);
|
|
1227
|
+
this.map = this._map._map; // For access to the OpenLayers map.
|
|
1228
|
+
this._map.on("loaded", this._onMapLoaded, this);
|
|
1229
|
+
// image readiness (IIIF/zoomify sources attach asynchronously) is
|
|
1230
|
+
// re-fired on the StoryMap, the coordination point for hosts that
|
|
1231
|
+
// overlay or measure their own layers
|
|
1232
|
+
this._map.on("imageready", (e: unknown) => {
|
|
1233
|
+
this.fire("imageready", e);
|
|
1234
|
+
});
|
|
605
1235
|
|
|
606
|
-
|
|
607
|
-
|
|
1236
|
+
// Map Background Color
|
|
1237
|
+
this._map_el().style.backgroundColor = this.options.map_background_color;
|
|
1238
|
+
}
|
|
608
1239
|
|
|
609
1240
|
// Create Menu Bar
|
|
610
1241
|
this._menubar = new MenuBar(this._el.menubar, this._el.container, this.options);
|
|
@@ -616,7 +1247,12 @@ class StoryMapBase {
|
|
|
616
1247
|
this._storyslider.init();
|
|
617
1248
|
|
|
618
1249
|
// LAYOUT
|
|
619
|
-
if (this.
|
|
1250
|
+
if (this._map_disabled) {
|
|
1251
|
+
// one branch, both orientations: nothing to split the height
|
|
1252
|
+
// with, and the menubar is the only thing above the slider
|
|
1253
|
+
this.options.menubar_height = this._el.menubar.offsetHeight;
|
|
1254
|
+
this._menubar.setSticky(this.options.menubar_height);
|
|
1255
|
+
} else if (this.options.layout === "portrait") {
|
|
620
1256
|
// Set Default Component Sizes
|
|
621
1257
|
this.options.map_height = this.options.height / this.options.map_size_sticky;
|
|
622
1258
|
this.options.storyslider_height =
|
|
@@ -650,16 +1286,33 @@ class StoryMapBase {
|
|
|
650
1286
|
this._storyslider.on("colorchange", this._onColorChange, this);
|
|
651
1287
|
|
|
652
1288
|
// Map Events
|
|
653
|
-
this._map
|
|
1289
|
+
this._map?.on("change", this._onMapChange, this);
|
|
654
1290
|
|
|
655
1291
|
// Global slide navigation (opt-in): the slider only listens on its
|
|
656
|
-
// own panel, which needs focus
|
|
1292
|
+
// own panel, which needs focus.
|
|
1293
|
+
//
|
|
1294
|
+
// With more than one viewer on the page, `window` is shared, and
|
|
1295
|
+
// e.preventDefault() does not stop a sibling listener on the same
|
|
1296
|
+
// target — so an arrow press used to advance every story at once. This
|
|
1297
|
+
// viewer takes part in the registry and then answers whether it owns
|
|
1298
|
+
// the event (focus first, then most-recently-interacted).
|
|
657
1299
|
if (this.options.keyboard) {
|
|
658
|
-
|
|
1300
|
+
this._onInteraction = () => {
|
|
1301
|
+
this._interaction = markInteraction();
|
|
1302
|
+
};
|
|
1303
|
+
for (const type of ["pointerdown", "focusin", "wheel"] as const) {
|
|
1304
|
+
this._el.container.addEventListener(type, this._onInteraction, {
|
|
1305
|
+
passive: true,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
registerParticipant(this);
|
|
1309
|
+
this._on_keydown_global = (e: KeyboardEvent) => this._onKeyDownGlobal(e);
|
|
1310
|
+
window.addEventListener("keydown", this._on_keydown_global);
|
|
659
1311
|
}
|
|
660
1312
|
|
|
661
1313
|
// Fullscreen state
|
|
662
|
-
|
|
1314
|
+
this._on_fullscreen = () => this._onFullscreenChange();
|
|
1315
|
+
document.addEventListener("fullscreenchange", this._on_fullscreen);
|
|
663
1316
|
}
|
|
664
1317
|
|
|
665
1318
|
_onKeyDownGlobal(e: KeyboardEvent) {
|
|
@@ -668,11 +1321,19 @@ class StoryMapBase {
|
|
|
668
1321
|
}
|
|
669
1322
|
const target = e.target as HTMLElement | null;
|
|
670
1323
|
const tag = target?.tagName?.toLowerCase();
|
|
671
|
-
if (
|
|
1324
|
+
if (
|
|
1325
|
+
tag === "input" ||
|
|
1326
|
+
tag === "textarea" ||
|
|
1327
|
+
tag === "select" ||
|
|
1328
|
+
target?.isContentEditable
|
|
1329
|
+
) {
|
|
672
1330
|
return;
|
|
673
1331
|
}
|
|
674
|
-
//
|
|
675
|
-
|
|
1332
|
+
// One viewer owns a page-wide keypress. This also covers the old
|
|
1333
|
+
// "let OpenLayers pan the map" carve-out: the map is inside the
|
|
1334
|
+
// viewer's own container, so a keypress aimed at another viewer (or at
|
|
1335
|
+
// nothing the visitor has focused) no longer reaches this slider.
|
|
1336
|
+
if (!ownsPageEvent(this)) {
|
|
676
1337
|
return;
|
|
677
1338
|
}
|
|
678
1339
|
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
@@ -684,8 +1345,12 @@ class StoryMapBase {
|
|
|
684
1345
|
}
|
|
685
1346
|
}
|
|
686
1347
|
|
|
687
|
-
//
|
|
1348
|
+
// View
|
|
688
1349
|
_updateDisplay(map_height?: number, animate?: boolean, d?: number) {
|
|
1350
|
+
// prefers-reduced-motion: glides collapse to instant size changes
|
|
1351
|
+
if (animate && prefersReducedMotion()) {
|
|
1352
|
+
animate = false;
|
|
1353
|
+
}
|
|
689
1354
|
let duration = this.options.duration,
|
|
690
1355
|
display_class = this.options.base_class;
|
|
691
1356
|
|
|
@@ -716,19 +1381,65 @@ class StoryMapBase {
|
|
|
716
1381
|
}
|
|
717
1382
|
|
|
718
1383
|
// LAYOUT
|
|
719
|
-
if (this.
|
|
1384
|
+
if (this._map_disabled) {
|
|
1385
|
+
// The mapless layout: the menubar on top, the slider filling
|
|
1386
|
+
// everything below it, in both orientations. No map element to
|
|
1387
|
+
// size, offset, animate or fit, so none of that runs, and
|
|
1388
|
+
// `map_area` has nothing to narrow.
|
|
1389
|
+
display_class += " vco-layout-no-map";
|
|
1390
|
+
// A narrow mapless story still needs the narrow-viewport slide
|
|
1391
|
+
// layout: every rule that stacks the two-column slide (the
|
|
1392
|
+
// 100px side padding, the floated 50% media block) keys on
|
|
1393
|
+
// vco-skinny, and without it a phone-width story renders broken.
|
|
1394
|
+
// The wide case needs nothing extra — the no-map rule already
|
|
1395
|
+
// owns the full-width opaque panel.
|
|
1396
|
+
if (this.options.layout === "portrait") {
|
|
1397
|
+
display_class += " vco-skinny vco-layout-portrait";
|
|
1398
|
+
}
|
|
1399
|
+
// A narrow mapless story still needs the narrow-viewport slide
|
|
1400
|
+
// layout: every rule that stacks the two-column slide (the
|
|
1401
|
+
// 100px side padding, the floated 50% media block) keys on
|
|
1402
|
+
// vco-skinny, and without it a phone-width story renders broken.
|
|
1403
|
+
// The wide case needs nothing extra — the no-map rule already
|
|
1404
|
+
// owns the full-width opaque panel.
|
|
1405
|
+
|
|
1406
|
+
this.options.menubar_height = this._el.menubar.offsetHeight;
|
|
1407
|
+
this.options.map_height = 0;
|
|
1408
|
+
this.options.storyslider_height = this.options.height - 1;
|
|
1409
|
+
this._menubar.setSticky(this.options.menubar_height);
|
|
1410
|
+
|
|
1411
|
+
this._el.storyslider.style.top = "0";
|
|
1412
|
+
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
1413
|
+
|
|
1414
|
+
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
1415
|
+
this._storyslider.updateDisplay(
|
|
1416
|
+
this.options.width,
|
|
1417
|
+
this.options.storyslider_height,
|
|
1418
|
+
animate,
|
|
1419
|
+
this.options.layout,
|
|
1420
|
+
);
|
|
1421
|
+
} else if (this.options.layout === "portrait") {
|
|
720
1422
|
display_class += " vco-skinny";
|
|
721
1423
|
// Map Offset
|
|
722
|
-
this.
|
|
723
|
-
|
|
724
|
-
|
|
1424
|
+
this._map_required().setMapOffset(0, 0);
|
|
1425
|
+
|
|
1426
|
+
// Portrait split. The collapse toggle is only offered in portrait
|
|
1427
|
+
// (MenuBar hides it in landscape), so this branch used to
|
|
1428
|
+
// unconditionally recompute map_height and discard the collapsed
|
|
1429
|
+
// height it was handed — the button was a visual no-op. Honour
|
|
1430
|
+
// the toggle state instead.
|
|
1431
|
+
if (this._collapsed) {
|
|
1432
|
+
this.options.map_height = COLLAPSED_MAP_HEIGHT;
|
|
1433
|
+
} else {
|
|
1434
|
+
this.options.map_height = this.options.height / this.options.map_size_sticky;
|
|
1435
|
+
}
|
|
725
1436
|
this.options.storyslider_height = this.options.height - this.options.map_height - 1;
|
|
726
1437
|
this._menubar.setSticky(0);
|
|
727
1438
|
|
|
728
1439
|
// Portrait: the map spans the full width again (a landscape
|
|
729
1440
|
// map_area "left" pass narrowed it)
|
|
730
1441
|
display_class += " vco-layout-portrait";
|
|
731
|
-
this.
|
|
1442
|
+
this._map_el().style.width = "100%";
|
|
732
1443
|
|
|
733
1444
|
if (animate) {
|
|
734
1445
|
// Animate Map
|
|
@@ -736,12 +1447,12 @@ class StoryMapBase {
|
|
|
736
1447
|
this.animator_map.stop();
|
|
737
1448
|
}
|
|
738
1449
|
|
|
739
|
-
this.animator_map = Animate(this.
|
|
1450
|
+
this.animator_map = Animate(this._map_el(), {
|
|
740
1451
|
height: this.options.map_height + "px",
|
|
741
1452
|
duration: duration,
|
|
742
1453
|
easing: easeOutStrong,
|
|
743
1454
|
complete: () => {
|
|
744
|
-
this.
|
|
1455
|
+
this._map_required().updateDisplay(
|
|
745
1456
|
this.options.width,
|
|
746
1457
|
this.options.map_height,
|
|
747
1458
|
animate,
|
|
@@ -762,7 +1473,7 @@ class StoryMapBase {
|
|
|
762
1473
|
});
|
|
763
1474
|
} else {
|
|
764
1475
|
// Map
|
|
765
|
-
this.
|
|
1476
|
+
this._map_el().style.height = Math.ceil(this.options.map_height) + "px";
|
|
766
1477
|
|
|
767
1478
|
// StorySlider
|
|
768
1479
|
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
@@ -770,7 +1481,7 @@ class StoryMapBase {
|
|
|
770
1481
|
|
|
771
1482
|
// Update Component Displays
|
|
772
1483
|
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
773
|
-
this.
|
|
1484
|
+
this._map_required().updateDisplay(this.options.width, this.options.height, false);
|
|
774
1485
|
this._storyslider.updateDisplay(
|
|
775
1486
|
this.options.width,
|
|
776
1487
|
this.options.storyslider_height,
|
|
@@ -786,12 +1497,11 @@ class StoryMapBase {
|
|
|
786
1497
|
// Set Default Component Sizes
|
|
787
1498
|
this.options.map_height = this.options.height;
|
|
788
1499
|
this.options.storyslider_height = this.options.height;
|
|
789
|
-
this._menubar.setSticky(this.options.menubar_height);
|
|
790
1500
|
|
|
791
1501
|
// Set Sticky state of MenuBar
|
|
792
1502
|
this._menubar.setSticky(this.options.menubar_height);
|
|
793
1503
|
|
|
794
|
-
this.
|
|
1504
|
+
this._map_el().style.height = this.options.height + "px";
|
|
795
1505
|
|
|
796
1506
|
// map_area "left": the map element is limited to the left, visible
|
|
797
1507
|
// half (the slide panel is opaque) — no view offset needed;
|
|
@@ -799,11 +1509,11 @@ class StoryMapBase {
|
|
|
799
1509
|
// fading slide panel and the view is offset by a quarter width
|
|
800
1510
|
if (this.options.map_area === "left") {
|
|
801
1511
|
display_class += " vco-map-area-left";
|
|
802
|
-
this.
|
|
803
|
-
this.
|
|
1512
|
+
this._map_el().style.width = Math.floor(this.options.width / 2) + "px";
|
|
1513
|
+
this._map_required().setMapOffset(0, 0);
|
|
804
1514
|
} else {
|
|
805
|
-
this.
|
|
806
|
-
this.
|
|
1515
|
+
this._map_el().style.width = "100%";
|
|
1516
|
+
this._map_required().setMapOffset(-(this.options.width / 4), 0);
|
|
807
1517
|
}
|
|
808
1518
|
|
|
809
1519
|
// StorySlider
|
|
@@ -811,7 +1521,7 @@ class StoryMapBase {
|
|
|
811
1521
|
this._el.storyslider.style.height = this.options.storyslider_height + "px";
|
|
812
1522
|
|
|
813
1523
|
this._menubar.updateDisplay(this.options.width, this.options.height, animate);
|
|
814
|
-
this.
|
|
1524
|
+
this._map_required().updateDisplay(this.options.width, this.options.height, animate, d);
|
|
815
1525
|
this._storyslider.updateDisplay(
|
|
816
1526
|
this.options.width / 2,
|
|
817
1527
|
this.options.storyslider_height,
|
|
@@ -820,7 +1530,8 @@ class StoryMapBase {
|
|
|
820
1530
|
);
|
|
821
1531
|
}
|
|
822
1532
|
|
|
823
|
-
|
|
1533
|
+
// the resolved locale decides the layout direction (issues #211, #245)
|
|
1534
|
+
if (isRtl()) {
|
|
824
1535
|
display_class += " vco-rtl";
|
|
825
1536
|
}
|
|
826
1537
|
|
|
@@ -832,6 +1543,11 @@ class StoryMapBase {
|
|
|
832
1543
|
================================================== */
|
|
833
1544
|
|
|
834
1545
|
_onDataLoaded(e?: unknown) {
|
|
1546
|
+
// Belt and braces with the _loadDataFromUrl guard: anything reaching
|
|
1547
|
+
// this point after a teardown must not rebuild the layout.
|
|
1548
|
+
if (this._disposed) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
835
1551
|
// attach the consent manager BEFORE the layout is created, so the
|
|
836
1552
|
// slider/map/media options copies all share it
|
|
837
1553
|
(this.options as Record<string, unknown>).consent_manager = new ConsentManager();
|
|
@@ -845,15 +1561,155 @@ class StoryMapBase {
|
|
|
845
1561
|
this._initEvents();
|
|
846
1562
|
this._initResizeHandling();
|
|
847
1563
|
this.ready = true;
|
|
1564
|
+
this._startConsentAsk();
|
|
848
1565
|
this._startAutoplay();
|
|
849
1566
|
}
|
|
850
1567
|
|
|
1568
|
+
/**
|
|
1569
|
+
* Start-of-story consent (issue: allow all / individually / decline
|
|
1570
|
+
* all): with `consent_required`, one dialog lists every external
|
|
1571
|
+
* service the story uses — map tiles, external web fonts and the media
|
|
1572
|
+
* services in the slides. Shown only while some service is unanswered;
|
|
1573
|
+
* per-service slide asks remain the fallback.
|
|
1574
|
+
*/
|
|
1575
|
+
_startConsentAsk(): void {
|
|
1576
|
+
const manager = consentManagerOf(this.options);
|
|
1577
|
+
if (!manager || !this.options.consent_required) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const services: ConsentService[] = [];
|
|
1581
|
+
// map tiles — the same service the map's layer code keys off. A story
|
|
1582
|
+
// with no map asks about its media and fonts only: there is no tile
|
|
1583
|
+
// request to consent to, so asking would be noise the visitor cannot
|
|
1584
|
+
// act on.
|
|
1585
|
+
if (!this._map_disabled) {
|
|
1586
|
+
services.push(tileService());
|
|
1587
|
+
}
|
|
1588
|
+
// media services with a real URL in the slides (a storymap is allowed
|
|
1589
|
+
// to have no slides at all)
|
|
1590
|
+
const seen = new Set<string>();
|
|
1591
|
+
for (const slide of this.data.slides ?? []) {
|
|
1592
|
+
const url = (slide.media as { url?: string | null } | null)?.url;
|
|
1593
|
+
if (!url) continue;
|
|
1594
|
+
const match = MediaType({ url } as never) as { type: string; name: string } | false;
|
|
1595
|
+
if (!match || seen.has(match.type)) continue;
|
|
1596
|
+
seen.add(match.type);
|
|
1597
|
+
// same registry the per-slide panel uses, so the two dialogs can
|
|
1598
|
+
// never disagree about a service's name
|
|
1599
|
+
services.push(mediaService(match.type, match.name));
|
|
1600
|
+
}
|
|
1601
|
+
// Slide narration is its own service: the recording plays through a
|
|
1602
|
+
// dedicated audio element, not through Media, so nothing else asks
|
|
1603
|
+
// for it — without this the GDPR mode would have a hole.
|
|
1604
|
+
const has_narration = (this.data.slides ?? []).some(
|
|
1605
|
+
(slide) => !!(slide.narration as { url?: string } | null)?.url,
|
|
1606
|
+
);
|
|
1607
|
+
if (has_narration && !seen.has("narration")) {
|
|
1608
|
+
services.push(narrationService());
|
|
1609
|
+
}
|
|
1610
|
+
// external web fonts (same-origin themes never ask). Compared by
|
|
1611
|
+
// origin, not by a prefix test on the resolved URL: resolveFontCssUrl
|
|
1612
|
+
// has already turned a relative path into an absolute one, so testing
|
|
1613
|
+
// that for "http" made every relative font_css look external.
|
|
1614
|
+
const font = resolveFontCssUrl(this.options.font_css || "stock:default");
|
|
1615
|
+
if (isExternalUrl(font)) {
|
|
1616
|
+
services.push(fontService());
|
|
1617
|
+
}
|
|
1618
|
+
manager.requestAll(services, this._el.container);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
851
1621
|
/* Autoplay (issue #380) and hash bookmarks (issue #146)
|
|
852
1622
|
================================================== */
|
|
1623
|
+
/* Narration (docs/plans/iiif-media-tours.md §2)
|
|
1624
|
+
================================================== */
|
|
1625
|
+
|
|
1626
|
+
/**
|
|
1627
|
+
* Play the current slide's narration, if it has one.
|
|
1628
|
+
*
|
|
1629
|
+
* One `<audio>` element is reused for the whole story rather than a
|
|
1630
|
+
* player per slide, so switching slides cannot leave a recording playing
|
|
1631
|
+
* behind. Two things gate it:
|
|
1632
|
+
*
|
|
1633
|
+
* - **browser autoplay policy**: unmuted audio only plays after a user
|
|
1634
|
+
* gesture, so before the first interaction the element is left paused
|
|
1635
|
+
* and nothing is reported as broken. Re-arm on the first interaction.
|
|
1636
|
+
* - **consent**: the `media:narration` service is requested at start-up
|
|
1637
|
+
* (see `_startConsentAsk`); a denied or unanswered service means no
|
|
1638
|
+
* playback, and the story still works.
|
|
1639
|
+
*/
|
|
1640
|
+
_playNarration(slide: StorymapSlide | undefined, allow_without_gesture = false) {
|
|
1641
|
+
const url = (slide?.narration as { url?: string } | null)?.url;
|
|
1642
|
+
this._stopNarration();
|
|
1643
|
+
if (!url || this._disposed || !this._narration_allowed) return;
|
|
1644
|
+
if (!allow_without_gesture && !this._has_user_gesture()) {
|
|
1645
|
+
this._replayNarrationAfterGesture = true;
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const el = this._narrationElement();
|
|
1649
|
+
el.src = url;
|
|
1650
|
+
el.currentTime = 0;
|
|
1651
|
+
const token = ++this._narration_token;
|
|
1652
|
+
// a rejected play() (policy, network) must not surface as an
|
|
1653
|
+
// unhandled rejection
|
|
1654
|
+
void el.play()?.catch?.(() => {
|
|
1655
|
+
if (token === this._narration_token) this._replayNarrationAfterGesture = true;
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
_narrationElement(): HTMLAudioElement {
|
|
1660
|
+
if (this._narration_el === null) {
|
|
1661
|
+
this._narration_el = document.createElement("audio");
|
|
1662
|
+
this._narration_el.className = "vco-media-item vco-narration";
|
|
1663
|
+
this._narration_el.preload = "none";
|
|
1664
|
+
this._narration_el.setAttribute("aria-hidden", "true");
|
|
1665
|
+
}
|
|
1666
|
+
return this._narration_el;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
_stopNarration() {
|
|
1670
|
+
this._narration_token++;
|
|
1671
|
+
const el = this._narration_el;
|
|
1672
|
+
if (!el) return;
|
|
1673
|
+
el.pause();
|
|
1674
|
+
el.removeAttribute("src");
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** A gesture has happened, so unmuted audio is allowed from now on. */
|
|
1678
|
+
_note_user_gesture_for_narration() {
|
|
1679
|
+
if (this._disposed) return;
|
|
1680
|
+
this._narration_allowed = this._narration_allowed && true;
|
|
1681
|
+
if (this._replayNarrationAfterGesture) {
|
|
1682
|
+
this._replayNarrationAfterGesture = false;
|
|
1683
|
+
this._playNarration(this.data.slides?.[this.current_slide], true);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
_has_user_gesture(): boolean {
|
|
1688
|
+
return this._user_gestured;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
853
1691
|
_startAutoplay() {
|
|
854
1692
|
this._stopAutoplay();
|
|
855
1693
|
this._autoplay_stopped = false;
|
|
856
|
-
|
|
1694
|
+
// a denied media:narration service means no narration for this story
|
|
1695
|
+
const manager = consentManagerOf(this.options);
|
|
1696
|
+
if (manager && this.options.consent_required) {
|
|
1697
|
+
this._narration_allowed = !manager.isDenied(narrationService().key);
|
|
1698
|
+
}
|
|
1699
|
+
// browsers only allow unmuted audio after a user gesture; the first
|
|
1700
|
+
// interaction arms narration for the rest of the story
|
|
1701
|
+
if (!this._user_gestured) {
|
|
1702
|
+
const on_gesture = () => {
|
|
1703
|
+
this._user_gestured = true;
|
|
1704
|
+
this._note_user_gesture_for_narration();
|
|
1705
|
+
this._el.container.removeEventListener("pointerdown", on_gesture);
|
|
1706
|
+
this._el.container.removeEventListener("keydown", on_gesture);
|
|
1707
|
+
};
|
|
1708
|
+
this._el.container.addEventListener("pointerdown", on_gesture, { once: true });
|
|
1709
|
+
this._el.container.addEventListener("keydown", on_gesture, { once: true });
|
|
1710
|
+
}
|
|
1711
|
+
// prefers-reduced-motion: no autoplay (WCAG 2.2.2)
|
|
1712
|
+
if (this.options.autoplay > 0 && !prefersReducedMotion()) {
|
|
857
1713
|
// any user interaction stops autoplay permanently
|
|
858
1714
|
const stop = () => {
|
|
859
1715
|
this._autoplay_stopped = true;
|
|
@@ -866,17 +1722,39 @@ class StoryMapBase {
|
|
|
866
1722
|
}
|
|
867
1723
|
}
|
|
868
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Arm autoplay for the current slide.
|
|
1727
|
+
*
|
|
1728
|
+
* With `autoplay_media` on and a slide whose media is playable audio or
|
|
1729
|
+
* video, the advance waits for that media to end instead of using the
|
|
1730
|
+
* `autoplay` millisecond timer. The timer is **kept as a fallback** in
|
|
1731
|
+
* that case too: a media element that never fires `ended` (a stalled
|
|
1732
|
+
* fetch, a codec the browser cannot play, or a slide whose media was
|
|
1733
|
+
* never loaded because the visitor skipped past it) would otherwise stop
|
|
1734
|
+
* the story dead. So both are armed, whichever fires first advances, and
|
|
1735
|
+
* the other is cleared.
|
|
1736
|
+
*/
|
|
869
1737
|
_scheduleAutoplay() {
|
|
870
1738
|
this._stopAutoplay();
|
|
871
|
-
if (this.options.autoplay > 0
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
1739
|
+
if (!(this.options.autoplay > 0) || this._autoplay_stopped) return;
|
|
1740
|
+
|
|
1741
|
+
const advance = () => {
|
|
1742
|
+
this._stopAutoplay();
|
|
1743
|
+
if (this.current_slide + 1 < this.data.slides.length) {
|
|
1744
|
+
this.goTo(this.current_slide + 1);
|
|
1745
|
+
this._scheduleAutoplay();
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
this._autoplay_advance = advance;
|
|
1749
|
+
|
|
1750
|
+
if (this.options.autoplay_media) {
|
|
1751
|
+
const slide = this._storyslider?._slides?.[this.current_slide];
|
|
1752
|
+
if (slide?.hasPlayableMedia?.()) {
|
|
1753
|
+
slide.onMediaEnded(advance);
|
|
1754
|
+
}
|
|
879
1755
|
}
|
|
1756
|
+
|
|
1757
|
+
this._autoplay_timer = setTimeout(advance, this.options.autoplay);
|
|
880
1758
|
}
|
|
881
1759
|
|
|
882
1760
|
_stopAutoplay() {
|
|
@@ -884,33 +1762,123 @@ class StoryMapBase {
|
|
|
884
1762
|
clearTimeout(this._autoplay_timer);
|
|
885
1763
|
this._autoplay_timer = null;
|
|
886
1764
|
}
|
|
1765
|
+
this._autoplay_advance = null;
|
|
887
1766
|
}
|
|
888
1767
|
|
|
889
|
-
/**
|
|
1768
|
+
/**
|
|
1769
|
+
* The current slide's `uniqueid`, or null when the data carries none.
|
|
1770
|
+
*
|
|
1771
|
+
* A storymap-JSON slide usually has no `uniqueid` — the slider generates
|
|
1772
|
+
* one per slide for its own DOM ids, but that is not a shareable identity,
|
|
1773
|
+
* so it is deliberately not used here. A IIIF manifest supplies one per
|
|
1774
|
+
* canvas (§2.3), and that is what a deep link should name.
|
|
1775
|
+
*/
|
|
1776
|
+
_currentSlideId(): string | null {
|
|
1777
|
+
const id = this.data?.slides?.[this.current_slide]?.uniqueid;
|
|
1778
|
+
return typeof id === "string" && id !== "" ? id : null;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/** The current slide as a Content State, or null when it has no id. */
|
|
1782
|
+
_currentContentState(): ContentState | null {
|
|
1783
|
+
const id = this._currentSlideId();
|
|
1784
|
+
if (id === null) return null;
|
|
1785
|
+
const region = this.data.slides[this.current_slide]?.location?.region;
|
|
1786
|
+
return Array.isArray(region) && region.length === 4
|
|
1787
|
+
? { id, region: region as [number, number, number, number] }
|
|
1788
|
+
: { id };
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Keep the URL in sync with the current slide: the `#slide-…` hash and,
|
|
1793
|
+
* when the slide has a shareable id, an `iiif-content` parameter (§5.1).
|
|
1794
|
+
*
|
|
1795
|
+
* The query string is rewritten rather than replaced, because pages like
|
|
1796
|
+
* the embed player and the test harness carry their own configuration in
|
|
1797
|
+
* it (`?url=…`, `?example=…`, `?manifest=…`).
|
|
1798
|
+
*/
|
|
890
1799
|
_syncHash() {
|
|
891
1800
|
try {
|
|
892
|
-
|
|
893
|
-
|
|
1801
|
+
const state = this._currentContentState();
|
|
1802
|
+
const params = new URLSearchParams(window.location.search);
|
|
1803
|
+
if (state !== null) {
|
|
1804
|
+
params.set(
|
|
1805
|
+
CONTENT_STATE_PARAM,
|
|
1806
|
+
formatContentState(
|
|
1807
|
+
state,
|
|
1808
|
+
(this.data as { uniqueid?: string }).uniqueid ?? null,
|
|
1809
|
+
),
|
|
1810
|
+
);
|
|
1811
|
+
} else {
|
|
1812
|
+
// a storymap-JSON slide with no id cannot be named, so the
|
|
1813
|
+
// parameter would only ever point at the wrong stop
|
|
1814
|
+
params.delete(CONTENT_STATE_PARAM);
|
|
1815
|
+
}
|
|
1816
|
+
const query = params.toString();
|
|
894
1817
|
history.replaceState(
|
|
895
1818
|
null,
|
|
896
1819
|
"",
|
|
897
|
-
window.location.pathname +
|
|
1820
|
+
window.location.pathname +
|
|
1821
|
+
(query === "" ? "" : "?" + query) +
|
|
1822
|
+
"#slide-" +
|
|
1823
|
+
(state !== null ? encodeURI(state.id) : this.current_slide),
|
|
898
1824
|
);
|
|
899
1825
|
} catch {
|
|
900
1826
|
// non-browser or sandboxed contexts
|
|
901
1827
|
}
|
|
902
1828
|
}
|
|
903
1829
|
|
|
904
|
-
/**
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1830
|
+
/**
|
|
1831
|
+
* A `#slide-…` hash or an `iiif-content` parameter deep-links the storymap
|
|
1832
|
+
* (applied on load + hashchange).
|
|
1833
|
+
*
|
|
1834
|
+
* The hash token is an index when it is all digits and a `uniqueid`
|
|
1835
|
+
* otherwise, which is unambiguous because an index never contains anything
|
|
1836
|
+
* else. The index form stays supported, so links shared before ids were
|
|
1837
|
+
* emitted keep working; they just stop being stable the moment a slide is
|
|
1838
|
+
* inserted.
|
|
1839
|
+
*/
|
|
1840
|
+
_applyHashSlide(captured?: { state: ContentState | null; hash: string } | null): boolean {
|
|
1841
|
+
const link = captured ?? {
|
|
1842
|
+
state: parseContentState(
|
|
1843
|
+
new URLSearchParams(window.location.search).get(CONTENT_STATE_PARAM),
|
|
1844
|
+
),
|
|
1845
|
+
hash: window.location.hash,
|
|
1846
|
+
};
|
|
1847
|
+
const target = this._resolveDeepLink(link.state, link.hash);
|
|
1848
|
+
if (target === null || target === this.current_slide) return false;
|
|
1849
|
+
this.goTo(target);
|
|
1850
|
+
return true;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* A content state or hash token as a slide index, or null when it names
|
|
1855
|
+
* nothing. A content state wins over the hash, being the standard spelling.
|
|
1856
|
+
*/
|
|
1857
|
+
_resolveDeepLink(state: ContentState | null, hash: string): number | null {
|
|
1858
|
+
if (state !== null) {
|
|
1859
|
+
const index = this._slideIndexById(state.id);
|
|
1860
|
+
if (index !== null) return index;
|
|
1861
|
+
console.warn(
|
|
1862
|
+
`StoryMapJS: the content state names a canvas this storymap does not have: ${state.id}`,
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
const match = /^#slide-(.+)$/.exec(hash);
|
|
1866
|
+
if (match === null) return null;
|
|
1867
|
+
const token = match[1];
|
|
1868
|
+
if (/^\d+$/.test(token)) {
|
|
1869
|
+
const n = parseInt(token, 10);
|
|
1870
|
+
return n >= 0 && n < this.data.slides.length ? n : null;
|
|
1871
|
+
}
|
|
1872
|
+
return this._slideIndexById(decodeURIComponent(token));
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/** A canvas id as a slide index, or null. */
|
|
1876
|
+
_slideIndexById(id: string): number | null {
|
|
1877
|
+
const slides = this.data?.slides ?? [];
|
|
1878
|
+
for (let i = 0; i < slides.length; i++) {
|
|
1879
|
+
if (slides[i]?.uniqueid === id) return i;
|
|
912
1880
|
}
|
|
913
|
-
return
|
|
1881
|
+
return null;
|
|
914
1882
|
}
|
|
915
1883
|
|
|
916
1884
|
/* Resize handling
|
|
@@ -938,6 +1906,7 @@ class StoryMapBase {
|
|
|
938
1906
|
this._resize_observer = new ResizeObserver(onResize);
|
|
939
1907
|
this._resize_observer.observe(this._el.container);
|
|
940
1908
|
}
|
|
1909
|
+
this._on_resize = onResize;
|
|
941
1910
|
window.addEventListener("resize", onResize);
|
|
942
1911
|
}
|
|
943
1912
|
|
|
@@ -947,6 +1916,34 @@ class StoryMapBase {
|
|
|
947
1916
|
* map container (given the vco-map class) and moved into place between
|
|
948
1917
|
* the menubar and the story slider.
|
|
949
1918
|
*/
|
|
1919
|
+
/**
|
|
1920
|
+
* The map pane, for the code paths that only run when there is a map.
|
|
1921
|
+
* `_el.map` is null exactly when the engine is; see `_map_required()`.
|
|
1922
|
+
*/
|
|
1923
|
+
_map_el(): HTMLElement {
|
|
1924
|
+
if (!this._el.map) {
|
|
1925
|
+
throw new Error(
|
|
1926
|
+
'StoryMapJS: this operation needs a map pane, but the story has none (map_type: "none").',
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
return this._el.map;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* The map engine, for the code paths that only run when there is a map.
|
|
1934
|
+
* Throws rather than returning null: reaching one of these from a mapless
|
|
1935
|
+
* story is a bug in this class, and a loud one, rather than a layout that
|
|
1936
|
+
* quietly misbehaves. Public entry points use `this._map?.` instead.
|
|
1937
|
+
*/
|
|
1938
|
+
_map_required(): OpenLayersMap {
|
|
1939
|
+
if (!this._map) {
|
|
1940
|
+
throw new Error(
|
|
1941
|
+
'StoryMapJS: this operation needs a map, but the story has none (map_type: "none").',
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
return this._map;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
950
1947
|
_resolveMapElement(): HTMLElement | null {
|
|
951
1948
|
const element = this.options.map_options?.element;
|
|
952
1949
|
if (!element) {
|
|
@@ -976,27 +1973,15 @@ class StoryMapBase {
|
|
|
976
1973
|
|
|
977
1974
|
_onSlideChange(e: { current_slide: number }) {
|
|
978
1975
|
if (this.current_slide !== e.current_slide) {
|
|
979
|
-
|
|
980
|
-
this.current_slide
|
|
981
|
-
this._map.goTo(this.current_slide);
|
|
982
|
-
this._beginTransition(duration);
|
|
983
|
-
this.fire("change", { current_slide: this.current_slide }, this);
|
|
984
|
-
this._syncHash();
|
|
985
|
-
this._scheduleAutoplay();
|
|
986
|
-
this._updateProgress();
|
|
1976
|
+
// the slider already moved itself
|
|
1977
|
+
this._navigate(e.current_slide, { navigate: "slider" });
|
|
987
1978
|
}
|
|
988
1979
|
}
|
|
989
1980
|
|
|
990
1981
|
_onMapChange(e: { current_marker: number }) {
|
|
991
1982
|
if (this.current_slide !== e.current_marker) {
|
|
992
|
-
|
|
993
|
-
this.
|
|
994
|
-
this._storyslider.goTo(this.current_slide);
|
|
995
|
-
this._beginTransition(duration);
|
|
996
|
-
this.fire("change", { current_slide: this.current_slide }, this);
|
|
997
|
-
this._syncHash();
|
|
998
|
-
this._scheduleAutoplay();
|
|
999
|
-
this._updateProgress();
|
|
1983
|
+
// the map already moved itself
|
|
1984
|
+
this._navigate(e.current_marker, { navigate: "map" });
|
|
1000
1985
|
}
|
|
1001
1986
|
}
|
|
1002
1987
|
|
|
@@ -1007,7 +1992,7 @@ class StoryMapBase {
|
|
|
1007
1992
|
}
|
|
1008
1993
|
|
|
1009
1994
|
_onOverview(e?: unknown) {
|
|
1010
|
-
this._map
|
|
1995
|
+
this._map?.markerOverview();
|
|
1011
1996
|
}
|
|
1012
1997
|
|
|
1013
1998
|
/**
|
|
@@ -1039,18 +2024,18 @@ class StoryMapBase {
|
|
|
1039
2024
|
}
|
|
1040
2025
|
|
|
1041
2026
|
_onBackToStart(e?: unknown) {
|
|
1042
|
-
this.current_slide
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.
|
|
1046
|
-
// keep hash, progress and autoplay in sync like the other paths
|
|
1047
|
-
this._syncHash();
|
|
1048
|
-
this._scheduleAutoplay();
|
|
1049
|
-
this._updateProgress();
|
|
2027
|
+
if (this.current_slide === 0) {
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
this._navigate(0);
|
|
1050
2031
|
}
|
|
1051
2032
|
|
|
1052
|
-
_onMenuBarCollapse(e: { y
|
|
1053
|
-
|
|
2033
|
+
_onMenuBarCollapse(e: { y?: number; collapsed?: boolean }) {
|
|
2034
|
+
// `y` is the menubar position, not a map height; `collapsed` is the
|
|
2035
|
+
// authoritative signal. Older menubars omit it, so infer it from a
|
|
2036
|
+
// near-zero y for backwards compatibility.
|
|
2037
|
+
this._collapsed = e.collapsed !== undefined ? e.collapsed : e.y !== undefined && e.y < 5;
|
|
2038
|
+
this._updateDisplay(undefined, true);
|
|
1054
2039
|
}
|
|
1055
2040
|
|
|
1056
2041
|
_onMapLoaded() {
|
|
@@ -1071,21 +2056,46 @@ class StoryMapBase {
|
|
|
1071
2056
|
if (!this.options.show_distance) {
|
|
1072
2057
|
return;
|
|
1073
2058
|
}
|
|
1074
|
-
const km = this._map
|
|
2059
|
+
const km = this._map?.getRouteDistance();
|
|
1075
2060
|
this._menubar.setDistance(km);
|
|
1076
2061
|
}
|
|
1077
2062
|
|
|
2063
|
+
/**
|
|
2064
|
+
* The URL is ours to own as soon as the story is: the deep link is read
|
|
2065
|
+
* and the hash is kept in step.
|
|
2066
|
+
*
|
|
2067
|
+
* This deliberately does **not** wait for the map. The map only reports
|
|
2068
|
+
* itself loaded on OpenLayers' first `loadend`, which is the first paint of
|
|
2069
|
+
* the base tiles — so a deep link used to be applied only once
|
|
2070
|
+
* `tile.openstreetmap.org` answered, and on a slow or unreachable tile
|
|
2071
|
+
* host it was never applied at all. Nothing about a URL depends on imagery:
|
|
2072
|
+
* the data and the slider are what it needs, and both are ready before the
|
|
2073
|
+
* map is. `_applyHashSlide` → `goTo` → `_navigate` already tolerates a map
|
|
2074
|
+
* that has not reported loaded, and the map re-syncs when it does.
|
|
2075
|
+
*/
|
|
2076
|
+
_initHash() {
|
|
2077
|
+
if (this._hash_initialized || !this._loaded.storyslider) {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
this._hash_initialized = true;
|
|
2081
|
+
// a #slide-N hash or an iiif-content parameter deep-links the initial
|
|
2082
|
+
// slide (issue #146) and keeps working through the browser
|
|
2083
|
+
// back/forward buttons
|
|
2084
|
+
this._applyHashSlide(this._initial_deep_link);
|
|
2085
|
+
// from here on the URL is ours to keep in sync, so a hashchange means
|
|
2086
|
+
// the user moved and can be read live
|
|
2087
|
+
this._on_hashchange = () => this._applyHashSlide();
|
|
2088
|
+
window.addEventListener("hashchange", this._on_hashchange);
|
|
2089
|
+
this._syncHash();
|
|
2090
|
+
}
|
|
2091
|
+
|
|
1078
2092
|
_onLoaded() {
|
|
1079
|
-
|
|
2093
|
+
this._initHash();
|
|
2094
|
+
// the slider is always required; the map only if there is one, so a
|
|
2095
|
+
// mapless story still fires `loaded` and gets its hash, progress and
|
|
2096
|
+
// distance applied
|
|
2097
|
+
if (this._loaded.storyslider && (this._map_disabled || this._loaded.map)) {
|
|
1080
2098
|
this.fire("loaded", this.data);
|
|
1081
|
-
if (!this._hash_initialized) {
|
|
1082
|
-
this._hash_initialized = true;
|
|
1083
|
-
// a #slide-N hash deep-links the initial slide (issue #146)
|
|
1084
|
-
// and keeps working through the browser back/forward buttons
|
|
1085
|
-
this._applyHashSlide();
|
|
1086
|
-
window.addEventListener("hashchange", () => this._applyHashSlide());
|
|
1087
|
-
this._syncHash();
|
|
1088
|
-
}
|
|
1089
2099
|
this._updateProgress();
|
|
1090
2100
|
this._updateDistance();
|
|
1091
2101
|
}
|
|
@@ -1098,7 +2108,7 @@ export default class StoryMap extends Evented(StoryMapBase) {
|
|
|
1098
2108
|
* Derived from `import.meta.url`, which works both for the source module
|
|
1099
2109
|
* (src/main.ts) and the built ESM bundle (js/storymap.js).
|
|
1100
2110
|
*/
|
|
1101
|
-
static SCRIPT_PATH = new URL("../", import.meta.url).href;
|
|
2111
|
+
static SCRIPT_PATH = new URL(/* @vite-ignore */ "../", import.meta.url).href;
|
|
1102
2112
|
|
|
1103
2113
|
constructor(...args: ConstructorParameters<typeof StoryMapBase>) {
|
|
1104
2114
|
super(...args);
|