diff options
| author | Ron Lucke <lucke@elan-ev.de> | 2023-06-15 08:03:09 +0000 |
|---|---|---|
| committer | David Siegfried <david.siegfried@uni-vechta.de> | 2023-06-15 08:03:09 +0000 |
| commit | 650de7b907980cda5e0fbf7c9c425e4c485f4a46 (patch) | |
| tree | 1231d843de0c0dfb5d36e1d742d7294d9cd1d42d /resources | |
| parent | c7cd732edfb20f073b1eee64ff9d87cef979b475 (diff) | |
Neuanordnung der Block-Hinzufügen-Liste mit Suchfunktion
Closes #2489
Merge request studip/studip!1796
Diffstat (limited to 'resources')
3 files changed, 219 insertions, 95 deletions
diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index a2c1b35..827c308 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -1791,6 +1791,67 @@ b l o c k a d d e r overflow-y: auto; scrollbar-width: thin; scrollbar-color: $base-color #f5f5f5; + + .input-group.files-search { + &.search { + border: thin solid var(--dark-gray-color-30); + margin-bottom: 0px; + input { + border: none; + } + } + + .input-group-append { + .button { + border: none; + border-left: thin solid var(--dark-gray-color-30); + &.active { + background-color: var(--base-color); + } + } + .reset-search { + border: none; + background-color: var(--white); + } + } + + .active-filter { + display: flex; + align-items: center; + justify-content: space-between; + border: solid thin var(--black); + background-color: var(--content-color-10); + margin: 3px; + padding: 2px 3px; + + .removefilter { + border: none; + background-color: transparent; + } + } + } + + .cw-block-search { + width: inherit; + } + + .filterpanel { + margin-bottom: 5px; + padding: 2px; + border: thin solid var(--dark-gray-color-30); + border-top: none; + background-color: #fff; + + .button { + min-width: inherit; + margin: 4px 2px; + + &.button-active { + background-color: var(--base-color); + color: var(--white); + } + } + } } .cw-collapsible { diff --git a/resources/vue/components/courseware/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/CoursewareRibbonToolbar.vue index edf356e..08da42a 100644 --- a/resources/vue/components/courseware/CoursewareRibbonToolbar.vue +++ b/resources/vue/components/courseware/CoursewareRibbonToolbar.vue @@ -31,6 +31,7 @@ :index="1" > <courseware-tools-blockadder + v-if="showBlockAdder" id="cw-ribbon-tool-blockadder" :stickyRibbon="stickyRibbon" @blockAdded="$emit('blockAdded')" diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue index 97e5c47..5f6ffd4 100644 --- a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue +++ b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue @@ -2,83 +2,68 @@ <div class="cw-tools-element-adder"> <courseware-tabs class="cw-tools-element-adder-tabs"> <courseware-tab :name="$gettext('Blöcke')" :selected="showBlockadder" :index="0" :style="{ maxHeight: maxHeight + 'px' }"> - <courseware-collapsible-box :title="textBlockHelper"> - <courseware-block-helper :blockTypes="blockTypes" /> - </courseware-collapsible-box> - <courseware-collapsible-box :title="textAdderFavs" :open="favoriteBlockTypes.length > 0"> - <div class="cw-element-adder-wrapper" v-if="!showEditFavs"> - <courseware-companion-box - v-if="favoriteBlockTypes.length === 0" - mood="sad" - :msgCompanion="textFavsEmpty" - /> - <courseware-blockadder-item - v-for="(block, index) in favoriteBlockTypes" - :key="index" - :title="block.title" - :icon="block.icon" - :type="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" + <form @submit.prevent="loadSearch"> + <div class="input-group files-search search cw-block-search"> + <input + ref="searchBox" + type="text" + v-model="searchInput" + @click.stop + :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')" /> + <span class="input-group-append" @click.stop> + <button v-if="searchInput" + type="button" + class="button reset-search" + id="reset-search" + :title="$gettext('Suche zurücksetzen')" + @click="resetSearch" + > + <studip-icon shape="decline" :size="20"></studip-icon> + </button> + <button + type="submit" + class="button" + id="search-btn" + :title="$gettext('Suche starten')" + @click="loadSearch" + > + <studip-icon shape="search" :size="20"></studip-icon> + </button> + </span> </div> - <div class="cw-element-adder-favs-wrapper" v-if="showEditFavs"> - <div class="cw-element-adder-all-blocks" :class="{ 'fav-edit-active': showEditFavs }"> - <courseware-blockadder-item - v-for="(block, index) in blockTypes" - :key="index" - :title="block.title" - :type="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" - /> - </div> - <div class="cw-element-adder-favs"> - <div - v-for="(block, index) in blockTypes" - :key="'fav-item-' + index" - class="cw-block-fav-item" - :class="[isBlockFav(block) ? 'cw-block-fav-item-active' : '']" - @click="toggleFavItem(block)" - ></div> - </div> - </div> - <button v-show="!showEditFavs" class="button" @click="showEditFavs = true"> - <translate>Favoriten bearbeiten</translate> - </button> - <button v-show="showEditFavs" class="button" @click="endEditFavs"> - <translate>Favoriten bearbeiten schließen</translate> + </form> + + <div class="filterpanel"> + <span class="sr-only">{{ $gettext('Kategorien Filter') }}</span> + <button + v-for="category in blockCategories" + :key="category.type" + class="button" + :class="{'button-active': category.type === currentFilterCategory }" + :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'" + @click="selectCategory(category.type)" + > + {{ category.title }} </button> - </courseware-collapsible-box> - <courseware-collapsible-box :title="textAdderAll"> - <div class="cw-element-adder-all-blocks" :class="{ 'fav-edit-active': showEditFavs }"> - <courseware-blockadder-item - v-for="(block, index) in blockTypes" - :key="index" - :title="block.title" - :type="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" - /> - </div> - </courseware-collapsible-box> - <courseware-collapsible-box - v-for="(category, index) in blockCategories" - :key="index" - :title="category.title" - :open="category.type === 'basis' && favoriteBlockTypes.length === 0" - > - <div v-for="(block, index) in blockTypes" :key="index"> - <courseware-blockadder-item - v-if="block.categories.includes(category.type)" - :title="block.title" - :icon="block.icon" - :type="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" - /> - </div> - </courseware-collapsible-box> + </div> + + <div v-if="filteredBlockTypes.length > 0"> + <courseware-blockadder-item + v-for="(block, index) in filteredBlockTypes" + :key="index" + :title="block.title" + :type="block.type" + :description="block.description" + @blockAdded="$emit('blockAdded')" + /> + </div> + <div v-else> + <courseware-companion-box + :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')" + mood="pointing" + /> + </div> </courseware-tab> <courseware-tab :name="$gettext('Abschnitte')" :selected="showContaineradder" :index="1" :style="{ maxHeight: maxHeight + 'px' }"> <courseware-collapsible-box @@ -109,7 +94,6 @@ import CoursewareTab from './CoursewareTab.vue'; import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; -import CoursewareBlockHelper from './CoursewareBlockHelper.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; import { mapActions, mapGetters } from 'vuex'; @@ -121,7 +105,6 @@ export default { CoursewareCollapsibleBox, CoursewareBlockadderItem, CoursewareContainerAdderItem, - CoursewareBlockHelper, CoursewareCompanionBox, }, props: { @@ -134,11 +117,10 @@ export default { return { showBlockadder: true, showContaineradder: false, - showEditFavs: false, - textAdderFavs: this.$gettext('Favoriten'), - textAdderAll: this.$gettext('Alle Blöcke'), - textBlockHelper: this.$gettext('Blockassistent'), - textFavsEmpty: this.$gettext('Sie haben noch keine Lieblingsblöcke ausgewählt.'), + searchInput: '', + currentFilterCategory: '', + filteredBlockTypes: [], + categorizedBlocks: [] }; }, computed: { @@ -166,12 +148,11 @@ export default { }, blockCategories() { return [ - { title: this.$gettext('Standard'), type: 'basis' }, + { title: this.$gettext('Favoriten'), type: 'favorite' }, { title: this.$gettext('Texte'), type: 'text' }, { title: this.$gettext('Multimedia'), type: 'multimedia' }, - { title: this.$gettext('Aufgaben & Interaktion'), type: 'interaction' }, + { title: this.$gettext('Interaktion'), type: 'interaction' }, { title: this.$gettext('Gestaltung'), type: 'layout' }, - { title: this.$gettext('Dateien'), type: 'files' }, { title: this.$gettext('Externe Inhalte'), type: 'external' }, { title: this.$gettext('Biografie'), type: 'biography' }, ]; @@ -188,7 +169,8 @@ export default { ...mapActions({ removeFavoriteBlockType: 'removeFavoriteBlockType', addFavoriteBlockType: 'addFavoriteBlockType', - coursewareContainerAdder: 'coursewareContainerAdder' + coursewareContainerAdder: 'coursewareContainerAdder', + companionWarning: 'companionWarning' }), displayContainerAdder() { this.showContaineradder = true; @@ -199,13 +181,6 @@ export default { this.showBlockadder = true; this.disableContainerAdder(); }, - toggleFavItem(block) { - if (this.isBlockFav(block)) { - this.removeFavoriteBlockType(block.type); - } else { - this.addFavoriteBlockType(block.type); - } - }, isBlockFav(block) { let isFav = false; this.favoriteBlockTypes.forEach((type) => { @@ -219,15 +194,85 @@ export default { disableContainerAdder() { this.coursewareContainerAdder(false); }, - endEditFavs() { - this.showEditFavs = false; - this.$emit('scrollTop'); + loadSearch() { + let searchTerms = this.searchInput.trim(); + if (searchTerms.length < 3 && !this.currentFilterCategory) { + this.companionWarning({info: this.$gettext('Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.')}); + return; + } + this.filteredBlockTypes = this.blockTypes; + + // filter results by given filter first so only these results are searched if an additional search term is given + if (this.currentFilterCategory) { + this.filterBlockTypesByCategory(); + this.categorizedBlocks = this.filteredBlockTypes; + } else { + this.categorizedBlocks = this.blockTypes; + } + + searchTerms = searchTerms.toLowerCase().split(' '); + + // sort out block types that don't contain all search words + searchTerms.forEach(term => { + this.filteredBlockTypes = this.filteredBlockTypes.filter(block => ( + block.title.toLowerCase().includes(term) + || block.description.toLowerCase().includes(term) + )); + }); + + // add block types to the search if a search term matches a tag even if they aren't in the given category + if (this.searchInput.trim().length > 0) { + this.filteredBlockTypes.push(...this.getBlockTypesByTags(searchTerms)); + // remove possible duplicates + this.filteredBlockTypes = [...new Map(this.filteredBlockTypes.map(item => [item['title'], item])).values()]; + } + }, + filterBlockTypesByCategory() { + if (this.currentFilterCategory !== 'favorite') { + this.filteredBlockTypes = this.filteredBlockTypes.filter(block => block.categories.includes(this.currentFilterCategory)); + } else { + this.filteredBlockTypes = this.favoriteBlockTypes; + } + + }, + getBlockTypesByTags(searchTags) { + return this.categorizedBlocks.filter(block => { + const lowercaseTags = block.tags.map(blockTag => blockTag.toLowerCase()); + for (const tag of searchTags) { + if (lowercaseTags.filter(blockTag => blockTag.includes(tag.toLowerCase())).length > 0) { + return true; + } + } + return false; + }); + }, + selectCategory(type) { + if (this.currentFilterCategory !== type) { + this.currentFilterCategory = type; + } else { + this.resetCategory(); + } + }, + resetCategory() { + this.currentFilterCategory = ''; + if (!this.searchInput) { + this.filteredBlockTypes = this.blockTypes; + } else { + this.loadSearch(); + } }, + resetSearch() { + this.filteredBlockTypes = this.blockTypes; + this.searchInput = ''; + this.currentFilterCategory = ''; + } }, mounted() { if (this.containerAdder === true) { this.displayContainerAdder(); } + this.filteredBlockTypes = this.blockTypes; + setTimeout(() => this.$refs.searchBox.focus(), 800); }, watch: { adderStorage(newValue) { @@ -244,6 +289,23 @@ export default { if (oldValue === true && newValue === false) { this.disableContainerAdder(); } + }, + searchInput(newValue, oldValue) { + if (newValue.length >= 3 && newValue !== oldValue) { + this.loadSearch(); + } + if (newValue.length < oldValue.length && newValue.length < 3) { + if (!this.currentFilterCategory) { + this.filteredBlockTypes = this.blockTypes; + } else { + this.loadSearch(); + } + } + }, + currentFilterCategory(newValue) { + if (newValue) { + this.loadSearch(); + } } } }; |
