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> <template>
<div <div
:class="{'hideable-element--greyed-out': hidden}" :class="{ 'hideable-element--greyed-out': hidden }"
class="content-block__container hideable-element content-list__parent" class="content-block__container hideable-element content-list__parent"
> >
<div <div :class="specialClass" :style="instrumentStyle" class="content-block" data-cy="content-block">
:class="specialClass" <div class="block-actions" v-if="canEditModule && !isInstrumentBlock">
:style="instrumentStyle" <user-widget v-bind="me" class="block-actions__user-widget content-block__user-widget" v-if="isMine" />
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> <more-options-widget>
<li <li class="popover-links__link" v-if="!isInstrumentBlock">
class="popover-links__link"
v-if="!isInstrumentBlock"
>
<popover-link <popover-link
data-cy="duplicate-content-block-link" data-cy="duplicate-content-block-link"
text="Duplizieren" text="Duplizieren"
@link-action="duplicateContentBlock(contentBlock)" @link-action="duplicateContentBlock(contentBlock)"
/> />
</li> </li>
<li <li class="popover-links__link" v-if="isMine">
class="popover-links__link"
v-if="isMine"
>
<popover-link <popover-link
data-cy="delete-content-block-link" data-cy="delete-content-block-link"
text="Löschen" text="Löschen"
@ -40,22 +22,13 @@
/> />
</li> </li>
<li <li class="popover-links__link" v-if="isMine">
class="popover-links__link" <popover-link text="Bearbeiten" @link-action="editContentBlock(contentBlock)" />
v-if="isMine"
>
<popover-link
text="Bearbeiten"
@link-action="editContentBlock(contentBlock)"
/>
</li> </li>
</more-options-widget> </more-options-widget>
</div> </div>
<div class="content-block__visibility"> <div class="content-block__visibility">
<visibility-action <visibility-action :block="contentBlock" v-if="canEditModule" />
:block="contentBlock"
v-if="canEditModule"
/>
</div> </div>
<h3 <h3
@ -66,10 +39,7 @@
> >
{{ instrumentLabel }} {{ instrumentLabel }}
</h3> </h3>
<h4 <h4 class="content-block__title" v-if="!contentBlock.indent">
class="content-block__title"
v-if="!contentBlock.indent"
>
{{ contentBlock.title }} {{ contentBlock.title }}
</h4> </h4>
@ -85,110 +55,109 @@
/> />
</div> </div>
<add-content-button <add-content-button :where="{ after: contentBlock }" v-if="canEditModule" />
:where="{after: contentBlock}"
v-if="canEditModule"
/>
</div> </div>
</template> </template>
<script> <script>
import AddContentButton from '@/components/AddContentButton'; import AddContentButton from '@/components/AddContentButton';
import MoreOptionsWidget from '@/components/MoreOptionsWidget'; import MoreOptionsWidget from '@/components/MoreOptionsWidget';
import UserWidget from '@/components/UserWidget'; import UserWidget from '@/components/UserWidget';
import VisibilityAction from '@/components/visibility/VisibilityAction'; import VisibilityAction from '@/components/visibility/VisibilityAction';
import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql'; import CHAPTER_QUERY from '@/graphql/gql/queries/chapterQuery.gql';
import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql'; import DELETE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/deleteContentBlock.gql';
import DUPLICATE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/duplicateContentBlock.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 { hidden } from '@/helpers/visibility';
import {CONTENT_TYPE} from '@/consts/types'; import { CONTENT_TYPE } from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink'; import PopoverLink from '@/components/ui/PopoverLink';
import {insertAtIndex, removeAtIndex} from '@/graphql/immutable-operations'; import { insertAtIndex, removeAtIndex } from '@/graphql/immutable-operations';
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names'; import { EDIT_CONTENT_BLOCK_PAGE } from '@/router/module.names';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
import {instrumentCategory} from '@/helpers/instrumentType'; 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 {
export default { name: 'ContentBlock',
name: 'ContentBlock', props: {
props: { contentBlock: {
contentBlock: { type: Object,
type: Object, default: () => ({}),
default: () => ({}),
},
parent: {
type: Object,
default: () => ({}),
},
editMode: {
type: Boolean,
default: true,
},
}, },
parent: {
mixins: [me], type: Object,
default: () => ({}),
components: {
PopoverLink,
ContentComponent,
AddContentButton,
VisibilityAction,
MoreOptionsWidget,
UserWidget,
}, },
editMode: {
type: Boolean,
default: true,
},
},
computed: { mixins: [me],
canEditModule() {
return !this.contentBlock.indent && this.editMode; components: {
}, PopoverLink,
specialClass() { ContentComponent,
return `content-block--${this.contentBlock.type.toLowerCase()}`; AddContentButton,
}, VisibilityAction,
isInstrumentBlock() { MoreOptionsWidget,
return !!this.contentBlock.instrumentCategory; UserWidget,
}, },
// 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() { computed: {
if (this.isInstrumentBlock) { canEditModule() {
return { return !this.contentBlock.indent && this.editMode;
backgroundColor: this.contentBlock.instrumentCategory.background },
}; specialClass() {
} return `content-block--${this.contentBlock.type.toLowerCase()}`;
return {}; },
}, isInstrumentBlock() {
instrumentLabel() { return !!this.contentBlock.instrumentCategory;
const contentType = this.contentBlock.type.toLowerCase(); },
if (contentType.startsWith('base')) { // all legacy instruments start with `base` // 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
return instrumentCategory(contentType); instrumentStyle() {
} if (this.isInstrumentBlock) {
if (this.isInstrumentBlock) { return {
return instrumentCategory(this.contentBlock.instrumentCategory.name); backgroundColor: this.contentBlock.instrumentCategory.background,
} };
return ''; }
}, 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() { instrumentLabel() {
if (this.isInstrumentBlock) { const contentType = this.contentBlock.type.toLowerCase();
return { if (contentType.startsWith('base')) {
color: this.contentBlock.instrumentCategory.foreground // all legacy instruments start with `base`
}; return instrumentCategory(contentType);
} }
return {}; if (this.isInstrumentBlock) {
}, return instrumentCategory(this.contentBlock.instrumentCategory.name);
canEditContentBlock() { }
return this.isMine && !this.contentBlock.indent; return '';
}, },
isMine() { // 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
return this.contentBlock.mine; instrumentLabelStyle() {
}, if (this.isInstrumentBlock) {
contentBlocksWithContentLists() { 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: collects all content_list_items in content_lists:
{ {
text_block, text_block,
@ -202,221 +171,238 @@
text_block text_block
} }
*/ */
let contentList = []; let contentList = [];
let newContent = this.contentBlock.contents.reduce((newContents, content, index) => { let newContent = this.contentBlock.contents.reduce((newContents, content, index) => {
// collect content_list_items // collect content_list_items
if (content.type === 'content_list_item') { if (content.type === 'content_list_item') {
contentList = [...contentList, content]; contentList = [...contentList, content];
if (index === this.contentBlock.contents.length - 1) { // content is last element of contents array if (index === this.contentBlock.contents.length - 1) {
let updatedContent = [...newContents, ...this.createContentListOrBlocks(contentList)]; // content is last element of contents array
return updatedContent; 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; return newContents;
} else { } else {
// handle all other items and reset current content_list if necessary return [...newContents, content];
if (contentList.length !== 0) {
newContents = [...newContents, ...this.createContentListOrBlocks(contentList), content];
contentList = [];
return newContents;
} else {
return [...newContents, content];
}
} }
}, []); }
return Object.assign({}, this.contentBlock, { }, []);
contents: newContent, 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;
},
}, },
methods: { hidden() {
duplicateContentBlock({id}) { return hidden({
const parent = this.parent; block: this.contentBlock,
this.$apollo.mutate({ schoolClass: this.schoolClass,
mutation: DUPLICATE_CONTENT_BLOCK_MUTATION, type: CONTENT_TYPE,
variables: { });
input: { },
id, 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) { update(
const query = CHAPTER_QUERY; store,
const variables = { {
id: parent.id, data: {
}; deleteContentBlock: { success },
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}}}) { ) {
if (success) { if (success) {
const query = CHAPTER_QUERY; const query = CHAPTER_QUERY;
const variables = { const variables = {
id: parent.id, id: parent.id,
}; };
const {chapter} = store.readQuery({query, variables}); const { chapter } = store.readQuery({ query, variables });
const index = chapter.contentBlocks.findIndex(contentBlock => contentBlock.id === id); const index = chapter.contentBlocks.findIndex((contentBlock) => contentBlock.id === id);
const contentBlocks = removeAtIndex(chapter.contentBlocks, index); const contentBlocks = removeAtIndex(chapter.contentBlocks, index);
const data = { const data = {
chapter: { chapter: {
...chapter, ...chapter,
contentBlocks, contentBlocks,
}, },
}; };
store.writeQuery({query, variables, data}); store.writeQuery({ query, variables, data });
} }
}, },
}); });
}, },
createContentListOrBlocks(contentList) { createContentListOrBlocks(contentList) {
return [{ return [
{
type: 'content_list', type: 'content_list',
contents: contentList, contents: contentList,
id: contentList[0].id, id: contentList[0].id,
}]; },
}, ];
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.content-block { .content-block {
margin-bottom: $section-spacing; margin-bottom: $section-spacing;
position: relative;
&__container {
position: relative; position: relative;
}
&__container { &__title {
position: relative; 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 { &--task {
line-height: 1.5; @include light-border(bottom);
margin-top: -0.5rem; // to offset the 1.5 line height, it leaves a padding on top
}
&__instrument-label { .content-block__title {
margin-bottom: $medium-spacing; color: $color-brand;
@include regular-text(); margin-top: $default-padding;
} margin-bottom: $large-spacing;
&__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 {
@include light-border(bottom); @include light-border(bottom);
.content-block__title { @include desktop {
color: $color-brand; margin-top: 0;
margin-top: $default-padding;
margin-bottom: $large-spacing;
@include light-border(bottom);
@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> </style>

View File

@ -1,114 +1,95 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div class="solution" data-cy="solution">
class="solution" <a class="solution__toggle" data-cy="show-solution" @click="toggle"
data-cy="solution" >Lösung
>
<a
class="solution__toggle"
data-cy="show-solution"
@click="toggle"
>Lösung
<template v-if="!visible">anzeigen</template> <template v-if="!visible">anzeigen</template>
<template v-else>ausblenden</template> <template v-else>ausblenden</template>
</a> </a>
<transition name="fade"> <transition name="fade">
<div <div class="solution__hidden fade" v-if="visible">
class="solution__hidden fade" <p class="solution__text solution-text" data-cy="solution-text" v-html="sanitizedText" />
v-if="visible" <cms-document-block :solution="true" class="solution__document" :value="value.document" v-if="value.document" />
>
<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> </div>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script>
import {sanitizeAsHtml} from '@/helpers/text'; import { sanitizeAsHtml } from '@/helpers/text';
import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock'; import CmsDocumentBlock from '@/components/content-blocks/CmsDocumentBlock';
export default { export default {
props: ['value'], props: ['value'],
components: {CmsDocumentBlock}, components: { CmsDocumentBlock },
data() { data() {
return { return {
visible: false, visible: false,
}; };
},
computed: {
sanitizedText() {
return sanitizeAsHtml(this.value.text);
}, },
},
computed: { methods: {
sanitizedText() { toggle() {
return sanitizeAsHtml(this.value.text); this.visible = !this.visible;
},
}, },
},
methods: { };
toggle() {
this.visible = !this.visible;
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.solution { .solution {
display: grid; display: grid;
grid-auto-rows: auto; grid-auto-rows: auto;
grid-row-gap: 15px; grid-row-gap: 15px;
margin-bottom: 1rem; margin-bottom: 1rem;
&__toggle { &__toggle {
font-family: $sans-serif-font-family; font-family: $sans-serif-font-family;
color: $color-silver-dark; color: $color-silver-dark;
font-size: toRem(15px); font-size: toRem(15px);
/*margin-bottom: 15px;*/ /*margin-bottom: 15px;*/
display: block; display: block;
cursor: pointer; cursor: pointer;
font-weight: $font-weight-regular; font-weight: $font-weight-regular;
} }
&__text { &__text {
font-size: toRem(18px);
color: $color-silver-dark;
:deep(p) {
font-size: toRem(18px); font-size: toRem(18px);
color: $color-silver-dark; color: $color-silver-dark;
}
/deep/ p { :deep(ul) {
font-size: toRem(18px); padding-left: $medium-spacing;
> li {
list-style: disc outside none;
color: $color-silver-dark; color: $color-silver-dark;
} }
/deep/ ul {
padding-left: $medium-spacing;
> li {
list-style: disc outside none;
color: $color-silver-dark;
}
}
} }
} }
}
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity .3s; transition: opacity 0.3s;
} }
.fade-enter-from, .fade-enter-from,
.fade-leave-active { .fade-leave-active {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@ -1,19 +1,9 @@
<template> <template>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div :data-scrollto="value.id" class="assignment">
:data-scrollto="value.id" <p class="assignment__main-text" data-cy="assignment-main-text" v-html="assignment.assignment" />
class="assignment"
>
<p
class="assignment__main-text"
data-cy="assignment-main-text"
v-html="assignment.assignment"
/>
<solution <solution :value="solution" v-if="assignment.solution" />
:value="solution"
v-if="assignment.solution"
/>
<template v-if="isStudent"> <template v-if="isStudent">
<submission-form <submission-form
@ -33,101 +23,97 @@
@spellcheck="spellcheck" @spellcheck="spellcheck"
/> />
<spell-check <spell-check :corrections="corrections" :text="submission.text" />
:corrections="corrections"
:text="submission.text"
/>
<p <p class="assignment__feedback" v-if="assignment.submission.submissionFeedback" v-html="feedbackText" />
class="assignment__feedback"
v-if="assignment.submission.submissionFeedback"
v-html="feedbackText"
/>
</template> </template>
<template v-if="!isStudent"> <template v-if="!isStudent">
<router-link <router-link :to="{ name: 'submissions', params: { id: assignment.id } }" class="button button--primary">
:to="{name: 'submissions', params: { id: assignment.id }}" Zu den Ergebnissen
class="button button--primary"
>
Zu den
Ergebnissen
</router-link> </router-link>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import {mapActions, mapGetters} from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql'; import ASSIGNMENT_QUERY from '@/graphql/gql/queries/assignmentQuery.gql';
import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import ME_QUERY from '@/graphql/gql/queries/meQuery.gql';
import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql'; import UPDATE_ASSIGNMENT_MUTATION from '@/graphql/gql/mutations/updateAssignmentMutation.gql';
import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql'; import UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS from '@/graphql/gql/mutations/updateAssignmentMutationWithSuccess.gql';
import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql'; import SPELL_CHECK_MUTATION from '@/graphql/gql/mutations/spellCheck.gql';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import {sanitize} from '@/helpers/text'; import { sanitize } from '@/helpers/text';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const SubmissionForm = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SubmissionForm')); const SubmissionForm = defineAsyncComponent(() =>
const Solution = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/Solution')); import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/assignment/SubmissionForm')
const SpellCheck = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/assignment/SpellCheck')); );
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 { export default {
props: ['value'], props: ['value'],
components: { components: {
Solution, Solution,
SubmissionForm, SubmissionForm,
SpellCheck, 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;
}, },
submission() {
data() { return this.assignment.submission ? this.assignment.submission : {};
},
isStudent() {
return !this.me.permissions.includes('users.can_manage_school_class_content');
},
solution() {
return { return {
assignment: { text: this.assignment.solution,
submission: this.initialSubmission(),
},
me: {
permissions: [],
},
inputType: 'text',
unsaved: false,
saving: 0,
corrections: '',
spellcheckLoading: false,
}; };
}, },
id() {
computed: { return this.assignment.id ? this.assignment.id.replace(/=/g, '') : '';
...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}`;
},
}, },
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: { methods: {
...mapActions(['scrollToAssignmentReady']), ...mapActions(['scrollToAssignmentReady']),
_save: debounce(function (submission) { _save: debounce(function (submission) {
this.saving++; this.saving++;
this.$apollo.mutate({ this.$apollo
.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS, mutation: UPDATE_ASSIGNMENT_MUTATION_WITH_SUCCESS,
variables: { variables: {
input: { input: {
@ -138,7 +124,14 @@
}, },
}, },
}, },
update(store, {data: {updateAssignment: {successful, updatedAssignment}}}) { update(
store,
{
data: {
updateAssignment: { successful, updatedAssignment },
},
}
) {
try { try {
if (successful) { if (successful) {
const query = ASSIGNMENT_QUERY; const query = ASSIGNMENT_QUERY;
@ -149,80 +142,82 @@
submission, submission,
}); });
const data = { const data = {
assignment assignment,
}; };
store.writeQuery({query, variables, data}); store.writeQuery({ query, variables, data });
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
// Query did not exist in the cache, and apollo throws a generic Error. Do nothing // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
} }
}, },
}).then(() => { })
.then(() => {
this.saving--; this.saving--;
if (this.saving === 0) { if (this.saving === 0) {
this.unsaved = false; this.unsaved = false;
} }
}); });
}, 500), }, 500),
saveInput: function (answer) { saveInput: function (answer) {
// reset corrections on input // reset corrections on input
this.corrections = ''; this.corrections = '';
this.unsaved = true; this.unsaved = true;
/* /*
We update the assignment on this component, so the changes are reflected on it. The server does not return 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 the updated entity, to prevent the UI to update when the user is entering his input
*/ */
this.assignment.submission.text = answer; this.assignment.submission.text = answer;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
changeDocumentUrl(documentUrl) { changeDocumentUrl(documentUrl) {
this.assignment.submission.document = documentUrl; this.assignment.submission.document = documentUrl;
this._save(this.assignment.submission); this._save(this.assignment.submission);
}, },
turnIn() { turnIn() {
// reset corrections on turn in // reset corrections on turn in
this.corrections = ''; this.corrections = '';
this.$apollo.mutate({ this.$apollo.mutate({
mutation: UPDATE_ASSIGNMENT_MUTATION, mutation: UPDATE_ASSIGNMENT_MUTATION,
variables: { variables: {
input: { input: {
assignment: { assignment: {
id: this.assignment.id, id: this.assignment.id,
answer: this.assignment.submission.text, answer: this.assignment.submission.text,
document: this.assignment.submission.document, document: this.assignment.submission.document,
final: true, final: true,
},
}, },
}, },
}); },
}, });
reopen() { },
this.$apollo.mutate({ reopen() {
mutation: UPDATE_ASSIGNMENT_MUTATION, this.$apollo.mutate({
variables: { mutation: UPDATE_ASSIGNMENT_MUTATION,
input: { variables: {
assignment: { input: {
id: this.assignment.id, assignment: {
answer: this.assignment.submission.text, id: this.assignment.id,
document: this.assignment.submission.document, answer: this.assignment.submission.text,
final: false, document: this.assignment.submission.document,
}, final: false,
}, },
}, },
}); },
}, });
initialSubmission() { },
return { initialSubmission() {
text: '', return {
document: '', text: '',
final: false, document: '',
}; final: false,
}, };
spellcheck() { },
let self = this; spellcheck() {
this.spellcheckLoading = true; let self = this;
this.$apollo.mutate({ this.spellcheckLoading = true;
this.$apollo
.mutate({
mutation: SPELL_CHECK_MUTATION, mutation: SPELL_CHECK_MUTATION,
variables: { variables: {
input: { input: {
@ -230,90 +225,96 @@
text: this.assignment.submission.text, text: this.assignment.submission.text,
}, },
}, },
update(store, {data: {spellCheck: {results}}}) { update(
store,
{
data: {
spellCheck: { results },
},
}
) {
self.corrections = results; self.corrections = results;
}, },
}).then(() => { })
.then(() => {
this.spellcheckLoading = false; this.spellcheckLoading = false;
}); });
},
}, },
},
apollo: { apollo: {
assignment: { assignment: {
query: ASSIGNMENT_QUERY, query: ASSIGNMENT_QUERY,
variables() { variables() {
return { return {
id: this.value.id, 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));
}
},
}, },
me: { result(response) {
query: ME_QUERY, 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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '@/styles/_variables.scss'; @import '@/styles/_variables.scss';
@import '@/styles/_functions.scss'; @import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss'; @import '@/styles/_mixins.scss';
.assignment { .assignment {
margin-bottom: 3rem; margin-bottom: 3rem;
position: relative; 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;
}
&__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> </style>

View File

@ -1,147 +1,130 @@
<template> <template>
<div class="tip-tap"> <div class="tip-tap">
<editor-content <editor-content class="tip-tap__editor-wrapper" :editor="editor" />
class="tip-tap__editor-wrapper"
:editor="editor"
/>
<toggle <toggle :bordered="false" :checked="isList" label="Als Liste formatieren" @input="toggleList" />
:bordered="false"
:checked="isList"
label="Als Liste formatieren"
@input="toggleList"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {PropType, defineComponent} from 'vue'; import { PropType, defineComponent } from 'vue';
import {Editor, EditorContent} from "@tiptap/vue-3"; import { Editor, EditorContent } from '@tiptap/vue-3';
import Document from '@tiptap/extension-document'; import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph'; import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text'; import Text from '@tiptap/extension-text';
import BulletList from '@tiptap/extension-bullet-list'; import BulletList from '@tiptap/extension-bullet-list';
import ListItem from '@tiptap/extension-list-item'; import ListItem from '@tiptap/extension-list-item';
import Toggle from "@/components/ui/Toggle.vue"; import Toggle from '@/components/ui/Toggle.vue';
interface Data { interface Data {
editor: Editor | undefined; editor: Editor | undefined;
} }
interface Value { interface Value {
text: string; text: string;
} }
export default defineComponent({ export default defineComponent({
props: { props: {
value: { value: {
type: Object as PropType<Value>, type: Object as PropType<Value>,
validator: (value: Value) => { validator: (value: Value) => {
return Object.prototype.hasOwnProperty.call(value, 'text'); 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',
}, },
}, },
}, content: this.text,
extensions: [Document, Paragraph, Text, BulletList, ListItem],
components: { onUpdate: () => {
Toggle, const text = (this.editor as Editor).getHTML();
EditorContent this.$emit('input', text);
}, this.$emit('change-text', text);
data(): Data {
return {
editor: undefined,
};
},
computed: {
isList(): boolean {
return this.editor?.isActive('bulletList') || false;
}, },
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import '~styles/helpers'; @import '~styles/helpers';
.tip-tap {
.tip-tap { &__editor-wrapper {
margin-bottom: $medium-spacing;
&__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;
}
} }
</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 /> <slot />
</div> </div>
<div <div class="activity-entry__link" @click="$emit('link')">
class="activity-entry__link"
@click="$emit('link')"
>
<chevron-right class="activity-entry__icon" /> <chevron-right class="activity-entry__icon" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const ChevronRight = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/ChevronRight')); const ChevronRight = defineAsyncComponent(() =>
import(/* webpackChunkName: "icons" */ '@/components/icons/ChevronRight')
);
export default { export default {
props: ['title'], props: ['title'],
components: { components: {
ChevronRight ChevronRight,
} },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import '@/styles/_variables.scss';
@import "@/styles/_mixins.scss"; @import '@/styles/_mixins.scss';
.activity-entry { .activity-entry {
padding: $small-spacing 0; padding: $small-spacing 0;
border-bottom: 1px solid $color-silver; border-bottom: 1px solid $color-silver;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
&__title { &__title {
@include small-text; @include small-text;
// todo: make style definition for small text and silver color // todo: make style definition for small text and silver color
color: $color-silver-dark; color: $color-silver-dark;
margin-bottom: 0; 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;
}
} }
&__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> </style>

View File

@ -1,72 +1,75 @@
<template> <template>
<div class="simple-file-upload"> <div class="simple-file-upload">
<component <component :is="button" @click="clickUploadCare" />
:is="button"
@click="clickUploadCare"
/>
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" /> <simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')); const SimpleFileUploadHiddenInput = defineAsyncComponent(() =>
const SimpleFileUploadIcon = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIcon')); import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')
const SimpleFileUploadIconAndText = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadIconAndText')); );
const DocumentIcon = defineAsyncComponent(() => import(/* webpackChunkName: "icons" */'@/components/icons/DocumentIcon')); 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 { export default {
props: { props: {
value: { value: {
type: String, type: String,
default: '' default: '',
},
withText: {
type: Boolean,
default: false
}
}, },
withText: {
type: Boolean,
default: false,
},
},
components: { components: {
SimpleFileUploadHiddenInput, SimpleFileUploadHiddenInput,
DocumentIcon, DocumentIcon,
SimpleFileUploadIcon, SimpleFileUploadIcon,
SimpleFileUploadIconAndText SimpleFileUploadIconAndText,
}, },
computed: { computed: {
button() { button() {
return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon'; return this.withText ? 'simple-file-upload-icon-and-text' : 'simple-file-upload-icon';
}
}, },
},
methods: { methods: {
clickUploadCare() { clickUploadCare() {
// workaround for styling the uploadcare widget // workaround for styling the uploadcare widget
let button = this.$el.querySelector('.uploadcare--widget__button'); let button = this.$el.querySelector('.uploadcare--widget__button');
button.click(); button.click();
}
}, },
}; },
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/_helpers"; @import '~styles/_helpers';
.simple-file-upload { .simple-file-upload {
height: 25px; height: 25px;
overflow: hidden;
cursor: pointer;
&__link {
display: inline-block;
overflow: hidden; overflow: hidden;
cursor: pointer; width: 25px;
height: 25px;
&__link {
display: inline-block;
overflow: hidden;
width: 25px;
height: 25px;
}
} }
}
/deep/ .uploadcare--widget { :deep(.uploadcare--widget) {
display: none; display: none;
} }
</style> </style>

View File

@ -1,61 +1,58 @@
<template> <template>
<div class="simple-file-upload"> <div class="simple-file-upload">
<button-with-icon-and-text <button-with-icon-and-text icon="document-icon" text="Dokument hochladen" v-if="!value" @click="clickUploadCare" />
icon="document-icon"
text="Dokument hochladen"
v-if="!value"
@click="clickUploadCare"
/>
<simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" /> <simple-file-upload-hidden-input @link-change-url="$emit('link-change-url', $event)" />
</div> </div>
</template> </template>
<script> <script>
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const SimpleFileUploadHiddenInput = defineAsyncComponent(() => import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')); const SimpleFileUploadHiddenInput = defineAsyncComponent(() =>
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText')); import('@/components/ui/file-upload/SimpleFileUploadHiddenInput')
);
const ButtonWithIconAndText = defineAsyncComponent(() => import('@/components/ui/ButtonWithIconAndText'));
export default { export default {
props: ['value'], props: ['value'],
components: { components: {
ButtonWithIconAndText, ButtonWithIconAndText,
SimpleFileUploadHiddenInput, 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> </script>
<style scoped lang="scss"> <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; width: 25px;
height: 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 { :deep(.uploadcare--widget) {
display: none; display: none;
} }
</style> </style>

View File

@ -1,18 +1,11 @@
<template> <template>
<div class="instrument"> <div class="instrument">
<h1 <h1 class="instrument__title" data-cy="instrument-title">
class="instrument__title"
data-cy="instrument-title"
>
{{ instrument.title }} {{ instrument.title }}
</h1> </h1>
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div class="instrument__intro intro" data-cy="instrument-intro" v-html="instrument.intro" />
class="instrument__intro intro"
data-cy="instrument-intro"
v-html="instrument.intro"
/>
<content-component <content-component
:component="component" :component="component"
@ -27,80 +20,82 @@
</template> </template>
<script> <script>
import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql'; import INSTRUMENT_QUERY from '@/graphql/gql/queries/instrumentQuery.gql';
import {defineAsyncComponent} from 'vue'; import { defineAsyncComponent } from 'vue';
const ContentComponent = defineAsyncComponent(() => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent')); const ContentComponent = defineAsyncComponent(() =>
import(/* webpackChunkName: "content-components" */ '@/components/content-blocks/ContentComponent')
);
export default { export default {
apollo: { apollo: {
instrument() { instrument() {
return {
query: INSTRUMENT_QUERY,
variables: {
slug: this.$route.params.slug
}
};
}
},
components: {
ContentComponent
},
data() {
return { return {
instrument: {} query: INSTRUMENT_QUERY,
variables: {
slug: this.$route.params.slug,
},
}; };
} },
}; },
components: {
ContentComponent,
},
data() {
return {
instrument: {},
};
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "~styles/helpers"; @import '~styles/helpers';
.instrument { .instrument {
&__title { &__title {
font-size: toRem(35px); font-size: toRem(35px);
margin-bottom: $large-spacing;
line-height: $default-heading-line-height;
}
& :deep() {
& p {
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
line-height: $default-heading-line-height;
} }
& /deep/ { & p:last-child {
& p { margin-bottom: 0;
margin-bottom: $large-spacing; }
}
& p:last-child { & ul {
margin-bottom: 0; @include list-parent;
} }
& ul { & p + ul {
@include list-parent; margin-top: -30px;
} }
& p + ul { & li {
margin-top: -30px; @include list-child;
} line-height: 1.5;
}
& li { & b {
@include list-child; font-weight: 600;
line-height: 1.5; }
}
& b { .brand {
font-weight: 600; color: $color-brand;
} font-weight: 600;
}
.brand { .secondary {
color: $color-brand; color: $color-accent-2;
font-weight: 600; font-weight: 600;
}
.secondary {
color: $color-accent-2;
font-weight: 600;
}
} }
} }
}
</style> </style>