{"version":3,"file":"stylesheets/courseware-shelf-app.css?h=1629fdfb8473084cf341","mappings":"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CAKA,uEACI,UACJ,CC1DJ,aACI,0BAAkB,CAAlB,iBAAkB,CAWlB,qDAA8C,CAA9C,6CAA8C,CAH9C,mCAA+C,CAA/C,oCAA+C,CAN/C,aAAc,CADd,SAAU,CAGV,eAAgB,CADhB,mBAAoB,CAMpB,+FACoE,CADpE,uFACoE,CAEpE,qDACJ,CACA,gCAEI,iBAAkB,CAElB,UAAW,CACX,iFAA2E,CAA3E,yEAA2E,CAF3E,SAGJ,CAGA,0BACI,SACJ,CCzBA,gCACI,qDACA,yGACA,sEACA,gBALG,CAMH,eANG,CAOH,eAPG,CAQH,gBARG,CASH,YACA,aACA,6BACA,0CACA,eAEA,oCACI,WACA,YACA,kBAEJ,qCACI,wBACA,gBAEJ,2CACI,iBCzBR,sCACI,qDACA,qGACA,kCACA,WACA,oECkPJ,wBACA,qBACA,CC/NA,iCACA,iBACA,CACA,qCAEA,OADA,iBAEA,CC+CA,sBACA,qDAEA,UADA,WAEA,CAEA,mCACA,8BACA,uCADA,iBACA,YACA,eACA,CAEA,kCACA,6BACA,uCADA,gBACA,WACA,CCSA,mCACA,YACA,gBACA,SACA,CAEA,yCACA,YACA,gBAEA,kBACA,CAEA,iFAJA,oDASA,CALA,wCACA,+CAEA,2DAFA,mBAEA,0CACA,QACA,CAEA,uDACA,mBACA,wBADA,kBACA,sBACA,CAEA,wCACA,qBACA,CAEA,wEAEA,eACA,WACA,CACA,mCACA,qDACA,SACA,iBACA,CACA,kCACA,mCACA,YACA,mBACA,eACA,kBACA,CACA,sCAIA,wBAHA,qDAEA,aACA,uBAFA,UAGA,CAEA,yCAEA,mBADA,eACA,iBACA,CCaA,+BACA,WACA,CAEA,wCACA,qDACA,QACA,CAEA,gCACA,iBACA,CAEA,sCACA,SACA,CCpHA,sCACA,qDACA,OACA,CACA,sDACA,8BACA,uCADA,iBACA,YACA,eACA,CACA,qDACA,6BACA,uCADA,gBACA,WACA,CACA,yDAGA,0BACA,wBAHA,eACA,gBACA,uBACA,sBACA,CCzFA,oDACI,yCACA,YACA,aACA,uBACA,kBAEJ,yBACI,kEAEA,mBAEA,0CACA,oDACA,qDACA,YACA,oEACA,UACA,kBAEA,kCACI,mCAEA,sDACI,mBAKZ,+BACI,YACA,WACA,gBAGJ,qCACI,wBACA,gBACA,gBACA,cACA,gBACA,kBACA,kBAGJ,wDACI,kBACA,MACA,QACA,SACA,OAEA,UACA,WACA,YACA,UC+BJ,sBACA,WACA","sources":["webpack://@studip/core/./resources/vue/components/stock-images/Thumbnail.vue","webpack://@studip/core/./resources/vue/components/stock-images/SelectableImageCard.vue","webpack://@studip/core/./resources/vue/components/ActiveFilter.vue","webpack://@studip/core/./resources/vue/components/SearchWithFilter.vue","webpack://@studip/core/./resources/vue/components/stock-images/SelectorSearch.vue","webpack://@studip/core/./resources/vue/components/stock-images/Selector.vue","webpack://@studip/core/VueSelect/src/css/global/variables.css","webpack://@studip/core/VueSelect/src/css/global/component.css","webpack://@studip/core/VueSelect/src/css/global/animations.css","webpack://@studip/core/VueSelect/src/css/global/states.css","webpack://@studip/core/VueSelect/src/css/modules/dropdown-toggle.css","webpack://@studip/core/VueSelect/src/css/modules/open-indicator.css","webpack://@studip/core/VueSelect/src/css/modules/clear.css","webpack://@studip/core/VueSelect/src/css/modules/dropdown-menu.css","webpack://@studip/core/VueSelect/src/css/modules/dropdown-option.css","webpack://@studip/core/VueSelect/src/css/modules/selected.css","webpack://@studip/core/VueSelect/src/css/modules/search-input.css","webpack://@studip/core/VueSelect/src/css/modules/spinner.css","webpack://@studip/core/./resources/vue/components/StudipSquareButton.vue","webpack://@studip/core/./resources/vue/components/courseware/unit/CoursewareShelfDialogAddChooser.vue","webpack://@studip/core/./resources/vue/components/StudipIdentImage.vue","webpack://@studip/core/./resources/vue/components/stock-images/ActionsWidget.vue","webpack://@studip/core/./resources/vue/components/stock-images/EditDialog.vue","webpack://@studip/core/./resources/vue/components/stock-images/ImagesListItem.vue","webpack://@studip/core/./resources/vue/components/stock-images/ImagesList.vue","webpack://@studip/core/./resources/vue/components/stock-images/MetadataBox.vue","webpack://@studip/core/./resources/vue/components/stock-images/UploadBox.vue","webpack://@studip/core/./resources/vue/components/stock-images/UploadDialog.vue"],"sourcesContent":["<template>\n    <div class=\"stock-images-thumbnail\" v-if=\"url\">\n        <div :style=\"{ width }\">\n            <img :src=\"url\" :style=\"{ 'object-fit': contain ? 'contain' : 'cover' }\" role=\"presentation\" />\n        </div>\n    </div>\n</template>\n\n<script>\nexport default {\n    props: {\n        url: {\n            type: String,\n            required: true,\n        },\n        width: {\n            type: String,\n            default: '6rem',\n        },\n        contain: {\n            type: Boolean,\n            default: false\n        }\n    },\n};\n</script>\n\n<style scoped>\n.stock-images-thumbnail {\n    display: inline-flex;\n    position: relative;\n}\n.stock-images-thumbnail > div {\n    aspect-ratio: 1/1;\n    display: block;\n    overflow: hidden;\n}\n\nimg {\n    display: block;\n    height: 100%;\n    max-width: 100%;\n    width: 100%;\n}\n</style>\n","<template>\n    <div class=\"stock-images-selectable-image\" tabindex=\"0\">\n        <Thumbnail :url=\"thumbnailUrl\" contain class=\"stock-images-image-card__thumbnail\" width=\"8rem\" />\n        <div>{{ stockImage.attributes?.title ?? '' }}</div>\n    </div>\n</template>\n\n<script>\nimport Thumbnail from './Thumbnail.vue';\n\nexport default {\n    props: {\n        stockImage: {\n            type: Object,\n            required: true,\n        },\n    },\n    components: { Thumbnail },\n    computed: {\n        thumbnailUrl() {\n            return (\n                this.stockImage.attributes['download-urls'].small ??\n                this.stockImage.attributes['download-urls'].original\n            );\n        },\n    },\n};\n</script>\n\n<style scoped>\n.stock-images-selectable-image {\n    overflow: hidden;\n    position: relative;\n}\n.stock-images-selectable-image > :last-child {\n    background: var(--white);\n    overflow: hidden;\n    text-overflow: ellipsis;\n    width: 8rem;\n    min-height: 3em;\n    -webkit-line-clamp: 2;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n}\n.stock-images-image-card__thumbnail {\n    background-image: url(../images/checkered-background.png);\n}\n</style>\n","<template>\n    <span class=\"activefilter\">\n        <slot></slot>\n        <button\n            @click=\"onRemoveActiveFilter\"\n            type=\"button\"\n            :title=\"$gettextInterpolate($gettext('Filter \\'%{name}\\' entfernen'), { name }, true)\"\n        >\n            <StudipIcon class=\"text-bottom\" shape=\"decline\" role=\"presentation\" alt=\"\" />\n        </button>\n    </span>\n</template>\n\n<script>\nimport StudipIcon from './StudipIcon.vue';\nexport default {\n    props: {\n        name: {\n            type: String,\n            required: true,\n        },\n    },\n    methods: {\n        onRemoveActiveFilter() {\n            this.$emit('remove');\n        },\n    },\n};\n</script>\n\n<style scoped>\n.activefilter {\n    align-items: center;\n    background-color: var(--content-color-20);\n    border: solid thin var(--black);\n    display: flex;\n    gap: 4px;\n    justify-content: space-between;\n    margin: 3px;\n    padding: 5px;\n    padding-inline-end: 0;\n    white-space: nowrap;\n}\n\nbutton {\n    align-items: center;\n    background-color: var(--content-color-20);\n    border: none;\n    display: flex;\n    justify-content: center;\n    margin-inline: 0;\n    padding-inline-start: 4px;\n}\n</style>\n","<template>\n    <div>\n        <form @submit.prevent=\"onSearch\">\n            <slot name=\"filters\"></slot>\n\n            <input\n                :id=\"`search-bar-input-${searchId}`\"\n                class=\"search-bar-input\"\n                type=\"text\"\n                v-model=\"searchTerm\"\n                :aria-label=\"$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')\"\n            />\n\n            <button\n                v-if=\"showSearchResults\"\n                class=\"search-bar-erase\"\n                type=\"button\"\n                :title=\"$gettext('Suchformular zurücksetzen')\"\n                @click=\"onReset\"\n            >\n                <StudipIcon shape=\"decline\" :size=\"20\" />\n            </button>\n\n            <button\n                type=\"button\"\n                :title=\"$gettext('Suchfilter einstellen')\"\n                class=\"search-bar-filter\"\n                :class=\"{ active: showFilterPanel }\"\n                @click=\"onToggleFilterPanel\"\n                :aria-controls=\"`search-bar-filter-panel-${searchId}`\"\n                :aria-expanded=\"showFilterPanel ? 'true' : 'false'\"\n            >\n                <StudipIcon shape=\"filter\" :role=\"showFilterPanel ? 'info_alt' : 'clickable'\" :size=\"20\" alt=\"\" />\n            </button>\n\n            <button\n                type=\"submit\"\n                :value=\"$gettext('Suchen')\"\n                :aria-controls=\"`search-bar-input-${searchId}`\"\n                class=\"submit-search\"\n                :title=\"$gettext('Suche starten')\"\n            >\n                <StudipIcon shape=\"search\" :size=\"20\" role=\"presentation\" alt=\"\" />\n            </button>\n        </form>\n        <div :id=\"`search-bar-filter-panel-${searchId}`\" class=\"filterpanel\" ref=\"filterPanel\" v-if=\"showFilterPanel\">\n            <slot></slot>\n        </div>\n    </div>\n</template>\n\n<script>\nimport StudipIcon from './StudipIcon.vue';\n\nlet searchIndex = 0;\n\nexport default {\n    props: {\n        query: {\n            type: String,\n            required: true,\n        },\n    },\n    components: {\n        StudipIcon,\n    },\n    data: () => ({\n        searchId: searchIndex++,\n        showFilterPanel: false,\n        searchTerm: '',\n    }),\n    computed: {\n        showSearchResults() {\n            return this.query.length > 0;\n        },\n    },\n    methods: {\n        onReset() {\n            this.searchTerm = '';\n            this.onSearch();\n        },\n        onSearch() {\n            this.$emit('search', this.searchTerm);\n        },\n        onToggleFilterPanel() {\n            this.showFilterPanel = !this.showFilterPanel;\n        },\n    },\n    mounted() {\n        this.searchTerm = this.query;\n    },\n    watch: {\n        query(searchTerm) {\n            this.searchTerm = searchTerm;\n        },\n    },\n};\n</script>\n\n<style scoped>\nform {\n    align-items: stretch;\n    border: thin solid var(--content-color-40);\n    display: flex;\n    justify-content: space-between;\n    width: 100%;\n}\n\ninput {\n    border: none;\n    flex-grow: 1;\n    padding-inline-start: 0.75em;\n    width: 100%;\n}\n\ninput.search-bar-input {\n    line-height: 1.5;\n    padding-block: 0.25em;\n}\n\nbutton {\n    align-items: center;\n    background-color: var(--content-color-20);\n    border: none;\n    border-inline-start: thin solid var(--content-color-40);\n    display: flex;\n    justify-content: center;\n    width: 2.5em;\n}\n\nbutton.active {\n    background-color: var(--base-color);\n}\n\nbutton.search-bar-erase {\n    background-color: var(--white);\n    border-inline-start: none;\n}\n\n.search-bar-filter--remove {\n    margin-inline-start: 5px;\n}\n\n.filterpanel {\n    width: calc(100% + 2px);\n    background-color: var(--white);\n    border: thin solid var(--content-color-40);\n    border-top: none;\n    box-sizing: border-box;\n    padding: 10px;\n}\n\n.filterpanel::before,\n.filterpanel::after {\n    right: 50px;\n}\n</style>\n","<template>\n    <SearchWithFilter :query=\"query\" @search=\"onSearch\">\n        <template #filters>\n            <ActiveFilter\n                v-if=\"hasOrientationFilter\"\n                :name=\"orientations[orientation].text\"\n                @remove=\"onRemoveOrientationFilter()\"\n            >\n                {{ orientations[orientation].text }}\n            </ActiveFilter>\n\n            <ActiveFilter\n                v-for=\"color in selectedColors\"\n                :key=\"color.hex\"\n                :name=\"$gettextInterpolate($gettext('Farbe %{color}'), { color: color.name })\"\n                @remove=\"onRemoveColorFilter(color)\"\n            >\n                <label>\n                    <b class=\"stock-images-color-patch\" :style=\"`background-color: ${color.hex}`\"></b>\n                </label>\n            </ActiveFilter>\n        </template>\n\n        <div class=\"stock-images-search-filter-panel\">\n            <div>\n                <label>\n                    <div>{{ $gettext('Seitenausrichtung') }}</div>\n\n                    <select v-model=\"orientation\">\n                        <option v-for=\"[key, value] in Object.entries(orientations)\" :value=\"key\" :key=\"`orientation-option-${key}`\">\n                            {{ value.text }}\n                        </option>\n                    </select>\n                </label>\n            </div>\n\n            <div>\n                <div>{{ $gettext('Farbfilter') }}</div>\n\n                <studip-select\n                    multiple\n                    v-model=\"selectedColors\"\n                    :options=\"selectableColors\"\n                    label=\"name\"\n                >\n                    <template #open-indicator>\n                        <span><studip-icon shape=\"arr_1down\" :size=\"10\" /></span>\n                    </template>\n\n                    <template #option=\"option\">\n                        <span class=\"vs__option-color\" :style=\"{ 'background-color': option.hex }\"></span>\n                        <span>{{ option.name }}</span>\n                    </template>\n\n                    <template #selected-option-container>{{ ' ' }}</template>\n\n                    <template #no-options>{{ $gettext('Keine Auswahlmöglichkeiten') }}</template>\n                </studip-select>\n            </div>\n        </div>\n    </SearchWithFilter>\n</template>\n\n<script>\nimport ActiveFilter from '../ActiveFilter.vue';\nimport SearchWithFilter from '../SearchWithFilter.vue';\nimport { colors as selectableColors } from './colors.js';\nimport { orientations, similarColors } from './filters.js';\n\nexport default {\n    props: {\n        activeFilters: {\n            type: Object,\n            required: true,\n        },\n        query: {\n            type: String,\n            required: true,\n        },\n    },\n    components: {\n        ActiveFilter,\n        SearchWithFilter,\n    },\n    data: () => ({\n        orientation: 'any',\n        selectedColors: [],\n    }),\n    computed: {\n        hasOrientationFilter() {\n            return this.orientation && this.orientation !== 'any';\n        },\n        orientations: () => orientations,\n        selectableColors: () => selectableColors,\n        showSearchResults() {\n            return this.query.length > 0;\n        },\n    },\n    methods: {\n        onRemoveColorFilter(color) {\n            this.selectedColors = this.selectedColors.filter((clr) => clr.hex !== color.hex);\n            this.updateActiveFilters();\n        },\n        onRemoveOrientationFilter() {\n            this.orientation = 'any';\n        },\n        onReset() {\n            this.onSearch();\n        },\n        onSearch(searchTerm = null) {\n            this.$emit('search', searchTerm);\n        },\n        resetLocalFilters() {\n            this.selectedColors = this.activeFilters?.colors\n                ? this.selectableColors.filter(({ hex }) => this.activeFilters.colors.includes(hex))\n                : [];\n            this.orientation = this.activeFilters?.orientation ?? 'any';\n        },\n        updateActiveFilters() {\n            const activeFilters = {\n                colors: this.selectedColors.map(({ hex }) => hex),\n                orientation: this.orientation,\n            };\n            this.$emit('update-active-filters', activeFilters);\n        },\n    },\n    mounted() {\n        this.resetLocalFilters();\n    },\n    watch: {\n        activeFilters() {\n            this.resetLocalFilters();\n        },\n        orientation(newVal, oldVal) {\n            this.updateActiveFilters();\n        },\n    },\n};\n</script>\n\n<style scoped>\n.stock-images-search-filter-panel {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 2rem;\n}\n.stock-images-search-filter-panel > * {\n    flex-grow: 1;\n    flex-basis: calc((30rem - 100%) * 999);\n}\n.stock-images-search-filter-panel select {\n    width: 100%;\n    max-width: 48em;\n}\nb.stock-images-color-patch {\n    border: solid thin var(--base-color-20);\n    display: inline-block;\n    vertical-align: bottom;\n    width: 20px;\n    height: 20px;\n}\n</style>\n","<template>\n    <div>\n        <div>\n            <SelectorSearch\n                :active-filters=\"activeFilters\"\n                :query=\"query\"\n                @search=\"onSearch\"\n                @update-active-filters=\"onUpdateActiveFilters\"\n            />\n        </div>\n        <ul>\n            <li v-for=\"stockImage in filteredStockImages\" :key=\"stockImage.id\">\n                <SelectableImageCard :stock-image=\"stockImage\" @click.native=\"onSelectImage(stockImage)\" @keyup.enter.native=\"onSelectImage(stockImage)\" />\n            </li>\n        </ul>\n    </div>\n</template>\n\n<script>\nimport SelectorSearch from './SelectorSearch.vue';\nimport SelectableImageCard from './SelectableImageCard.vue';\nimport { searchFilterAndSortImages } from './filters.js';\n\nexport default {\n    props: {\n        stockImages: {\n            type: Array,\n            required: true,\n        },\n    },\n    data: () => ({\n        activeFilters: {\n            colors: [],\n            orientation: 'landscape',\n        },\n        query: '',\n    }),\n    components: { SelectorSearch, SelectableImageCard },\n    computed: {\n        filteredStockImages() {\n            return searchFilterAndSortImages(this.stockImages, this.query, this.activeFilters);\n        },\n    },\n    methods: {\n        onUpdateActiveFilters(activeFilters) {\n            this.activeFilters = activeFilters;\n        },\n        onSearch(query) {\n            this.query = query;\n        },\n        onSelectImage(stockImage) {\n            this.$emit('select', stockImage);\n        },\n    },\n};\n</script>\n\n<style scoped>\nul {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 1rem;\n    justify-content: flex-start;\n    align-items: center;\n    list-style: none;\n    padding: 1rem 0;\n}\n</style>\n",":root {\n    --vs-colors--lightest: rgba(60, 60, 60, 0.26);\n    --vs-colors--light: rgba(60, 60, 60, 0.5);\n    --vs-colors--dark: #333;\n    --vs-colors--darkest: rgba(0, 0, 0, 0.15);\n\n    /* Search Input */\n    --vs-search-input-color: inherit;\n    --vs-search-input-bg: rgb(255, 255, 255);\n    --vs-search-input-placeholder-color: inherit;\n\n    /* Font */\n    --vs-font-size: 1rem;\n    --vs-line-height: 1.4;\n\n    /* Disabled State */\n    --vs-state-disabled-bg: rgb(248, 248, 248);\n    --vs-state-disabled-color: var(--vs-colors--light);\n    --vs-state-disabled-controls-color: var(--vs-colors--light);\n    --vs-state-disabled-cursor: not-allowed;\n\n    /* Borders */\n    --vs-border-color: var(--vs-colors--lightest);\n    --vs-border-width: 1px;\n    --vs-border-style: solid;\n    --vs-border-radius: 4px;\n\n    /* Actions: house the component controls */\n    --vs-actions-padding: 4px 6px 0 3px;\n\n    /* Component Controls: Clear, Open Indicator */\n    --vs-controls-color: var(--vs-colors--light);\n    --vs-controls-size: 1;\n    --vs-controls--deselect-text-shadow: 0 1px 0 #fff;\n\n    /* Selected */\n    --vs-selected-bg: #f0f0f0;\n    --vs-selected-color: var(--vs-colors--dark);\n    --vs-selected-border-color: var(--vs-border-color);\n    --vs-selected-border-style: var(--vs-border-style);\n    --vs-selected-border-width: var(--vs-border-width);\n\n    /* Dropdown */\n    --vs-dropdown-bg: #fff;\n    --vs-dropdown-color: inherit;\n    --vs-dropdown-z-index: 1000;\n    --vs-dropdown-min-width: 160px;\n    --vs-dropdown-max-height: 350px;\n    --vs-dropdown-box-shadow: 0px 3px 6px 0px var(--vs-colors--darkest);\n\n    /* Options */\n    --vs-dropdown-option-bg: #000;\n    --vs-dropdown-option-color: var(--vs-dropdown-color);\n    --vs-dropdown-option-padding: 3px 20px;\n\n    /* Active State */\n    --vs-dropdown-option--active-bg: #5897fb;\n    --vs-dropdown-option--active-color: #fff;\n\n    /* Deselect State */\n    --vs-dropdown-option--deselect-bg: #fb5858;\n    --vs-dropdown-option--deselect-color: #fff;\n\n    /* Transitions */\n    --vs-transition-timing-function: cubic-bezier(1, -0.115, 0.975, 0.855);\n    --vs-transition-duration: 150ms;\n}\n",".v-select {\n  position: relative;\n  font-family: inherit;\n}\n\n.v-select,\n.v-select * {\n  box-sizing: border-box;\n}\n",":root {\n    --vs-transition-timing-function: cubic-bezier(1, 0.5, 0.8, 1);\n    --vs-transition-duration: 0.15s;\n}\n\n/* KeyFrames */\n@-webkit-keyframes vSelectSpinner {\n    0% {\n        transform: rotate(0deg);\n    }\n    100% {\n        transform: rotate(360deg);\n    }\n}\n\n@keyframes vSelectSpinner {\n    0% {\n        transform: rotate(0deg);\n    }\n    100% {\n        transform: rotate(360deg);\n    }\n}\n\n/* Dropdown Default Transition */\n.vs__fade-enter-active,\n.vs__fade-leave-active {\n    pointer-events: none;\n    transition: opacity var(--vs-transition-duration)\n        var(--vs-transition-timing-function);\n}\n.vs__fade-enter,\n.vs__fade-leave-to {\n    opacity: 0;\n}\n","/** Component States */\n\n/*\n * Disabled\n *\n * When the component is disabled, all interaction\n * should be prevented. Here we modify the bg color,\n * and change the cursor displayed on the interactive\n * components.\n */\n\n:root {\n    --vs-disabled-bg: var(--vs-state-disabled-bg);\n    --vs-disabled-color: var(--vs-state-disabled-color);\n    --vs-disabled-cursor: var(--vs-state-disabled-cursor);\n}\n\n.vs--disabled {\n    .vs__dropdown-toggle,\n    .vs__clear,\n    .vs__search,\n    .vs__selected,\n    .vs__open-indicator {\n        cursor: var(--vs-disabled-cursor);\n        background-color: var(--vs-disabled-bg);\n    }\n}\n\n/*\n *  RTL - Right to Left Support\n *\n *  Because we're using a flexbox layout, the `dir=\"rtl\"`\n *  HTML attribute does most of the work for us by\n *  rearranging the child elements visually.\n */\n\n.v-select[dir='rtl'] {\n    .vs__actions {\n        padding: 0 3px 0 6px;\n    }\n\n    .vs__clear {\n        margin-left: 6px;\n        margin-right: 0;\n    }\n\n    .vs__deselect {\n        margin-left: 0;\n        margin-right: 2px;\n    }\n\n    .vs__dropdown-menu {\n        text-align: right;\n    }\n}\n","/**\n    Dropdown Toggle\n\n    The dropdown toggle is the primary wrapper of the component. It\n    has two direct descendants: .vs__selected-options, and .vs__actions.\n\n    .vs__selected-options holds the .vs__selected's as well as the\n    main search input.\n\n    .vs__actions holds the clear button and dropdown toggle.\n */\n\n.vs__dropdown-toggle {\n    appearance: none;\n    display: flex;\n    padding: 0 0 4px 0;\n    background: var(--vs-search-input-bg);\n    border: var(--vs-border-width) var(--vs-border-style) var(--vs-border-color);\n    border-radius: var(--vs-border-radius);\n    white-space: normal;\n}\n\n.vs__selected-options {\n    display: flex;\n    flex-basis: 100%;\n    flex-grow: 1;\n    flex-wrap: wrap;\n    padding: 0 2px;\n    position: relative;\n}\n\n.vs__actions {\n    display: flex;\n    align-items: center;\n    padding: var(--vs-actions-padding);\n}\n\n/* Dropdown Toggle States */\n.vs--searchable .vs__dropdown-toggle {\n    cursor: text;\n}\n.vs--unsearchable .vs__dropdown-toggle {\n    cursor: pointer;\n}\n.vs--open .vs__dropdown-toggle {\n    border-bottom-color: transparent;\n    border-bottom-left-radius: 0;\n    border-bottom-right-radius: 0;\n}\n","/* Open Indicator */\n\n/*\n  The open indicator appears as a down facing\n  caret on the right side of the select.\n */\n\n.vs__open-indicator {\n    fill: var(--vs-controls-color);\n    transform: scale(var(--vs-controls-size));\n    transition: transform var(--vs-transition-duration)\n        var(--vs-transition-timing-function);\n    transition-timing-function: var(--vs-transition-timing-function);\n}\n\n/* Open State */\n\n.vs--open .vs__open-indicator {\n    transform: rotate(180deg) scale(var(--vs-controls-size));\n}\n\n/* Loading State */\n\n.vs--loading .vs__open-indicator {\n    opacity: 0;\n}\n","/* Clear Button */\n\n.vs__clear {\n    fill: var(--vs-controls-color);\n    padding: 0;\n    border: 0;\n    background-color: transparent;\n    cursor: pointer;\n    margin-right: 8px;\n}\n","/* Dropdown Menu */\n\n.vs__dropdown-menu {\n    display: block;\n    box-sizing: border-box;\n    position: absolute;\n    /* calc to ensure the left and right borders of the dropdown appear flush with the toggle. */\n    top: calc(100% - var(--vs-border-width));\n    left: 0;\n    z-index: var(--vs-dropdown-z-index);\n    padding: 5px 0;\n    margin: 0;\n    width: 100%;\n    max-height: var(--vs-dropdown-max-height);\n    min-width: var(--vs-dropdown-min-width);\n    overflow-y: auto;\n    box-shadow: var(--vs-dropdown-box-shadow);\n    border: var(--vs-border-width) var(--vs-border-style) var(--vs-border-color);\n    border-top-style: none;\n    border-radius: 0 0 var(--vs-border-radius) var(--vs-border-radius);\n    text-align: left;\n    list-style: none;\n    background: var(--vs-dropdown-bg);\n    color: var(--vs-dropdown-color);\n}\n\n.vs__no-options {\n    text-align: center;\n}\n","/* List Items */\n.vs__dropdown-option {\n    line-height: 1.42857143; /* Normalize line height */\n    display: block;\n    padding: var(--vs-dropdown-option-padding);\n    clear: both;\n    color: var(--vs-dropdown-option-color); /* Overrides most CSS frameworks */\n    white-space: nowrap;\n    cursor: pointer;\n}\n\n.vs__dropdown-option--highlight {\n    background: var(--vs-dropdown-option--active-bg);\n    color: var(--vs-dropdown-option--active-color);\n}\n\n.vs__dropdown-option--deselect {\n    background: var(--vs-dropdown-option--deselect-bg);\n    color: var(--vs-dropdown-option--deselect-color);\n}\n\n.vs__dropdown-option--disabled {\n    background: var(--vs-state-disabled-bg);\n    color: var(--vs-state-disabled-color);\n    cursor: var(--vs-state-disabled-cursor);\n}\n","/* Selected Tags */\n.vs__selected {\n    display: flex;\n    align-items: center;\n    background-color: var(--vs-selected-bg);\n    border: var(--vs-selected-border-width) var(--vs-selected-border-style)\n        var(--vs-selected-border-color);\n    border-radius: var(--vs-border-radius);\n    color: var(--vs-selected-color);\n    line-height: var(--vs-line-height);\n    margin: 4px 2px 0px 2px;\n    padding: 0 0.25em;\n    z-index: 0;\n}\n\n.vs__deselect {\n    display: inline-flex;\n    appearance: none;\n    margin-left: 4px;\n    padding: 0;\n    border: 0;\n    cursor: pointer;\n    background: none;\n    fill: var(--vs-controls-color);\n    text-shadow: var(--vs-controls--deselect-text-shadow);\n}\n\n/* States */\n\n.vs--single {\n    .vs__selected {\n        background-color: transparent;\n        border-color: transparent;\n    }\n    &.vs--open .vs__selected,\n    &.vs--loading .vs__selected {\n        position: absolute;\n        opacity: 0.4;\n    }\n    &.vs--searching .vs__selected {\n        display: none;\n    }\n}\n","/* Search Input */\n\n/**\n * Super weird bug... If this declaration is grouped\n * below, the cancel button will still appear in chrome.\n * If it's up here on it's own, it'll hide it.\n */\n.vs__search::-webkit-search-cancel-button {\n    display: none;\n}\n\n.vs__search::-webkit-search-decoration,\n.vs__search::-webkit-search-results-button,\n.vs__search::-webkit-search-results-decoration,\n.vs__search::-ms-clear {\n    display: none;\n}\n\n.vs__search,\n.vs__search:focus {\n    color: var(--vs-search-input-color);\n    appearance: none;\n    line-height: var(--vs-line-height);\n    font-size: var(--vs-font-size);\n    border: 1px solid transparent;\n    border-left: none;\n    outline: none;\n    margin: 4px 0 0 0;\n    padding: 0 7px;\n    background: none;\n    box-shadow: none;\n    width: 0;\n    max-width: 100%;\n    flex-grow: 1;\n    z-index: 1;\n}\n\n.vs__search::placeholder {\n    color: var(--vs-search-input-placeholder-color);\n}\n\n/**\n    States\n */\n\n/* Unsearchable */\n.vs--unsearchable {\n    .vs__search {\n        opacity: 1;\n    }\n    &:not(.vs--disabled) .vs__search {\n        cursor: pointer;\n    }\n}\n\n/* Single, when searching but not loading or open */\n.vs--single.vs--searching:not(.vs--open):not(.vs--loading) {\n    .vs__search {\n        opacity: 0.2;\n    }\n}\n","/* Loading Spinner */\n.vs__spinner {\n    align-self: center;\n    opacity: 0;\n    font-size: 5px;\n    text-indent: -9999em;\n    overflow: hidden;\n    border-top: 0.9em solid rgba(100, 100, 100, 0.1);\n    border-right: 0.9em solid rgba(100, 100, 100, 0.1);\n    border-bottom: 0.9em solid rgba(100, 100, 100, 0.1);\n    border-left: 0.9em solid rgba(60, 60, 60, 0.45);\n    transform: translateZ(0)\n        scale(var(--vs-controls--spinner-size, var(--vs-controls-size)));\n    animation: vSelectSpinner 1.1s infinite linear;\n    transition: opacity 0.1s;\n}\n.vs__spinner,\n.vs__spinner:after {\n    border-radius: 50%;\n    width: 5em;\n    height: 5em;\n    transform: scale(var(--vs-controls--spinner-size, var(--vs-controls-size)));\n}\n\n/* Loading Spinner States */\n.vs--loading .vs__spinner {\n    opacity: 1;\n}\n","\n$size: 130px;\n.square-button {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-start;\n    max-height: $size;\n    max-width: $size;\n    min-width: $size;\n    min-height: $size;\n    margin: 10px;\n    padding: 10px;\n    background-color: transparent;\n    border: solid thin var(--content-color-40);\n    cursor: pointer;\n\n    img {\n        width: 100%;\n        height: 50px;\n        margin-bottom: 8px;\n    }\n    span {\n        color: var(--base-color);\n        min-width: 110px;\n    }\n    &:hover span {\n        color: var(--red);\n    }\n}\n","\n.square-button-panel {\n    display: flex;\n    flex-direction: row;\n    flex-wrap: wrap;\n    width: 100%;\n    justify-content: center;\n}\n","<template>\n    <canvas v-show=\"showCanvas\" ref=\"canvas\"></canvas>\n</template>\n\n<script>\nexport default {\n    name: 'studip-ident-image',\n    props: {\n        value: {\n            type: String,\n        },\n        showCanvas: {\n            type: Boolean,\n            default: false,\n        },\n        baseColor: {\n            type: String, // hex color\n        },\n        pattern: {\n            type: String,\n            required: true,\n        },\n        width: {\n            type: Number,\n            default: 1080,\n        },\n        height: {\n            type: Number,\n            default: 720,\n        },\n        shapesMin: {\n            type: Number,\n            default: 5,\n        },\n        shapesMax: {\n            type: Number,\n            default: 8,\n        },\n    },\n    data() {\n        return {\n            random: null,\n            ellipse: null,\n        };\n    },\n    methods: {\n        randint(min, max) {\n            return Math.floor(this.random() * (max - min) + min);\n        },\n        renderIdentimage() {\n            let canvas = this.$refs.canvas;\n            canvas.width = this.width;\n            canvas.height = this.height;\n\n            const minSize = Math.min(this.width, this.height) * 0.2;\n            const ctx = canvas.getContext('2d');\n            const backgroundHSL = this.hexToHSL(this.baseColor);\n            const numShape = this.randint(this.shapesMin, this.shapesMax);\n            const shapeSizes = [];\n\n            ctx.fillStyle = this.hexToRgbA(this.baseColor, 0.8);\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n            const curveStart = this.randint(10, 70)/100 * this.height;\n            const curveEnd = this.randint(10, 70)/100 * this.height;\n            ctx.strokeStyle = `rgba(255, 255, 255, ${this.randint(50, 70) / 100})`;\n            const curvedistance = this.randint(20, 40);\n            const xFactor = this.randint(10, 45) / 100;\n            const yFactor = this.randint(10, 45) / 100;\n            for (let c = 0; c < numShape * 2; c++) {\n                ctx.beginPath();\n                ctx.moveTo(0, curveStart + curvedistance * c);\n                ctx.bezierCurveTo(this.width * xFactor, this.height * yFactor, this.width * (xFactor + 0.5), this.height * (yFactor + 0.5), this.width, curveEnd + curvedistance * c);\n                ctx.stroke();\n            }\n\n            for (let i = 0; i < numShape; i++) {\n                shapeSizes.push(this.randint(minSize*0.2, minSize*2) + minSize);\n            }\n\n            shapeSizes.sort((a, b) => {\n                return a < b ? 1 : a > b ? -1 : 0;\n            });\n\n            shapeSizes.forEach((shapeSizes, index) => {\n                const radius = shapeSizes / 2;\n                const [x, y] = this.createPointInEllipse(ctx);\n                const x_center = x * (this.width + radius / 2) - radius / 4;\n                const y_center = y * (this.height + radius / 2) - radius / 4;\n\n                ctx.fillStyle = `rgba(255, 255, 255, ${this.randint(10, 80) / 100})`;\n\n                ctx.beginPath();\n\n                if (index % 2 === 0) {\n                    ctx.arc(x_center, y_center, radius, 0, 2 * Math.PI);\n                } else {\n                    const size = radius;\n                    ctx.moveTo(x_center + size * Math.cos(0), y_center + size * Math.sin(0));\n\n                    for (let side = 0; side < 7; side++) {\n                        ctx.lineTo(\n                            x_center + size * Math.cos((side * 2 * Math.PI) / 6),\n                            y_center + size * Math.sin((side * 2 * Math.PI) / 6)\n                        );\n                    }\n                }\n\n                ctx.fill();\n            });\n\n            this.$emit('input', canvas.toDataURL());\n        },\n        createPointInEllipse(ctx) {\n            const x = this.random();\n            const y = this.random();\n\n            if (ctx.isPointInPath(this.ellipse, x, y)) {\n                return [x, y];\n            }\n\n            return this.createPointInEllipse(...arguments);\n        },\n\n        cyrb128(value) {\n            let h1 = 1779033703,\n                h2 = 3144134277,\n                h3 = 1013904242,\n                h4 = 2773480762;\n\n            for (let i = 0, k; i < value.length; i++) {\n                k = value.charCodeAt(i);\n                h1 = h2 ^ Math.imul(h1 ^ k, 597399067);\n                h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);\n                h3 = h4 ^ Math.imul(h3 ^ k, 951274213);\n                h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);\n            }\n\n            h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);\n            h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);\n            h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);\n            h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);\n\n            return [(h1 ^ h2 ^ h3 ^ h4) >>> 0, (h2 ^ h1) >>> 0, (h3 ^ h1) >>> 0, (h4 ^ h1) >>> 0];\n        },\n        sfc32(a, b, c, d) {\n            return function () {\n                a >>>= 0;\n                b >>>= 0;\n                c >>>= 0;\n                d >>>= 0;\n                var t = (a + b) | 0;\n                a = b ^ (b >>> 9);\n                b = (c + (c << 3)) | 0;\n                c = (c << 21) | (c >>> 11);\n                d = (d + 1) | 0;\n                t = (t + d) | 0;\n                c = (c + t) | 0;\n\n                return (t >>> 0) / 4294967296;\n            };\n        },\n\n        hexToRGB(color) {\n            color = color.slice(1); // remove #\n            let val = parseInt(color, 16);\n            let r = val >> 16;\n            let g = (val >> 8) & 0x00ff;\n            let b = val & 0x0000ff;\n\n            if (g > 255) {\n                g = 255;\n            } else if (g < 0) {\n                g = 0;\n            }\n            if (b > 255) {\n                b = 255;\n            } else if (b < 0) {\n                b = 0;\n            }\n\n            return { r: r, g: g, b: b };\n        },\n        RGBToHSL(r, g, b) {\n            r /= 255;\n            g /= 255;\n            b /= 255;\n\n            let cmin = Math.min(r, g, b),\n                cmax = Math.max(r, g, b),\n                delta = cmax - cmin,\n                h = 0,\n                s = 0,\n                l = 0;\n            if (delta == 0) h = 0;\n            // Red is max\n            else if (cmax == r) h = ((g - b) / delta) % 6;\n            // Green is max\n            else if (cmax == g) h = (b - r) / delta + 2;\n            // Blue is max\n            else h = (r - g) / delta + 4;\n\n            h = Math.round(h * 60);\n\n            if (h < 0) h += 360;\n            l = (cmax + cmin) / 2;\n\n            s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));\n\n            s = +(s * 100).toFixed(1);\n            l = +(l * 100).toFixed(1);\n\n            return { h: h, s: s, l: l };\n            // return 'hsl(' + h + ',' + s + '%,' + l + '%)';\n        },\n        hexToHSL(color) {\n            const RGB = this.hexToRGB(color);\n            return this.RGBToHSL(RGB.r, RGB.g, RGB.b);\n        },\n        hexToRgbA(hex, a){\n            const RGB = this.hexToRGB(hex);\n\n            return 'rgba(' + RGB.r + ',' + RGB.g + ',' + RGB.b + ',' + a +')';\n        },\n        init() {\n            const seed = this.cyrb128(this.pattern);\n            this.random = this.sfc32(...seed);\n            this.ellipse = new Path2D();\n            this.ellipse.ellipse(0.5, 0.5, 0.5, 0.5, 0, 0, Math.PI * 2);\n            this.renderIdentimage();\n        }\n    },\n    mounted() {\n        this.init();\n    },\n    watch: {\n        baseColor() {\n            this.init();\n        },\n    },\n};\n</script>\n<style scoped>\n  canvas {\n    background-color: #fff;\n  }\n</style>\n","<template>\n    <SidebarWidget :title=\"$gettext('Aktionen')\">\n        <template #content>\n            <ul class=\"widget-list widget-links\">\n                <li>\n                    <studip-icon shape=\"upload\" class=\"widget-action-icon\" />\n                    <button @click=\"onUploadClick\">{{ $gettext('Bild hinzufügen') }}</button>\n                </li>\n            </ul>\n        </template>\n    </SidebarWidget>\n</template>\n<script>\nimport SidebarWidget from '../SidebarWidget.vue';\n\nexport default {\n    components: {\n        SidebarWidget,\n    },\n    methods: {\n        onUploadClick() {\n            this.$emit('initiateUpload');\n        },\n    },\n};\n</script>\n<style scoped>\n.widget-list li {\n    position: relative;\n}\n.widget-action-icon {\n    position: absolute;\n    left: 0;\n}\n</style>\n","<template>\n    <studip-dialog\n        v-if=\"stockImage\"\n        height=\"720\"\n        width=\"960\"\n        :title=\"$gettext('Bild bearbeiten')\"\n        @close=\"onCancel\"\n        closeClass=\"cancel\"\n        :closeText=\"$gettext('Schließen')\"\n    >\n        <template v-slot:dialogContent>\n            <form id=\"stock-images-edit-form\" class=\"default\" @submit.prevent=\"onSubmit\">\n                <div>\n                    <ThumbnailCard\n                        :chdate=\"new Date(stockImage.attributes.chdate)\"\n                        :height=\"stockImage.attributes.height\"\n                        :mime-type=\"stockImage.attributes['mime-type']\"\n                        :mkdate=\"new Date(stockImage.attributes.mkdate)\"\n                        :size=\"stockImage.attributes.size\"\n                        :url=\"stockImage.attributes['download-urls'].small\"\n                        :width=\"stockImage.attributes.width\"\n                    />\n                </div>\n\n                <div>\n                    <AttributesFieldset :metadata=\"metadata\" :suggested-tags=\"suggestedTags\" @change=\"onChange\" />\n                </div>\n            </form>\n        </template>\n\n        <template #dialogButtons>\n            <button form=\"stock-images-edit-form\" type=\"submit\" class=\"button accept\">\n                {{ $gettext('Speichern') }}\n            </button>\n        </template>\n    </studip-dialog>\n</template>\n<script>\nimport ThumbnailCard from './ThumbnailCard.vue';\nimport AttributesFieldset from './AttributesFieldset.vue';\n\nexport default {\n    props: ['stockImage', 'suggestedTags'],\n    components: { AttributesFieldset, ThumbnailCard },\n    data: () => ({\n        metadata: {},\n    }),\n    methods: {\n        onCancel() {\n            this.$emit('cancel');\n        },\n        onChange(metadata) {\n            this.metadata = metadata;\n        },\n        onSubmit() {\n            this.$emit('confirm', { ...this.metadata });\n        },\n        resetLocalCopy() {\n            const {\n                title = '',\n                description = '',\n                author = '',\n                license = '',\n                tags = [],\n            } = this.stockImage?.attributes ?? [];\n            this.metadata = { title, description, author, license, tags };\n        },\n    },\n    mounted() {\n        this.resetLocalCopy();\n    },\n    watch: {\n        stockImage() {\n            this.resetLocalCopy();\n        },\n    },\n};\n</script>\n\n<style scoped>\nform {\n    display: flex;\n    height: 100%;\n    gap: 1.5em;\n}\n\nform > *:first-child {\n    flex-basis: 200px;\n    flex-grow: 0;\n    overflow: hidden;\n}\n\nform > *:last-child {\n    flex-basis: 30em;\n    flex-grow: 1;\n}\n</style>\n","<template>\n    <tr @click=\"onSelect\">\n        <td>\n            <label>\n                <input type=\"checkbox\" :checked=\"isChecked\" @change=\"onCheckboxChange\" />\n                <span class=\"sr-only\">{{\n                    $gettextInterpolate($gettext('%{context} auswählen'), { context: stockImage.attributes.title })\n                }}</span>\n            </label>\n        </td>\n        <td>\n            <div>\n                <Thumbnail\n                    v-if=\"thumbnailUrl\"\n                    :url=\"thumbnailUrl\"\n                    width=\"6rem\"\n                    style=\"background: var(--light-gray-color-40)\"\n                    contain\n                />\n                <div>\n                    <div>{{ stockImage.attributes.title }}</div>\n                    <div>\n                        <span class=\"stock-image-author\">{{ stockImage.attributes.author }}</span>\n                        <span class=\"stock-image-tags\">\n                            <button\n                                type=\"button\"\n                                class=\"stock-image-tag\"\n                                v-for=\"tag in stockImage.attributes.tags\"\n                                :key=\"tag\"\n                                @click=\"onTagClick(tag)\"\n                            >\n                                {{ tag }}\n                            </button>\n                        </span>\n                    </div>\n\n                    <ul class=\"stock-image-palette\" :title=\"$gettext('Bildfarben')\" role=\"presentation\">\n                        <li\n                            v-for=\"(color, index) in palette\"\n                            :key=\"index\"\n                            :style=\"`background-color: rgb(${color[0]} ${color[1]} ${color[2]});`\"\n                            :alt=\"color.join(',')\"\n                        ></li>\n                    </ul>\n                </div>\n            </div>\n        </td>\n        <td>\n            <studip-icon shape=\"file-pic\" alt=\"\" />\n            {{ imageFormat(stockImage) }}\n        </td>\n        <td><studip-file-size :size=\"stockImage.attributes.size\" /></td>\n        <td>{{ stockImage.attributes.width }} × {{ stockImage.attributes.height }}</td>\n    </tr>\n</template>\n<script>\nimport Thumbnail from './Thumbnail.vue';\nimport { getFormat } from './format.js';\n\nexport default {\n    props: {\n        stockImage: {\n            type: Object,\n            required: true,\n        },\n        isChecked: {\n            type: Boolean,\n            default: false,\n        },\n    },\n    components: {\n        Thumbnail,\n    },\n    computed: {\n        palette() {\n            return this.stockImage.attributes.palette ?? [];\n        },\n        thumbnailUrl() {\n            return (\n                this.stockImage.attributes['download-urls'].small ??\n                this.stockImage.attributes['download-urls'].original\n            );\n        },\n    },\n    methods: {\n        imageFormat(image) {\n            return getFormat(image.attributes['mime-type']);\n        },\n        onCheckboxChange() {\n            this.$emit('checked');\n        },\n        onSelect({ target }) {\n            if (!['INPUT', 'LABEL', 'BUTTON'].includes(target.tagName)) {\n                this.$emit('select');\n            }\n        },\n        onTagClick(tag) {\n            this.$emit('search', tag);\n        },\n    },\n};\n</script>\n\n<style scoped>\ntr > td:nth-child(1) {\n    height: 100%;\n    min-height: 100%;\n    padding: 0;\n}\n\ntr > td:nth-child(1) > label {\n    height: 100%;\n    min-height: 100%;\n    display: flex;\n    padding-inline: 1em;\n}\n\ntr > td:nth-child(2) > div {\n    align-items: center;\n    display: flex;\n    flex-direction: row;\n    gap: 1rem;\n}\n\ntr > td:nth-child(2) > div div:last-child {\n    flex: 1;\n    margin-inline-end: 1rem;\n}\n\ntr > td:nth-child(3) img {\n    vertical-align: middle;\n}\n\n.stock-image-author,\n.stock-image-tags {\n    font-size: 0.8em;\n    opacity: 0.75;\n}\n.stock-image-tags {\n    display: flex;\n    gap: 0.5em;\n    margin-block: 0.5em;\n}\n.stock-image-tag {\n    background-color: var(--base-color);\n    border: none;\n    color: var(--white);\n    cursor: pointer;\n    padding: 0.25em 0.5em;\n}\n.stock-image-palette {\n    display: flex;\n    width: 100%;\n    height: 0.25em;\n    padding-inline-start: 0;\n}\n\n.stock-image-palette li {\n    display: inline;\n    flex: 1;\n}\n</style>\n","<template>\n    <div>\n        <table class=\"default\">\n            <caption>\n                <div class=\"caption-container\">\n                    <div>\n                        <studip-icon shape=\"folder-public-full\" :size=\"30\" alt=\"\" />\n                        <span>{{ caption }}</span>\n                    </div>\n                </div>\n            </caption>\n            <thead>\n                <tr>\n                    <th>\n                        <label>\n                            <input type=\"checkbox\" ref=\"checkAll\" :checked=\"allChecked\" @change=\"onCheckedAllChange\" />\n                            <span class=\"sr-only\">{{ $gettext('Alle Bilder auswählen') }}</span>\n                        </label>\n                    </th>\n                    <th>{{ $gettext('Name') }}</th>\n                    <th>{{ $gettext('Format') }}</th>\n                    <th>{{ $gettext('Größe') }}</th>\n                    <th>{{ $gettext('Abmessungen') }}</th>\n                </tr>\n            </thead>\n            <tbody v-if=\"paged.length\">\n                <ImagesListItem\n                    :stock-image=\"stockImage\"\n                    v-for=\"stockImage in paged\"\n                    :key=\"stockImage.id\"\n                    :is-checked=\"checkedImages.includes(stockImage.id)\"\n                    @checked=\"$emit('checked', stockImage)\"\n                    @search=\"(query) => $emit('search', query)\"\n                    @select=\"$emit('select', stockImage)\"\n                />\n            </tbody>\n            <tbody v-else>\n                <tr>\n                    <td colspan=\"5\">\n                        <span v-if=\"query.length\">{{\n                            $gettext('Zu diesem Suchbegriff konnten keine Bilder gefunden werden.')\n                        }}</span>\n                        <span v-else>{{ $gettext('Es konnten keine Bilder gefunden werden.') }}</span>\n                    </td>\n                </tr>\n            </tbody>\n            <tfoot v-if=\"paged.length\">\n                <tr>\n                    <td colspan=\"5\">\n                        <button\n                            type=\"button\"\n                            class=\"button\"\n                            @click=\"showConfirmDelete = true\"\n                            :disabled=\"!checkedImages.length\"\n                        >\n                            {{ $gettext('Löschen') }}\n                        </button>\n                    </td>\n                </tr>\n            </tfoot>\n        </table>\n\n        <studip-dialog\n            v-if=\"showConfirmDelete\"\n            :title=\"\n                this.$ngettext(\n                    'Ausgewähltes Bild unwideruflich löschen?',\n                    'Ausgewählte Bilder unwideruflich löschen?',\n                    checkedImages.length\n                )\n            \"\n            :question=\"\n                $ngettext(\n                    'Möchten Sie das ausgewählte Bild wirklich löschen?',\n                    'Möchten Sie die ausgewählten Bilder wirklich löschen?',\n                    checkedImages.length\n                )\n            \"\n            height=\"200\"\n            width=\"450\"\n            @confirm=\"onDelete\"\n            @close=\"showConfirmDelete = false\"\n        ></studip-dialog>\n    </div>\n</template>\n\n<script>\nimport ImagesListItem from './ImagesListItem.vue';\nimport { mapActions } from 'vuex';\n\nexport default {\n    props: {\n        checkedImages: {\n            type: Array,\n            required: true,\n        },\n        perPage: {\n            type: Number,\n            default: 10,\n        },\n        page: {\n            type: Number,\n            default: 1,\n        },\n        query: {\n            type: String,\n            default: '',\n        },\n        stockImages: {\n            type: Array,\n            required: true,\n        },\n    },\n    components: {\n        ImagesListItem,\n    },\n    data: () => ({\n        latestMkdate: null,\n        showConfirmDelete: false,\n    }),\n    computed: {\n        allChecked() {\n            return this.paged.length && this.paged.every(({ id }) => this.checkedImages.includes(id));\n        },\n        caption() {\n            const n = this.stockImages.length;\n            return this.$gettextInterpolate(this.$ngettext('%{ n } Bild gefunden', '%{ n } Bilder gefunden', n), { n });\n        },\n        paged() {\n            return this.stockImages.slice((this.page - 1) * this.perPage, this.page * this.perPage);\n        },\n        totalItems() {\n            return this.stockImages.length;\n        },\n    },\n    methods: {\n        ...mapActions({ deleteStockImage: 'studip/stockImages/delete' }),\n        checkAll() {\n            this.paged\n                .filter(({ id }) => !this.checkedImages.includes(id))\n                .forEach((image) => this.$emit('checked', image));\n        },\n        checkNone() {\n            this.paged\n                .filter(({ id }) => this.checkedImages.includes(id))\n                .forEach((image) => this.$emit('checked', image));\n        },\n        onCheckedAllChange() {\n            this.allChecked ? this.checkNone() : this.checkAll();\n        },\n        onDelete() {\n            const checkedImages = [...this.checkedImages];\n            this.showConfirmDelete = false;\n            this.checkNone();\n            Promise.allSettled(checkedImages.map((id) => this.deleteStockImage(id))).then(() => {\n                this.revalidatePage();\n            });\n        },\n        revalidatePage() {\n            if (this.totalItems < this.page * this.perPage) {\n                this.$emit('open-page', Math.ceil(this.totalItems / this.perPage));\n            }\n        },\n    },\n    watch: {\n        checkedImages({ length }) {\n            this.$refs.checkAll.indeterminate = 0 < length && length < this.paged.length;\n        },\n    },\n};\n</script>\n\n<style scoped>\ntable.default {\n    height: 100%;\n}\n\n.caption-container div {\n    display: flex;\n    gap: 0.5em;\n}\n\nthead th input {\n    margin-inline: 1em;\n}\n\nthead th:first-child {\n    width: 3em;\n}\n</style>\n","<template>\n    <div class=\"upload-metadata-box\">\n        <div>\n            <ThumbnailCard\n                v-if=\"fileURL\"\n                :height=\"height ?? 0\"\n                :mime-type=\"file.type\"\n                :size=\"file.size\"\n                :url=\"fileURL\"\n                :width=\"width ?? 0\"\n            />\n        </div>\n        <div>\n            <AttributesFieldset :metadata=\"metadata\" :suggested-tags=\"suggestedTags\" @change=\"onChange\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport ThumbnailCard from './ThumbnailCard.vue';\nimport AttributesFieldset from './AttributesFieldset.vue';\nimport { getFormat } from './format.js';\n\nexport default {\n    props: ['file', 'metadata', 'suggestedTags'],\n\n    components: { AttributesFieldset, ThumbnailCard },\n\n    data: () => ({\n        fileURL: null,\n        height: null,\n        image: null,\n        width: null,\n    }),\n\n    computed: {\n        tags: {\n            get() {\n                return this.metadata.tags;\n            },\n            set(tags) {\n                this.$set(this.metadata, 'tags', tags);\n            },\n        },\n    },\n\n    methods: {\n        onChange(metadata) {\n            this.$emit('change', metadata);\n        },\n    },\n\n    mounted() {\n        this.fileURL = URL.createObjectURL(this.file);\n        this.image = new Image();\n        this.image.onload = ({ target }) => {\n            this.height = target.height;\n            this.width = target.width;\n        };\n        this.image.src = this.fileURL;\n        this.$set(this.metadata, 'title', this.file.name);\n    },\n\n    beforeDestroy() {\n        if (this.fileURL) {\n            URL.revokeObjectURL(this.fileURL);\n        }\n    },\n};\n</script>\n\n<style scoped>\n.upload-metadata-box {\n    display: flex;\n    gap: 1em;\n}\n.upload-metadata-box > div:first-child {\n    flex-basis: 200px;\n    flex-grow: 0;\n    overflow: hidden;\n}\n.upload-metadata-box > div:last-child {\n    flex-basis: 30em;\n    flex-grow: 1;\n}\n.upload-metadata-box div:first-child ul {\n    font-size: 0.9em;\n    line-height: 1.5;\n    margin-block-start: 1em;\n    padding-inline-start: 0;\n}\n</style>\n","\n#stock-images-upload-box-drag-area {\n    background-color: var(--content-color-20);\n    height: 100%;\n    margin: -15px;\n    padding: 18px 15px 10px;\n    text-align: center;\n}\n.holder {\n    align-items: center;\n    border-color: var(--content-color-60);\n    border-radius: 0.5em;\n    border-style: dashed;\n    border-width: 1px;\n    box-sizing: border-box;\n    display: flex;\n    height: 100%;\n    justify-content: center;\n    padding: 0;\n    position: relative;\n\n    &.dragging {\n        background-color: var(--base-color);\n\n        .icon-upload + strong {\n            color: var(--white);\n        }\n    }\n}\n\n.box-centered {\n    height: auto;\n    width: 100%;\n    max-height: 100%;\n}\n\n.icon-upload + strong {\n    color: var(--base-color);\n    font-size: 1.5em;\n    line-height: 1.2;\n    display: block;\n    font-weight: 500;\n    text-align: center;\n    margin: 0 2em 14px;\n}\n\n.upload-button-holder input[type='file'] {\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n\n    opacity: 0;\n    width: 100%;\n    height: 100%;\n    padding: 0;\n}\n","<template>\n    <studip-dialog\n        v-if=\"show\"\n        height=\"720\"\n        width=\"960\"\n        :title=\"$gettext('Bild hinzufügen')\"\n        @close=\"onCancel\"\n        closeClass=\"cancel\"\n        :closeText=\"$gettext('Abbrechen')\"\n    >\n        <template #dialogContent>\n            <form id=\"stock-images-upload-form\" class=\"default\" @submit.prevent=\"onSubmit\">\n                <UploadBox v-if=\"state === STATES.IDLE\" @upload=\"onUpload\" />\n                <MetadataBox\n                    v-if=\"state === STATES.UPLOADED\"\n                    :file=\"file\"\n                    :metadata=\"metadata\"\n                    :suggested-tags=\"suggestedTags\"\n                    @change=\"onChangeMetadata\"\n                />\n            </form>\n        </template>\n\n        <template #dialogButtons>\n            <button\n                form=\"stock-images-upload-form\"\n                type=\"submit\"\n                class=\"button accept\"\n                :disabled=\"state !== STATES.UPLOADED\"\n            >\n                {{ $gettext('Hinzufügen') }}\n            </button>\n        </template>\n    </studip-dialog>\n</template>\n<script>\nimport MetadataBox from './MetadataBox.vue';\nimport UploadBox from './UploadBox.vue';\nimport { mapActions } from 'vuex';\n\nconst STATES = { IDLE: 'idle', UPLOADED: 'uploaded' };\n\nexport default {\n    props: ['show', 'suggestedTags'],\n    components: { MetadataBox, UploadBox },\n    data: () => ({\n        file: null,\n        metadata: {\n            title: '',\n            description: '',\n            author: '',\n            license: '',\n            tags: [],\n        },\n        state: STATES.IDLE,\n        STATES,\n    }),\n    methods: {\n        onCancel() {\n            this.$emit('cancel');\n            this.resetLocalCopy();\n        },\n        onChangeMetadata(metadata) {\n            this.metadata = metadata;\n        },\n        onSubmit() {\n            this.$emit('confirm', { file: this.file, metadata: this.metadata });\n        },\n        onUpload({ file }) {\n            this.file = file;\n            this.state = STATES.UPLOADED;\n        },\n        resetLocalCopy() {\n            this.file = null;\n            this.metadata = {};\n            this.state = STATES.IDLE;\n        },\n    },\n    watch: {\n        show() {\n            this.resetLocalCopy();\n        },\n    },\n};\n</script>\n\n<style scoped>\nform {\n    height: 100%;\n}\n</style>\n"],"names":[],"sourceRoot":""}