From dd20ec005462f4e13d89a44d366bf0ba6714d1d9 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Fri, 1 Dec 2023 11:35:23 +0000 Subject: [PATCH] =?UTF-8?q?Werkzeugleiste=20f=C3=BCr=20Courseware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2488 Merge request studip/studip!2005 --- .../assets/stylesheets/scss/courseware.scss | 1 + .../scss/courseware/blockadder.scss | 97 ++-- .../scss/courseware/layouts/ribbon.scss | 12 +- .../stylesheets/scss/courseware/sortable.scss | 2 + .../stylesheets/scss/courseware/toolbar.scss | 175 +++++++ .../containers/CoursewareBlockAdderArea.vue | 4 - .../structural-element/CoursewareRibbon.vue | 2 +- .../CoursewareRibbonToolbar.vue | 60 +-- .../CoursewareStructuralElement.vue | 251 +++++----- .../CoursewareToolsBlockadder.vue | 436 ------------------ .../structural-element-components.js | 2 + .../toolbar/CoursewareBlockadderItem.vue | 115 +++++ .../toolbar/CoursewareClipboardItem.vue | 246 ++++++++++ .../toolbar/CoursewareContainerAdderItem.vue | 52 +++ .../courseware/toolbar/CoursewareToolbar.vue | 170 +++++++ .../toolbar/CoursewareToolbarBlocks.vue | 212 +++++++++ .../toolbar/CoursewareToolbarClipboard.vue | 134 ++++++ .../toolbar/CoursewareToolbarContainers.vue | 66 +++ 18 files changed, 1354 insertions(+), 683 deletions(-) create mode 100644 resources/assets/stylesheets/scss/courseware/toolbar.scss delete mode 100644 resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareToolbar.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue create mode 100644 resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index f5caa333a45..2ec74e45855 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -8,6 +8,7 @@ @import './courseware/content-courses.scss'; @import './courseware/dashboard.scss'; @import './courseware/sortable.scss'; +@import './courseware/toolbar.scss'; @import './courseware/widgets.scss'; @import './courseware/wizards.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss index 12a43a9b2d7..774c3d4a1a0 100644 --- a/resources/assets/stylesheets/scss/courseware/blockadder.scss +++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss @@ -89,21 +89,17 @@ } } -.cw-element-adder-wrapper { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} .cw-blockadder-item-list { display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); grid-auto-rows: auto; grid-gap: 4px; + margin-bottom: 8px; .cw-blockadder-item-wrapper { display: flex; border: solid thin var(--content-color-40); - max-width: 254px; + max-width: 268px; &:hover { border-color: var(--base-color); @@ -238,16 +234,12 @@ &:hover { color: var(--active-color); } - &:not(:first-child) { - border-left: solid thin transparent; - } &.cw-container-style-selector-active { background-color: var(--content-color-20); border: solid thin var(--base-color); } - } - input[type=radio] { + input[type='radio'] { position: absolute; opacity: 0; width: 0; @@ -261,54 +253,53 @@ } } .cw-element-inserter-wrapper { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - + display: grid; + grid-template-columns: repeat(auto-fit, minmax(225px, 1fr)); + grid-auto-rows: auto; + grid-gap: 4px; + margin-bottom: 8px; -.cw-clipboard-item-wrapper { - display: flex; - width: calc(50% - 4px); - border: solid thin var(--content-color-40); - margin-bottom: 4px; + .cw-clipboard-item-wrapper { + display: flex; + border: solid thin var(--content-color-40); + max-width: 248px; - &:hover { - border-color: var(--base-color); - } + &:hover { + border-color: var(--base-color); + } - .cw-clipboard-item { - width: 207px; - padding: 64px 10px 4px 10px; - @include background-icon(unit-test, clickable, 48); - background-position: 10px 10px; - background-repeat: no-repeat; - cursor: pointer; - background-color: var(--white); - border: none; - text-align: left; - color: var(--base-color); + .cw-clipboard-item { + width: calc(100% - 36px); + padding: 64px 10px 4px 10px; + @include background-icon(unit-test, clickable, 48); + background-position: 10px 10px; + background-repeat: no-repeat; + cursor: pointer; + background-color: var(--white); + border: none; + text-align: left; + color: var(--base-color); - @each $item, $icon in $blockadder-items { - &.cw-clipboard-item-#{$item} { - @include background-icon($icon, clickable, 48); + @each $item, $icon in $blockadder-items { + &.cw-clipboard-item-#{$item} { + @include background-icon($icon, clickable, 48); + } } - } - @each $item, $icon in $containeradder-items { - &.cw-clipboard-item-#{$item} { - @include background-icon($icon, clickable, 48); + @each $item, $icon in $containeradder-items { + &.cw-clipboard-item-#{$item} { + @include background-icon($icon, clickable, 48); + } + } + + .cw-clipboard-item-title { + display: inline-block; + font-weight: 600; + margin-bottom: 2px; } } - - .cw-clipboard-item-title { - display: inline-block; - font-weight: 600; - margin-bottom: 2px; + .cw-clipboard-item-action-menu-wrapper { + padding: 8px; } - - } - .cw-clipboard-item-action-menu-wrapper { - padding: 8px; } } .action-menu.is-open, @@ -316,4 +307,4 @@ &.cw-clipboard-item-action-menu { z-index: 42; } -} \ No newline at end of file +} diff --git a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss index 1285f05ca6d..f05a518c9a6 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/ribbon.scss @@ -185,13 +185,13 @@ $consum_ribbon_width: calc(100% - 58px); cursor: default; } } +} - .cw-ribbon-action-menu { - vertical-align: text-top; - margin: 2px 0 0 2px; - &.is-open { - z-index: 32; - } +.cw-ribbon-action-menu { + vertical-align: text-top; + margin: 2px 0 0 2px; + &.is-open { + z-index: 32; } } diff --git a/resources/assets/stylesheets/scss/courseware/sortable.scss b/resources/assets/stylesheets/scss/courseware/sortable.scss index 107b3a8056a..9f90de4e827 100644 --- a/resources/assets/stylesheets/scss/courseware/sortable.scss +++ b/resources/assets/stylesheets/scss/courseware/sortable.scss @@ -36,6 +36,8 @@ .cw-container-wrapper-edit { + width: calc(100% - 64px); + .cw-structural-element-list { width: 100%; padding: 0; diff --git a/resources/assets/stylesheets/scss/courseware/toolbar.scss b/resources/assets/stylesheets/scss/courseware/toolbar.scss new file mode 100644 index 00000000000..bf52e79a058 --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware/toolbar.scss @@ -0,0 +1,175 @@ +$toolbar-icons: ( + toggle-out: arr_2right, + toggle-in: arr_2left, + add: add, + clipboard: clipboard +); + +.cw-toolbar { + z-index: 30; + display: flex; + position: fixed; + top: 0; + flex-direction: row; + justify-content: flex-end; + right: 0; + margin-left: 4px; + height: 600px; + + .cw-toolbar-tools { + width: 270px; + min-height: 100%; + border: solid thin var(--content-color-40); + background-color: var(--white); + overflow-y: auto; + overflow-x: hidden; + position: relative; + padding: 0 4px; + top: 0; + right: -270px; + transition: right 0.6s; + &.hd { + width: 480px; + right: -480px; + } + &.wqhd { + width: 558px; + right: -558px; + } + &.unfold { + right: 0; + } + + .cw-toolbar-blocks { + .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-toolbar-button-wrapper { + position: sticky; + top: 0; + background-color: var(--white); + border-bottom: solid thin var(--content-color-40); + display: flex; + z-index: 31; + margin: 0 0 8px -4px; + width: calc(100% + 8px); + } + .cw-toolbar-button { + height: 44px; + margin: 0 4px 0 4px; + padding: 2px 8px 0 8px; + border: none; + background-color: var(--white); + background-repeat: no-repeat; + background-position: center center; + cursor: pointer; + border-bottom: solid 2px transparent; + + @each $type, $icon in $toolbar-icons { + &.cw-toolbar-button-#{$type} { + @include background-icon(#{$icon}, clickable, 24); + } + } + &.cw-toolbar-button-toggle { + right: 0; + width: 42px; + + &.cw-toolbar-button-toggle-out { + position: absolute; + } + &.cw-toolbar-button-toggle-in { + position: relative; + } + } + + &.active { + border-bottom: solid 2px var(--base-color); + } + } + .cw-toolbar-spacer-right { + z-index: 39; + flex-shrink: 0; + position: relative; + background-color: var(--white); + width: 15px; + height: calc(100% + 2px); + } + + .cw-toolbar-tools.hd { + .cw-toolbar-button-wrapper { + .cw-toolbar-button { + width: 128px; + padding: 2px 16px 0 16px; + &.cw-toolbar-button-toggle { + width: 42px; + } + } + } + } +} +#contents-courseware-courseware, +#course-courseware-courseware { + #content-wrapper { + overflow-x: hidden; + } +} diff --git a/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue b/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue index b81ee96c254..de7ca38c325 100644 --- a/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue +++ b/resources/vue/components/courseware/containers/CoursewareBlockAdderArea.vue @@ -39,8 +39,6 @@ export default { methods: { ...mapActions({ coursewareBlockAdder: 'coursewareBlockAdder', - coursewareSelectedToolbarItem: 'coursewareSelectedToolbarItem', - coursewareShowToolbar: 'coursewareShowToolbar' }), selectBlockAdder() { if (this.adderActive) { @@ -49,8 +47,6 @@ export default { } else { this.adderActive = true; this.coursewareBlockAdder({ container: this.container, section: this.section }); - this.coursewareSelectedToolbarItem('blockadder'); - this.coursewareShowToolbar(true); } }, }, diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue index ddf5c9b2149..25c72607d84 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRibbon.vue @@ -1,7 +1,7 @@ <template> <div :class="{ 'cw-ribbon-wrapper-consume': consumeMode }" :id="isContentBar ? 'contentbar' : null" > <div v-show="stickyRibbon" class="cw-ribbon-sticky-top"></div> - <header class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }"> + <header :id="isContentBar ? 'cw-ribbon' : null" class="cw-ribbon" :class="{ 'cw-ribbon-sticky': stickyRibbon, 'cw-ribbon-consume': consumeMode }"> <div class="cw-ribbon-wrapper-left"> <nav class="cw-ribbon-nav" :class="buttonsClass"> <slot name="buttons" /> diff --git a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue index 164dcaef85f..992c3a1f8b3 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareRibbonToolbar.vue @@ -9,7 +9,6 @@ <courseware-tabs class="cw-ribbon-tool-content-tablist" ref="tabs" - @selectTab="selectTool($event.alias)" > <courseware-tab :name="$gettext('Inhaltsverzeichnis')" @@ -22,21 +21,6 @@ id="cw-ribbon-tool-contents" /> </courseware-tab> - <courseware-tab - v-if="displayAdder" - :name="$gettext('Elemente hinzufügen')" - :selected="showBlockAdder" - alias="blockadder" - class="cw-ribbon-tool-blockadder-tab" - :index="1" - > - <courseware-tools-blockadder - v-if="showBlockAdder" - id="cw-ribbon-tool-blockadder" - :stickyRibbon="stickyRibbon" - @blockAdded="$emit('blockAdded')" - /> - </courseware-tab> </courseware-tabs> <button :title="$gettext('schließen')" @@ -52,7 +36,6 @@ <script> import CoursewareTabs from '../layouts/CoursewareTabs.vue'; import CoursewareTab from '../layouts/CoursewareTab.vue'; -import CoursewareToolsBlockadder from './CoursewareToolsBlockadder.vue'; import CoursewareToolsContents from './CoursewareToolsContents.vue'; import { FocusTrap } from 'focus-trap-vue'; import { mapActions, mapGetters } from 'vuex'; @@ -62,7 +45,6 @@ export default { components: { CoursewareTabs, CoursewareTab, - CoursewareToolsBlockadder, CoursewareToolsContents, FocusTrap, }, @@ -106,22 +88,6 @@ export default { showEditMode() { return this.viewMode === 'edit'; }, - displayAdder() { - if (this.disableAdder) { - return false; - } else { - return !this.consumeMode && this.showEditMode && this.canEdit && !this.currentElementisLink; - } - }, - displaySettings() { - if (this.disableSettings) { - return false; - } else { - let user = this.userById({ id: this.userId }); - return !this.consumeMode && this.context.type === 'courses' && (this.isTeacher || ['root', 'admin'].includes(user.attributes.permission)); - } - - }, isTeacher() { return this.userIsTeacher; }, @@ -134,30 +100,6 @@ export default { setToolbarItem: 'coursewareSelectedToolbarItem', coursewareContainerAdder: 'coursewareContainerAdder' }), - selectTool(alias) { - this.showContents = false; - this.showBlockAdder = false; - - switch (alias) { - case 'contents': - this.showContents = true; - this.disableContainerAdder(); - this.scrollToCurrent(); - break; - case 'blockadder': - this.showBlockAdder = true; - break; - } - - if (this.selectedToolbarItem !== alias) { - this.setToolbarItem(alias); - } - }, - disableContainerAdder() { - if (this.containerAdder !== false) { - this.coursewareContainerAdder(false); - } - }, scrollToCurrent() { setTimeout(() => { let contents = this.$refs.contents.$el; @@ -169,7 +111,7 @@ export default { }, }, mounted () { - this.selectTool(this.selectedToolbarItem); + this.scrollToCurrent(); }, watch: { adderStorage(newValue) { diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index 5edf736fdba..34d7fcbce2c 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -2,11 +2,11 @@ <focus-trap v-model="consumModeTrap"> <div> <div + v-if="validContext" :class="{ 'cw-structural-element-consumemode': consumeMode }" class="cw-structural-element" - v-if="validContext" > - <div class="cw-structural-element-content" v-if="structuralElement"> + <div v-if="structuralElement" class="cw-structural-element-content"> <courseware-ribbon :canEdit="canEdit && canAddElements" :isContentBar="true" @blockAdded="updateContainerList"> <template #buttons> <router-link v-if="prevElement" :to="'/structural_element/' + prevElement.id"> @@ -80,133 +80,136 @@ /> </template> </courseware-ribbon> - - <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper"> - <courseware-companion-box - v-if="!canVisit" - mood="sad" - :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung.')" - /> - <courseware-companion-box - v-if="blockedByAnotherUser" - :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet'), {blockingUserName: blockingUserName})" - mood="pointing" - > - <template #companionActions> - <button v-if="userIsTeacher" class="button" @click="menuAction('removeLock')"> - {{ textRemoveLock.title }} - </button> - </template> - </courseware-companion-box> - <courseware-empty-element-box - v-if="showEmptyElementBox" - :canEdit="canEdit" - :noContainers="noContainers" - /> - <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" /> - </div> - - <div - v-if="canVisit && !editView && !isLink" - class="cw-container-wrapper" - :class="{ - 'cw-container-wrapper-consume': consumeMode, - 'cw-container-wrapper-discuss': discussView, - }" - > - <courseware-structural-element-discussion - v-if="!noContainers && discussView" - :structuralElement="structuralElement" - :canEdit="canEdit" - /> - <component - v-for="container in containers" - :key="container.id" - :is="containerComponent(container)" - :container="container" - :canEdit="canEdit" - :canAddElements="canAddElements" - :isTeacher="userIsTeacher" - class="cw-container-item" - /> - </div> - <div - v-if="isLink" - class="cw-container-wrapper" - :class="{ - 'cw-container-wrapper-consume': consumeMode, - 'cw-container-wrapper-discuss': discussView, - }" - > - <courseware-structural-element-discussion - v-if="discussView" - :structuralElement="structuralElement" - :canEdit="canEdit" - /> - <div v-if="editView" class="cw-companion-box-wrapper"> - <courseware-companion-box - :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })" - mood="pointing" - /> - </div> - <component - v-for="container in linkedContainers" - :key="container.id" - :is="containerComponent(container)" - :container="container" - :canEdit="false" - :canAddElements="false" - :isTeacher="userIsTeacher" - class="cw-container-item" - /> - </div> - <div v-if="canVisit && canEdit && editView && !isLink" class="cw-container-wrapper cw-container-wrapper-edit"> - <template v-if="!processing"> - <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span> - <span id="operation" class="assistive-text"> - {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}} - </span> - <draggable - class="cw-structural-element-list" - tag="ol" - role="listbox" - v-model="containerList" - v-bind="dragOptions" - handle=".cw-sortable-handle" - @start="isDragging = true" - @end="dropContainer" + <div class="cw-page-wrapper"> + <div class="cw-page-content"> + <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper"> + <courseware-companion-box + v-if="!canVisit" + mood="sad" + :msgCompanion="$gettext('Diese Seite steht Ihnen leider nicht zur Verfügung.')" + /> + <courseware-companion-box + v-if="blockedByAnotherUser" + :msgCompanion="$gettextInterpolate($gettext('Die Einstellungen dieser Seite werden im Moment von %{blockingUserName} bearbeitet'), {blockingUserName: blockingUserName})" + mood="pointing" + > + <template #companionActions> + <button v-if="userIsTeacher" class="button" @click="menuAction('removeLock')"> + {{ textRemoveLock.title }} + </button> + </template> + </courseware-companion-box> + <courseware-empty-element-box + v-if="showEmptyElementBox" + :canEdit="canEdit" + :noContainers="noContainers" + /> + <courseware-welcome-screen v-if="noContainers && isRoot && canEdit" /> + </div> + + <div + v-if="canVisit && !editView && !isLink" + class="cw-container-wrapper" + :class="{ + 'cw-container-wrapper-consume': consumeMode, + 'cw-container-wrapper-discuss': discussView, + }" > - <li - v-for="container in containerList" + <courseware-structural-element-discussion + v-if="!noContainers && discussView" + :structuralElement="structuralElement" + :canEdit="canEdit" + /> + <component + v-for="container in containers" :key="container.id" - class="cw-container-item-sortable" - > - <span - :class="{ 'cw-sortable-handle-dragging': isDragging }" - class="cw-sortable-handle" - tabindex="0" - role="button" - aria-describedby="operation" - :ref="'sortableHandle' + container.id" - @keydown="keyHandler($event, container.id)" - ></span> - <component - :is="containerComponent(container)" - :container="container" - :canEdit="canEdit" - :canAddElements="canAddElements" - :isTeacher="userIsTeacher" - class="cw-container-item" - ref="containers" - :class="{ 'cw-container-item-selected': keyboardSelected === container.id}" + :is="containerComponent(container)" + :container="container" + :canEdit="canEdit" + :canAddElements="canAddElements" + :isTeacher="userIsTeacher" + class="cw-container-item" + /> + </div> + <div + v-if="isLink" + class="cw-container-wrapper" + :class="{ + 'cw-container-wrapper-consume': consumeMode, + 'cw-container-wrapper-discuss': discussView, + }" + > + <courseware-structural-element-discussion + v-if="discussView" + :structuralElement="structuralElement" + :canEdit="canEdit" + /> + <div v-if="editView" class="cw-companion-box-wrapper"> + <courseware-companion-box + :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })" + mood="pointing" /> - </li> - </draggable> - </template> - <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> + </div> + <component + v-for="container in linkedContainers" + :key="container.id" + :is="containerComponent(container)" + :container="container" + :canEdit="false" + :canAddElements="false" + :isTeacher="userIsTeacher" + class="cw-container-item" + /> + </div> + <div v-if="canVisit && canEdit && editView && !isLink" class="cw-container-wrapper cw-container-wrapper-edit"> + <template v-if="!processing"> + <span aria-live="assertive" class="assistive-text">{{ assistiveLive }}</span> + <span id="operation" class="assistive-text"> + {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}} + </span> + <draggable + class="cw-structural-element-list" + tag="ol" + role="listbox" + v-model="containerList" + v-bind="dragOptions" + handle=".cw-sortable-handle" + @start="isDragging = true" + @end="dropContainer" + > + <li + v-for="container in containerList" + :key="container.id" + class="cw-container-item-sortable" + > + <span + :class="{ 'cw-sortable-handle-dragging': isDragging }" + class="cw-sortable-handle" + tabindex="0" + role="option" + aria-describedby="operation" + :ref="'sortableHandle' + container.id" + @keydown="keyHandler($event, container.id)" + ></span> + <component + :is="containerComponent(container)" + :container="container" + :canEdit="canEdit" + :canAddElements="canAddElements" + :isTeacher="userIsTeacher" + class="cw-container-item" + ref="containers" + :class="{ 'cw-container-item-selected': keyboardSelected === container.id}" + /> + </li> + </draggable> + </template> + <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> + </div> + </div> + <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> </div> </div> - <studip-dialog v-if="showEditDialog" :title="textEdit.title" diff --git a/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue deleted file mode 100644 index 86b42467fdf..00000000000 --- a/resources/vue/components/courseware/structural-element/CoursewareToolsBlockadder.vue +++ /dev/null @@ -1,436 +0,0 @@ -<template> - <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' }"> - <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> - </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> - </div> - - <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list"> - <courseware-blockadder-item - v-for="(block, index) in filteredBlockTypes" - :key="index" - :title="block.title" - :type="block.type" - :description="block.description" - @blockAdded="$emit('blockAdded')" - /> - </div> - <courseware-companion-box - v-else - :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')" - mood="pointing" - /> - </courseware-tab> - <courseware-tab :name="$gettext('Abschnitte')" :selected="showContaineradder" :index="1" :style="{ maxHeight: maxHeight + 'px' }"> - <div class="cw-container-style-selector" role="group" aria-labelledby="cw-containeradder-style"> - <p class="sr-only" id="cw-containeradder-style">{{ $gettext('Abschnitt-Stil') }}</p> - <template - v-for="style in containerStyles" - > - <input - :key="style.key + '-input'" - type="radio" - name="container-style" - :id="'style-' + style.colspan" - v-model="selectedContainerStyle" - :value="style.colspan" - /> - <label - :key="style.key + '-label'" - :for="'style-' + style.colspan" - :class="[selectedContainerStyle === style.colspan ? 'cw-container-style-selector-active' : '', style.colspan]" - > - {{ style.title }} - </label> - - </template> - </div> - <courseware-container-adder-item - v-for="container in containerTypes" - :key="container.type" - :title="container.title" - :type="container.type" - :colspan="selectedContainerStyle" - :description="container.description" - :firstSection="$gettext('erstes Element')" - :secondSection="$gettext('zweites Element')" - ></courseware-container-adder-item> - </courseware-tab> - <courseware-tab :name="$gettext('Merkliste')" :selected="showClipboard" :index="2" :style="{ maxHeight: maxHeight + 'px' }"> - <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0"> - <template v-if="clipboardBlocks.length > 0"> - <div class="cw-element-inserter-wrapper"> - <courseware-clipboard-item - v-for="(clipboard, index) in clipboardBlocks" - :key="index" - :clipboard="clipboard" - @inserted="$emit('blockAdded')" - /> - </div> - <button class="button trash" @click="clearClipboard('courseware-blocks')"> - {{ $gettext('Alle Blöcke aus Merkliste entfernen') }} - </button> - </template> - <courseware-companion-box - v-else - mood="pointing" - :msgCompanion="$gettext('Die Merkliste enthält keine Blöcke.')" - /> - </courseware-collapsible-box> - <courseware-collapsible-box :title="$gettext('Abschnitte')" :open="clipboardContainers.length > 0"> - <template v-if="clipboardContainers.length > 0"> - <div class="cw-element-inserter-wrapper"> - <courseware-clipboard-item - v-for="(clipboard, index) in clipboardContainers" - :key="index" - :clipboard="clipboard" - /> - </div> - <button class="button trash" @click="clearClipboard('courseware-containers')"> - {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }} - </button> - </template> - <courseware-companion-box - v-else - mood="pointing" - :msgCompanion="$gettext('Die Merkliste enthält keine Abschnitte.')" - /> - </courseware-collapsible-box> - </courseware-tab> - </courseware-tabs> - <studip-dialog - v-if="showDeleteClipboardDialog" - :title="textDeleteClipboardTitle" - :question="textDeleteClipboardAlert" - height="200" - width="500" - @confirm="executeDeleteClipboard" - @close="closeDeleteClipboardDialog" - ></studip-dialog> - </div> -</template> - -<script> -import CoursewareTabs from '../layouts/CoursewareTabs.vue'; -import CoursewareTab from '../layouts/CoursewareTab.vue'; -import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; -import CoursewareClipboardItem from './CoursewareClipboardItem.vue'; -import CoursewareContainerAdderItem from '../containers/CoursewareContainerAdderItem.vue'; -import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; -import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue'; -import StudipDialog from '../../StudipDialog.vue'; -import { mapActions, mapGetters } from 'vuex'; - -export default { - name: 'cw-tools-blockadder', - components: { - CoursewareTabs, - CoursewareTab, - CoursewareBlockadderItem, - CoursewareClipboardItem, - CoursewareContainerAdderItem, - CoursewareCompanionBox, - CoursewareCollapsibleBox, - - StudipDialog, - }, - props: { - stickyRibbon: { - type: Boolean, - default: false, - }, - }, - data() { - return { - showBlockadder: true, - showContaineradder: false, - showClipboard: false, - searchInput: '', - currentFilterCategory: '', - filteredBlockTypes: [], - categorizedBlocks: [], - selectedContainerStyle: 'full', - showDeleteClipboardDialog: false, - deleteClipboardType: null - }; - }, - computed: { - ...mapGetters({ - adderStorage: 'blockAdder', - containerAdder: 'containerAdder', - unorderedBlockTypes: 'blockTypes', - containerTypes: 'containerTypes', - favoriteBlockTypes: 'favoriteBlockTypes', - showToolbar: 'showToolbar', - usersClipboards: 'courseware-clipboards/all', - userId: 'userId' - }), - blockTypes() { - let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes)); - blockTypes.sort((a, b) => { - return a.title > b.title ? 1 : b.title > a.title ? -1 : 0; - }); - return blockTypes; - }, - containerStyles() { - return [ - { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'}, - { key: 1, title: this.$gettext('Halbe Breite'), colspan: 'half' }, - { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' }, - ]; - }, - blockCategories() { - return [ - { title: this.$gettext('Favoriten'), type: 'favorite' }, - { title: this.$gettext('Texte'), type: 'text' }, - { title: this.$gettext('Multimedia'), type: 'multimedia' }, - { title: this.$gettext('Interaktion'), type: 'interaction' }, - { title: this.$gettext('Gestaltung'), type: 'layout' }, - { title: this.$gettext('Externe Inhalte'), type: 'external' }, - { title: this.$gettext('Biografie'), type: 'biography' }, - ]; - }, - maxHeight() { - if (this.stickyRibbon) { - return parseInt(window.innerHeight * 0.75) - 120; - } else { - return parseInt(Math.min(window.innerHeight * 0.75, window.innerHeight - 197)) - 120; - } - }, - clipboardBlocks() { - return this.usersClipboards - .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks') - .sort((a, b) => b.attributes.mkdate - a.attributes.mkdate); - }, - clipboardContainers() { - return this.usersClipboards - .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-containers') - .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate); - }, - textDeleteClipboardTitle() { - if (this.deleteClipboardType === 'courseware-blocks') { - return this.$gettext('Merkliste für Blöcke leeren'); - } - if (this.deleteClipboardType === 'courseware-containers') { - return this.$gettext('Merkliste für Abschnitte leeren'); - } - return ''; - }, - textDeleteClipboardAlert() { - if (this.deleteClipboardType === 'courseware-blocks') { - return this.$gettext('Möchten Sie die Merkliste für Blöcke unwiderruflich leeren?'); - } - if (this.deleteClipboardType === 'courseware-containers') { - return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?'); - } - return ''; - } - }, - methods: { - ...mapActions({ - removeFavoriteBlockType: 'removeFavoriteBlockType', - addFavoriteBlockType: 'addFavoriteBlockType', - coursewareContainerAdder: 'coursewareContainerAdder', - companionWarning: 'companionWarning', - deleteUserClipboards: 'deleteUserClipboards' - }), - displayContainerAdder() { - this.showContaineradder = true; - this.showBlockadder = false; - this.showClipboard = false; - }, - displayBlockAdder() { - this.showContaineradder = false; - this.showClipboard = false; - this.showBlockadder = true; - this.disableContainerAdder(); - }, - displayClipboard() { - this.showClipboard = true; - }, - isBlockFav(block) { - let isFav = false; - this.favoriteBlockTypes.forEach((type) => { - if (type.type === block.type) { - isFav = true; - } - }); - - return isFav; - }, - disableContainerAdder() { - this.coursewareContainerAdder(false); - }, - 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 = ''; - }, - clearClipboard(type) { - this.deleteClipboardType = type; - this.showDeleteClipboardDialog = true; - }, - executeDeleteClipboard() { - if (this.deleteClipboardType) { - this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType}); - } - this.closeDeleteClipboardDialog(); - }, - closeDeleteClipboardDialog() { - this.showDeleteClipboardDialog = false; - this.deleteClipboardType = null; - } - }, - mounted() { - if (this.containerAdder === true) { - this.displayContainerAdder(); - } - this.filteredBlockTypes = this.blockTypes; - setTimeout(() => this.$refs.searchBox.focus(), 800); - }, - watch: { - adderStorage(newValue) { - if (Object.keys(newValue).length !== 0) { - this.displayBlockAdder(); - } - }, - containerAdder(newValue) { - if (newValue === true) { - this.displayContainerAdder(); - } - }, - showToolbar(newValue, oldValue) { - 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(); - } - } - } -}; -</script> diff --git a/resources/vue/components/courseware/structural-element/structural-element-components.js b/resources/vue/components/courseware/structural-element/structural-element-components.js index fdba4b41e1d..40586c2e683 100644 --- a/resources/vue/components/courseware/structural-element/structural-element-components.js +++ b/resources/vue/components/courseware/structural-element/structural-element-components.js @@ -1,3 +1,4 @@ +import CoursewareToolbar from './../toolbar/CoursewareToolbar.vue'; // contentbar import CoursewareRibbon from './CoursewareRibbon.vue'; import CoursewareTabs from '../layouts/CoursewareTabs.vue'; @@ -13,6 +14,7 @@ import CoursewareListContainer from '../containers/CoursewareListContainer.vue'; import CoursewareTabsContainer from '../containers/CoursewareTabsContainer.vue'; const StructuralElementComponents = { + CoursewareToolbar, //contentbar CoursewareRibbon, CoursewareTabs, diff --git a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue new file mode 100644 index 00000000000..f7fe3030d69 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue @@ -0,0 +1,115 @@ +<template> + <div class="cw-blockadder-item-wrapper"> + <a href="#" @click.prevent="addBlock" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]"> + <header class="cw-blockadder-item-title"> + {{ title }} + </header> + <p class="cw-blockadder-item-description"> + {{ description }} + </p> + </a> + <button + class="cw-blockadder-item-fav" + :title="favButtonTitle" + @click="toggleFavItem()" + > + <studip-icon :shape="blockTypeIsFav ? 'star' : 'star-empty'" :size="20" /> + </button> + </div> + +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-blockadder-item', + components: {}, + props: { + title: String, + description: String, + type: String, + }, + data() { + return { + showInfo: false, + }; + }, + computed: { + ...mapGetters({ + blockAdder: 'blockAdder', + blockById: 'courseware-blocks/byId', + lastCreatedBlock: 'courseware-blocks/lastCreated', + favoriteBlockTypes: 'favoriteBlockTypes', + }), + blockTypeIsFav() { + return this.favoriteBlockTypes.some((type) => type.type === this.type); + }, + favButtonTitle() { + if (this.blockTypeIsFav) { + return this.$gettextInterpolate( + this.$gettext('%{ blockName } Block aus den Favoriten entfernen'), + { blockName: this.title } + ); + } + + return this.$gettextInterpolate( + this.$gettext('%{ blockName } Block zu Favoriten hinzufügen'), + { blockName: this.title } + ); + } + }, + methods: { + ...mapActions({ + companionInfo: 'companionInfo', + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + createBlock: 'createBlockInContainer', + lockObject: 'lockObject', + unlockObject: 'unlockObject', + loadBlock: 'courseware-blocks/loadById', + updateContainer: 'updateContainer', + removeFavoriteBlockType: 'removeFavoriteBlockType', + addFavoriteBlockType: 'addFavoriteBlockType', + }), + async addBlock() { + if (Object.keys(this.blockAdder).length !== 0) { + // lock parent container + await this.lockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' }); + // create new block + await this.createBlock({ + container: this.blockAdder.container, + section: this.blockAdder.section, + blockType: this.type, + }); + //get new Block + const newBlock = this.lastCreatedBlock; + // update container information -> new block id in sections + let container = this.blockAdder.container; + container.attributes.payload.sections[this.blockAdder.section].blocks.push(newBlock.id); + const structuralElementId = container.relationships['structural-element'].data.id; + // update container + await this.updateContainer({ container, structuralElementId }); + // unlock container + await this.unlockObject({ id: this.blockAdder.container.id, type: 'courseware-containers' }); + this.companionSuccess({ + info: this.$gettext('Der Block wurde erfolgreich eingefügt.'), + }); + this.$emit('blockAdded'); + } else { + // companion action + this.companionWarning({ + info: this.$gettext('Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.'), + }); + } + }, + toggleFavItem() { + if (this.blockTypeIsFav) { + this.removeFavoriteBlockType(this.type); + } else { + this.addFavoriteBlockType(this.type); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue new file mode 100644 index 00000000000..27d9e58c5f1 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue @@ -0,0 +1,246 @@ +<template> + <div class="cw-clipboard-item-wrapper"> + <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem"> + <header class="sr-only"> + {{ srTitle }} + </header> + <header class="cw-clipboard-item-title" aria-hidden="true"> + {{ name }} + </header> + <p class="cw-clipboard-item-description"> + {{ description }} + </p> + </button> + <div class="cw-clipboard-item-action-menu-wrapper"> + <studip-action-menu + class="cw-clipboard-item-action-menu" + :items="menuItems" + :context="name" + @insertItemCopy="insertItemCopy" + @editItem="showEditItem" + @deleteItem="deleteItem" + /> + </div> + <studip-dialog + v-if="showEditDialog" + :title="$gettext('Umbenennen')" + :confirmText="$gettext('Speichern')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + height="360" + width="500" + @close="closeEditItem" + @confirm="storeItem" + > + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <label> + {{ $gettext('Titel') }} + <input type="text" v-model="currentClipboard.attributes.name" /> + </label> + <label> + {{ $gettext('Beschreibung') }} + <textarea v-model="currentClipboard.attributes.description"></textarea> + </label> + </form> + </template> + </studip-dialog> + </div> +</template> + +<script> +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-clipboard-item', + components: {}, + props: { + clipboard: Object, + }, + data() { + return { + showEditDialog: false, + currentClipboard: null, + + text: { + errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'), + positionWarning: this.$gettext( + 'Bitte wählen Sie einen Ort aus, an dem der Block eingefügt werden soll.' + ), + blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'), + containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'), + }, + }; + }, + computed: { + ...mapGetters({ + blockAdder: 'blockAdder', + currentElement: 'currentElement', + }), + name() { + return this.clipboard.attributes.name; + }, + description() { + return this.clipboard.attributes.description; + }, + isBlock() { + return this.clipboard.attributes['object-type'] === 'courseware-blocks'; + }, + kind() { + return this.clipboard.attributes['object-kind']; + }, + blockId() { + return this.clipboard.attributes['block-id']; + }, + blockNotFound() { + return this.clipboard.relationships.block.data === null; + }, + containerId() { + return this.clipboard.attributes['container-id']; + }, + containerNotFound() { + return this.clipboard.relationships.container.data === null; + }, + itemNotFound() { + if (this.isBlock) { + return this.blockNotFound; + } + + return this.containerNotFound; + }, + menuItems() { + let menuItems = []; + if (!this.itemNotFound) { + menuItems.push({ + id: 1, + label: this.$gettext('Aktuellen Stand einfügen'), + icon: 'copy', + emit: 'insertItemCopy', + }); + } + menuItems.push({ id: 2, label: this.$gettext('Umbenennen'), icon: 'edit', emit: 'editItem' }); + menuItems.push({ id: 3, label: this.$gettext('Löschen'), icon: 'trash', emit: 'deleteItem' }); + + menuItems.sort((a, b) => a.id - b.id); + return menuItems; + }, + blockAdderActive() { + return Object.keys(this.blockAdder).length !== 0; + }, + srTitle() { + return this.isBlock ? + this.$gettextInterpolate(this.$gettext(`Block %{name} einfügen`), { name: this.name }) : + this.$gettextInterpolate(this.$gettext(`Abschnitt %{name} einfügen`), { name: this.name }); + } + }, + methods: { + ...mapActions({ + companionInfo: 'companionInfo', + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + copyContainer: 'copyContainer', + copyBlock: 'copyBlock', + clipboardInsertBlock: 'clipboardInsertBlock', + clipboardInsertContainer: 'clipboardInsertContainer', + loadStructuralElement: 'loadStructuralElement', + loadContainer: 'loadContainer', + deleteClipboard: 'courseware-clipboards/delete', + updateClipboard: 'courseware-clipboards/update', + loadClipboard: 'courseware-clipboards/loadById', + }), + + async insertItem() { + let insertError = false; + + if (this.isBlock) { + if (!this.blockAdderActive) { + this.companionWarning({ info: this.text.positionWarning }); + return; + } + try { + await this.clipboardInsertBlock({ + parentId: this.blockAdder.container.id, + section: this.blockAdder.section, + clipboard: this.clipboard, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadContainer(this.blockAdder.container.id); + this.companionSuccess({ info: this.text.blockSuccess }); + } + } else { + try { + await this.clipboardInsertContainer({ + parentId: this.currentElement, + clipboard: this.clipboard, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + this.loadStructuralElement(this.currentElement); + this.companionSuccess({ info: this.text.containerSuccess }); + } + } + }, + + async insertItemCopy() { + let insertError = false; + + if (this.isBlock) { + if (!this.blockAdderActive) { + this.companionWarning({ info: this.text.positionWarning }); + return; + } + try { + await this.copyBlock({ + parentId: this.blockAdder.container.id, + section: this.blockAdder.section, + block: { id: this.blockId }, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadContainer(this.blockAdder.container.id); + this.companionSuccess({ info: this.text.blockSuccess }); + } + } else { + try { + await this.copyContainer({ parentId: this.currentElement, container: { id: this.containerId } }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + this.loadStructuralElement(this.currentElement); + this.companionSuccess({ info: this.text.containerSuccess }); + } + } + }, + deleteItem() { + this.deleteClipboard({ id: this.clipboard.id }); + }, + showEditItem() { + this.showEditDialog = true; + }, + closeEditItem() { + this.showEditDialog = false; + }, + async storeItem() { + this.closeEditItem(); + await this.updateClipboard(this.currentClipboard); + this.loadClipboard({ id: this.currentClipboard.id }); + }, + }, + mounted() { + this.currentClipboard = _.cloneDeep(this.clipboard); + }, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue new file mode 100644 index 00000000000..78cc4eda3bc --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue @@ -0,0 +1,52 @@ +<template> + <a href="#" @click.prevent="addContainer"> + <div class="cw-containeradder-item" :class="['cw-containeradder-item-' + type]"> + <header class="cw-containeradder-item-title"> + {{ title }} + </header> + <p class="cw-containeradder-item-description"> + {{ description }} + </p> + </div> + </a> +</template> +<script> +import { mapActions } from 'vuex'; +export default { + name: 'courseware-container-adder-item', + components: {}, + props: { + title: String, + description: String, + type: String, + colspan: String, + firstSection: String, + secondSection: String, + }, + methods: { + ...mapActions({ + createContainer: 'createContainer', + companionSuccess: 'companionSuccess', + }), + async addContainer() { + let attributes = {}; + attributes["container-type"] = this.type; + let sections = []; + if (this.type === 'list') { + sections = [{ name: this.firstSection, icon: '', blocks: [] }]; + } else { + sections = [{ name: this.firstSection, icon: '', blocks: [] },{ name: this.secondSection, icon: '', blocks: [] }]; + } + attributes.payload = { + colspan: this.colspan, + sections: sections, + }; + await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes }); + this.companionSuccess({ + info: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'), + }); + }, + }, + mounted() {}, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue new file mode 100644 index 00000000000..252f3fe6ce6 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue @@ -0,0 +1,170 @@ +<template> + <div class="cw-toolbar-wrapper"> + <div id="cw-toolbar" class="cw-toolbar" :style="toolbarStyle"> + <div v-if="showTools" class="cw-toolbar-tools" :class="{ unfold: unfold, hd: isHd, wqhd: isWqhd}"> + <div class="cw-toolbar-button-wrapper"> + <button + class="cw-toolbar-button" + :class="{active: activeTool === 'blockAdder'}" + :title="$gettext('Blöcke hinzufügen')" + @click="activateTool('blockAdder')" + > + {{ $gettext('Blöcke') }} + </button> + <button + class="cw-toolbar-button" + :class="{active: activeTool === 'containerAdder'}" + :title="$gettext('Abschnitte hinzufügen')" + @click="activateTool('containerAdder')" + > + {{ $gettext('Abschnitte') }} + </button> + <button + class="cw-toolbar-button" + :class="{active: activeTool === 'clipboard'}" + :title="$gettext('Block Merkliste')" + @click="activateTool('clipboard')" + > + {{ $gettext('Merkliste') }} + </button> + <button + class="cw-toolbar-button cw-toolbar-button-toggle cw-toolbar-button-toggle-out" + :title="$gettext('Werkzeugleiste einklappen')" + @click="toggleToolbar" + ></button> + </div> + <courseware-toolbar-blocks v-if="activeTool === 'blockAdder'" /> + <courseware-toolbar-containers v-if="activeTool === 'containerAdder'" /> + <courseware-toolbar-clipboard v-if="activeTool === 'clipboard'" /> + </div> + <button + v-else + class="cw-toolbar-button cw-toolbar-button-toggle cw-toolbar-button-toggle-in" + :title="$gettext('Werkzeugleiste ausklappen')" + @click="toggleToolbar" + ></button> + <div class="cw-toolbar-spacer-right"></div> + </div> + </div> +</template> + +<script> +import CoursewareToolbarBlocks from './CoursewareToolbarBlocks.vue'; +import CoursewareToolbarContainers from './CoursewareToolbarContainers.vue'; +import CoursewareToolbarClipboard from './CoursewareToolbarClipboard.vue'; + +export default { + name: 'courseware-toolbar', + components: { + CoursewareToolbarBlocks, + CoursewareToolbarContainers, + CoursewareToolbarClipboard + }, + data() { + return { + toolsActive: true, + unfold: true, + showTools: true, + toolbarTop: 0, + activeTool: 'blockAdder', + + windowWidth: window.outerWidth, + windowInnerHeight: window.innerHeight + }; + }, + computed: { + toolbarStyle() { + const footerHeight = document.getElementById('main-footer').getBoundingClientRect().height; + const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top')); + const scrollTopHeight = parseInt(scrollTopStyles['height']) + parseInt(scrollTopStyles['padding-top']) + parseInt(scrollTopStyles['padding-bottom']); + let height = parseInt( + Math.min(this.windowInnerHeight * 0.9, this.windowInnerHeight - this.toolbarTop - scrollTopHeight - footerHeight) + ); + + return { + height: height + 'px', + minHeight: height + 'px', + top: this.toolbarTop + 'px', + }; + }, + toolbarHeader() { + let header = ''; + if (this.activeTool === 'blockAdder') { + header = this.$gettext('Block hinzufügen'); + } + if (this.activeTool === 'containerAdder') { + header = this.$gettext('Abschnitt hinzufügen'); + } + + return header; + }, + isHd() { + return this.windowWidth >= 1920; + }, + isWqhd() { + return this.windowWidth >= 2560; + }, + }, + methods: { + toggleToolbar() { + this.toolsActive = !this.toolsActive; + }, + activateTool(tool) { + this.activeTool = tool; + }, + updateToolbarTop() { + const responsiveContentbar = document.getElementById('responsive-contentbar'); + if (responsiveContentbar) { + const contentbarRect = this.responsiveContentbar.getBoundingClientRect(); + this.toolbarTop = contentbarRect.bottom + 35; + return; + } + + const ribbon = document.getElementById('cw-ribbon') ?? document.getElementById('contentbar'); + if (ribbon) { + const contentbarRect = ribbon.getBoundingClientRect(); + if (ribbon.classList.contains("cw-ribbon-sticky")) { + this.toolbarTop = contentbarRect.bottom + 16; + } else { + this.toolbarTop = contentbarRect.bottom + 15; + } + } + + }, + onResize() { + this.windowWidth = window.outerWidth; + this.windowInnerHeight = window.innerHeight; + } + }, + mounted() { + this.updateToolbarTop(); + this.$nextTick(() => { + window.addEventListener('scroll', this.updateToolbarTop); + window.addEventListener('resize', this.onResize); + }); + }, + beforeDestroy() { + window.removeEventListener('scroll', this.updateToolbarTop); + window.removeEventListener('resize', this.onResize); + }, + + watch: { + toolsActive(newState, oldState) { + let view = this; + if (newState) { + this.showTools = true; + setTimeout(() => { + view.unfold = true; + }, 10); + } else { + this.unfold = false; + setTimeout(() => { + if (!view.toolsActive) { + view.showTools = false; + } + }, 600); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue new file mode 100644 index 00000000000..3cd783c3fc6 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue @@ -0,0 +1,212 @@ +<template> + <div class="cw-toolbar-blocks"> + <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> + </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> + </div> + + <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list"> + <courseware-blockadder-item + v-for="(block, index) in filteredBlockTypes" + :key="index" + :title="block.title" + :type="block.type" + :description="block.description" + @blockAdded="$emit('blockAdded')" + /> + </div> + <courseware-companion-box + v-else + :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')" + mood="pointing" + /> + </div> +</template> + +<script> +import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'courseware-toolbar-blocks', + components: { + CoursewareBlockadderItem, + CoursewareCompanionBox + }, + data() { + return { + searchInput: '', + currentFilterCategory: '', + filteredBlockTypes: [], + categorizedBlocks: [] + }; + }, + computed: { + ...mapGetters({ + adderStorage: 'blockAdder', + unorderedBlockTypes: 'blockTypes', + favoriteBlockTypes: 'favoriteBlockTypes', + }), + blockTypes() { + let blockTypes = JSON.parse(JSON.stringify(this.unorderedBlockTypes)); + blockTypes.sort((a, b) => { + return a.title > b.title ? 1 : b.title > a.title ? -1 : 0; + }); + return blockTypes; + }, + blockCategories() { + return [ + { title: this.$gettext('Favoriten'), type: 'favorite' }, + { title: this.$gettext('Texte'), type: 'text' }, + { title: this.$gettext('Multimedia'), type: 'multimedia' }, + { title: this.$gettext('Interaktion'), type: 'interaction' }, + { title: this.$gettext('Gestaltung'), type: 'layout' }, + { title: this.$gettext('Externe Inhalte'), type: 'external' }, + { title: this.$gettext('Biografie'), type: 'biography' } + ]; + } + }, + methods: { + ...mapActions({ + companionWarning: 'companionWarning' + }), + 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() { + this.filteredBlockTypes = this.blockTypes; + setTimeout(() => this.$refs.searchBox.focus(), 800); + }, + watch: { + 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(); + } + } + } +} +</script> \ No newline at end of file diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue new file mode 100644 index 00000000000..e7898cd03c1 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue @@ -0,0 +1,134 @@ +<template> + <div class="cw-toolbar-clipboard"> + <courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0"> + <template v-if="clipboardBlocks.length > 0"> + <div class="cw-element-inserter-wrapper"> + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardBlocks" + :key="index" + :clipboard="clipboard" + @inserted="$emit('blockAdded')" + /> + </div> + <button class="button trash" @click="clearClipboard('courseware-blocks')"> + {{ $gettext('Alle Blöcke aus Merkliste entfernen') }} + </button> + </template> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Die Merkliste enthält keine Blöcke.')" + /> + </courseware-collapsible-box> + <courseware-collapsible-box :title="$gettext('Abschnitte')" :open="clipboardContainers.length > 0"> + <template v-if="clipboardContainers.length > 0"> + <div class="cw-element-inserter-wrapper"> + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardContainers" + :key="index" + :clipboard="clipboard" + /> + </div> + <button class="button trash" @click="clearClipboard('courseware-containers')"> + {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }} + </button> + </template> + <courseware-companion-box + v-else + mood="pointing" + :msgCompanion="$gettext('Die Merkliste enthält keine Abschnitte.')" + /> + </courseware-collapsible-box> + <studip-dialog + v-if="showDeleteClipboardDialog" + :title="textDeleteClipboardTitle" + :question="textDeleteClipboardAlert" + height="200" + width="500" + @confirm="executeDeleteClipboard" + @close="closeDeleteClipboardDialog" + ></studip-dialog> + + </div> +</template> + +<script> +import CoursewareClipboardItem from './CoursewareClipboardItem.vue'; +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue'; +import StudipDialog from '../../StudipDialog.vue'; + +import { mapActions, mapGetters } from 'vuex'; + +export default { + name: 'cw-tools-blockadder', + components: { + CoursewareClipboardItem, + CoursewareCompanionBox, + CoursewareCollapsibleBox, + StudipDialog, + }, + + data() { + return { + showDeleteClipboardDialog: false, + deleteClipboardType: null + }; + }, + computed: { + ...mapGetters({ + usersClipboards: 'courseware-clipboards/all', + userId: 'userId' + }), + clipboardBlocks() { + return this.usersClipboards + .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks') + .sort((a, b) => b.attributes.mkdate - a.attributes.mkdate); + }, + clipboardContainers() { + return this.usersClipboards + .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-containers') + .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate); + }, + textDeleteClipboardTitle() { + if (this.deleteClipboardType === 'courseware-blocks') { + return this.$gettext('Merkliste für Blöcke leeren'); + } + if (this.deleteClipboardType === 'courseware-containers') { + return this.$gettext('Merkliste für Abschnitte leeren'); + } + return ''; + }, + textDeleteClipboardAlert() { + if (this.deleteClipboardType === 'courseware-blocks') { + return this.$gettext('Möchten Sie die Merkliste für Blöcke unwiderruflich leeren?'); + } + if (this.deleteClipboardType === 'courseware-containers') { + return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?'); + } + return ''; + } + }, + methods: { + ...mapActions({ + companionWarning: 'companionWarning', + deleteUserClipboards: 'deleteUserClipboards' + }), + clearClipboard(type) { + this.deleteClipboardType = type; + this.showDeleteClipboardDialog = true; + }, + executeDeleteClipboard() { + if (this.deleteClipboardType) { + this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType}); + } + this.closeDeleteClipboardDialog(); + }, + closeDeleteClipboardDialog() { + this.showDeleteClipboardDialog = false; + this.deleteClipboardType = null; + } + } +}; + +</script> \ No newline at end of file diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue new file mode 100644 index 00000000000..12fea1bbd05 --- /dev/null +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue @@ -0,0 +1,66 @@ +<template> + <div class="cw-toolbar-containers"> + <div class="cw-container-style-selector" role="group" aria-labelledby="cw-containeradder-style"> + <p class="sr-only" id="cw-containeradder-style">{{ $gettext('Abschnitt-Stil') }}</p> + <template + v-for="style in containerStyles" + > + <input + :key="style.key + '-input'" + type="radio" + name="container-style" + :id="'style-' + style.colspan" + v-model="selectedContainerStyle" + :value="style.colspan" + /> + <label + :key="style.key + '-label'" + :for="'style-' + style.colspan" + :class="[selectedContainerStyle === style.colspan ? 'cw-container-style-selector-active' : '', style.colspan]" + > + {{ style.title }} + </label> + + </template> + </div> + <courseware-container-adder-item + v-for="container in containerTypes" + :key="container.type" + :title="container.title" + :type="container.type" + :colspan="selectedContainerStyle" + :description="container.description" + :firstSection="$gettext('erstes Element')" + :secondSection="$gettext('zweites Element')" + ></courseware-container-adder-item> + </div> +</template> + +<script> +import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; +import { mapGetters } from 'vuex'; + +export default { + name: 'courseware-toolbar-containers', + components: { + CoursewareContainerAdderItem + }, + data() { + return { + selectedContainerStyle: 'full' + }; + }, + computed: { + ...mapGetters({ + containerTypes: 'containerTypes' + }), + containerStyles() { + return [ + { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'}, + { key: 1, title: this.$gettext('Halbe Breite'), colspan: 'half' }, + { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' } + ]; + }, + } +} +</script> \ No newline at end of file -- GitLab