Fix deep-selectors

This commit is contained in:
Ramon Wenger 2023-02-02 14:36:08 +01:00
parent 4477b7c5ed
commit 4b55f8952c
8 changed files with 896 additions and 951 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>