Fix deep-selectors
This commit is contained in:
parent
4477b7c5ed
commit
4b55f8952c
|
|
@ -1,38 +1,20 @@
|
|||
<template>
|
||||
<div
|
||||
:class="{'hideable-element--greyed-out': hidden}"
|
||||
:class="{ 'hideable-element--greyed-out': hidden }"
|
||||
class="content-block__container hideable-element content-list__parent"
|
||||
>
|
||||
<div
|
||||
:class="specialClass"
|
||||
:style="instrumentStyle"
|
||||
class="content-block"
|
||||
data-cy="content-block"
|
||||
>
|
||||
<div
|
||||
class="block-actions"
|
||||
v-if="canEditModule && !isInstrumentBlock"
|
||||
>
|
||||
<user-widget
|
||||
v-bind="me"
|
||||
class="block-actions__user-widget content-block__user-widget"
|
||||
v-if="isMine"
|
||||
/>
|
||||
<div :class="specialClass" :style="instrumentStyle" class="content-block" data-cy="content-block">
|
||||
<div class="block-actions" v-if="canEditModule && !isInstrumentBlock">
|
||||
<user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget" v-if="isMine" />
|
||||
<more-options-widget>
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="!isInstrumentBlock"
|
||||
>
|
||||
<li class="popover-links__link" v-if="!isInstrumentBlock">
|
||||
<popover-link
|
||||
data-cy="duplicate-content-block-link"
|
||||
text="Duplizieren"
|
||||
@link-action="duplicateContentBlock(contentBlock)"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="isMine"
|
||||
>
|
||||
<li class="popover-links__link" v-if="isMine">
|
||||
<popover-link
|
||||
data-cy="delete-content-block-link"
|
||||
text="Löschen"
|
||||
|
|
@ -40,22 +22,13 @@
|
|||
/>
|
||||
</li>
|
||||
|
||||
<li
|
||||
class="popover-links__link"
|
||||
v-if="isMine"
|
||||
>
|
||||
<popover-link
|
||||
text="Bearbeiten"
|
||||
@link-action="editContentBlock(contentBlock)"
|
||||
/>
|
||||
<li class="popover-links__link" v-if="isMine">
|
||||
<popover-link text="Bearbeiten" @link-action="editContentBlock(contentBlock)" />
|
||||
</li>
|
||||
</more-options-widget>
|
||||
</div>
|
||||
<div class="content-block__visibility">
|
||||
<visibility-action
|
||||
:block="contentBlock"
|
||||
v-if="canEditModule"
|
||||
/>
|
||||
<visibility-action :block="contentBlock" v-if="canEditModule" />
|
||||
</div>
|
||||
|
||||
<h3
|
||||
|
|
@ -66,10 +39,7 @@
|
|||
>
|
||||
{{ instrumentLabel }}
|
||||
</h3>
|
||||
<h4
|
||||
class="content-block__title"
|
||||
v-if="!contentBlock.indent"
|
||||
>
|
||||
<h4 class="content-block__title" v-if="!contentBlock.indent">
|
||||
{{ contentBlock.title }}
|
||||
</h4>
|
||||
|
||||
|
|
@ -85,110 +55,109 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<add-content-button
|
||||
:where="{after: contentBlock}"
|
||||
v-if="canEditModule"
|
||||
/>
|
||||
<add-content-button :where="{ after: contentBlock }" v-if="canEditModule" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddContentButton from '@/components/AddContentButton';
|
||||
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
|
||||
import UserWidget from '@/components/UserWidget';
|
||||
import VisibilityAction from '@/components/visibility/VisibilityAction';
|
||||
import AddContentButton from '@/components/AddContentButton';
|
||||
import MoreOptionsWidget from '@/components/MoreOptionsWidget';
|
||||
import UserWidget from '@/components/UserWidget';
|
||||
import VisibilityAction from '@/components/visibility/VisibilityAction';
|
||||
|
||||
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
|
||||
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
||||
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql';
|
||||
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
|
||||
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
|
||||
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.gql';
|
||||
|
||||
import me from '@/mixins/me';
|
||||
import me from '@/mixins/me';
|
||||
|
||||
import {hidden} from '@/helpers/visibility';
|
||||
import {CONTENT_TYPE} from '@/consts/types';
|
||||
import PopoverLink from '@/components/ui/PopoverLink';
|
||||
import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations';
|
||||
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names';
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
import {instrumentCategory} from '@/helpers/instrumentType';
|
||||
import { hidden } from '@/helpers/visibility';
|
||||
import { CONTENT_TYPE } from '@/consts/types';
|
||||
import PopoverLink from '@/components/ui/PopoverLink';
|
||||
import { insertAtIndex, removeAtIndex } from '@/graphql/immutable-operations';
|
||||
import { EDIT_CONTENT_BLOCK_PAGE } from '@/router/module.names';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { instrumentCategory } from '@/helpers/instrumentType';
|
||||
|
||||
const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'));
|
||||
const ContentComponent = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent')
|
||||
);
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ContentBlock',
|
||||
props: {
|
||||
contentBlock: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
parent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
export default {
|
||||
name: 'ContentBlock',
|
||||
props: {
|
||||
contentBlock: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
mixins: [me],
|
||||
|
||||
components: {
|
||||
PopoverLink,
|
||||
ContentComponent,
|
||||
AddContentButton,
|
||||
VisibilityAction,
|
||||
MoreOptionsWidget,
|
||||
UserWidget,
|
||||
parent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
editMode: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
canEditModule() {
|
||||
return !this.contentBlock.indent && this.editMode;
|
||||
},
|
||||
specialClass() {
|
||||
return `content-block--${this.contentBlock.type.toLowerCase()}`;
|
||||
},
|
||||
isInstrumentBlock() {
|
||||
return !!this.contentBlock.instrumentCategory;
|
||||
},
|
||||
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||
instrumentStyle() {
|
||||
if (this.isInstrumentBlock) {
|
||||
return {
|
||||
backgroundColor: this.contentBlock.instrumentCategory.background
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
instrumentLabel() {
|
||||
const contentType = this.contentBlock.type.toLowerCase();
|
||||
if (contentType.startsWith('base')) { // all legacy instruments start with `base`
|
||||
return instrumentCategory(contentType);
|
||||
}
|
||||
if (this.isInstrumentBlock) {
|
||||
return instrumentCategory(this.contentBlock.instrumentCategory.name);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||
instrumentLabelStyle() {
|
||||
if (this.isInstrumentBlock) {
|
||||
return {
|
||||
color: this.contentBlock.instrumentCategory.foreground
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
canEditContentBlock() {
|
||||
return this.isMine && !this.contentBlock.indent;
|
||||
},
|
||||
isMine() {
|
||||
return this.contentBlock.mine;
|
||||
},
|
||||
contentBlocksWithContentLists() {
|
||||
/*
|
||||
mixins: [me],
|
||||
|
||||
components: {
|
||||
PopoverLink,
|
||||
ContentComponent,
|
||||
AddContentButton,
|
||||
VisibilityAction,
|
||||
MoreOptionsWidget,
|
||||
UserWidget,
|
||||
},
|
||||
|
||||
computed: {
|
||||
canEditModule() {
|
||||
return !this.contentBlock.indent && this.editMode;
|
||||
},
|
||||
specialClass() {
|
||||
return `content-block--${this.contentBlock.type.toLowerCase()}`;
|
||||
},
|
||||
isInstrumentBlock() {
|
||||
return !!this.contentBlock.instrumentCategory;
|
||||
},
|
||||
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||
instrumentStyle() {
|
||||
if (this.isInstrumentBlock) {
|
||||
return {
|
||||
backgroundColor: this.contentBlock.instrumentCategory.background,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
instrumentLabel() {
|
||||
const contentType = this.contentBlock.type.toLowerCase();
|
||||
if (contentType.startsWith('base')) {
|
||||
// all legacy instruments start with `base`
|
||||
return instrumentCategory(contentType);
|
||||
}
|
||||
if (this.isInstrumentBlock) {
|
||||
return instrumentCategory(this.contentBlock.instrumentCategory.name);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
// todo: use dynamic css class with v-bind once we're on Vue 3: https://vuejs.org/api/sfc-css-features.html#v-bind-in-css
|
||||
instrumentLabelStyle() {
|
||||
if (this.isInstrumentBlock) {
|
||||
return {
|
||||
color: this.contentBlock.instrumentCategory.foreground,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
canEditContentBlock() {
|
||||
return this.isMine && !this.contentBlock.indent;
|
||||
},
|
||||
isMine() {
|
||||
return this.contentBlock.mine;
|
||||
},
|
||||
contentBlocksWithContentLists() {
|
||||
/*
|
||||
collects all content_list_items in content_lists:
|
||||
{
|
||||
text_block,
|
||||
|
|
@ -202,221 +171,238 @@
|
|||
text_block
|
||||
}
|
||||
*/
|
||||
let contentList = [];
|
||||
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
|
||||
// collect content_list_items
|
||||
if (content.type === 'content_list_item') {
|
||||
contentList = [...contentList, content];
|
||||
if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array
|
||||
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)];
|
||||
return updatedContent;
|
||||
}
|
||||
let contentList = [];
|
||||
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
|
||||
// collect content_list_items
|
||||
if (content.type === 'content_list_item') {
|
||||
contentList = [...contentList, content];
|
||||
if (index === this.contentBlock.contents.length - 1) {
|
||||
// content is last element of contents array
|
||||
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)];
|
||||
return updatedContent;
|
||||
}
|
||||
return newContents;
|
||||
} else {
|
||||
// handle all other items and reset current content_list if necessary
|
||||
if (contentList.length !== 0) {
|
||||
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
|
||||
contentList = [];
|
||||
return newContents;
|
||||
} else {
|
||||
// handle all other items and reset current content_list if necessary
|
||||
if (contentList.length !== 0) {
|
||||
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
|
||||
contentList = [];
|
||||
return newContents;
|
||||
} else {
|
||||
return [...newContents, content];
|
||||
}
|
||||
return [...newContents, content];
|
||||
}
|
||||
}, []);
|
||||
return Object.assign({}, this.contentBlock, {
|
||||
contents: newContent,
|
||||
});
|
||||
},
|
||||
hidden() {
|
||||
return hidden({
|
||||
block: this.contentBlock,
|
||||
schoolClass: this.schoolClass,
|
||||
type: CONTENT_TYPE,
|
||||
});
|
||||
},
|
||||
root() {
|
||||
// we need the root content block id, not the generated content block if inside a content list block
|
||||
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
|
||||
},
|
||||
}
|
||||
}, []);
|
||||
return Object.assign({}, this.contentBlock, {
|
||||
contents: newContent,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
duplicateContentBlock({id}) {
|
||||
const parent = this.parent;
|
||||
this.$apollo.mutate({
|
||||
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
hidden() {
|
||||
return hidden({
|
||||
block: this.contentBlock,
|
||||
schoolClass: this.schoolClass,
|
||||
type: CONTENT_TYPE,
|
||||
});
|
||||
},
|
||||
root() {
|
||||
// we need the root content block id, not the generated content block if inside a content list block
|
||||
return this.contentBlock.root ? this.contentBlock.root : this.contentBlock.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
duplicateContentBlock({ id }) {
|
||||
const parent = this.parent;
|
||||
this.$apollo.mutate({
|
||||
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
duplicateContentBlock: { contentBlock },
|
||||
},
|
||||
}
|
||||
) {
|
||||
if (contentBlock) {
|
||||
const query = CHAPTER_QUERY;
|
||||
const variables = {
|
||||
id: parent.id,
|
||||
};
|
||||
const { chapter } = store.readQuery({ query, variables });
|
||||
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
|
||||
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
|
||||
const data = {
|
||||
chapter: {
|
||||
...chapter,
|
||||
contentBlocks,
|
||||
},
|
||||
};
|
||||
store.writeQuery({ query, variables, data });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
editContentBlock(contentBlock) {
|
||||
const route = {
|
||||
name: EDIT_CONTENT_BLOCK_PAGE,
|
||||
params: {
|
||||
id: contentBlock.id,
|
||||
},
|
||||
};
|
||||
this.$router.push(route);
|
||||
},
|
||||
deleteContentBlock(contentBlock) {
|
||||
this.$modal
|
||||
.open('confirm')
|
||||
.then(() => {
|
||||
this.doDeleteContentBlock(contentBlock);
|
||||
})
|
||||
.catch();
|
||||
},
|
||||
doDeleteContentBlock(contentBlock) {
|
||||
const parent = this.parent;
|
||||
const id = contentBlock.id;
|
||||
this.$apollo.mutate({
|
||||
mutation: DELETE_CONTENT_BLOCK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
update(store, {data: {duplicateContentBlock: {contentBlock}}}) {
|
||||
if (contentBlock) {
|
||||
const query = CHAPTER_QUERY;
|
||||
const variables = {
|
||||
id: parent.id,
|
||||
};
|
||||
const {chapter} = store.readQuery({query, variables});
|
||||
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
|
||||
const contentBlocks = insertAtIndex(chapter.contentBlocks, index, contentBlock);
|
||||
const data = {
|
||||
chapter: {
|
||||
...chapter,
|
||||
contentBlocks,
|
||||
},
|
||||
};
|
||||
store.writeQuery({query, variables, data});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
},
|
||||
editContentBlock(contentBlock) {
|
||||
const route = {
|
||||
name: EDIT_CONTENT_BLOCK_PAGE,
|
||||
params: {
|
||||
id: contentBlock.id,
|
||||
},
|
||||
};
|
||||
this.$router.push(route);
|
||||
},
|
||||
deleteContentBlock(contentBlock) {
|
||||
this.$modal.open('confirm').then(() => {
|
||||
this.doDeleteContentBlock(contentBlock);
|
||||
})
|
||||
.catch();
|
||||
},
|
||||
doDeleteContentBlock(contentBlock) {
|
||||
const parent = this.parent;
|
||||
const id = contentBlock.id;
|
||||
this.$apollo.mutate({
|
||||
mutation: DELETE_CONTENT_BLOCK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
},
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
deleteContentBlock: { success },
|
||||
},
|
||||
},
|
||||
update(store, {data: {deleteContentBlock: {success}}}) {
|
||||
if (success) {
|
||||
const query = CHAPTER_QUERY;
|
||||
const variables = {
|
||||
id: parent.id,
|
||||
};
|
||||
const {chapter} = store.readQuery({query, variables});
|
||||
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id);
|
||||
const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
|
||||
const data = {
|
||||
chapter: {
|
||||
...chapter,
|
||||
contentBlocks,
|
||||
},
|
||||
};
|
||||
store.writeQuery({query, variables, data});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
createContentListOrBlocks(contentList) {
|
||||
return [{
|
||||
}
|
||||
) {
|
||||
if (success) {
|
||||
const query = CHAPTER_QUERY;
|
||||
const variables = {
|
||||
id: parent.id,
|
||||
};
|
||||
const { chapter } = store.readQuery({ query, variables });
|
||||
const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
|
||||
const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
|
||||
const data = {
|
||||
chapter: {
|
||||
...chapter,
|
||||
contentBlocks,
|
||||
},
|
||||
};
|
||||
store.writeQuery({ query, variables, data });
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
createContentListOrBlocks(contentList) {
|
||||
return [
|
||||
{
|
||||
type: 'content_list',
|
||||
contents: contentList,
|
||||
id: contentList[0].id,
|
||||
}];
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "~styles/helpers";
|
||||
@import '~styles/helpers';
|
||||
|
||||
.content-block {
|
||||
margin-bottom: $section-spacing;
|
||||
.content-block {
|
||||
margin-bottom: $section-spacing;
|
||||
position: relative;
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
&__title {
|
||||
line-height: 1.5;
|
||||
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
|
||||
}
|
||||
|
||||
&__instrument-label {
|
||||
margin-bottom: $medium-spacing;
|
||||
@include regular-text();
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__user-widget {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&--base_communication {
|
||||
@include content-box($color-accent-1-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-1-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
line-height: 1.5;
|
||||
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
|
||||
}
|
||||
&--task {
|
||||
@include light-border(bottom);
|
||||
|
||||
&__instrument-label {
|
||||
margin-bottom: $medium-spacing;
|
||||
@include regular-text();
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__user-widget {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&--base_communication {
|
||||
@include content-box($color-accent-1-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-1-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--task {
|
||||
.content-block__title {
|
||||
color: $color-brand;
|
||||
margin-top: $default-padding;
|
||||
margin-bottom: $large-spacing;
|
||||
@include light-border(bottom);
|
||||
|
||||
.content-block__title {
|
||||
color: $color-brand;
|
||||
margin-top: $default-padding;
|
||||
margin-bottom: $large-spacing;
|
||||
@include light-border(bottom);
|
||||
|
||||
@include desktop {
|
||||
margin-top: 0;
|
||||
}
|
||||
@include desktop {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--base_society {
|
||||
@include content-box($color-accent-2-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-2-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--base_interdisciplinary {
|
||||
@include content-box($color-accent-4-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-4-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--instrument {
|
||||
@include content-box-base;
|
||||
}
|
||||
|
||||
/deep/ p {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .text-block {
|
||||
ul {
|
||||
@include list-parent;
|
||||
}
|
||||
|
||||
li {
|
||||
@include list-child;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&--base_society {
|
||||
@include content-box($color-accent-2-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-2-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--base_interdisciplinary {
|
||||
@include content-box($color-accent-4-list);
|
||||
|
||||
.content-block__instrument-label {
|
||||
color: $color-accent-4-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&--instrument {
|
||||
@include content-box-base;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.text-block) {
|
||||
ul {
|
||||
@include list-parent;
|
||||
}
|
||||
|
||||
li {
|
||||
@include list-child;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,114 +1,95 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="solution"
|
||||
data-cy="solution"
|
||||
>
|
||||
<a
|
||||
class="solution__toggle"
|
||||
data-cy="show-solution"
|
||||
@click="toggle"
|
||||
>Lösung
|
||||
<div class="solution" data-cy="solution">
|
||||
<a class="solution__toggle" data-cy="show-solution" @click="toggle"
|
||||
>Lösung
|
||||
<template v-if="!visible">anzeigen</template>
|
||||
<template v-else>ausblenden</template>
|
||||
</a>
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="solution__hidden fade"
|
||||
v-if="visible"
|
||||
>
|
||||
<p
|
||||
class="solution__text solution-text"
|
||||
data-cy="solution-text"
|
||||
|
||||
v-html="sanitizedText"
|
||||
/>
|
||||
<cms-document-block
|
||||
:solution="true"
|
||||
class="solution__document"
|
||||
:value="value.document"
|
||||
v-if="value.document"
|
||||
/>
|
||||
<div class="solution__hidden fade" v-if="visible">
|
||||
<p class="solution__text solution-text" data-cy="solution-text" v-html="sanitizedText" />
|
||||
<cms-document-block :solution="true" class="solution__document" :value="value.document" v-if="value.document" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {sanitizeAsHtml} from '@/helpers/text';
|
||||
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
|
||||
import { sanitizeAsHtml } from '@/helpers/text';
|
||||
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: {CmsDocumentBlock},
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: { CmsDocumentBlock },
|
||||
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
};
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
sanitizedText() {
|
||||
return sanitizeAsHtml(this.value.text);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
sanitizedText() {
|
||||
return sanitizeAsHtml(this.value.text);
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggle() {
|
||||
this.visible = !this.visible;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "~styles/helpers";
|
||||
@import '~styles/helpers';
|
||||
|
||||
.solution {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 15px;
|
||||
.solution {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: 15px;
|
||||
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&__toggle {
|
||||
font-family: $sans-serif-font-family;
|
||||
color: $color-silver-dark;
|
||||
font-size: toRem(15px);
|
||||
/*margin-bottom: 15px;*/
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-weight: $font-weight-regular;
|
||||
}
|
||||
&__toggle {
|
||||
font-family: $sans-serif-font-family;
|
||||
color: $color-silver-dark;
|
||||
font-size: toRem(15px);
|
||||
/*margin-bottom: 15px;*/
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-weight: $font-weight-regular;
|
||||
}
|
||||
|
||||
&__text {
|
||||
&__text {
|
||||
font-size: toRem(18px);
|
||||
color: $color-silver-dark;
|
||||
|
||||
:deep(p) {
|
||||
font-size: toRem(18px);
|
||||
color: $color-silver-dark;
|
||||
}
|
||||
|
||||
/deep/ p {
|
||||
font-size: toRem(18px);
|
||||
:deep(ul) {
|
||||
padding-left: $medium-spacing;
|
||||
|
||||
> li {
|
||||
list-style: disc outside none;
|
||||
color: $color-silver-dark;
|
||||
}
|
||||
|
||||
/deep/ ul {
|
||||
padding-left: $medium-spacing;
|
||||
|
||||
> li {
|
||||
list-style: disc outside none;
|
||||
color: $color-silver-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .3s;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,9 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
:data-scrollto="value.id"
|
||||
class="assignment"
|
||||
>
|
||||
<p
|
||||
class="assignment__main-text"
|
||||
data-cy="assignment-main-text"
|
||||
v-html="assignment.assignment"
|
||||
/>
|
||||
<div :data-scrollto="value.id" class="assignment">
|
||||
<p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" />
|
||||
|
||||
<solution
|
||||
:value="solution"
|
||||
v-if="assignment.solution"
|
||||
/>
|
||||
<solution :value="solution" v-if="assignment.solution" />
|
||||
|
||||
<template v-if="isStudent">
|
||||
<submission-form
|
||||
|
|
@ -33,101 +23,97 @@
|
|||
@spellcheck="spellcheck"
|
||||
/>
|
||||
|
||||
<spell-check
|
||||
:corrections="corrections"
|
||||
:text="submission.text"
|
||||
/>
|
||||
<spell-check :corrections="corrections" :text="submission.text" />
|
||||
|
||||
<p
|
||||
class="assignment__feedback"
|
||||
v-if="assignment.submission.submissionFeedback"
|
||||
v-html="feedbackText"
|
||||
/>
|
||||
<p class="assignment__feedback" v-if="assignment.submission.submissionFeedback" v-html="feedbackText" />
|
||||
</template>
|
||||
<template v-if="!isStudent">
|
||||
<router-link
|
||||
:to="{name: 'submissions', params: { id: assignment.id }}"
|
||||
class="button button--primary"
|
||||
>
|
||||
Zu den
|
||||
Ergebnissen
|
||||
<router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary">
|
||||
Zu den Ergebnissen
|
||||
</router-link>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters} from 'vuex';
|
||||
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
|
||||
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
|
||||
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
|
||||
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
|
||||
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
|
||||
import debounce from 'lodash/debounce';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {sanitize} from '@/helpers/text';
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
|
||||
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
|
||||
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
|
||||
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
|
||||
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
|
||||
import debounce from 'lodash/debounce';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { sanitize } from '@/helpers/text';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const SubmissionForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm'));
|
||||
const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution'));
|
||||
const SpellCheck = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck'));
|
||||
const SubmissionForm = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm')
|
||||
);
|
||||
const Solution = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/Solution')
|
||||
);
|
||||
const SpellCheck = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SpellCheck')
|
||||
);
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
export default {
|
||||
props: ['value'],
|
||||
|
||||
components: {
|
||||
Solution,
|
||||
SubmissionForm,
|
||||
SpellCheck,
|
||||
components: {
|
||||
Solution,
|
||||
SubmissionForm,
|
||||
SpellCheck,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
assignment: {
|
||||
submission: this.initialSubmission(),
|
||||
},
|
||||
me: {
|
||||
permissions: [],
|
||||
},
|
||||
inputType: 'text',
|
||||
unsaved: false,
|
||||
saving: 0,
|
||||
corrections: '',
|
||||
spellcheckLoading: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['scrollToAssignmentId']),
|
||||
final() {
|
||||
return !!this.submission && this.submission.final;
|
||||
},
|
||||
|
||||
data() {
|
||||
submission() {
|
||||
return this.assignment.submission ? this.assignment.submission : {};
|
||||
},
|
||||
isStudent() {
|
||||
return !this.me.permissions.includes('users.can_manage_school_class_content');
|
||||
},
|
||||
solution() {
|
||||
return {
|
||||
assignment: {
|
||||
submission: this.initialSubmission(),
|
||||
},
|
||||
me: {
|
||||
permissions: [],
|
||||
},
|
||||
inputType: 'text',
|
||||
unsaved: false,
|
||||
saving: 0,
|
||||
corrections: '',
|
||||
spellcheckLoading: false,
|
||||
text: this.assignment.solution,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(['scrollToAssignmentId']),
|
||||
final() {
|
||||
return !!this.submission && this.submission.final;
|
||||
},
|
||||
submission() {
|
||||
return this.assignment.submission ? this.assignment.submission : {};
|
||||
},
|
||||
isStudent() {
|
||||
return !this.me.permissions.includes('users.can_manage_school_class_content');
|
||||
},
|
||||
solution() {
|
||||
return {
|
||||
text: this.assignment.solution,
|
||||
};
|
||||
},
|
||||
id() {
|
||||
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
|
||||
},
|
||||
feedbackText() {
|
||||
let feedback = this.assignment.submission.submissionFeedback;
|
||||
let sanitizedFeedbackText = sanitize(feedback.text);
|
||||
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
|
||||
},
|
||||
id() {
|
||||
return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
|
||||
},
|
||||
feedbackText() {
|
||||
let feedback = this.assignment.submission.submissionFeedback;
|
||||
let sanitizedFeedbackText = sanitize(feedback.text);
|
||||
return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(['scrollToAssignmentReady']),
|
||||
_save: debounce(function (submission) {
|
||||
this.saving++;
|
||||
this.$apollo.mutate({
|
||||
methods: {
|
||||
...mapActions(['scrollToAssignmentReady']),
|
||||
_save: debounce(function (submission) {
|
||||
this.saving++;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS,
|
||||
variables: {
|
||||
input: {
|
||||
|
|
@ -138,7 +124,14 @@
|
|||
},
|
||||
},
|
||||
},
|
||||
update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) {
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
updateAssignment: { successful, updatedAssignment },
|
||||
},
|
||||
}
|
||||
) {
|
||||
try {
|
||||
if (successful) {
|
||||
const query = ASSIGNMENT_QUERY;
|
||||
|
|
@ -149,80 +142,82 @@
|
|||
submission,
|
||||
});
|
||||
const data = {
|
||||
assignment
|
||||
assignment,
|
||||
};
|
||||
store.writeQuery({query, variables, data});
|
||||
store.writeQuery({ query, variables, data });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing
|
||||
}
|
||||
},
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.saving--;
|
||||
if (this.saving === 0) {
|
||||
this.unsaved = false;
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
saveInput: function (answer) {
|
||||
// reset corrections on input
|
||||
this.corrections = '';
|
||||
this.unsaved = true;
|
||||
/*
|
||||
}, 500),
|
||||
saveInput: function (answer) {
|
||||
// reset corrections on input
|
||||
this.corrections = '';
|
||||
this.unsaved = true;
|
||||
/*
|
||||
We update the assignment on this component, so the changes are reflected on it. The server does not return
|
||||
the updated entity, to prevent the UI to update when the user is entering his input
|
||||
*/
|
||||
this.assignment.submission.text = answer;
|
||||
this._save(this.assignment.submission);
|
||||
},
|
||||
changeDocumentUrl(documentUrl) {
|
||||
this.assignment.submission.document = documentUrl;
|
||||
this._save(this.assignment.submission);
|
||||
},
|
||||
turnIn() {
|
||||
// reset corrections on turn in
|
||||
this.corrections = '';
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_ASSIGNMENT_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
assignment: {
|
||||
id: this.assignment.id,
|
||||
answer: this.assignment.submission.text,
|
||||
document: this.assignment.submission.document,
|
||||
final: true,
|
||||
},
|
||||
this.assignment.submission.text = answer;
|
||||
this._save(this.assignment.submission);
|
||||
},
|
||||
changeDocumentUrl(documentUrl) {
|
||||
this.assignment.submission.document = documentUrl;
|
||||
this._save(this.assignment.submission);
|
||||
},
|
||||
turnIn() {
|
||||
// reset corrections on turn in
|
||||
this.corrections = '';
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_ASSIGNMENT_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
assignment: {
|
||||
id: this.assignment.id,
|
||||
answer: this.assignment.submission.text,
|
||||
document: this.assignment.submission.document,
|
||||
final: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
reopen() {
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_ASSIGNMENT_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
assignment: {
|
||||
id: this.assignment.id,
|
||||
answer: this.assignment.submission.text,
|
||||
document: this.assignment.submission.document,
|
||||
final: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
reopen() {
|
||||
this.$apollo.mutate({
|
||||
mutation: UPDATE_ASSIGNMENT_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
assignment: {
|
||||
id: this.assignment.id,
|
||||
answer: this.assignment.submission.text,
|
||||
document: this.assignment.submission.document,
|
||||
final: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
initialSubmission() {
|
||||
return {
|
||||
text: '',
|
||||
document: '',
|
||||
final: false,
|
||||
};
|
||||
},
|
||||
spellcheck() {
|
||||
let self = this;
|
||||
this.spellcheckLoading = true;
|
||||
this.$apollo.mutate({
|
||||
},
|
||||
});
|
||||
},
|
||||
initialSubmission() {
|
||||
return {
|
||||
text: '',
|
||||
document: '',
|
||||
final: false,
|
||||
};
|
||||
},
|
||||
spellcheck() {
|
||||
let self = this;
|
||||
this.spellcheckLoading = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: SPELL_CHECK_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
|
|
@ -230,90 +225,96 @@
|
|||
text: this.assignment.submission.text,
|
||||
},
|
||||
},
|
||||
update(store, {data: {spellCheck: {results}}}) {
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
spellCheck: { results },
|
||||
},
|
||||
}
|
||||
) {
|
||||
self.corrections = results;
|
||||
},
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
this.spellcheckLoading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
apollo: {
|
||||
assignment: {
|
||||
query: ASSIGNMENT_QUERY,
|
||||
variables() {
|
||||
return {
|
||||
id: this.value.id,
|
||||
};
|
||||
},
|
||||
result(response) {
|
||||
const data = response.data;
|
||||
this.assignment = cloneDeep(data.assignment);
|
||||
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
|
||||
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
|
||||
this.$nextTick(() => this.scrollToAssignmentReady(true));
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
assignment: {
|
||||
query: ASSIGNMENT_QUERY,
|
||||
variables() {
|
||||
return {
|
||||
id: this.value.id,
|
||||
};
|
||||
},
|
||||
me: {
|
||||
query: ME_QUERY,
|
||||
result(response) {
|
||||
const data = response.data;
|
||||
this.assignment = cloneDeep(data.assignment);
|
||||
this.assignment.submission = Object.assign(this.initialSubmission(), this.assignment.submission);
|
||||
if (this.assignment.id === this.scrollToAssignmentId && 'stale' in response) {
|
||||
this.$nextTick(() => this.scrollToAssignmentReady(true));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
me: {
|
||||
query: ME_QUERY,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/_variables.scss';
|
||||
@import '@/styles/_functions.scss';
|
||||
@import '@/styles/_mixins.scss';
|
||||
@import '@/styles/_variables.scss';
|
||||
@import '@/styles/_functions.scss';
|
||||
@import '@/styles/_mixins.scss';
|
||||
|
||||
.assignment {
|
||||
margin-bottom: 3rem;
|
||||
position: relative;
|
||||
|
||||
&__title {
|
||||
font-size: toRem(17px);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__main-text {
|
||||
/deep/ ul{
|
||||
@include list-parent
|
||||
}
|
||||
|
||||
/deep/ li {
|
||||
@include list-child;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-input-container {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__toggle-input {
|
||||
border: 0;
|
||||
font-family: $sans-serif-font-family;
|
||||
background: transparent;
|
||||
font-size: toRem(14px);
|
||||
padding: 5px 0;
|
||||
margin-right: 15px;
|
||||
outline: 0;
|
||||
color: $color-silver-dark;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&--active {
|
||||
border-bottom-color: $color-charcoal-dark;
|
||||
color: $color-charcoal-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&__feedback {
|
||||
@include regular-text;
|
||||
}
|
||||
.assignment {
|
||||
margin-bottom: 3rem;
|
||||
position: relative;
|
||||
|
||||
&__title {
|
||||
font-size: toRem(17px);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__main-text {
|
||||
:deep(ul) {
|
||||
@include list-parent;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
@include list-child;
|
||||
}
|
||||
}
|
||||
|
||||
&__toggle-input-container {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__toggle-input {
|
||||
border: 0;
|
||||
font-family: $sans-serif-font-family;
|
||||
background: transparent;
|
||||
font-size: toRem(14px);
|
||||
padding: 5px 0;
|
||||
margin-right: 15px;
|
||||
outline: 0;
|
||||
color: $color-silver-dark;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
&--active {
|
||||
border-bottom-color: $color-charcoal-dark;
|
||||
color: $color-charcoal-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&__feedback {
|
||||
@include regular-text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,147 +1,130 @@
|
|||
<template>
|
||||
<div class="tip-tap">
|
||||
<editor-content
|
||||
class="tip-tap__editor-wrapper"
|
||||
:editor="editor"
|
||||
/>
|
||||
<editor-content class="tip-tap__editor-wrapper" :editor="editor" />
|
||||
|
||||
<toggle
|
||||
:bordered="false"
|
||||
:checked="isList"
|
||||
label="Als Liste formatieren"
|
||||
@input="toggleList"
|
||||
/>
|
||||
<toggle :bordered="false" :checked="isList" label="Als Liste formatieren" @input="toggleList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {PropType, defineComponent} from 'vue';
|
||||
import {Editor, EditorContent} from "@tiptap/vue-3";
|
||||
import Document from '@tiptap/extension-document';
|
||||
import Paragraph from '@tiptap/extension-paragraph';
|
||||
import Text from '@tiptap/extension-text';
|
||||
import BulletList from '@tiptap/extension-bullet-list';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import Toggle from "@/components/ui/Toggle.vue";
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { Editor, EditorContent } from '@tiptap/vue-3';
|
||||
import Document from '@tiptap/extension-document';
|
||||
import Paragraph from '@tiptap/extension-paragraph';
|
||||
import Text from '@tiptap/extension-text';
|
||||
import BulletList from '@tiptap/extension-bullet-list';
|
||||
import ListItem from '@tiptap/extension-list-item';
|
||||
import Toggle from '@/components/ui/Toggle.vue';
|
||||
|
||||
interface Data {
|
||||
editor: Editor | undefined;
|
||||
}
|
||||
interface Value {
|
||||
text: string;
|
||||
}
|
||||
interface Data {
|
||||
editor: Editor | undefined;
|
||||
}
|
||||
interface Value {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<Value>,
|
||||
validator: (value: Value) => {
|
||||
return Object.prototype.hasOwnProperty.call(value, 'text');
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Object as PropType<Value>,
|
||||
validator: (value: Value) => {
|
||||
return Object.prototype.hasOwnProperty.call(value, 'text');
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
Toggle,
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data(): Data {
|
||||
return {
|
||||
editor: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isList(): boolean {
|
||||
return this.editor?.isActive('bulletList') || false;
|
||||
},
|
||||
text(): string {
|
||||
return this.value?.text || '';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
value({ text }: Value) {
|
||||
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
|
||||
const isSame = editor.getHTML() === text;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.commands.setContent(text, false);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'tip-tap__editor',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
Toggle,
|
||||
EditorContent
|
||||
},
|
||||
|
||||
data(): Data {
|
||||
return {
|
||||
editor: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isList(): boolean {
|
||||
return this.editor?.isActive('bulletList') || false;
|
||||
content: this.text,
|
||||
extensions: [Document, Paragraph, Text, BulletList, ListItem],
|
||||
onUpdate: () => {
|
||||
const text = (this.editor as Editor).getHTML();
|
||||
this.$emit('input', text);
|
||||
this.$emit('change-text', text);
|
||||
},
|
||||
text(): string {
|
||||
return this.value?.text || '';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor?.destroy();
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleList() {
|
||||
const editor = this.editor as Editor;
|
||||
editor.chain().selectAll().toggleBulletList().run();
|
||||
},
|
||||
|
||||
watch: {
|
||||
value({text}: Value) {
|
||||
const editor = this.editor as Editor; // editor is always initialized on mount, cast it
|
||||
const isSame = editor.getHTML() === text;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.commands.setContent(text, false);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'tip-tap__editor'
|
||||
}
|
||||
},
|
||||
content: this.text,
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
BulletList,
|
||||
ListItem
|
||||
],
|
||||
onUpdate: () => {
|
||||
const text=(this.editor as Editor).getHTML();
|
||||
this.$emit('input', text);
|
||||
this.$emit('change-text', text);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor?.destroy();
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleList() {
|
||||
const editor = this.editor as Editor;
|
||||
editor.chain().selectAll().toggleBulletList().run();
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~styles/helpers';
|
||||
@import '~styles/helpers';
|
||||
|
||||
|
||||
.tip-tap {
|
||||
|
||||
&__editor-wrapper {
|
||||
margin-bottom: $medium-spacing;
|
||||
}
|
||||
|
||||
/deep/ &__editor {
|
||||
@include inputstyle;
|
||||
flex-direction: column;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/deep/ ul {
|
||||
padding-left: $medium-spacing;
|
||||
list-style: initial;
|
||||
}
|
||||
|
||||
/deep/ li {
|
||||
@include inputfont;
|
||||
}
|
||||
|
||||
/deep/ div {
|
||||
@include inputfont;
|
||||
}
|
||||
|
||||
/deep/ p {
|
||||
@include inputfont;
|
||||
}
|
||||
.tip-tap {
|
||||
&__editor-wrapper {
|
||||
margin-bottom: $medium-spacing;
|
||||
}
|
||||
</style>
|
||||
|
||||
:deep(.tip-tap__editor) {
|
||||
@include inputstyle;
|
||||
flex-direction: column;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
:deep(ul) {
|
||||
padding-left: $medium-spacing;
|
||||
list-style: initial;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
@include inputfont;
|
||||
}
|
||||
|
||||
:deep(div) {
|
||||
@include inputfont;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
@include inputfont;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,64 +7,63 @@
|
|||
<slot />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="activity-entry__link"
|
||||
@click="$emit('link')"
|
||||
>
|
||||
<div class="activity-entry__link" @click="$emit('link')">
|
||||
<chevron-right class="activity-entry__icon" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight'));
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
const ChevronRight = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight')
|
||||
);
|
||||
|
||||
export default {
|
||||
props: ['title'],
|
||||
export default {
|
||||
props: ['title'],
|
||||
|
||||
components: {
|
||||
ChevronRight
|
||||
}
|
||||
};
|
||||
components: {
|
||||
ChevronRight,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/_variables.scss";
|
||||
@import "@/styles/_mixins.scss";
|
||||
@import '@/styles/_variables.scss';
|
||||
@import '@/styles/_mixins.scss';
|
||||
|
||||
.activity-entry {
|
||||
padding: $small-spacing 0;
|
||||
border-bottom: 1px solid $color-silver;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.activity-entry {
|
||||
padding: $small-spacing 0;
|
||||
border-bottom: 1px solid $color-silver;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&__title {
|
||||
@include small-text;
|
||||
// todo: make style definition for small text and silver color
|
||||
color: $color-silver-dark;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex-grow: 1;
|
||||
@include regular-text;
|
||||
line-height: $default-line-height;
|
||||
}
|
||||
&__link {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
align-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
fill: $color-brand;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
/deep/ p {
|
||||
@include regular-text;
|
||||
}
|
||||
&__title {
|
||||
@include small-text;
|
||||
// todo: make style definition for small text and silver color
|
||||
color: $color-silver-dark;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex-grow: 1;
|
||||
@include regular-text;
|
||||
line-height: $default-line-height;
|
||||
}
|
||||
&__link {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
align-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
fill: $color-brand;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
@include regular-text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,72 +1,75 @@
|
|||
<template>
|
||||
<div class="simple-file-upload">
|
||||
<component
|
||||
:is="button"
|
||||
@click="clickUploadCare"
|
||||
/>
|
||||
<component :is="button" @click="clickUploadCare" />
|
||||
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
|
||||
const SimpleFileUploadIcon = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIcon'));
|
||||
const SimpleFileUploadIconAndText = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIconAndText'));
|
||||
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon'));
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
const SimpleFileUploadHiddenInput = defineAsyncComponent(() =>
|
||||
import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')
|
||||
);
|
||||
const SimpleFileUploadIcon = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIcon'));
|
||||
const SimpleFileUploadIconAndText = defineAsyncComponent(() =>
|
||||
import('@/components/ui/file-upload/SimpleFileUploadIconAndText')
|
||||
);
|
||||
const DocumentIcon = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "icons" */ '@/components/icons/DocumentIcon')
|
||||
);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
withText: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
withText: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
SimpleFileUploadHiddenInput,
|
||||
DocumentIcon,
|
||||
SimpleFileUploadIcon,
|
||||
SimpleFileUploadIconAndText
|
||||
},
|
||||
components: {
|
||||
SimpleFileUploadHiddenInput,
|
||||
DocumentIcon,
|
||||
SimpleFileUploadIcon,
|
||||
SimpleFileUploadIconAndText,
|
||||
},
|
||||
|
||||
computed: {
|
||||
button() {
|
||||
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
|
||||
}
|
||||
computed: {
|
||||
button() {
|
||||
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickUploadCare() {
|
||||
// workaround for styling the uploadcare widget
|
||||
let button = this.$el.querySelector('.uploadcare--widget__button');
|
||||
button.click();
|
||||
}
|
||||
methods: {
|
||||
clickUploadCare() {
|
||||
// workaround for styling the uploadcare widget
|
||||
let button = this.$el.querySelector('.uploadcare--widget__button');
|
||||
button.click();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "~styles/_helpers";
|
||||
@import '~styles/_helpers';
|
||||
|
||||
.simple-file-upload {
|
||||
height: 25px;
|
||||
.simple-file-upload {
|
||||
height: 25px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .uploadcare--widget {
|
||||
display: none;
|
||||
}
|
||||
:deep(.uploadcare--widget) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,61 +1,58 @@
|
|||
<template>
|
||||
<div class="simple-file-upload">
|
||||
<button-with-icon-and-text
|
||||
icon="document-icon"
|
||||
text="Dokument hochladen"
|
||||
v-if="!value"
|
||||
@click="clickUploadCare"
|
||||
/>
|
||||
<button-with-icon-and-text icon="document-icon" text="Dokument hochladen" v-if="!value" @click="clickUploadCare" />
|
||||
|
||||
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput'));
|
||||
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText'));
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
const SimpleFileUploadHiddenInput = defineAsyncComponent(() =>
|
||||
import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')
|
||||
);
|
||||
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText'));
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
export default {
|
||||
props: ['value'],
|
||||
|
||||
components: {
|
||||
ButtonWithIconAndText,
|
||||
SimpleFileUploadHiddenInput,
|
||||
components: {
|
||||
ButtonWithIconAndText,
|
||||
SimpleFileUploadHiddenInput,
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickUploadCare() {
|
||||
// workaround for styling the uploadcare widget
|
||||
let button = this.$el.querySelector('.uploadcare--widget__button');
|
||||
button.click();
|
||||
},
|
||||
|
||||
methods: {
|
||||
clickUploadCare() {
|
||||
// workaround for styling the uploadcare widget
|
||||
let button = this.$el.querySelector('.uploadcare--widget__button');
|
||||
button.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "~styles/_helpers";
|
||||
@import '~styles/_helpers';
|
||||
|
||||
.simple-file-upload {
|
||||
.simple-file-upload {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
overflow: hidden;
|
||||
|
||||
&__icon {
|
||||
width: 25px;
|
||||
fill: $color-silver-dark;
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
overflow: hidden;
|
||||
|
||||
&__icon {
|
||||
width: 25px;
|
||||
fill: $color-silver-dark;
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .uploadcare--widget {
|
||||
display: none;
|
||||
}
|
||||
:deep(.uploadcare--widget) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
<template>
|
||||
<div class="instrument">
|
||||
<h1
|
||||
class="instrument__title"
|
||||
data-cy="instrument-title"
|
||||
>
|
||||
<h1 class="instrument__title" data-cy="instrument-title">
|
||||
{{ instrument.title }}
|
||||
</h1>
|
||||
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
class="instrument__intro intro"
|
||||
data-cy="instrument-intro"
|
||||
v-html="instrument.intro"
|
||||
/>
|
||||
<div class="instrument__intro intro" data-cy="instrument-intro" v-html="instrument.intro" />
|
||||
|
||||
<content-component
|
||||
:component="component"
|
||||
|
|
@ -27,80 +20,82 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql';
|
||||
import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql';
|
||||
|
||||
import {defineAsyncComponent} from 'vue';
|
||||
const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent'));
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
const ContentComponent = defineAsyncComponent(() =>
|
||||
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent')
|
||||
);
|
||||
|
||||
export default {
|
||||
apollo: {
|
||||
instrument() {
|
||||
return {
|
||||
query: INSTRUMENT_QUERY,
|
||||
variables: {
|
||||
slug: this.$route.params.slug
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ContentComponent
|
||||
},
|
||||
|
||||
data() {
|
||||
export default {
|
||||
apollo: {
|
||||
instrument() {
|
||||
return {
|
||||
instrument: {}
|
||||
query: INSTRUMENT_QUERY,
|
||||
variables: {
|
||||
slug: this.$route.params.slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ContentComponent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
instrument: {},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "~styles/helpers";
|
||||
@import '~styles/helpers';
|
||||
|
||||
.instrument {
|
||||
&__title {
|
||||
font-size: toRem(35px);
|
||||
.instrument {
|
||||
&__title {
|
||||
font-size: toRem(35px);
|
||||
margin-bottom: $large-spacing;
|
||||
line-height: $default-heading-line-height;
|
||||
}
|
||||
|
||||
& :deep() {
|
||||
& p {
|
||||
margin-bottom: $large-spacing;
|
||||
line-height: $default-heading-line-height;
|
||||
}
|
||||
|
||||
& /deep/ {
|
||||
& p {
|
||||
margin-bottom: $large-spacing;
|
||||
}
|
||||
& p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
& ul {
|
||||
@include list-parent;
|
||||
}
|
||||
|
||||
& ul {
|
||||
@include list-parent;
|
||||
}
|
||||
& p + ul {
|
||||
margin-top: -30px;
|
||||
}
|
||||
|
||||
& p + ul {
|
||||
margin-top: -30px;
|
||||
}
|
||||
& li {
|
||||
@include list-child;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
& li {
|
||||
@include list-child;
|
||||
line-height: 1.5;
|
||||
}
|
||||
& b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& b {
|
||||
font-weight: 600;
|
||||
}
|
||||
.brand {
|
||||
color: $color-brand;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand {
|
||||
color: $color-brand;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: $color-accent-2;
|
||||
font-weight: 600;
|
||||
}
|
||||
.secondary {
|
||||
color: $color-accent-2;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue