diff --git a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php index 219395d2209930a09441e6894fc5cf729a0903cb..7f2467f946d70290b918c77c9df19c50ea4db389 100644 --- a/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/ContainersUpdate.php @@ -73,6 +73,12 @@ class ContainersUpdate extends JsonApiController 'data.relationships.structural-element.data.id' ); } + if (self::arrayHas($json, 'data.attributes.container-type')) { + $resource->container_type = self::arrayGet( + $json, + 'data.attributes.container-type' + ); + } $resource->position = $json['data']['attributes']['position']; diff --git a/public/assets/images/icons/blue/column-full.svg b/public/assets/images/icons/blue/column-full.svg new file mode 100644 index 0000000000000000000000000000000000000000..41898e99d58369b38b907b89ed5d2da3a60b511f --- /dev/null +++ b/public/assets/images/icons/blue/column-full.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#28497c;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m56,20v24H8v-24h48m4-4H4v32h56V16h0Z"/></g></g></svg> \ No newline at end of file diff --git a/public/assets/images/icons/blue/column-half-centered.svg b/public/assets/images/icons/blue/column-half-centered.svg new file mode 100644 index 0000000000000000000000000000000000000000..07936918f48680cfb01b2375ac59a42e4735dacf --- /dev/null +++ b/public/assets/images/icons/blue/column-half-centered.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#28497c;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m4,16v32h56V16H4Zm39,4v24h-22v-24h22Zm-35,0h9v24H8v-24Zm48,24h-9v-24h9v24Z"/></g></g></svg> \ No newline at end of file diff --git a/public/assets/images/icons/blue/column-half.svg b/public/assets/images/icons/blue/column-half.svg new file mode 100644 index 0000000000000000000000000000000000000000..f941d1b3249177a924407fc67c39ec17e5c94d86 --- /dev/null +++ b/public/assets/images/icons/blue/column-half.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#28497c;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m4,16v32h56V16H4Zm4,4h22v24H8v-24Zm48,24h-22v-24h22v24Z"/></g></g></svg> \ No newline at end of file diff --git a/public/assets/images/icons/white/column-full.svg b/public/assets/images/icons/white/column-full.svg new file mode 100644 index 0000000000000000000000000000000000000000..8d75738e63a0f02a6acd6189dbd0d05bda0be927 --- /dev/null +++ b/public/assets/images/icons/white/column-full.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#fff;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m56,20v24H8v-24h48m4-4H4v32h56V16h0Z"/></g></g></svg> \ No newline at end of file diff --git a/public/assets/images/icons/white/column-half-centered.svg b/public/assets/images/icons/white/column-half-centered.svg new file mode 100644 index 0000000000000000000000000000000000000000..424d1b93c22c94760e58c555e6a93bdbf81739cc --- /dev/null +++ b/public/assets/images/icons/white/column-half-centered.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#fff;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m4,16v32h56V16H4Zm39,4v24h-22v-24h22Zm-35,0h9v24H8v-24Zm48,24h-9v-24h9v24Z"/></g></g></svg> \ No newline at end of file diff --git a/public/assets/images/icons/white/column-half.svg b/public/assets/images/icons/white/column-half.svg new file mode 100644 index 0000000000000000000000000000000000000000..9ad612b7b4ca0d5b3b818cca5ec71cc5f7a2c881 --- /dev/null +++ b/public/assets/images/icons/white/column-half.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="UTF-8"?><svg id="b" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.f{fill:none;}.g{fill:#fff;}</style></defs><g id="c"><rect class="f" width="64" height="64"/></g><g id="d"><g id="e"><path class="g" d="m4,16v32h56V16H4Zm4,4h22v24H8v-24Zm48,24h-22v-24h22v24Z"/></g></g></svg> \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 77e849103cbde1a743e527f39f5fa2f2db9293ca..eb3a9eaca46ad63ed85d4dd65460dd2583bb935a 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -2006,7 +2006,54 @@ b l o c k a d d e r } } +.cw-container-style-selector { + display: flex; + margin-bottom: 8px; + + label { + border: solid thin var(--content-color-40); + padding: calc(0.5em + 32px) 1em 0.5em 1em; + color: var(--base-color); + text-align: center; + width: 33%; + background-position: center 0.5em; + background-repeat: no-repeat; + cursor: pointer; + + &.full { + @include background-icon(column-full, clickable, 32); + } + &.half { + @include background-icon(column-half, clickable, 32); + } + &.half-center { + @include background-icon(column-half-centered, clickable, 32); + } + &: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] { + position: absolute; + opacity: 0; + width: 0; + + &:focus + label { + outline-color: Highlight; + outline-color: -webkit-focus-ring-color; + outline-style: auto; + outline-width: 1px; + } + } +} /* * * * * * * * * * * * * b l o c k a d d e r e n d @@ -5577,3 +5624,75 @@ s h e l f i t e m s /* * * * * * * * * * * * * * * * * * s h e l f i t e m s e n d * * * * * * * * * * * * * * * * * */ + +/* * * * * * * * * +r a d i o s e t +* * * * * * * * */ + +.cw-radioset { + display: flex; + flex-direction: row; + justify-content: center; + margin-bottom: 1em; + .cw-radioset-box { + width: 128px; + height: 128px; + text-align: center; + margin-right: 16px; + border: solid thin var(--content-color-40); + &.selected { + border-color: var(--base-color); + background-color: var(--content-color-20); + } + &:last-child { + margin-right: 0; + } + label { + height: 100%; + width: 100%; + margin: 0; + cursor: pointer; + .label-icon { + background-position: center 8px; + background-repeat: no-repeat; + height: 64px; + padding: 8px; + &.accordion { + @include background-icon(block-accordion, clickable, 64); + } + &.list { + @include background-icon(view-list, clickable, 64); + } + &.tabs { + @include background-icon(block-tabs, clickable, 64); + } + &.full { + @include background-icon(column-full, clickable, 64); + } + &.half { + @include background-icon(column-half, clickable, 64); + } + &.half-center { + @include background-icon(column-half-centered, clickable, 64); + } + } + + } + input[type=radio] { + position: absolute; + opacity: 0; + width: 0; + + &:focus + label { + outline-color: Highlight; + outline-color: -webkit-focus-ring-color; + outline-style: auto; + outline-width: 1px; + } + } + } +} + +/* * * * * * * * * * * * +r a d i o s e t e n d +* * * * * * * * * * * */ diff --git a/resources/vue/components/courseware/CoursewareContainerActions.vue b/resources/vue/components/courseware/CoursewareContainerActions.vue index 4795984797de6435cd486085d5a01cf586979381..87c0e12a5e608ccc41ccde6b79af61476e5d1270 100644 --- a/resources/vue/components/courseware/CoursewareContainerActions.vue +++ b/resources/vue/components/courseware/CoursewareContainerActions.vue @@ -4,6 +4,7 @@ :items="menuItems" :context="container.attributes.title" @editContainer="editContainer" + @changeContainer="changeContainer" @deleteContainer="deleteContainer" @removeLock="removeLock" /> @@ -41,7 +42,8 @@ export default { if (this.container.attributes["container-type"] !== 'list') { menuItems.push({ id: 1, label: this.$gettext('Abschnitt bearbeiten'), icon: 'edit', emit: 'editContainer' }); } - menuItems.push({ id: 2, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }); + menuItems.push({ id: 2, label: this.$gettext('Abschnitt verändern'), icon: 'settings', emit: 'changeContainer' }); + menuItems.push({ id: 3, label: this.$gettext('Abschnitt löschen'), icon: 'trash', emit: 'deleteContainer' }); } if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) { @@ -67,6 +69,9 @@ export default { editContainer() { this.$emit('editContainer'); }, + changeContainer() { + this.$emit('changeContainer'); + }, deleteContainer() { this.$emit('deleteContainer'); }, diff --git a/resources/vue/components/courseware/CoursewareDefaultContainer.vue b/resources/vue/components/courseware/CoursewareDefaultContainer.vue index e1fa87bf5480d6e36eff4007c39934da1d0c626d..a3f99d0f07220a19904720102fb1f007599ef706 100644 --- a/resources/vue/components/courseware/CoursewareDefaultContainer.vue +++ b/resources/vue/components/courseware/CoursewareDefaultContainer.vue @@ -17,6 +17,7 @@ :canEdit="canEdit" :container="container" @editContainer="displayEditDialog" + @changeContainer="displayChangeDialog" @deleteContainer="displayDeleteDialog" @removeLock="displayRemoveLockDialog" /> @@ -47,6 +48,60 @@ </template> </studip-dialog> + <studip-dialog + v-if="showChangeDialog" + :title="$gettext('Abschnitt verändern')" + :confirmText="$gettext('Speichern')" + confirmClass="accept" + :closeText="$gettext('Abbrechen')" + closeClass="cancel" + @close="closeChange" + @confirm="storeChange" + height="520" + width="480" + > + <template v-slot:dialogContent> + <form class="default" @submit.prevent=""> + <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-type"> + <p id="container-type">{{ $gettext('Typ') }}</p> + <div class="cw-radioset"> + <div + v-for="(container, index) in containerTypes" + :key="index" + class="cw-radioset-box" + :class="[container.type === changeType ? 'selected' : '']" + > + <input type="radio" :id="'type-' + container.type" :value="container.type" v-model="changeType" name="container-type"/> + <label :for="'type-' + container.type" > + <div class="label-icon" :class="[container.type, container.type === changeType ? 'selected' : '']"></div> + <p>{{ container.title }}</p> + </label> + + </div> + </div> + </div> + <div class="cw-radioset-wrapper" role="group" aria-labelledby="container-style"> + <p id="container-style">{{ $gettext('Stil') }}</p> + <div class="cw-radioset"> + <div + v-for="(style, index) in containerStyles" + :key="index" + class="cw-radioset-box" + :class="[style.colspan === changeStyle ? 'selected' : '']" + > + <input type="radio" :id="'style-' + style.colspan" :value="style.colspan" v-model="changeStyle" name="container-style"/> + <label :for="'style-' + style.colspan"> + <div class="label-icon" :class="[style.colspan, style.colspan === changeStyle ? 'selected' : '']"></div> + <p>{{ style.title }}</p> + </label> + </div> + </div> + </div> + </form> + </template> + </studip-dialog> + + <studip-dialog v-if="showDeleteDialog" :title="textDeleteTitle" @@ -92,6 +147,7 @@ export default { return { showDeleteDialog: false, showEditDialog: false, + showChangeDialog: false, showRemoveLockDialog: false, textEditConfirm: this.$gettext('Speichern'), textEditClose: this.$gettext('Schließen'), @@ -101,6 +157,9 @@ export default { textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Abschnitts wirklich aufheben?'), isOpen: true, + + changeType: '', + changeStyle: '', }; }, computed: { @@ -109,7 +168,8 @@ export default { userId: 'userId', userById: 'users/byId', viewMode: 'viewMode', - currentElementisLink: 'currentElementisLink' + currentElementisLink: 'currentElementisLink', + containerTypes: 'containerTypes', }), showEditMode() { return this.viewMode === 'edit' && !this.currentElementisLink; @@ -139,11 +199,22 @@ export default { blockingUserName() { return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : ''; }, + containerStyles() { + return [ + { title: this.$gettext('Volle Breite'), colspan: 'full'}, + { title: this.$gettext('Halbe Breite'), colspan: 'half' }, + { title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' }, + ]; + }, + type() { + return this.container.attributes['container-type']; + } }, methods: { ...mapActions({ companionInfo: 'companionInfo', companionWarning: 'companionWarning', + updateContainer: 'updateContainer', loadContainer: 'courseware-containers/loadById', deleteContainer: 'deleteContainer', lockObject: 'lockObject', @@ -161,6 +232,53 @@ export default { await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); this.showEditDialog = true; }, + async displayChangeDialog() { + await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); + if (this.blockedByAnotherUser) { + this.companionInfo({ info: this.$gettext('Dieser Abschnitt wird bereits bearbeitet.') }); + + return false; + } + + await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + this.changeType = this.type; + this.changeStyle = this.colSpan; + this.showChangeDialog = true; + }, + async storeChange() { + await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); + this.closeChange(); + if (this.blockedByAnotherUser) { + this.companionWarning({ + info: this.$gettextInterpolate( + this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, da %{blockingUserName} die Bearbeitung übernommen hat.'), + {blockingUserName: this.blockingUserName} + ) + }); + return; + } + if (this.blockerId === null) { + await this.lockObject({ id: this.container.id, type: 'courseware-containers' }); + } + + let container = this.container; + container.attributes['container-type'] = this.changeType; + container.attributes.payload.colspan = this.changeStyle; + await this.updateContainer({ + container: container, + structuralElementId: this.container.relationships['structural-element'].data.id, + }); + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + await this.loadContainer({id : this.container.id }); + }, + async closeChange() { + await this.loadContainer({ id: this.container.id }); + this.showChangeDialog = false; + if (this.blockedByThisUser) { + await this.unlockObject({ id: this.container.id, type: 'courseware-containers' }); + } + await this.loadContainer({ id: this.container.id, options: { include: 'edit-blocker' } }); + }, async closeEdit() { await this.loadContainer({ id: this.container.id }); this.$emit('closeEdit'); diff --git a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue index efdeb4ef50e57cbe02b0034d9aa7e1aebc537419..15adf2bdcb489d96af277b8c3c859e523df8e2ae 100644 --- a/resources/vue/components/courseware/CoursewareToolsBlockadder.vue +++ b/resources/vue/components/courseware/CoursewareToolsBlockadder.vue @@ -65,23 +65,39 @@ /> </courseware-tab> <courseware-tab :name="$gettext('Abschnitte')" :selected="showContaineradder" :index="1" :style="{ maxHeight: maxHeight + 'px' }"> - <courseware-collapsible-box - v-for="(style, index) in containerStyles" - :key="index" - :title="style.title" - :open="index === 0" - > - <courseware-container-adder-item - v-for="(container, index) in containerTypes" - :key="index" - :title="container.title" - :type="container.type" - :colspan="style.colspan" - :description="container.description" - :firstSection="$gettext('erstes Element')" - :secondSection="$gettext('zweites Element')" - ></courseware-container-adder-item> - </courseware-collapsible-box> + <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-tabs> </div> @@ -90,7 +106,6 @@ <script> import CoursewareTabs from './CoursewareTabs.vue'; import CoursewareTab from './CoursewareTab.vue'; -import CoursewareCollapsibleBox from './CoursewareCollapsibleBox.vue'; import CoursewareBlockadderItem from './CoursewareBlockadderItem.vue'; import CoursewareContainerAdderItem from './CoursewareContainerAdderItem.vue'; import CoursewareCompanionBox from './CoursewareCompanionBox.vue'; @@ -101,7 +116,6 @@ export default { components: { CoursewareTabs, CoursewareTab, - CoursewareCollapsibleBox, CoursewareBlockadderItem, CoursewareContainerAdderItem, CoursewareCompanionBox, @@ -119,7 +133,8 @@ export default { searchInput: '', currentFilterCategory: '', filteredBlockTypes: [], - categorizedBlocks: [] + categorizedBlocks: [], + selectedContainerStyle: 'full' }; }, computed: { @@ -140,9 +155,9 @@ export default { }, containerStyles() { return [ - { title: this.$gettext('Standard'), colspan: 'full'}, - { title: this.$gettext('Halbe Breite'), colspan: 'half' }, - { title: this.$gettext('Halbe Breite (zentriert)'), colspan: 'half-center' }, + { 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() {