diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 8fdce40864954d9a7d7e9510923bcee2202be09e..6c3d7ea620e6cd0e52aa3cf50bd333d9af18a9bc 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -15,6 +15,9 @@ @import './courseware/shelf.scss'; @import './courseware/structural-element.scss'; @import './courseware/containers/default-container.scss'; +@import './courseware/containers/accordion.scss'; +@import './courseware/containers/list.scss'; +@import './courseware/containers/tabs.scss'; @import './courseware/blocks/default-block.scss'; @import './courseware/layouts/collapsible.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/blockadder.scss b/resources/assets/stylesheets/scss/courseware/blockadder.scss index 774c3d4a1a02bb84126f3eca065da80d69aaa340..ad8424b9071e55836c2fb8d81352abfe64793e29 100644 --- a/resources/assets/stylesheets/scss/courseware/blockadder.scss +++ b/resources/assets/stylesheets/scss/courseware/blockadder.scss @@ -101,13 +101,21 @@ border: solid thin var(--content-color-40); max-width: 268px; + .cw-sortable-handle { + opacity: 0; + } + &:hover { border-color: var(--base-color); + + .cw-sortable-handle { + opacity: 1; + } } .cw-blockadder-item { padding: 64px 10px 4px 10px; @include background-icon(unit-test, clickable, 48); - background-position: 10px 10px; + background-position: 16px 10px; background-repeat: no-repeat; cursor: pointer; @@ -116,7 +124,6 @@ @include background-icon($icon, clickable, 48); } } - .cw-clipboard-item-title, .cw-blockadder-item-title { display: inline-block; font-weight: 600; @@ -124,7 +131,7 @@ } .cw-blockadder-item-description { display: inline-block; - margin: 0 0 4px; + margin: 0 0 4px; } } .cw-blockadder-item-fav { @@ -184,30 +191,42 @@ margin-top: 5px; } -.cw-containeradder-item { - margin-bottom: 4px; - padding: 1em 1em 1em 6em; - @include background-icon(unit-test, clickable, 48); - background-position: 12px center; - background-repeat: no-repeat; +.cw-containeradder-item-wrapper { border: solid thin var(--content-color-40); - cursor: pointer; + .cw-sortable-handle { + opacity: 0; + } &:hover { border-color: var(--base-color); - } - @each $item, $icon in $containeradder-items { - &.cw-containeradder-item-#{$item} { - @include background-icon($icon, clickable, 48); + .cw-sortable-handle { + opacity: 1; } } - .cw-containeradder-item-title { - font-weight: 600; + .cw-containeradder-item { + margin-bottom: 4px; + padding: 1em 1em 1em 6em; + @include background-icon(unit-test, clickable, 48); + background-position: 16px center; + background-repeat: no-repeat; + cursor: pointer; + + @each $item, $icon in $containeradder-items { + &.cw-containeradder-item-#{$item} { + @include background-icon($icon, clickable, 48); + } + } + + .cw-containeradder-item-title { + font-weight: 600; + } } } + + .cw-container-style-selector { display: flex; margin-bottom: 8px; @@ -264,15 +283,23 @@ border: solid thin var(--content-color-40); max-width: 248px; + .cw-sortable-handle { + opacity: 0; + } + &:hover { border-color: var(--base-color); + + .cw-sortable-handle { + opacity: 1; + } } .cw-clipboard-item { width: calc(100% - 36px); padding: 64px 10px 4px 10px; @include background-icon(unit-test, clickable, 48); - background-position: 10px 10px; + background-position: 16px 10px; background-repeat: no-repeat; cursor: pointer; background-color: var(--white); @@ -296,6 +323,16 @@ font-weight: 600; margin-bottom: 2px; } + + .cw-clipboard-item-description { + display: -webkit-box; + margin: 0 0 4px; + max-height: 4em; + word-break: break-word; + overflow: hidden; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } } .cw-clipboard-item-action-menu-wrapper { padding: 8px; diff --git a/resources/assets/stylesheets/scss/courseware/containers/accordion.scss b/resources/assets/stylesheets/scss/courseware/containers/accordion.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..44cb56151f0078d3f4ffe0f852175998cf8b5d44 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/accordion.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/accordion.scss @@ -0,0 +1,23 @@ +.cw-container-accordion { + + .cw-block-wrapper-active { + .cw-container-accordion-block-list:empty { + height: 4em; + border: dashed 2px var(--content-color-40); + } + } + + .cw-collapsible-content > .cw-companion-box { + border: none; + margin-bottom: 0; + } + + .cw-container-accordion-block-list { + list-style: none; + padding: 0; + + &.cw-container-accordion-sort-mode { + padding: 8px 0 0 0; + } + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss index 489a1fa0ea59c1c39f8e799657f962f5908126e3..7798d502dd97e18a6d8a65c4a2650ed5ab1e8f0c 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss @@ -75,31 +75,16 @@ &.cw-block-wrapper-active { padding: 14px 10px; - - .cw-tabs-content { - padding: 14px 0; - } } .cw-block-item { padding: 0; margin: 0 0 1em 0; - } - } - - .cw-container-list-block-list { - padding: 0; - list-style: none; - } - - .cw-container-tabs-block-list { - list-style: none; - padding: 1em 1em 0 1em; - } - .cw-container-accordion-block-list { - list-style: none; - padding: 0 1em; + &:last-child { + margin: 0; + } + } } } diff --git a/resources/assets/stylesheets/scss/courseware/containers/list.scss b/resources/assets/stylesheets/scss/courseware/containers/list.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..063addaa0fda235ad5d4f4de853465239b7664cb 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/list.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/list.scss @@ -0,0 +1,19 @@ +.cw-container-list { + .cw-block-wrapper-active { + + >.cw-companion-box { + border: none; + margin-bottom: 0; + } + + .cw-container-list-block-list:empty { + height: 4em; + border: dashed 2px var(--content-color-40); + } + } + + .cw-container-list-block-list { + padding: 0; + list-style: none; + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/containers/tabs.scss b/resources/assets/stylesheets/scss/courseware/containers/tabs.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bb3760580e60c6c2c2aff2bf8ac2d9e44c246ea7 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/tabs.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/tabs.scss @@ -0,0 +1,23 @@ +.cw-container-tabs { + .cw-tab-active > .cw-companion-box { + border: none; + margin-bottom: 0; + } + + .cw-container-tabs-block-list { + list-style: none; + padding: 4px 0; + } + + .cw-block-wrapper-active { + + .cw-container-tabs-block-list:empty { + height: 4em; + border: dashed 2px var(--content-color-40); + } + + .cw-tabs-content { + padding: 8px 0 0 0; + } + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/sortable.scss b/resources/assets/stylesheets/scss/courseware/sortable.scss index 9f90de4e827a727d9c8fefe90562ed471afe2d13..7eaccdad97c299536d56c077835dd8c1fdac7bb1 100644 --- a/resources/assets/stylesheets/scss/courseware/sortable.scss +++ b/resources/assets/stylesheets/scss/courseware/sortable.scss @@ -7,6 +7,47 @@ &.cw-sortable-handle-dragging { cursor: grabbing; } + &.cw-sortable-handle-blocks { + position: relative; + top: -42px; + left: -2px; + margin-right: -15px; + margin-bottom: -10px; + } + &.cw-sortable-handle-containers { + position: absolute; + left: 0; + top: 50%; + margin-top: -12px; + } + + &.cw-sortable-handle-blockadder { + display: block; + position: absolute; + padding: 8px 64px 24px 4px; + margin: 8px 4px; + background-position: top left; + } + + &.cw-sortable-handle-containeradder { + display: block; + position: absolute; + padding: 8px 64px 48px 4px; + margin: 8px 4px; + background-position: top left; + } + + &.cw-sortable-handle-clipboard { + display: block; + position: absolute; + padding: 8px 64px 24px 4px; + margin: 8px 4px; + background-position: top left; + } +} + +.cw-container-dragitem { + display: none; } .cw-block-item-sortable { @@ -29,12 +70,6 @@ } } -.container-ghost, -.block-ghost { - opacity: 0.6; -} - - .cw-container-wrapper-edit { width: calc(100% - 64px); @@ -56,4 +91,67 @@ .cw-container-header { font-style: italic; } +} + +.cw-sortable-handle-blocks { + position: relative; +} + +.cw-container-dragitem { + display: none; +} + +.container-ghost { + background: var(--white); + border: dashed 2px var(--content-color-40); + margin-top: -5px; + margin-bottom: 15px; + + a { + opacity: 0; + } + + button { + opacity: 0; + } + + &.cw-clipboard-item-wrapper { + .cw-clipboard-item-action-menu-wrapper { + opacity: 0; + } + } + + .cw-sortable-handle, + .cw-container { + opacity: 0; + } +} + +.block-ghost { + background: var(--white); + border: dashed 2px var(--content-color-40); + padding-top: 5px; + padding-left: 5px; + height: 100px; + margin-bottom: 15px; + margin-top: -5px; + + a { + opacity: 0; + } + + button { + opacity: 0; + } + + &.cw-clipboard-item-wrapper { + .cw-clipboard-item-action-menu-wrapper { + opacity: 0; + } + } + + .cw-sortable-handle, + .cw-block { + opacity: 0; + } } \ No newline at end of file diff --git a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue index 2c6ac693d886998d7771858b1055df6ca3ccb682..8d4e496403b5090352f3bf82a76d0701a47a2f81 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBeforeAfterBlock.vue @@ -152,22 +152,25 @@ export default { }, }, mounted() { - this.loadFileRefs(this.block.id).then((response) => { - for (let i = 0; i < response.length; i++) { - if (response[i].id === this.beforeFileId) { - this.beforeFile = response[i]; - } + if (this.block.id) { + this.loadFileRefs(this.block.id).then((response) => { + for (let i = 0; i < response.length; i++) { + if (response[i].id === this.beforeFileId) { + this.beforeFile = response[i]; + } - if (response[i].id === this.afterFileId) { - this.afterFile = response[i]; + if (response[i].id === this.afterFileId) { + this.afterFile = response[i]; + } } - } - this.currentBeforeFile = this.beforeFile; - this.currentAfterFile = this.afterFile; - }); + this.currentBeforeFile = this.beforeFile; + this.currentAfterFile = this.afterFile; + }); - this.loadImages(); + this.loadImages(); + } + this.initCurrentData(); }, methods: { diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue index 794e4a9adce3607043df1d35df2efb580301b043..d26446825d9ddf4b5ed8f34eeb2d25df57c9cf7a 100644 --- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue @@ -322,13 +322,15 @@ export default { } }, mounted() { - this.loadFileRefs(this.block.id).then((response) => { - this.file = response[0]; - this.currentFile = this.file; - this.initCurrentData(); - this.buildCanvas(); - }); - this.loadImageFile(); + if (this.block.id) { + this.loadFileRefs(this.block.id).then((response) => { + this.file = response[0]; + this.currentFile = this.file; + this.initCurrentData(); + this.buildCanvas(); + }); + this.loadImageFile(); + } }, methods: { ...mapActions({ @@ -632,7 +634,9 @@ export default { data.attributes.payload.canvas_draw.clickTool = JSON.stringify(this.clickTool); data.attributes.payload.canvas_draw.Text = JSON.stringify(this.Text); - await this.updateUserDataFields(data); + if (data.id) { + await this.updateUserDataFields(data); + } }, storeBlock() { let attributes = {}; diff --git a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue index 42d2d8156b0d4e69002ff5a996a5df05f2cf385e..fa85a4119e3d3bcae7363bef4f933208574bf0c0 100644 --- a/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareChartBlock.vue @@ -172,7 +172,7 @@ export default { updateBlock: 'updateBlockInContainer', }), initCurrentData() { - this.currentContent = this.content; + this.currentContent = this.content || []; this.currentLabel = this.label; this.currentType = this.type; this.setItemTab = 0; diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue index 7f7b10ce003f1805cc7ea6fbd8f8f99750fccffc..0af177e27c52cca228a5898c30d2113070133b23 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue @@ -371,11 +371,13 @@ export default { }, }, mounted() { - this.loadFileRefs(this.block.id).then((response) => { - this.file = response[0]; - this.currentFile = this.file; - this.initPdfTask(); - }); + if (this.block.id) { + this.loadFileRefs(this.block.id).then((response) => { + this.file = response[0]; + this.currentFile = this.file; + this.initPdfTask(); + }); + } this.initCurrentData(); }, methods: { diff --git a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue index 3ee9a96b2224d617f3ab6db4087f8dc6c9de8a1d..c45f03af11043f4432c449265eb57f35ea12a19a 100644 --- a/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue +++ b/resources/vue/components/courseware/containers/CoursewareAccordionContainer.vue @@ -35,10 +35,15 @@ </ul> <template v-else> <template v-if="!processing"> + <courseware-companion-box + v-if="section.blocks.length === 0" + mood="pointing" + :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')"> + </courseware-companion-box> <draggable v-if="canEdit" - class="cw-container-list-block-list cw-container-list-sort-mode" - :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']" + class="cw-container-accordion-block-list cw-container-accordion-sort-mode" + :class="[section.blocks.length === 0 ? 'cw-container-accordion-sort-mode-empty' : '']" tag="ol" role="listbox" v-model="section.blocks" @@ -46,7 +51,6 @@ handle=".cw-sortable-handle" group="blocks" @start="isDragging = true" - @end="dropBlock" :containerId="container.id" :sectionId="index" > @@ -70,9 +74,6 @@ /> </li> </draggable> - <template v-if="canAddElements"> - <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/> - </template> </template> <div v-else class="progress-wrapper"> <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" /> @@ -184,7 +185,7 @@ export default { return this.viewMode === 'edit'; }, blocks() { - if (!this.container) { + if (!this.container || this.container.newContainer) { return []; } @@ -211,13 +212,14 @@ export default { let sections = this.currentContainer.attributes.payload.sections; const unallocated = new Set(this.blocks.map(({ id }) => id)); - - for (let section of sections) { - section.locked = false; - section.blocks = section.blocks.map((id) => view.blockById({id})).filter(Boolean); - for (let sectionBlock of section.blocks) { - if (sectionBlock?.id && unallocated.has(sectionBlock.id)) { - unallocated.delete(sectionBlock.id); + if (sections) { + for (let section of sections) { + section.locked = false; + section.blocks = section.blocks.map((id) => view.blockById({id})).filter(Boolean); + for (let sectionBlock of section.blocks) { + if (sectionBlock?.id && unallocated.has(sectionBlock.id)) { + unallocated.delete(sectionBlock.id); + } } } } @@ -417,7 +419,14 @@ export default { } }, currentSections: { - handler() { + handler(newSections, oldSections) { + if (oldSections.length > 0 && + newSections[oldSections.length -1].blocks.length > oldSections[oldSections.length - 1].blocks.length) { + this.$emit('blockAdded'); + this.$nextTick(() => { + this.sortInSlots.push(oldSections.length - 1); + }); + } if (this.keyboardSelected) { this.$nextTick(() => { this.$refs['sortableHandle' + this.keyboardSelected][0].focus(); diff --git a/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue deleted file mode 100644 index 78cc4eda3bc714b7717b9763e07916f98c1ec235..0000000000000000000000000000000000000000 --- a/resources/vue/components/courseware/containers/CoursewareContainerAdderItem.vue +++ /dev/null @@ -1,52 +0,0 @@ -<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/containers/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue index 1fa05f50ae8ac5393448a2579983630106e63737..ad60b1453fc1dffa3b2d2ea3fde6c31ac323f7bf 100644 --- a/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/containers/CoursewareDefaultContainer.vue @@ -1,7 +1,7 @@ <template> <div - class="cw-container cw-container-list" - :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '']" + class="cw-container" + :class="['cw-container-colspan-' + colSpan, showEditMode && canEdit ? 'cw-container-active' : '', containerClass]" > <div class="cw-container-content"> <header v-if="showEditMode && canEdit" class="cw-container-header" :class="{ 'cw-container-header-open': isOpen }"> diff --git a/resources/vue/components/courseware/containers/CoursewareListContainer.vue b/resources/vue/components/courseware/containers/CoursewareListContainer.vue index 6d8089f61a0d1835275b99883fc606b64fa785c5..7f23210676a334c1db6b90ae9a2be4c92b43c9b9 100644 --- a/resources/vue/components/courseware/containers/CoursewareListContainer.vue +++ b/resources/vue/components/courseware/containers/CoursewareListContainer.vue @@ -18,6 +18,11 @@ <span id="operation" class="assistive-text"> {{$gettext('Drücken Sie die Leertaste, um neu anzuordnen.')}} </span> + <courseware-companion-box + v-if="empty" + mood="pointing" + :msgCompanion="$gettext('Dieser Abschnitt enthält keine Blöcke.')"> + </courseware-companion-box> <draggable v-if="showEditMode && canEdit" class="cw-container-list-block-list cw-container-list-sort-mode" @@ -57,7 +62,6 @@ /> </li> </draggable> - <courseware-block-adder-area :container="container" :section="0" /> </template> <div v-else class="progress-wrapper" :style="{ height: contentHeight + 'px' }"> <studip-progress-indicator :description="$gettext('Vorgang wird bearbeitet...')" /> @@ -124,7 +128,7 @@ export default { return this.viewMode === 'edit'; }, blocks() { - if (!this.container) { + if (!this.container || this.container.newContainer) { return []; } let containerBlocks = this.container.relationships.blocks.data.map(({ id }) => this.blockById({ id })).filter(Boolean); @@ -135,6 +139,9 @@ export default { return sortedBlocks.concat(unallocatedBlocks); }, + empty() { + return this.blockList.length === 0; + } }, methods: { ...mapActions({ diff --git a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue index 725c017eaab8f8dbea3da49b57dff319616121ef..391cc820e918ab32f51594d2b35c05e6ebe3698f 100644 --- a/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue +++ b/resources/vue/components/courseware/containers/CoursewareTabsContainer.vue @@ -45,9 +45,14 @@ </ul> <template v-else> <template v-if="canEdit"> + <courseware-companion-box + v-if="section.blocks.length === 0" + mood="pointing" + :msgCompanion="$gettext('Dieses Fach enthält keine Blöcke.')"> + </courseware-companion-box> <draggable - class="cw-container-list-block-list cw-container-list-sort-mode" - :class="[section.blocks.length === 0 ? 'cw-container-list-sort-mode-empty' : '']" + class="cw-container-tabs-block-list cw-container-tabs-sort-mode" + :class="[section.blocks.length === 0 ? 'cw-container-tabs-sort-mode-empty' : '']" tag="ol" role="listbox" v-model="section.blocks" @@ -79,9 +84,6 @@ /> </li> </draggable> - <template v-if="canAddElements"> - <courseware-block-adder-area :container="container" :section="index" @updateContainerContent="updateContent"/> - </template> </template> </template> </courseware-tab> @@ -187,7 +189,7 @@ export default { return this.viewMode === 'edit'; }, blocks() { - if (!this.container) { + if (!this.container || this.container.newContainer) { return []; } @@ -215,12 +217,14 @@ export default { const unallocated = new Set(this.blocks.map(({ id }) => id)); - for (let section of sections) { - section.locked = false; - section.blocks = section.blocks.map((id) => view.blockById({id})).filter(Boolean); - for (let sectionBlock of section.blocks) { - if (sectionBlock?.id && unallocated.has(sectionBlock.id)) { - unallocated.delete(sectionBlock.id); + if (sections) { + for (let section of sections) { + section.locked = false; + section.blocks = section.blocks.map((id) => view.blockById({id})).filter(Boolean); + for (let sectionBlock of section.blocks) { + if (sectionBlock?.id && unallocated.has(sectionBlock.id)) { + unallocated.delete(sectionBlock.id); + } } } } diff --git a/resources/vue/components/courseware/containers/container-components.js b/resources/vue/components/courseware/containers/container-components.js index 03c98b4359261531a88b701f14bbf7414ce09eee..3bb48dc926f0ff40b5509b087c5e574d447ed504 100644 --- a/resources/vue/components/courseware/containers/container-components.js +++ b/resources/vue/components/courseware/containers/container-components.js @@ -32,6 +32,7 @@ import CoursewareTimelineBlock from '../blocks/CoursewareTimelineBlock.vue'; import CoursewareTypewriterBlock from '../blocks/CoursewareTypewriterBlock.vue'; import CoursewareVideoBlock from '../blocks/CoursewareVideoBlock.vue'; //layout +import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; import StudipIcon from '../../StudipIcon.vue'; import StudipProgressIndicator from '../../StudipProgressIndicator.vue'; @@ -70,6 +71,7 @@ const ContainerComponents = { CoursewareTypewriterBlock, CoursewareVideoBlock, //layout + CoursewareCompanionBox, StudipIcon, StudipProgressIndicator, }; diff --git a/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue deleted file mode 100644 index 921014aae394cf76fce0c9a3d9adbbecf390eafb..0000000000000000000000000000000000000000 --- a/resources/vue/components/courseware/structural-element/CoursewareBlockadderItem.vue +++ /dev/null @@ -1,115 +0,0 @@ -<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/structural-element/CoursewareClipboardItem.vue b/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue deleted file mode 100644 index aa975c8b46ce33ac58f7211914756899b067f8aa..0000000000000000000000000000000000000000 --- a/resources/vue/components/courseware/structural-element/CoursewareClipboardItem.vue +++ /dev/null @@ -1,246 +0,0 @@ -<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('Kopie des aktuellen Stands 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/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index ea0d92be4aacd136507bd44ca4f40ecf3a5ccb25..dcb396984dd3908efea4b360f59c7f523a4d5bb7 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -209,7 +209,7 @@ <studip-progress-indicator v-if="processing" :description="$gettext('Vorgang wird bearbeitet...')" /> </div> </div> - <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> + <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> </div> </div> <studip-dialog @@ -613,6 +613,7 @@ import CoursewareDateInput from '../layouts/CoursewareDateInput.vue'; import StockImageSelector from '../../stock-images/SelectorDialog.vue'; import StudipDialog from '../../StudipDialog.vue'; import draggable from 'vuedraggable'; +import containerMixin from '@/vue/mixins/courseware/container.js'; import { mapActions, mapGetters } from 'vuex'; export default { @@ -638,7 +639,7 @@ export default { }), props: ['canVisit', 'orderedStructuralElements', 'structuralElement'], - mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin], + mixins: [CoursewareExport, CoursewareOerMessage, colorMixin, wizardMixin, containerMixin], data() { return { @@ -1239,6 +1240,7 @@ export default { showElementRemoveLockDialog: 'showElementRemoveLockDialog', updateShowSuggestOerDialog: 'updateShowSuggestOerDialog', updateContainer: 'updateContainer', + createContainer: 'createContainer', sortContainersInStructualElements: 'sortContainersInStructualElements', loadTask: 'loadTask', loadStructuralElement: 'loadStructuralElement', diff --git a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue index f7fe3030d69daa8655274f5ac5896ed53fba2955..47b66b19f319e1313b9622f40172c87b854105a7 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareBlockadderItem.vue @@ -1,6 +1,7 @@ <template> <div class="cw-blockadder-item-wrapper"> - <a href="#" @click.prevent="addBlock" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]"> + <span class="cw-sortable-handle cw-sortable-handle-blockadder"></span> + <a href="#" class="cw-blockadder-item" :class="['cw-blockadder-item-' + type]" @click.prevent="addBlock"> <header class="cw-blockadder-item-title"> {{ title }} </header> @@ -20,10 +21,12 @@ </template> <script> +import containerMixin from '@/vue/mixins/courseware/container.js'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-blockadder-item', + mixins: [containerMixin], components: {}, props: { title: String, @@ -39,8 +42,9 @@ export default { ...mapGetters({ blockAdder: 'blockAdder', blockById: 'courseware-blocks/byId', - lastCreatedBlock: 'courseware-blocks/lastCreated', + containerById: 'courseware-containers/byId', favoriteBlockTypes: 'favoriteBlockTypes', + lastCreatedBlock: 'courseware-blocks/lastCreated', }), blockTypeIsFav() { return this.favoriteBlockTypes.some((type) => type.type === this.type); @@ -71,37 +75,16 @@ export default { updateContainer: 'updateContainer', removeFavoriteBlockType: 'removeFavoriteBlockType', addFavoriteBlockType: 'addFavoriteBlockType', + setAdderStorage: 'coursewareBlockAdder', }), 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.'), - }); - } + this.setAdderStorage({ + container: this.blockAdder.container, + section: this.blockAdder.section, + type: this.type , + position: false + }); + this.addNewBlock(); }, toggleFavItem() { if (this.blockTypeIsFav) { diff --git a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue index 27d9e58c5f18487ebbe419d4261cb6b078e3fbe2..132362628a75b04c901036bdaa5345afde2ef606 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareClipboardItem.vue @@ -1,13 +1,14 @@ <template> <div class="cw-clipboard-item-wrapper"> - <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertItem"> + <span class="cw-sortable-handle cw-sortable-handle-clipboard"></span> + <button class="cw-clipboard-item" :class="['cw-clipboard-item-' + kind]" @click.prevent="insertClipboardItem"> <header class="sr-only"> {{ srTitle }} </header> <header class="cw-clipboard-item-title" aria-hidden="true"> {{ name }} </header> - <p class="cw-clipboard-item-description"> + <p class="cw-clipboard-item-description" :title="description"> {{ description }} </p> </button> @@ -16,7 +17,7 @@ class="cw-clipboard-item-action-menu" :items="menuItems" :context="name" - @insertItemCopy="insertItemCopy" + @insertItemCopy="insertClipboardItemCopy" @editItem="showEditItem" @deleteItem="deleteItem" /> @@ -37,7 +38,7 @@ <form class="default" @submit.prevent=""> <label> {{ $gettext('Titel') }} - <input type="text" v-model="currentClipboard.attributes.name" /> + <input type="text" v-model="currentClipboard.attributes.name" > </label> <label> {{ $gettext('Beschreibung') }} @@ -50,11 +51,12 @@ </template> <script> +import clipboardMixin from '@/vue/mixins/courseware/clipboard.js'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-clipboard-item', - components: {}, + mixins: [clipboardMixin], props: { clipboard: Object, }, @@ -62,20 +64,10 @@ export default { 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() { @@ -125,9 +117,6 @@ export default { 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 }) : @@ -150,80 +139,14 @@ export default { 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 }); - } - } + insertClipboardItem() { + this.insertItem(this.clipboard); }, - 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 }); - } - } + insertClipboardItemCopy() { + this.insertItemCopy(this.clipboard); }, + deleteItem() { this.deleteClipboard({ id: this.clipboard.id }); }, diff --git a/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue index 78cc4eda3bc714b7717b9763e07916f98c1ec235..539d87187580b8db57806c34310a36e0b042686f 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareContainerAdderItem.vue @@ -1,20 +1,33 @@ <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> + <div class="cw-containeradder-item-wrapper"> + <span class="cw-sortable-handle cw-sortable-handle-containeradder"></span> + <a href="#" @click.prevent="addNewContainer"> + <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> + <li class="cw-container-dragitem cw-container-item-sortable" ref="container-drag-item"> + <div class="cw-container cw-container-list cw-container-item" :class="['cw-container-colspan-' + colspan]"> + <div class="cw-container-content"> + <header class="cw-container-header">{{ title }}</header> + <div class="cw-block-wrapper">{{ description }}</div> + </div> + </div> + </li> + </div> </template> <script> +import containerMixin from '@/vue/mixins/courseware/container'; import { mapActions } from 'vuex'; + export default { name: 'courseware-container-adder-item', - components: {}, + mixins: [containerMixin], props: { title: String, description: String, @@ -22,31 +35,24 @@ export default { colspan: String, firstSection: String, secondSection: String, + newPosition: Number, }, 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 = { + addNewContainer() { + this.addContainer({ + type: this.type, 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.'), + sections: { + firstSection: this.firstSection, + secondSection: this.secondSection + }, + newPosition: null }); }, }, - mounted() {}, }; </script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue index 252f3fe6ce635d2d2cf1881dfdd2b8f663356c44..300a4e24748d2826f470e953fb443f70ab806ca8 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue @@ -52,9 +52,12 @@ import CoursewareToolbarBlocks from './CoursewareToolbarBlocks.vue'; import CoursewareToolbarContainers from './CoursewareToolbarContainers.vue'; import CoursewareToolbarClipboard from './CoursewareToolbarClipboard.vue'; +import containerMixin from '@/vue/mixins/courseware/container.js'; +import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-toolbar', + mixins: [containerMixin], components: { CoursewareToolbarBlocks, CoursewareToolbarContainers, @@ -73,6 +76,10 @@ export default { }; }, computed: { + ...mapGetters({ + relatedContainers: 'courseware-containers/related', + structuralElementById: 'courseware-structural-elements/byId', + }), toolbarStyle() { const footerHeight = document.getElementById('main-footer').getBoundingClientRect().height; const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top')); @@ -87,6 +94,12 @@ export default { top: this.toolbarTop + 'px', }; }, + containers() { + return this.relatedContainers({ + parent: this.structuralElementById({id: this.$route.params.id}), + relationship: 'containers' + }); + }, toolbarHeader() { let header = ''; if (this.activeTool === 'blockAdder') { @@ -142,6 +155,7 @@ export default { window.addEventListener('scroll', this.updateToolbarTop); window.addEventListener('resize', this.onResize); }); + this.resetAdderStorage(); }, beforeDestroy() { window.removeEventListener('scroll', this.updateToolbarTop); @@ -149,6 +163,11 @@ export default { }, watch: { + containers(oldValue, newValue) { + if (newValue && oldValue.length !== newValue.length) { + this.resetAdderStorage(); + } + }, toolsActive(newState, oldState) { let view = this; if (newState) { diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue index 3cd783c3fc620d53c46ce716a76de79b7cfb20fd..b4081aa2781d26b541228c7beb81e434b1dda6a3 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue @@ -10,7 +10,8 @@ :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')" /> <span class="input-group-append" @click.stop> - <button v-if="searchInput" + <button + v-if="searchInput" type="button" class="button reset-search" id="reset-search" @@ -19,7 +20,7 @@ > <studip-icon shape="decline" :size="20"></studip-icon> </button> - <button + <button type="submit" class="button" id="search-btn" @@ -38,8 +39,8 @@ v-for="category in blockCategories" :key="category.type" class="button" - :class="{'button-active': category.type === currentFilterCategory }" - :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'" + :class="{ 'button-active': category.type === currentFilterCategory }" + :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'" @click="selectCategory(category.type)" > {{ category.title }} @@ -47,14 +48,32 @@ </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')" - /> + <draggable + v-if="filteredBlockTypes.length > 0" + class="cw-blockadder-item-list" + tag="div" + role="listbox" + v-model="filteredBlockTypes" + handle=".cw-sortable-handle-blockadder" + :group="{ name: 'blocks', pull: 'clone', put: 'false' }" + :clone="cloneBlock" + :sort="false" + :emptyInsertThreshold="20" + @start="dragBlockStart($event)" + @end="dropNewBlock($event)" + ref="sortables" + sectionId="0" + > + <courseware-blockadder-item + v-for="(block, index) in filteredBlockTypes" + :key="index" + :title="block.title" + :type="block.type" + :data-blocktype="block.type" + :description="block.description" + @blockAdded="$emit('blockAdded')" + /> + </draggable> </div> <courseware-companion-box v-else @@ -67,25 +86,31 @@ <script> import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; +import containerMixin from '@/vue/mixins/courseware/container.js'; +import draggable from 'vuedraggable'; + import { mapActions, mapGetters } from 'vuex'; export default { name: 'courseware-toolbar-blocks', + mixins: [containerMixin], components: { CoursewareBlockadderItem, - CoursewareCompanionBox + CoursewareCompanionBox, + draggable, }, data() { return { searchInput: '', currentFilterCategory: '', filteredBlockTypes: [], - categorizedBlocks: [] + categorizedBlocks: [], + + isDragging: false, }; }, computed: { ...mapGetters({ - adderStorage: 'blockAdder', unorderedBlockTypes: 'blockTypes', favoriteBlockTypes: 'favoriteBlockTypes', }), @@ -104,18 +129,24 @@ export default { { 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' } + { title: this.$gettext('Biografie'), type: 'biography' }, ]; - } + }, }, methods: { ...mapActions({ - companionWarning: 'companionWarning' + companionWarning: 'companionWarning', + createBlock: 'createBlockInContainer', + setAdderStorage: 'coursewareBlockAdder', }), 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.')}); + this.companionWarning({ + info: this.$gettext( + 'Leider ist Ihr Suchbegriff zu kurz. Der Suchbegriff muss mindestens 3 Zeichen lang sein.' + ), + }); return; } this.filteredBlockTypes = this.blockTypes; @@ -131,33 +162,36 @@ export default { 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) - )); + 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()]; + 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)); + 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()); + 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) { + if (lowercaseTags.filter((blockTag) => blockTag.includes(tag.toLowerCase())).length > 0) { return true; } } @@ -183,7 +217,55 @@ export default { this.filteredBlockTypes = this.blockTypes; this.searchInput = ''; this.currentFilterCategory = ''; - } + }, + cloneBlock(original) { + original.attributes = { + 'block-type': original.type, + payload: { + file_id: '', + folder_id: '', + background_image_id: '', + files: [], + url: 'studip.de', + sort: 'none', + tool_id: '', + cards: [], + text: ' ', + shapes: {}, + type: 'Persönliches Ziel', + content: [{ color: 'blue', label: '', value: '0' }], + }, + }; + original.relationships = { + 'user-data-field': { + data: { id: null }, + }, + }; + return original; + }, + dragBlockStart(e) { + this.isDragging = true; + }, + async dropNewBlock(e) { + const target = e.to.__vue__.$attrs; + const blockType = e.item.__vue__.$attrs['data-blocktype']; + + // only execute if dropped in destined list + if (!target.containerId) { + return; + } + // set chosen container and section and pass block data + this.setAdderStorage({ + container: this.containerById({ id: target.containerId }), + section: target.sectionId, + type: blockType, + position: e.newIndex, + }); + + await this.addNewBlock(); + this.resetAdderStorage(); + this.isDragging = false; + }, }, mounted() { this.filteredBlockTypes = this.blockTypes; @@ -206,7 +288,7 @@ export default { if (newValue) { this.loadSearch(); } - } - } -} -</script> \ No newline at end of file + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue index e7898cd03c1813bcf0f1aba770f72810d69472ce..98cd5730b408f79a23d3541cc4f76fa29c77de7f 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue @@ -3,12 +3,27 @@ <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')" - /> + <draggable + class="cw-element-inserter-wrapper" + tag="div" + role="listbox" + v-model="clipboardBlocks" + handle=".cw-sortable-handle-clipboard" + :group="{ name: 'blocks', pull: 'clone', put: 'false' }" + :sort="false" + :clone="cloneClipboard" + :emptyInsertThreshold="20" + @end="dropClipboardBlock($event)" + ref="clipboardSortables" + sectionId="0" + > + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardBlocks" + :key="index" + :clipboard="clipboard" + @inserted="$emit('blockAdded')" + /> + </draggable> </div> <button class="button trash" @click="clearClipboard('courseware-blocks')"> {{ $gettext('Alle Blöcke aus Merkliste entfernen') }} @@ -23,11 +38,25 @@ <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" - /> + <draggable + class="cw-element-inserter-wrapper" + tag="div" + role="listbox" + v-model="clipboardContainers" + handle=".cw-sortable-handle-clipboard" + :group="{ name: 'description', pull: 'clone', put: 'false' }" + :sort="false" + :emptyInsertThreshold="20" + :clone="cloneClipboardContainer" + @end="dropNewContainer($event)" + ref="clipboardContainerSortables" + > + <courseware-clipboard-item + v-for="(clipboard, index) in clipboardContainers" + :key="index" + :clipboard="clipboard" + /> + </draggable> </div> <button class="button trash" @click="clearClipboard('courseware-containers')"> {{ $gettext('Alle Abschnitte aus Merkliste entfernen') }} @@ -48,7 +77,6 @@ @confirm="executeDeleteClipboard" @close="closeDeleteClipboardDialog" ></studip-dialog> - </div> </template> @@ -56,38 +84,45 @@ import CoursewareClipboardItem from './CoursewareClipboardItem.vue'; import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue'; +import containerMixin from '@/vue/mixins/courseware/container.js'; +import clipboardMixin from '@/vue/mixins/courseware/clipboard.js'; +import draggable from 'vuedraggable'; import StudipDialog from '../../StudipDialog.vue'; import { mapActions, mapGetters } from 'vuex'; export default { name: 'cw-tools-blockadder', + mixins: [containerMixin, clipboardMixin], components: { CoursewareClipboardItem, CoursewareCompanionBox, CoursewareCollapsibleBox, StudipDialog, + draggable, }, data() { return { showDeleteClipboardDialog: false, - deleteClipboardType: null + deleteClipboardType: null, + isDragging: false, }; }, computed: { ...mapGetters({ + containerById: 'courseware-containers/byId', usersClipboards: 'courseware-clipboards/all', - userId: 'userId' + userId: 'userId', }), clipboardBlocks() { return this.usersClipboards - .filter(clipboard => clipboard.attributes['object-type'] === 'courseware-blocks') + .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') + .filter((clipboard) => clipboard.attributes['object-type'] === 'courseware-containers') .sort((a, b) => b.attributes.mkdate < a.attributes.mkdate); }, textDeleteClipboardTitle() { @@ -107,12 +142,12 @@ export default { return this.$gettext('Möchten Sie die Merkliste für Abschnitte unwiderruflich leeren?'); } return ''; - } + }, }, methods: { ...mapActions({ companionWarning: 'companionWarning', - deleteUserClipboards: 'deleteUserClipboards' + deleteUserClipboards: 'deleteUserClipboards', }), clearClipboard(type) { this.deleteClipboardType = type; @@ -120,15 +155,77 @@ export default { }, executeDeleteClipboard() { if (this.deleteClipboardType) { - this.deleteUserClipboards({uid: this.userId, type: this.deleteClipboardType}); + this.deleteUserClipboards({ uid: this.userId, type: this.deleteClipboardType }); } this.closeDeleteClipboardDialog(); }, closeDeleteClipboardDialog() { this.showDeleteClipboardDialog = false; this.deleteClipboardType = null; - } - } -}; + }, + cloneClipboard(original) { + original.attributes['block-type'] = original.attributes['object-kind']; + original.attributes.payload = {}; + original.relationships = { + 'user-data-field': { + data: { id: null }, + }, + block: {}, + }; + return original; + }, + async dropClipboardBlock(e) { + const target = e.to.__vue__.$attrs; + // only execute if dropped in destined list + if (!target.containerId) { + return; + } + // set chosen container and section and insert the clipboard block + this.setAdderStorage({ + container: this.containerById({ id: target.containerId }), + section: target.sectionId, + position: e.newIndex, + }); + await this.insertItem(e.item.__vue__._data.currentClipboard); + this.resetAdderStorage(); + }, + cloneClipboardContainer(original) { + original.newContainer = true; + original.clipContainer = true; + original.attributes['container-type'] = original.attributes['object-kind']; + original.type = 'courseware-containers'; + original.attributes.payload = {}; + original.relationships = {}; + original.relationships.container = {}; + original.relationships.blocks = {}; + original.relationships.blocks.data = {}; + return original; + }, + dropNewContainer(e) { + // if the container is dropped back to its original list, do nothing / cancel the operation + if (e.to.className === 'cw-containeradder-item-list' || e.to.className === 'cw-element-inserter-wrapper') { + this.isDragging = false; + return; + } -</script> \ No newline at end of file + const item = e.item._underlying_vm_; + + // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin + if (item.clipContainer) { + this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex); + } else { + const data = { + type: item.attributes['container-type'], + colspan: item.containerStyle, + sections: { + firstSection: item.firstSection, + secondSection: item.secondSection, + }, + newPosition: e.newIndex, + }; + this.addContainer(data); + } + }, + }, +}; +</script> diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue index 12fea1bbd058545acdf625385663038bb9cb8b09..c5781b24e49164e23b734bcd7ea0bb356bf88b76 100644 --- a/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue +++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarContainers.vue @@ -1,66 +1,160 @@ <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> + <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> + <draggable + class="cw-containeradder-item-list" + tag="div" + role="listbox" + v-model="containerTypes" + handle=".cw-sortable-handle-containeradder" + :group="{ name: 'description', pull: 'clone', put: 'false' }" + :clone="cloneContainer" + :sort="false" + :emptyInsertThreshold="20" + @start="dragContainerStart($event)" + @end="dropNewContainer($event)" + ref="containerSortables" + > + <courseware-container-adder-item + v-for="container in containerTypes" + :key="container.type" + :title="container.title" + :type="container.type" + :colspan="selectedContainerStyle" + :description="container.description" + :firstSection="firstSection" + :secondSection="secondSection" + :newPosition="newContainerPosition" + ></courseware-container-adder-item> + </draggable> </div> </template> <script> import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; +import containerMixin from '@/vue/mixins/courseware/container.js'; +import draggable from 'vuedraggable'; import { mapGetters } from 'vuex'; export default { name: 'courseware-toolbar-containers', + mixins: [containerMixin], components: { - CoursewareContainerAdderItem + CoursewareContainerAdderItem, + draggable, }, data() { return { - selectedContainerStyle: 'full' + selectedContainerStyle: 'full', + isDragging: false, }; }, computed: { ...mapGetters({ - containerTypes: 'containerTypes' + containerTypes: 'containerTypes', + structuralElementById: 'courseware-structural-elements/byId', + relatedContainers: 'courseware-containers/related', + }), containerStyles() { return [ - { key: 0, title: this.$gettext('Volle Breite'), colspan: 'full'}, + { 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' } + { key: 2, title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' }, ]; }, + containers() { + return this.relatedContainers({ + parent: this.structuralElementById({id: this.$route.params.id}), + relationship: 'containers' + }); + }, + newContainerPosition() { + return this.containers?.length || 0; + }, + firstSection() { + return this.$gettext('erstes Element'); + }, + secondSection() { + return this.$gettext('zweites Element'); + }, + }, + methods: { + cloneContainer(original) { + original.newContainer = true; + original.attributes = {}; + original.attributes['container-type'] = original.type; + original.attributes.payload = {}; + original.relationships = {}; + original.relationships.blocks = {}; + original.relationships.blocks.data = {}; + original.firstSection = this.firstSection; + original.secondSection = this.secondSection; + original.containerStyle = this.selectedContainerStyle; + return original; + }, + cloneClipboardContainer(original) { + original.newContainer = true; + original.clipContainer = true; + original.attributes['container-type'] = original.attributes['object-kind']; + original.type = 'courseware-containers'; + original.attributes.payload = {}; + original.relationships = {}; + original.relationships.container = {}; + original.relationships.blocks = {}; + original.relationships.blocks.data = {}; + return original; + }, + dragContainerStart(e) { + this.isDragging = true; + }, + dropNewContainer(e) { + // if the container is dropped back to its original list, do nothing / cancel the operation + if (e.to.className === 'cw-containeradder-item-list' || e.to.className === 'cw-element-inserter-wrapper') { + this.isDragging = false; + return; + } + + const item = e.item._underlying_vm_; + + // if the container is from the clipboard, insert it via clipboard mixin, else add it via container mixin + if (item.clipContainer) { + this.insertItem(e.item.__vue__._data.currentClipboard, e.newIndex); + } else { + const data = { + type: item.attributes['container-type'], + colspan: item.containerStyle, + sections: { + firstSection: item.firstSection, + secondSection: item.secondSection + }, + newPosition: e.newIndex + }; + this.addContainer(data); + } + }, } -} -</script> \ No newline at end of file +}; +</script> diff --git a/resources/vue/mixins/courseware/clipboard.js b/resources/vue/mixins/courseware/clipboard.js new file mode 100644 index 0000000000000000000000000000000000000000..8ae4170dbdb4bee5059e032b1807d4f76b4e25ed --- /dev/null +++ b/resources/vue/mixins/courseware/clipboard.js @@ -0,0 +1,125 @@ +import containerMixin from './container'; +import { mapActions, mapGetters } from 'vuex'; + +const clipboardMixin = { + mixins: [containerMixin], + data() { + return { + text: { + errorMessage: this.$gettext('Es ist ein Fehler aufgetreten.'), + positionWarning: this.$gettext( + 'Bitte fügen Sie der Seite einen Abschnitt hinzu, damit der Block eingefügt werden kann.' + ), + blockSuccess: this.$gettext('Der Block wurde erfolgreich eingefügt.'), + containerSuccess: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'), + } + } + }, + computed: { + ...mapGetters({ + blockAdder: 'blockAdder', + currentElement: 'currentElement', + }), + + blockAdderActive() { + return Object.keys(this.blockAdder).length !== 0; + }, + }, + methods: { + ...mapActions({ + companionInfo: 'companionInfo', + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + clipboardInsertBlock: 'clipboardInsertBlock', + clipboardInsertContainer: 'clipboardInsertContainer', + loadStructuralElement: 'loadStructuralElement', + loadContainer: 'loadContainer', + loadClipboard: 'courseware-clipboards/loadById', + }), + + async insertItem(clipboard, itemPosition) { + const isBlock = clipboard.attributes['object-type'] === 'courseware-blocks'; + let insertError = false; + + if (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: clipboard, + }); + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadContainer(this.blockAdder.container.id); + if (this.blockAdder.position !== undefined) { + await this.sortClipboardBlock(); + } + this.companionSuccess({ info: this.text.blockSuccess }); + } + } else { + try { + await this.clipboardInsertContainer({ + parentId: this.currentElement, + clipboard: clipboard, + }); + + } catch (error) { + insertError = true; + this.companionWarning({ info: this.text.errorMessage }); + } + if (!insertError) { + await this.loadStructuralElement(this.currentElement); + itemPosition = itemPosition ? itemPosition : 'last'; + this.sortContainer(itemPosition); + this.companionSuccess({ info: this.text.containerSuccess }); + } + } + }, + + async insertItemCopy(clipboard) { + const isBlock = clipboard.attributes['object-type'] === 'courseware-blocks'; + let insertError = false; + + if (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 }); + } + } + }, + } +} + +export default clipboardMixin; \ No newline at end of file diff --git a/resources/vue/mixins/courseware/container.js b/resources/vue/mixins/courseware/container.js index e4e02ddef53c7fb025ed400ca6bbbb13be0758fb..db60af2f30b26a205c93348f9974a84bb8d4fbcc 100644 --- a/resources/vue/mixins/courseware/container.js +++ b/resources/vue/mixins/courseware/container.js @@ -3,9 +3,14 @@ import { mapActions, mapGetters } from 'vuex'; const containerMixin = { computed: { ...mapGetters({ + blockAdder: 'blockAdder', blockById: 'courseware-blocks/byId', containerById: 'courseware-containers/byId', pluginManager: 'pluginManager', + lastCreatedBlock: 'courseware-blocks/lastCreated', + lastCreatedContainers: 'courseware-containers/lastCreated', + blockedByAnotherUser: 'currentElementBlockedByAnotherUser', + currentStructuralElement: 'currentStructuralElement', }), }, created: function () { @@ -16,8 +21,16 @@ const containerMixin = { updateBlock: 'updateBlock', updateContainer: 'updateContainer', loadContainer: 'courseware-containers/loadById', + loadStructuralElement: 'loadStructuralElement', lockObject: 'lockObject', unlockObject: 'unlockObject', + createBlock: 'createBlockInContainer', + createContainer: 'createContainer', + companionInfo: 'companionInfo', + companionSuccess: 'companionSuccess', + companionWarning: 'companionWarning', + sortContainersInStructualElements: 'sortContainersInStructualElements', + setAdderStorage: 'coursewareBlockAdder', }), dropBlock(e) { this.isDragging = false; // implemented bei echt container type @@ -51,14 +64,16 @@ const containerMixin = { await this.unlockObject({ id: data.blockId, type: 'courseware-blocks' }); // update origin container - let originContainer = this.containerById({ id: data.originContainerId}); - originContainer.attributes.payload.sections[data.originSectionId].blocks = data.originSectionBlockList; - await this.lockObject({ id: data.originContainerId, type: 'courseware-containers' }); - await this.updateContainer({ - container: originContainer, - structuralElementId: originContainer.relationships['structural-element'].data.id, - }); - await this.unlockObject({ id: data.originContainerId, type: 'courseware-containers' }); + if (data.originContainerId) { + let originContainer = this.containerById({ id: data.originContainerId}); + originContainer.attributes.payload.sections[data.originSectionId].blocks = data.originSectionBlockList; + await this.lockObject({ id: data.originContainerId, type: 'courseware-containers' }); + await this.updateContainer({ + container: originContainer, + structuralElementId: originContainer.relationships['structural-element'].data.id, + }); + await this.unlockObject({ id: data.originContainerId, type: 'courseware-containers' }); + } // update target container let targetContainer = this.containerById({ id: data.targetContainerId}); @@ -77,7 +92,171 @@ const containerMixin = { return Array.isArray(firstSet) && Array.isArray(secondSet) && firstSet.length === secondSet.length && firstSet.every((val, index) => val === secondSet[index]); - } + }, + async addNewBlock() { + if (this.blockAdder.container) { + const targetContainer = this.blockAdder.container; + const section = this.blockAdder.section; + const type = this.blockAdder.type; + const position = this.blockAdder.position; + + try { + await this.lockObject({ id: targetContainer.id, type: 'courseware-containers' }); + } catch (error) { + if (error.status === 409) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + } else { + console.log(error); + } + } + + await this.createBlock({ + container: targetContainer, + section: section, + blockType: type, + }); + // get the just created block to add it to a container and adjust its position if applicable + const newBlock = this.lastCreatedBlock; + + // if the block is dropped to a specific position, save it at the correct position + if (position !== false) { + targetContainer.attributes.payload.sections[section].blocks.splice( + position, 0, newBlock.id); + // otherwise put it in the last position of the last container + } else { + targetContainer.attributes.payload.sections[section].blocks.push(newBlock.id); + } + + const structuralElementId = targetContainer.relationships['structural-element'].data.id; + + await this.updateContainer({ container: targetContainer, structuralElementId: structuralElementId }); + await this.unlockObject({ id: targetContainer.id, type: 'courseware-containers' }); + this.companionSuccess({ + info: this.$gettext('Der Block wurde erfolgreich eingefügt.'), + }); + } else { + // companion action + this.companionWarning({ + info: this.$gettext('Bitte fügen Sie der Seite einen Abschnitt hinzu, damit der Block eingefügt werden kann.'), + }); + } + }, + async sortClipboardBlock() { + const targetContainer = this.blockAdder.container; + const position = this.blockAdder.position; + + try { + await this.lockObject({ id: targetContainer.id, type: 'courseware-containers' }); + } catch (error) { + if (error.status === 409) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + } else { + console.log(error); + } + } + const containerBlocks = targetContainer.attributes.payload.sections[this.blockAdder.section].blocks; + containerBlocks.splice(position, 0, containerBlocks.pop()); + + const structuralElementId = targetContainer.relationships['structural-element'].data.id; + try { + await this.updateContainer({ container: targetContainer, structuralElementId: structuralElementId }); + await this.unlockObject({ id: targetContainer.id, type: 'courseware-containers' }); + } catch (error) { + this.companionWarning({ + info: this.$gettext('Der Block konnte nicht hinzugefügt werden, bitte versuchen Sie es erneut.'), + }); + console.log(error); + } + }, + async addContainer(data) { + const type = data.type; + const colspan = data.colspan; + const firstSection = data.sections.firstSection; + const secondSection = data.sections.secondSection; + + let attributes = {}; + attributes["container-type"] = type; + let sections = []; + if (type === 'list') { + sections = [{ name: firstSection, icon: '', blocks: [] }]; + } else { + sections = [{ name: firstSection, icon: '', blocks: [] },{ name: secondSection, icon: '', blocks: [] }]; + } + attributes.payload = { + colspan: colspan, + sections: sections, + }; + await this.createContainer({ structuralElementId: this.$route.params.id, attributes: attributes }); + this.companionSuccess({ + info: this.$gettext('Der Abschnitt wurde erfolgreich eingefügt.'), + }); + + // if the container was dropped to a specific position, sort it and update the structural element + if (data.newPosition != null) { + this.sortContainer(data.newPosition); + } + }, + async sortContainer(newContainerPos) { + if (this.blockedByAnotherUser) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + return false; + } + try { + await this.lockObject({ id: this.currentStructuralElement.id, type: 'courseware-structural-elements' }); + } catch (error) { + if (error.status === 409) { + this.companionInfo({ info: this.$gettext('Diese Seite wird bereits bearbeitet.') }); + } else { + console.log(error); + } + + return false; + } + // insert the newly created container at the correct position + let containerList = []; + this.currentStructuralElement.relationships.containers.data.forEach(container => { + containerList.push(container); + }); + + if (newContainerPos != null) { + // find the container with the highest index (= latest addition) because it isn't + // added at the bottom when it is a clipboard + const highestIndexContainer = containerList.reduce((previous, current) => { + return (previous && parseInt(previous.id) > parseInt(current.id)) ? previous : current; + }, 0); + + // get the last created container if a new container is added, or + // the highest index container in the case of a clipboard + const newestContainer = this.lastCreatedContainers?.id || highestIndexContainer.id; + const tempPosition = containerList.findIndex(x => x.id === newestContainer); + const newContainer = containerList.splice(tempPosition, 1)[0]; + + if (newContainerPos === 'last') { + newContainerPos = containerList.length; + } + containerList.splice(newContainerPos, 0, newContainer); + } + await this.sortContainersInStructualElements({ + structuralElement: this.currentStructuralElement, + containers: containerList, + }); + await this.loadStructuralElement(this.currentStructuralElement.id); + + this.$emit('select', this.currentStructuralElement.id); + + return false; + }, + resetAdderStorage() { + // choose the last container and its last section as the default adder slot + // for adding blocks and containers via click + if (this.containers) { + this.setAdderStorage({ + container: this.containers[this.containers.length - 1], + section: this.containers[this.containers.length - 1].attributes.payload.sections.length - 1 + }); + } + }, + } };