aboutsummaryrefslogtreecommitdiff
path: root/resources/vue
diff options
context:
space:
mode:
Diffstat (limited to 'resources/vue')
-rw-r--r--resources/vue/base-components.js88
-rw-r--r--resources/vue/components/ActiveFilter.vue2
-rw-r--r--resources/vue/components/AdminCourses.vue45
-rw-r--r--resources/vue/components/CacheAdministration.vue34
-rw-r--r--resources/vue/components/ConsultationCreator.vue496
-rw-r--r--resources/vue/components/ContentModulesControl.vue2
-rw-r--r--resources/vue/components/ContentModulesEditTiles.vue9
-rw-r--r--resources/vue/components/ContentmodulesEditTable.vue8
-rw-r--r--resources/vue/components/Datepicker.vue146
-rw-r--r--resources/vue/components/EditableList.vue2
-rw-r--r--resources/vue/components/FilesTable.vue120
-rw-r--r--resources/vue/components/MyCoursesTables.vue15
-rw-r--r--resources/vue/components/MyCoursesTiles.vue6
-rw-r--r--resources/vue/components/Quicksearch.vue1
-rw-r--r--resources/vue/components/StudipAssetImg.vue2
-rw-r--r--resources/vue/components/StudipDialog.vue16
-rw-r--r--resources/vue/components/StudipFileChooser.vue4
-rw-r--r--resources/vue/components/StudipIcon.vue2
-rw-r--r--resources/vue/components/StudipTooltipIcon.vue3
-rw-r--r--resources/vue/components/StudipWysiwyg.vue20
-rw-r--r--resources/vue/components/SystemNotification.vue173
-rw-r--r--resources/vue/components/SystemNotificationManager.vue73
-rw-r--r--resources/vue/components/Timepicker.vue37
-rw-r--r--resources/vue/components/blubber/Composer.vue4
-rw-r--r--resources/vue/components/courseware/CoursewareContentPermissions.vue6
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue4
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockActions.vue1
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue12
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue15
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue40
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue19
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue4
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue4
-rw-r--r--resources/vue/components/courseware/containers/CoursewareContainerActions.vue1
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue49
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue58
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareRootContent.vue171
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue4
-rw-r--r--resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue4
-rw-r--r--resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue4
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbar.vue48
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue170
-rw-r--r--resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue14
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItem.vue2
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue119
-rw-r--r--resources/vue/components/courseware/unit/CoursewareUnitProgress.vue2
-rw-r--r--resources/vue/components/file-chooser/FileChooserDialog.vue4
-rw-r--r--resources/vue/components/form_inputs/CalendarPermissionsTable.vue6
-rw-r--r--resources/vue/components/form_inputs/CaptchaInput.vue70
-rw-r--r--resources/vue/components/form_inputs/DateListInput.vue20
-rw-r--r--resources/vue/components/form_inputs/MyCoursesColouredTable.vue2
-rw-r--r--resources/vue/components/questionnaires/FreetextEdit.vue39
-rw-r--r--resources/vue/components/questionnaires/InputArray.vue279
-rw-r--r--resources/vue/components/questionnaires/LikertEdit.vue189
-rw-r--r--resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue44
-rw-r--r--resources/vue/components/questionnaires/RangescaleEdit.vue216
-rw-r--r--resources/vue/components/questionnaires/VoteEdit.vue51
-rw-r--r--resources/vue/components/responsive/ResponsiveContentBar.vue8
-rw-r--r--resources/vue/components/responsive/ResponsiveNavigation.vue22
-rw-r--r--resources/vue/components/tree/StudipTree.vue37
-rw-r--r--resources/vue/components/tree/StudipTreeList.vue13
-rw-r--r--resources/vue/components/tree/StudipTreeTable.vue11
-rw-r--r--resources/vue/components/tree/StudipTreeViewWidget.vue56
-rw-r--r--resources/vue/components/tree/TreeBreadcrumb.vue2
-rw-r--r--resources/vue/components/tree/TreeCourseDetails.vue8
-rw-r--r--resources/vue/components/tree/TreeNodeCoursePath.vue8
-rw-r--r--resources/vue/components/tree/TreeNodeTile.vue2
-rw-r--r--resources/vue/components/tree/TreeSearchResult.vue5
-rw-r--r--resources/vue/mixins/QuestionnaireComponent.js24
-rw-r--r--resources/vue/mixins/courseware/import.js32
-rw-r--r--resources/vue/store/ContentModulesStore.js2
-rw-r--r--resources/vue/store/MyCoursesStore.js2
-rw-r--r--resources/vue/store/blubber.js2
-rw-r--r--resources/vue/store/courseware/courseware-shelf.module.js24
-rw-r--r--resources/vue/store/courseware/courseware.module.js20
75 files changed, 2116 insertions, 1141 deletions
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 2390bb9..5d11daa 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -1,63 +1,33 @@
-import CalendarPermissionsTable from "./components/form_inputs/CalendarPermissionsTable.vue";
-import DayOfWeekSelect from './components/form_inputs/DayOfWeekSelect.vue';
-import DateListInput from './components/form_inputs/DateListInput.vue';
-import Multiselect from './components/Multiselect.vue';
-import MyCoursesColouredTable from './components/form_inputs/MyCoursesColouredTable.vue';
-import EditableList from "./components/EditableList.vue";
-import Quicksearch from './components/Quicksearch.vue';
-import RepetitionInput from "./components/form_inputs/RepetitionInput.vue";
-import SidebarWidget from './components/SidebarWidget.vue';
-import StudipActionMenu from './components/StudipActionMenu.vue';
-import StudipAssetImg from './components/StudipAssetImg.vue';
-import StudipDateTime from './components/StudipDateTime.vue';
-import StudipDialog from './components/StudipDialog.vue';
-import StudipFileSize from './components/StudipFileSize.vue';
-import StudipFolderSize from './components/StudipFolderSize.vue';
-import StudipIcon from './components/StudipIcon.vue';
-import RangeInput from './components/RangeInput.vue';
-import Datepicker from './components/Datepicker.vue';
-import Datetimepicker from './components/Datetimepicker.vue';
-import TextareaWithToolbar from './components/TextareaWithToolbar.vue';
-import I18nTextarea from "./components/I18nTextarea.vue";
-import StudipWysiwyg from "./components/StudipWysiwyg.vue";
-// import StudipLoadingIndicator from './StudipLoadingIndicator.vue';
-import StudipMessageBox from './components/StudipMessageBox.vue';
-import StudipProxyCheckbox from './components/StudipProxyCheckbox.vue';
-import StudipProxiedCheckbox from './components/StudipProxiedCheckbox.vue';
-import StudipTooltipIcon from './components/StudipTooltipIcon.vue';
-import StudipSelect from './components/StudipSelect.vue';
-import StudipMultiPersonSearch from './components/StudipMultiPersonSearch.vue';
-
const BaseComponents = {
- CalendarPermissionsTable,
- DayOfWeekSelect,
- DateListInput,
- Multiselect,
- MyCoursesColouredTable,
- EditableList,
- Quicksearch,
- RangeInput,
- RepetitionInput,
- SidebarWidget,
- StudipActionMenu,
- StudipAssetImg,
- StudipDateTime,
- Datepicker,
- Datetimepicker,
- StudipDialog,
- StudipFileSize,
- StudipFolderSize,
- StudipIcon,
- I18nTextarea,
- StudipWysiwyg,
-// StudipLoadingIndicator,
- StudipMessageBox,
- StudipProxyCheckbox,
- StudipProxiedCheckbox,
- StudipTooltipIcon,
- StudipSelect,
- TextareaWithToolbar,
- StudipMultiPersonSearch
+ CaptchaInput: () => import('./components/form_inputs/CaptchaInput.vue'),
+ CalendarPermissionsTable: () => import("./components/form_inputs/CalendarPermissionsTable.vue"),
+ DateListInput: () => import('./components/form_inputs/DateListInput.vue'),
+ Datepicker: () => import('./components/Datepicker.vue'),
+ Datetimepicker: () => import('./components/Datetimepicker.vue'),
+ DayOfWeekSelect: () => import('./components/form_inputs/DayOfWeekSelect.vue'),
+ EditableList: () => import("./components/EditableList.vue"),
+ I18nTextarea: () => import("./components/I18nTextarea.vue"),
+ Multiselect: () => import('./components/Multiselect.vue'),
+ MyCoursesColouredTable: () => import('./components/form_inputs/MyCoursesColouredTable.vue'),
+ Quicksearch: () => import('./components/Quicksearch.vue'),
+ RangeInput: () => import('./components/RangeInput.vue'),
+ RepetitionInput: () => import("./components/form_inputs/RepetitionInput.vue"),
+ SidebarWidget: () => import('./components/SidebarWidget.vue'),
+ StudipActionMenu: () => import('./components/StudipActionMenu.vue'),
+ StudipAssetImg: () => import('./components/StudipAssetImg.vue'),
+ StudipDateTime: () => import('./components/StudipDateTime.vue'),
+ StudipDialog: () => import('./components/StudipDialog.vue'),
+ StudipFileSize: () => import('./components/StudipFileSize.vue'),
+ StudipFolderSize: () => import('./components/StudipFolderSize.vue'),
+ StudipIcon: () => import('./components/StudipIcon.vue'),
+ StudipMessageBox: () => import('./components/StudipMessageBox.vue'),
+ StudipMultiPersonSearch: () => import('./components/StudipMultiPersonSearch.vue'),
+ StudipProxiedCheckbox: () => import('./components/StudipProxiedCheckbox.vue'),
+ StudipProxyCheckbox: () => import('./components/StudipProxyCheckbox.vue'),
+ StudipSelect: () => import('./components/StudipSelect.vue'),
+ StudipTooltipIcon: () => import('./components/StudipTooltipIcon.vue'),
+ StudipWysiwyg: () => import("./components/StudipWysiwyg.vue"),
+ TextareaWithToolbar: () => import('./components/TextareaWithToolbar.vue'),
};
export default BaseComponents;
diff --git a/resources/vue/components/ActiveFilter.vue b/resources/vue/components/ActiveFilter.vue
index d4654fa..5394bce 100644
--- a/resources/vue/components/ActiveFilter.vue
+++ b/resources/vue/components/ActiveFilter.vue
@@ -4,7 +4,7 @@
<button
@click="onRemoveActiveFilter"
type="button"
- :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name })"
+ :title="$gettextInterpolate($gettext('Filter \'%{name}\' entfernen'), { name }, true)"
>
<StudipIcon class="text-bottom" shape="decline" role="presentation" alt="" />
</button>
diff --git a/resources/vue/components/AdminCourses.vue b/resources/vue/components/AdminCourses.vue
index a2f61d7..d24a900 100644
--- a/resources/vue/components/AdminCourses.vue
+++ b/resources/vue/components/AdminCourses.vue
@@ -23,7 +23,7 @@
<th v-for="activeField in sortedActivatedFields" :key="`field-${activeField}`" :class="sort.by === activeField ? 'sort' + sort.direction.toLowerCase() : ''">
<a href="#"
@click.prevent="changeSort(activeField)"
- :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}))"
+ :title="sort.by === activeField && sort.direction === 'ASC' ? $gettextInterpolate('Sortiert aufsteigend nach %{field}', {field: fields[activeField]}, true) : (sort.by === activeField && sort.direction === 'DESC' ? $gettextInterpolate('Sortiert absteigend nach %{ field } ', { field: fields[activeField]}, true) : $gettextInterpolate('Sortieren nach %{ field }', { field: fields[activeField]}, true))"
v-if="!unsortableFields.includes(activeField)"
>
{{ fields[activeField] }}
@@ -221,22 +221,28 @@ export default {
});
},
sortArray (array) {
+ const mappedFields = {
+ last_activity: 'last_activity_raw',
+ semester: 'semester_sort',
+ };
+
if (!array.length) {
return [];
}
- let sortby = this.sort.by;
- if (!this.activatedFields.includes(sortby) && sortby !== 'completion') {
+ if (!this.activatedFields.includes(this.sort.by) && this.sort.by !== 'completion') {
return array;
}
const striptags = function (text) {
- if (typeof text === "string") {
+ if (typeof text === 'string') {
return text.replace(/(<([^>]+)>)/gi, "");
} else {
return text;
}
};
+ let sortby = mappedFields[this.sort.by] ?? this.sort.by;
+
// Define sort direction by this factor
const directionFactor = this.sort.direction === 'ASC' ? 1 : -1;
@@ -246,33 +252,28 @@ export default {
sensitivity: 'base'
});
let sortFunction = function (a, b) {
- return collator.compare(striptags(a[sortby]), striptags(b[sortby]));
+ return collator.compare(striptags(a[sortby]), striptags(b[sortby]))
+ || collator.compare(striptags(a.number), striptags(b.number));
};
- if (sortby === 'last_activity') {
- sortFunction = (a, b) => a.last_activity_raw - b.last_activity_raw;
- } else if (sortby === 'name') {
- sortFunction = (a, b) => {
- return collator.compare(striptags(a.name), striptags(b.name))
- || collator.compare(striptags(a.number), striptags(b.number));
- };
- } else if (sortby === 'number') {
+ if (sortby === 'number') {
sortFunction = (a, b) => {
return collator.compare(striptags(a.number), striptags(b.number))
|| collator.compare(striptags(a.name), striptags(b.name));
};
} else {
- let is_numeric = true;
- for (let i in array) {
- if (striptags(array[i][sortby]) && isNaN(striptags(array[i][sortby]))) {
- is_numeric = false;
- break;
- }
- }
+ let is_numeric = !array.some(i => {
+ const value = striptags(i[sortby]);
+ return value && isNaN(parseInt(value, 10));
+ });
+
if (is_numeric) {
sortFunction = function (a, b) {
- return (striptags(a[sortby]) ? parseInt(striptags(a[sortby]), 10) : 0)
- - (striptags(b[sortby]) ? parseInt(striptags(b[sortby]), 10) : 0);
+ const aValue = (striptags(a[sortby]) ? parseInt(striptags(a[sortby]), 10) : 0);
+ const bValue = (striptags(b[sortby]) ? parseInt(striptags(b[sortby]), 10) : 0);
+
+ return aValue - bValue
+ || collator.compare(striptags(a.number), striptags(b.number));
};
}
}
diff --git a/resources/vue/components/CacheAdministration.vue b/resources/vue/components/CacheAdministration.vue
index af3d461..5da9568 100644
--- a/resources/vue/components/CacheAdministration.vue
+++ b/resources/vue/components/CacheAdministration.vue
@@ -82,24 +82,24 @@ export default {
* @param event
*/
getCacheConfig (event) {
- fetch(STUDIP.URLHelper.getURL(`dispatch.php/admin/cache/get_config/${this.selectedCacheType}`))
- .then((response) => {
- if (!response.ok) {
- throw response
- }
+ const url = STUDIP.URLHelper.getURL(
+ 'dispatch.php/admin/cache/get_config',
+ {cache: this.selectedCacheType},
+ true
+ );
+ fetch(url).then((response) => {
+ if (!response.ok) {
+ throw response
+ }
- response.json()
- .then((json) => {
- this.configComponent = json.component
- this.configProps = json.props
- }).catch((error) => {
- console.error(error)
- console.error(error.status + ': ', error.statusText)
- })
- }).catch((error) => {
- console.error(error)
- console.error(error.status + ': ', error.statusText)
- })
+ response.json().then((json) => {
+ this.configComponent = json.component
+ this.configProps = json.props
+ });
+ }).catch((error) => {
+ console.error(error)
+ console.error(error.status + ': ', error.statusText)
+ })
},
validateConfig () {
if (this.configComponent == null || this.isValid) {
diff --git a/resources/vue/components/ConsultationCreator.vue b/resources/vue/components/ConsultationCreator.vue
new file mode 100644
index 0000000..375e98a
--- /dev/null
+++ b/resources/vue/components/ConsultationCreator.vue
@@ -0,0 +1,496 @@
+<template>
+ <form :action="storeUrl" method="post" class="default" :data-dialog="asDialog ? '' : null" @submit="validateInputs">
+ <input type="hidden" :name="csrf.name" :value="csrf.value">
+ <input v-for="id in responsibleGroups" type="hidden" name="responsibilities[statusgroup][]" :value="id" :key="`group-${id}`">
+ <input v-for="id in responsibleInstitutes" type="hidden" name="responsibilities[institute][]" :value="id" :key="`institute-${id}`">
+ <input v-for="id in responsibleUsers" type="hidden" name="responsibilities[user][]" :value="id" :key="`user-${id}`">
+
+ <StudipMessageBox type="info" v-if="errors.length > 0">
+ {{ $gettext('Folgende Angaben müssen korrigiert werden, um das Formular abschicken zu können:') }}
+
+ <template #details>
+ <ul>
+ <li v-for="(error, index) in errors" :key="`error-${index}`">
+ {{ error }}
+ </li>
+ </ul>
+ </template>
+ </StudipMessageBox>
+
+ <fieldset>
+ <legend>{{ $gettext('Ort und Zeit') }}</legend>
+
+ <label>
+ <span class="required">{{ $gettext('Ort') }}</span>
+
+ <input required type="text" name="room"
+ v-model="room"
+ :placeholder="$gettext('Ort')">
+ </label>
+
+ <label :class="{'col-3': !isSingleDay}">
+ <span class="required">{{ $gettext('Intervall') }}</span>
+ <select required name="interval" v-model="interval">
+ <option v-for="(label, value) in intervals" :key="value" :value="value">
+ {{ label }}
+ </option>
+ </select>
+ </label>
+
+ <label class="col-3" v-if="!isSingleDay">
+ <span class="required">{{ $gettext('Am Wochentag') }}</span>
+
+ <select required name="day-of-week" v-model="dayOfWeek">
+ <option v-for="(label, value) in daysOfTheWeek" :value="value" :key="value">
+ {{ label }}
+ </option>
+ </select>
+ </label>
+
+ <label :class="{'col-3': !isSingleDay}">
+ <span class="required">{{ isSingleDay ? $gettext('Datum') : $gettext('Beginn') }}</span>
+
+ <Datepicker v-model="startDate"
+ name="start-date"
+ :disable-holidays="true"
+ :placeholder="$gettext('tt.mm.jjjj')"
+ mindate="today"
+ :emit-date="true"
+ ></Datepicker>
+ </label>
+
+ <label class="col-3" v-if="!isSingleDay">
+ <span class="required">{{ $gettext('Ende') }}</span>
+
+ <Datepicker v-model="endDate"
+ name="end-date"
+ :disable-holidays="true"
+ :placeholder="$gettext('tt.mm.jjjj')"
+ :mindate="startDate"
+ :emit-date="true"
+ ></Datepicker>
+ </label>
+
+ <label for="start-time" class="col-3">
+ <span class="required">{{ $gettext('Von') }}</span>
+
+ <Timepicker name="start-time"
+ v-model="startTime"
+ :maxtime="endTime"
+ ></Timepicker>
+ </label>
+
+ <label for="ende_hour" class="col-3">
+ <span class="required">{{ $gettext('Bis') }}</span>
+
+ <Timepicker name="end-time"
+ v-model="endTime"
+ :mintime="startTime"
+ ></Timepicker>
+ </label>
+
+ <label class="col-3">
+ <span class="required">{{ $gettext('Dauer eines Termins in Minuten') }}</span>
+ <input required type="number" name="duration" min="1"
+ v-model="duration">
+ </label>
+
+ <label class="col-3">
+ {{ $gettext('Maximale Teilnehmerzahl') }}
+ <StudipTooltipIcon :text="$gettext('Falls Sie mehrere Personen zulassen wollen (wie z.B. zu einer Klausureinsicht), so geben Sie hier die maximale Anzahl an Personen an, die sich anmelden dürfen.')"></StudipTooltipIcon>
+ <input required type="text" name="size" id="size" min="1" max="50"
+ v-model="size">
+ </label>
+
+ <label>
+ <input type="checkbox" name="pause" value="1"
+ v-model="pause">
+ {{ $gettext('Pausen zwischen den Terminen einfügen?') }}
+ </label>
+
+ <label class="col-3" v-if="pause">
+ {{ $gettext('Eine Pause nach wie vielen Minuten einfügen?') }}
+ <input type="number" name="pause_time" min="1"
+ v-model="pauseTime">
+ </label>
+
+ <label class="col-3" v-if="pause">
+ {{ $gettext('Dauer der Pause in Minuten') }}
+ <input type="number" name="pause_duration" min="1"
+ v-model="pauseDuration">
+ </label>
+
+ <label>
+ <input type="checkbox" name="lock" value="1"
+ v-model="lock">
+ {{ $gettext('Termine für Buchungen sperren?') }}
+ </label>
+
+ <label v-if="lock">
+ {{ $gettext('Wieviele Stunden vor Beginn des Blocks sollen die Termine für Buchungen gesperrt werden?') }}
+ <input type="number" name="lock_time" min="1"
+ v-model="lockTime">
+ </label>
+
+ <label>
+ <input type="checkbox" name="consecutive" value="1"
+ v-model="consecutive">
+ {{ $gettext('Termine innerhalb der Blöcke nur fortlaufend vergeben') }}
+ </label>
+
+ <slot name="extension-point-1"></slot>
+ </fieldset>
+
+ <fieldset v-if="withResponsible">
+ <legend>{{ $gettext('Durchführende Personen, Gruppen oder Einrichtungen') }}</legend>
+
+ <template v-if="isInstitute">
+ <p>
+ {{ $gettext('Bei Einrichtungen muss mindestens eine durchführende Person, Gruppe oder Einrichtung zugewiesen werden.') }}
+ </p>
+ <p>
+ {{ $gettext('Bitte beachten Sie, dass bei Zuweisungen von Statusgruppen alle Personen der Gruppe mit dem Status '
+ + '"tutor" und "dozent" als durchführende Personen zugewiesen werden und über alle Buchungen '
+ + 'informiert werden.') }}
+ {{ $gettext('Gleiches gilt für eine zugewiesene Einrichtung. Bitte achten Sie darauf, dass Sie Ihre hier '
+ + ' getroffene Auswahl in Absprache tätigen.') }}
+ </p>
+ </template>
+
+ <label v-if="withResponsible.users">
+ {{ $gettext('Durchführende Personen') }}
+ <StudipSelect v-model="responsibleUsers"
+ :options="withResponsible.users"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+
+ <label v-if="withResponsible.groups">
+ {{ $gettext('Durchführende Gruppen') }}
+ <StudipSelect v-model="responsibleGroups"
+ :options="withResponsible.groups"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+
+ <label v-if="withResponsible.institutes">
+ {{ $gettext('Durchführende Einrichtungen') }}
+ <StudipSelect v-model="responsibleInstitutes"
+ :options="withResponsible.institutes"
+ :reduce="option => option.id"
+ multiple
+ :clearable="true"
+ >
+ <template #open-indicator>
+ <span><studip-icon shape="arr_1down" :size="10" /></span>
+ </template>
+ </StudipSelect>
+ </label>
+ </fieldset>
+
+ <fieldset>
+ <legend>{{ $gettext('Weitere Einstellungen') }}</legend>
+
+ <label>
+ {{ $gettext('Information zu den Terminen in diesem Block') }}
+ <textarea name="note" v-model="note"></textarea>
+ </label>
+
+ <label>
+ <input type="checkbox" name="calender-events" value="1"
+ v-model="calendarEvents">
+ {{ $gettext('Die freien Termine auch im Kalender markieren') }}
+ </label>
+
+ <label v-if="isCourse">
+ <input type="checkbox" name="mail-to-tutors" value="1"
+ v-model="mailToTutors">
+ {{ $gettext('Tutor/innen beim Versand von Buchungsbenachrichtigungen berücksichtigen?') }}
+ </label>
+
+ <label>
+ <input type="checkbox" name="show-participants" value="1"
+ v-model="showParticipants">
+ {{ $gettext('Namen der buchenden Personen sind öffentlich sichtbar') }}
+ </label>
+
+ <label>{{ $gettext('Grund der Buchung abfragen') }}</label>
+ <div class="hgroup">
+ <label>
+ <input type="radio" name="require-reason" value="yes"
+ v-model="requireReason">
+ {{ $gettext('Ja, zwingend erforderlich') }}
+ </label>
+
+ <label>
+ <input type="radio" name="require-reason" value="optional"
+ v-model="requireReason">
+ {{ $gettext('Ja, optional') }}
+ </label>
+
+ <label>
+ <input type="radio" name="require-reason" value="no"
+ v-model="requireReason">
+ {{ $gettext('Nein') }}
+ </label>
+ </div>
+
+ <label>
+ {{ $gettext('Bestätigung für folgenden Text einholen') }}
+ ({{ $gettext('optional') }})
+ <StudipTooltipIcon :text="$gettext('Wird hier ein Text eingegeben, so müssen Buchende bestätigen, dass sie diesen Text gelesen haben.')"></StudipTooltipIcon>
+ <textarea name="confirmation-text" v-model="confirmationText"></textarea>
+ </label>
+
+ <slot name="extension-point-2"></slot>
+ </fieldset>
+
+ <fieldset v-if="needsConfirmation">
+ <legend>{{ $gettext('Bestätigung der Erstellung vieler Termine') }}</legend>
+
+ <p>
+ {{ $gettext('Sie erstellen eine sehr große Anzahl an Terminen.') }}
+ {{ $gettext('Bitte bestätigen Sie diese Aktion.') }}
+ </p>
+
+ <label>
+ <input type="checkbox" v-model="confirmed">
+ {{ $gettextInterpolate(
+ $gettext('Ja, ich möchte wirklich %{ n } Termine erstellen.'),
+ { n: slotCount }
+ ) }}
+ </label>
+ </fieldset>
+
+ <footer data-dialog-button>
+ <button class="accept button" :disabled="!confirmed">
+ {{ $gettext('Termin speichern') }}
+ </button>
+ <a :href="cancelUrl" class="cancel button" @click="evt => closeCreator(evt)">
+ {{ $gettext('Abbrechen') }}
+ </a>
+ </footer>
+ </form>
+</template>
+<script>
+import StudipTooltipIcon from './StudipTooltipIcon.vue';
+import Datepicker from './Datepicker.vue';
+
+import moment from 'moment';
+import StudipSelect from './StudipSelect.vue';
+import Timepicker from './Timepicker.vue';
+
+export default {
+ name: 'ConsultationCreator',
+ components: {Datepicker, StudipSelect, StudipTooltipIcon, Timepicker},
+ props: {
+ asDialog: {
+ type: Boolean,
+ default: false,
+ },
+ cancelUrl: {
+ type: String,
+ required: true
+ },
+ defaultRoom: String,
+ rangeType: {
+ type: String,
+ required: true,
+ },
+ slotCountThreshold: {
+ type: Number,
+ required: true,
+ },
+ storeUrl: {
+ type: String,
+ required: true
+ },
+ withResponsible: {
+ type: [Boolean, Object],
+ default: false,
+ },
+ },
+ data() {
+ return {
+ calendarEvents: false,
+ confirmationText: '',
+ confirmed: false,
+ consecutive: false,
+ dayOfWeek: (new Date()).getDay(),
+ duration: 15,
+ endDate: moment().add(4, 'weeks').toDate(),
+ endTime: '09:00',
+ errors: [],
+ interval: 1,
+ lock: false,
+ lockTime: 24,
+ mailToTutors: true,
+ note: '',
+ pause: false,
+ pauseDuration: 15,
+ pauseTime: 45,
+ requireReason: 'optional',
+ responsibleGroups: [],
+ responsibleInstitutes: [],
+ responsibleUsers: [],
+ room: this.defaultRoom,
+ showParticipants: false,
+ size: 1,
+
+ slotCount: null,
+ startDate: moment().add(1, 'weeks').toDate(),
+ startTime: '08:00',
+ }
+ },
+ computed: {
+ csrf() {
+ return STUDIP.CSRF_TOKEN;
+ },
+ daysOfTheWeek() {
+ return {
+ 1: this.$gettext('Montag'),
+ 2: this.$gettext('Dienstag'),
+ 3: this.$gettext('Mittwoch'),
+ 4: this.$gettext('Donnerstag'),
+ 5: this.$gettext('Freitag'),
+ 6: this.$gettext('Samstag'),
+ 0: this.$gettext('Sonntag'),
+ };
+ },
+ intervals() {
+ return {
+ 0: this.$gettext('einmalig (ohne Wiederholung)'),
+ 1: this.$gettext('wöchentlich'),
+ 2: this.$gettext('zweiwöchentlich'),
+ 3: this.$gettext('dreiwöchentlich'),
+ 4: this.$gettext('monatlich'),
+ };
+ },
+ isCourse() {
+ return this.rangeType === 'Course';
+ },
+ isInstitute() {
+ return this.rangeType === 'Institute';
+ },
+ isSingleDay() {
+ return this.interval === '0';
+ },
+ needsConfirmation() {
+ return this.slotCount > this.slotCountThreshold;
+ },
+ recalculationProperty() {
+ return [
+ this.startDate,
+ this.startTime,
+ this.endDate,
+ this.endTime,
+ this.dayOfWeek,
+ this.interval,
+ this.duration,
+ this.pause,
+ this.pauseTime,
+ this.pauseDuration,
+ ].join();
+ },
+ },
+ methods: {
+ closeCreator(event) {
+ if (this.$el.closest('.studip-dialog')) {
+ STUDIP.Dialog.close();
+ event.preventDefault();
+ }
+ },
+ validateInputs(event) {
+ const errors = [];
+
+ if (this.startTime > this.endTime) {
+ errors.push(this.$gettext('Die Endzeit liegt vor der Startzeit!'));
+ }
+
+ if (this.startDate > this.endDate) {
+ errors.push(this.$gettext('Das Enddatum liegt vor dem Startdatum!'));
+ }
+
+ if (this.pauseTime && this.pauseTime < this.duration) {
+ errors.push(this.$gettext('Die definierte Zeit bis zur Pause ist kleiner als die Dauer eines Termins.'));
+ }
+
+ if (
+ this.isInstitute
+ && this.responsibleGroups.length === 0
+ && this.responsibleInstitutes.length === 0
+ && this.responsibleUsers.length === 0
+ ) {
+ errors.push(this.$gettext('Es muss mindestens eine durchführende Person, Statusgruppe oder Einrichtung ausgewählt werden.'));
+ }
+
+ if (this.needsConfirmation && !this.confirmed) {
+ errors.push(this.$gettext('Sie müssen bestätigen, dass sie eine große Anzahl von Terminen erstellen möchten.'));
+
+ }
+
+ if (errors.length > 0) {
+ this.errors = errors;
+ event.preventDefault();
+ }
+ },
+ combineDateAndTime(date, time) {
+ const [hour, minute] = time.split(':').map(item => parseInt(item, 10));
+ const result = new Date(date);
+ result.setHours(hour);
+ result.setMinutes(minute);
+ result.setSeconds(0);
+ return result;
+ }
+ },
+ watch: {
+ interval(current) {
+ if (current === '0') {
+ this.endDate = new Date(this.startDate);
+ }
+ },
+ recalculationProperty: {
+ handler() {
+ STUDIP.jsonapi.withPromises().GET('consultation-slots/count', {
+ data: {
+ start: this.combineDateAndTime(this.startDate, this.startTime).toISOString(),
+ end: this.combineDateAndTime(this.endDate, this.endTime).toISOString(),
+ dow: this.dayOfWeek,
+ interval: this.interval,
+ duration: this.duration,
+ pause_time: this.pause ? this.pauseTime : null,
+ pause_duration: this.pause ? this.pauseDuration : null,
+ }
+ }).then((count) => {
+ this.slotCount = count;
+ this.confirmed = count <= this.slotCountThreshold;
+ });
+ },
+ immediate: true
+ },
+ startDate(current) {
+ this.dayOfWeek = current.getDay();
+ },
+ },
+ beforeCreate() {
+ STUDIP.Vue.emit('ConsultationCreatorWillCreate', this);
+ }
+}
+</script>
+<style scoped>
+form.default label input[type="time"] {
+ max-width: 48em;
+}
+</style>
diff --git a/resources/vue/components/ContentModulesControl.vue b/resources/vue/components/ContentModulesControl.vue
index f403bdd..34f4125 100644
--- a/resources/vue/components/ContentModulesControl.vue
+++ b/resources/vue/components/ContentModulesControl.vue
@@ -15,7 +15,7 @@
@click.prevent="toggleModuleVisibility(module)">
<studip-icon :shape="module.visibility !== 'tutor' ? 'visibility-visible' : 'visibility-invisible'"
class="text-bottom"
- :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname})"></studip-icon>
+ :title="$gettextInterpolate($gettext('Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'), { name: module.displayname}, true)"></studip-icon>
</a>
</div>
</div>
diff --git a/resources/vue/components/ContentModulesEditTiles.vue b/resources/vue/components/ContentModulesEditTiles.vue
index 383c6ab..7407632 100644
--- a/resources/vue/components/ContentModulesEditTiles.vue
+++ b/resources/vue/components/ContentModulesEditTiles.vue
@@ -35,7 +35,8 @@
$gettext(
'Sortierelement für Werkzeug %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
),
- { module: module.displayname }
+ { module: module.displayname },
+ true
)
"
@keydown="keyboardHandler($event, module)"
@@ -76,7 +77,8 @@
$gettext(
'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
@@ -90,7 +92,8 @@
$gettext(
'Umbenennen des Inhaltsmoduls %{ name }'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
diff --git a/resources/vue/components/ContentmodulesEditTable.vue b/resources/vue/components/ContentmodulesEditTable.vue
index 8724a24..0a8a0aa 100644
--- a/resources/vue/components/ContentmodulesEditTable.vue
+++ b/resources/vue/components/ContentmodulesEditTable.vue
@@ -25,7 +25,8 @@
$gettext(
'Sortierelement für Module %{module}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'
),
- { module: module.displayname }
+ { module: module.displayname },
+ true
)
"
@keydown="keyboardHandler($event, module)"
@@ -71,7 +72,8 @@
$gettext(
'Inhaltsmodul %{ name } für Teilnehmende unsichtbar bzw. sichtbar schalten'
),
- { name: module.displayname }
+ { name: module.displayname },
+ true
)
"
></studip-icon>
@@ -83,7 +85,7 @@
:title="
$gettextInterpolate($gettext('Umbenennen des Inhaltsmoduls %{ name }'), {
name: module.displayname,
- })
+ }, true)
"
></studip-icon>
</a>
diff --git a/resources/vue/components/Datepicker.vue b/resources/vue/components/Datepicker.vue
index 3db44ce..5c2c0f7 100644
--- a/resources/vue/components/Datepicker.vue
+++ b/resources/vue/components/Datepicker.vue
@@ -1,75 +1,143 @@
<template>
<span>
- <input type="hidden" :name="name" :value="value">
+ <input type="hidden" :name="name" :value="returnValue">
<input type="text"
ref="visibleInput"
class="visible_input"
- @change="setUnixTimestamp"
v-bind="$attrs"
- v-on="$listeners">
+ v-on="$listeners"
+ :placeholder="placeholder">
</span>
</template>
<script>
+import RestrictedDatesHelper from '../../assets/javascripts/lib/RestrictedDatesHelper';
+
export default {
- name: "datepicker",
+ name: 'Datepicker',
inheritAttrs: false,
props: {
name: {
type: String,
required: false
},
- value: {
- required: false
+ value: [Date, String, Number],
+ mindate: [Date, Number, String],
+ maxdate: [Date, Number, String],
+ placeholder: String,
+ disableHolidays: {
+ type: Boolean,
+ default: false,
},
- mindate: {
- required: false
+ emitDate: {
+ type: Boolean,
+ default: false,
},
- maxdate: {
- required: false
+ returnAs: {
+ type: String,
+ default: 'localized',
+ validator(value) {
+ return ['localized', 'unix', 'iso'].includes(value);
+ }
+ }
+ },
+ computed: {
+ input() {
+ return $(this.$refs.visibleInput);
+ },
+ parameters() {
+ let params = {
+ onSelect: () => {
+ this.setUnixTimestamp();
+ },
+ maxDate: this.convertInputToNativeDate(this.maxdate),
+ minDate: this.convertInputToNativeDate(this.mindate),
+ };
+ if (this.disableHolidays) {
+ params.beforeShowDay = (date) => {
+ RestrictedDatesHelper.loadRestrictedDatesByYear(date.getFullYear()).then(
+ () => this.input.datepicker('refresh'),
+ () => null
+ );
+
+ const {reason, lock} = RestrictedDatesHelper.isDateRestricted(date);
+ return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason];
+ };
+ }
+
+ return params;
+ },
+ returnValue() {
+ if (this.returnAs === 'unix') {
+ return this.convertInputToUnixTimestamp(this.value);
+ }
+
+ if (this.returnAs === 'iso') {
+ return this.convertInputToNativeDate(this.value).toISOString();
+ }
+
+ return this.convertInputToNativeDate(this.value).toLocaleDateString();
}
},
methods: {
+ convertInputToNativeDate(input) {
+ if (input instanceof Date) {
+ return input;
+ }
+
+ if (input === 'today') {
+ return new Date();
+ }
+
+ return input ? new Date(input * 1000) : null;
+ },
+ convertInputToUnixTimestamp(input) {
+ if (input instanceof Date) {
+ return Math.floor(input.getTime() / 1000);
+ }
+
+ if (!isNaN(parseInt(input, 10))) {
+ return parseInt(input, 10);
+ }
+
+ return input;
+ },
setUnixTimestamp () {
- let formatted_date = this.$refs.visibleInput.value;
- let date = formatted_date.match(/(\d+)/g);
- date = new Date(`${date[2]}-${date[1]}-${date[0]} ${date[3]}:${date[4]}`);
- this.$emit('input', Math.floor(date / 1000));
+ let date = this.input.datepicker('getDate');
+ this.$emit('input', this.emitDate ? date : Math.floor(date.getTime() / 1000));
}
},
mounted () {
- let value = !isNaN(parseInt(this.value, 10)) ? parseInt(this.value, 10) : this.value;
+ let value = this.convertInputToUnixTimestamp(this.value);
+
if (Number.isInteger(value)) {
let date = new Date(value * 1000);
- let formatted_date =
- (date.getDate() < 10 ? "0" : "") + date.getDate()
- + "."
- + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1)
- + "."
- + date.getFullYear();
- this.$refs.visibleInput.value = formatted_date;
+ this.input.val(date.toLocaleDateString());
} else {
- this.$refs.visibleInput.value = value;
- }
- let params = {
- onSelect: () => {
- this.setUnixTimestamp();
- }
- };
- if (this.mindate) {
- params.minDate = new Date(this.mindate * 1000)
- }
- if (this.maxdate) {
- params.maxDate = new Date(this.maxdate * 1000)
+ this.input.val(value);
}
- $(this.$refs.visibleInput).datetimepicker(params);
+ this.input.datepicker(this.parameters);
},
watch: {
- mindate (new_data, old_data) {
- $(this.$refs.visibleInput).datetimepicker('option', 'minDate', new Date(new_data * 1000));
+ maxdate(current) {
+ this.input.datepicker(
+ 'option',
+ 'maxDate',
+ this.convertInputToNativeDate(current)
+ );
},
- maxdate (new_data, old_data) {
- $(this.$refs.visibleInput).datetimepicker('option', 'maxDate', new Date(new_data * 1000));
+ mindate(current) {
+ this.input.datepicker(
+ 'option',
+ 'minDate',
+ this.convertInputToNativeDate(current)
+ );
+ },
+ value(current, previous) {
+ if (current.toISOString() !== previous.toISOString()) {
+ this.input.datepicker('setDate', current);
+ this.input.datepicker('refresh');
+ }
}
}
}
diff --git a/resources/vue/components/EditableList.vue b/resources/vue/components/EditableList.vue
index cf1716b..39c32ac 100644
--- a/resources/vue/components/EditableList.vue
+++ b/resources/vue/components/EditableList.vue
@@ -12,7 +12,7 @@
<studip-icon v-if="item.icon" :shape="item.icon" role="info" :size="20" class="text-bottom" alt=""></studip-icon>
<input v-if="name" type="hidden" :name="name + '[]'" :value="item.value">
<span>{{item.name}}</span>
- <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name})" class="undecorated">
+ <button v-if="item.deletable" @click.prevent="deleteItem(item)" :title="$gettextInterpolate($gettext('%{ name } löschen'), {name: item.name}, true)" class="undecorated">
<studip-icon shape="trash" role="clickable" :size="20" class="text-bottom"></studip-icon>
</button>
</li>
diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue
index 646e914..1e19cc9 100644
--- a/resources/vue/components/FilesTable.vue
+++ b/resources/vue/components/FilesTable.vue
@@ -42,40 +42,83 @@
</colgroup>
<thead>
<tr class="sortable">
- <th v-if="show_bulk_actions" data-sort="false" :aria-label="$gettext('Ordner und Dateien auswählen')">
+ <th v-if="show_bulk_actions"
+ data-sort="false"
+ :aria-label="$gettext('Ordner und Dateien auswählen')">
<studip-proxy-checkbox
v-model="selectedIds"
:total="allIds"
:title="$gettext('Alle Ordner und Dateien auswählen')"
></studip-proxy-checkbox>
</th>
- <th @click="sort('mime_type')" :class="sortClasses('mime_type')">
- <a href="#" @click.prevent>
+ <th @click="sort('mime_type')"
+ :class="sortClasses('mime_type')"
+ :aria-sort="getAriaSortString('mime_type')"
+ :aria-label="getAriaSortLabel('mime_type', $gettext('Typ'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Typ sortieren')">
{{ $gettext('Typ') }}
</a>
</th>
- <th @click="sort('name')" :class="sortClasses('name')">
- <a href="#" @click.prevent>
+ <th @click="sort('name')"
+ :class="sortClasses('name')"
+ :aria-sort="getAriaSortString('name')"
+ :aria-label="getAriaSortLabel('name', $gettext('Name'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Name sortieren')">
{{ $gettext('Name') }}
</a>
</th>
- <th @click="sort('size')" class="responsive-hidden" :class="sortClasses('size')">
- <a href="#" @click.prevent>
+ <th @click="sort('size')"
+ class="responsive-hidden"
+ :class="sortClasses('size')"
+ :aria-sort="getAriaSortString('size')"
+ :aria-label="getAriaSortLabel('size', $gettext('Größe'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Größe sortieren')">
{{ $gettext('Größe') }}
</a>
</th>
- <th v-if="showdownloads" @click="sort('downloads')" class="responsive-hidden" :class="sortClasses('downloads')">
- <a href="#" @click.prevent>
+ <th v-if="showdownloads"
+ @click="sort('downloads')"
+ class="responsive-hidden"
+ :class="sortClasses('downloads')"
+ :aria-sort="getAriaSortString('downloads')"
+ :aria-label="getAriaSortLabel('downloads', $gettext('Downloads'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Downloads sortieren')">
{{ $gettext('Downloads') }}
</a>
</th>
- <th class="responsive-hidden" @click="sort('author_name')" :class="sortClasses('author_name')">
- <a href="#" @click.prevent>
+ <th class="responsive-hidden"
+ @click="sort('author_name')"
+ :class="sortClasses('author_name')"
+ :aria-sort="getAriaSortString('author_name')"
+ :aria-label="getAriaSortLabel('author_name', $gettext('Autor/-in'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Autor/-in sortieren')">
{{ $gettext('Autor/-in') }}
</a>
</th>
- <th class="responsive-hidden" @click="sort('chdate')" :class="sortClasses('chdate')">
- <a href="#" @click.prevent>
+ <th class="responsive-hidden"
+ @click="sort('chdate')"
+ :class="sortClasses('chdate')"
+ :aria-sort="getAriaSortString('chdate')"
+ :aria-label="getAriaSortLabel('chdate', $gettext('Datum'))"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettext('Nach Datum sortieren')">
{{ $gettext('Datum') }}
</a>
</th>
@@ -83,11 +126,15 @@
:key="index"
@click="sort(index)"
class="responsive-hidden"
- :class="sortClasses(index)">
- <a href="#" @click.prevent>
+ :class="sortClasses(index)"
+ :aria-sort="getAriaSortString(name)"
+ :aria-label="getAriaSortLabel(name, name)"
+ >
+ <a href="#"
+ @click.prevent
+ :title="$gettextInterpolate($gettext('Nach %{ colName } sortieren'), {colName: name}, true)">
{{name}}
</a>
-
</th>
<th class="actions" data-sort="false">{{ $gettext('Aktionen') }}</th>
</tr>
@@ -120,12 +167,17 @@
></studip-proxied-checkbox>
</td>
<td class="document-icon">
- <a :href="folder.url" :id="`folder-${folder.id}`">
- <studip-icon :shape="folder.icon" :size="26" class="text-bottom"></studip-icon>
+ <a :href="folder.url"
+ :id="`folder-${folder.id}`"
+ :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
+ { foldername: folder.name}, true)">
+ <studip-icon :shape="folder.icon" :size="26" class="text-bottom" alt=""></studip-icon>
</a>
</td>
<td :class="{'filter-match': valueMatchesFilter(folder.name)}">
- <a :href="folder.url">
+ <a :href="folder.url"
+ :title="$gettextInterpolate($gettext('Ordner %{foldername} öffnen'),
+ { foldername: folder.name}, true)">
<span v-html="highlightString(folder.name)"></span>
</a>
</td>
@@ -172,7 +224,11 @@
></studip-proxied-checkbox>
</td>
<td class="document-icon">
- <a v-if="file.download_url" :href="file.download_url" target="_blank" rel="noopener noreferrer">
+ <a v-if="file.download_url"
+ :href="file.download_url"
+ target="_blank" rel="noopener noreferrer"
+ :title="$gettextInterpolate($gettext('Datei %{filename} herunterladen'),
+ { filename: file.name }, true)">
<studip-icon :shape="file.icon" :size="24" class="text-bottom"></studip-icon>
</a>
<studip-icon v-else :shape="file.icon" :size="24"></studip-icon>
@@ -180,10 +236,16 @@
<a :href="file.download_url"
v-if="file.download_url && file.mime_type.indexOf('image/') === 0"
class="lightbox-image"
- data-lightbox="gallery"></a>
+ data-lightbox="gallery"
+ :title="$gettextInterpolate($gettext('Datei %{filename} anzeigen'),
+ { filename: file.name }, true)"></a>
</td>
<td :class="{'filter-match': valueMatchesFilter(file.name)}">
- <a :href="file.details_url" data-dialog :id="`file-${file.id}`">
+ <a :href="file.details_url"
+ data-dialog
+ :id="`file-${file.id}`"
+ :title="$gettextInterpolate($gettext('Details zur Datei %{filename} anzeigen'),
+ { filename: file.name }, true)">
<span v-html="highlightString(file.name)"></span>
<studip-icon v-if="file.isAccessible"
shape="accessibility"
@@ -405,6 +467,20 @@ export default {
this.$gettext('Datei %{name} auswählen'),
{name: file.name}
);
+ },
+ getAriaSortString(column) {
+ return column === this.sortedBy
+ ? (this.sortDirection === 'asc' ? 'ascending' : 'descending')
+ : null;
+ },
+ getAriaSortLabel(column, label) {
+ if (column !== this.sortedBy) {
+ return null;
+ }
+ const template = this.sortDirection === 'asc'
+ ? this.$gettext('Es wird aufsteigend nach der Spalte %{ label } sortiert.')
+ : this.$gettext('Es wird absteigend nach der Spalte %{ label } sortiert.');
+ return this.$gettextInterpolate(template, { label });
}
},
computed: {
diff --git a/resources/vue/components/MyCoursesTables.vue b/resources/vue/components/MyCoursesTables.vue
index 80d04dd..1c39d95 100644
--- a/resources/vue/components/MyCoursesTables.vue
+++ b/resources/vue/components/MyCoursesTables.vue
@@ -12,7 +12,11 @@
</colgroup>
<thead>
<tr class="sortable">
- <th></th>
+ <th>
+ <span class="sr-only">
+ {{ $gettext('Zugeordnete Farbgruppe') }}
+ </span>
+ </th>
<th></th>
<th v-if="getConfig('sem_number') && !responsiveDisplay" :class="getOrderClasses('number')">
<a href="#" @click.prevent="changeOrder('number')">
@@ -37,7 +41,14 @@
<th v-if="!responsiveDisplay" class="dont-hide" colspan="2"></th>
</tr>
<tr v-for="course in getOrderedCourses(subgroup.ids)" :data-course-id="course.id" :class="getCourseClasses(course)" :key="course.id">
- <td :class="`gruppe${course.group}`"></td>
+ <td :class="`gruppe${course.group}`">
+ <span class="sr-only">
+ {{ $gettextInterpolate(
+ $gettext('Diese Veranstaltung gehört zur Farbgruppe %{group}'),
+ course
+ ) }}
+ </span>
+ </td>
<td :class="{'subcourse-indented': isChild(course)}">
<span :style="{backgroundImage: `url(${course.avatar}`}" class="my-courses-avatar course-avatar-small" :title="course.name" alt=""></span>
</td>
diff --git a/resources/vue/components/MyCoursesTiles.vue b/resources/vue/components/MyCoursesTiles.vue
index 73f8aad..12b2627 100644
--- a/resources/vue/components/MyCoursesTiles.vue
+++ b/resources/vue/components/MyCoursesTiles.vue
@@ -136,7 +136,7 @@ export default {
return this.shownColorPicker === course.id;
},
changeColor(course, index) {
- STUDIP.jsonapi.PATCH(`course-memberships/${course.id}_${this.userid}`, {
+ STUDIP.jsonapi.withPromises().patch(`course-memberships/${course.id}_${this.userid}`, {
data: {
data: {
type: 'course-memberships',
@@ -145,9 +145,9 @@ export default {
}
}
}
- }).done(() => {
+ }).then(() => {
course.group = index;
- }).always(() => {
+ }).finally(() => {
this.shownColorPicker = null;
});
},
diff --git a/resources/vue/components/Quicksearch.vue b/resources/vue/components/Quicksearch.vue
index 94b0a3a..0f37ae0 100644
--- a/resources/vue/components/Quicksearch.vue
+++ b/resources/vue/components/Quicksearch.vue
@@ -119,6 +119,7 @@ export default {
this.results = [];
this.$emit('input', this.returnValue, this.inputValue);
+ this.inputValue = '';
},
selectUp () {
if (this.selected > 0) {
diff --git a/resources/vue/components/StudipAssetImg.vue b/resources/vue/components/StudipAssetImg.vue
index 6a250a9..b60915b 100644
--- a/resources/vue/components/StudipAssetImg.vue
+++ b/resources/vue/components/StudipAssetImg.vue
@@ -1,6 +1,6 @@
<template>
<img :src="url"
- :width="width">
+ :width="width" alt="">
</template>
<script>
diff --git a/resources/vue/components/StudipDialog.vue b/resources/vue/components/StudipDialog.vue
index f14dd37..79337f8 100644
--- a/resources/vue/components/StudipDialog.vue
+++ b/resources/vue/components/StudipDialog.vue
@@ -1,6 +1,6 @@
<template>
<MountingPortal mountTo="body" append>
- <focus-trap v-model="trap" :initial-focus="() => defaultFocus ? $refs.buttonB : null">
+ <focus-trap v-model="trap">
<div class="studip-dialog" @keydown.esc="closeDialog">
<transition name="dialog-fade">
<div class="studip-dialog-backdrop">
@@ -38,7 +38,11 @@
<header
class="studip-dialog-header"
>
- <span :id="dialogTitleId" class="studip-dialog-title" :title="dialogTitle">
+ <span :id="dialogTitleId"
+ class="studip-dialog-title"
+ :title="dialogTitle"
+ role="heading"
+ aria-level="2">
{{ dialogTitle }}
</span>
<slot name="dialogHeader"></slot>
@@ -262,5 +266,13 @@ export default {
return typeof value !== "number" ? 0 : value;
}
},
+ mounted() {
+ if (this.defaultFocus) {
+ this.$nextTick()
+ .then(() => {
+ this.$refs.buttonB.focus();
+ });
+ }
+ }
};
</script>
diff --git a/resources/vue/components/StudipFileChooser.vue b/resources/vue/components/StudipFileChooser.vue
index e021aec..798d646 100644
--- a/resources/vue/components/StudipFileChooser.vue
+++ b/resources/vue/components/StudipFileChooser.vue
@@ -128,7 +128,7 @@ export default {
max-width: 48em;
button {
margin: 0.5ex 0 0.5ex 0;
- min-width: 140px;
+ width: 150px;
}
span {
box-sizing: border-box;
@@ -138,7 +138,7 @@ export default {
font-size: 14px;
line-height: 130%;
min-width: 100px;
- width: calc(100% - 140px);
+ width: calc(100% - 150px);
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 15px;
diff --git a/resources/vue/components/StudipIcon.vue b/resources/vue/components/StudipIcon.vue
index 9ca1c7b..5e27372 100644
--- a/resources/vue/components/StudipIcon.vue
+++ b/resources/vue/components/StudipIcon.vue
@@ -9,6 +9,7 @@
:role="ariaRole"
v-bind="$attrs"
v-on="$listeners"
+ :alt="$attrs.alt ?? ''"
/>
<img v-else
:src="url"
@@ -17,6 +18,7 @@
:role="ariaRole"
v-bind="$attrs"
v-on="$listeners"
+ :alt="$attrs.alt ?? ''"
/>
</template>
diff --git a/resources/vue/components/StudipTooltipIcon.vue b/resources/vue/components/StudipTooltipIcon.vue
index 39856bb..30d2033 100644
--- a/resources/vue/components/StudipTooltipIcon.vue
+++ b/resources/vue/components/StudipTooltipIcon.vue
@@ -37,6 +37,9 @@
</script>
<style lang="scss" scoped>
+.tooltip img {
+ vertical-align: text-bottom;
+}
.tooltip.tooltip-icon::before {
display: none;
}
diff --git a/resources/vue/components/StudipWysiwyg.vue b/resources/vue/components/StudipWysiwyg.vue
index e343e80..799c5f1 100644
--- a/resources/vue/components/StudipWysiwyg.vue
+++ b/resources/vue/components/StudipWysiwyg.vue
@@ -2,7 +2,7 @@
<ckeditor
:editor="editor"
:config="editorConfig"
- @ready="prefill"
+ @ready="onReady"
v-model="currentText"
@input="onInput"
/>
@@ -29,11 +29,15 @@ export default {
},
default: 'classic',
},
+ autofocus: Boolean,
},
data() {
return {
currentText: '',
editorConfig: {},
+
+ createdEditor: null,
+ shouldFocus: this.autofocus,
};
},
computed: {
@@ -48,13 +52,25 @@ export default {
},
},
methods: {
- prefill(editor) {
+ onReady(editor) {
+ this.createdEditor = editor;
this.currentText = this.text;
+
+ if (this.shouldFocus) {
+ this.focus();
+ }
},
onInput(value) {
this.currentText = value;
this.$emit('input', value);
},
+ focus() {
+ if (this.createdEditor) {
+ this.createdEditor.focus();
+ } else {
+ this.shouldFocus = true;
+ }
+ }
},
created() {
STUDIP.loadChunk('mathjax');
diff --git a/resources/vue/components/SystemNotification.vue b/resources/vue/components/SystemNotification.vue
new file mode 100644
index 0000000..60cefd7
--- /dev/null
+++ b/resources/vue/components/SystemNotification.vue
@@ -0,0 +1,173 @@
+<template>
+ <div v-cloak
+ class="system-notification"
+ :class="cssClasses"
+ @mouseover="disruptTimeout"
+ @mouseout="initTimeout"
+ @focus="disruptTimeout"
+ @blur="initTimeout"
+ >
+ <div class="system-notification-icon">
+ <studip-icon :shape="icon.shape"
+ :size="48"
+ :role="icon.color"
+ alt=""
+ title=""></studip-icon>
+ </div>
+ <div class="system-notification-content">
+ <p v-html="notification.message"></p>
+ <p class="sr-only" v-if="hasTimeout">
+ {{ $gettext('Strg+Alt+T hält das automatische Ausblenden der Meldung an bzw. setzt es wieder fort.') }}
+ </p>
+ <details v-if="notification.details?.length > 0"
+ class="system-notification-details">
+ <summary>
+ {{ $gettext('Details') }}
+ </summary>
+ <template v-if="Array.isArray(notification.details)">
+ <p v-for="(detail, index) in notification.details"
+ :key="index"
+ v-html="detail"></p>
+ </template>
+ <p v-else v-html="notification.details"></p>
+ </details>
+ </div>
+ <button v-if="allowClosing"
+ class="system-notification-close undecorated"
+ :title="$gettext('Diese Meldung schließen')"
+ @click.prevent="destroyMe"
+ @keydown.space="destroyMe"
+ tabindex="0">
+ <studip-icon shape="decline"
+ :size="20"
+ class="close-system-notification"/>
+ </button>
+ <transition v-if="hasTimeout"
+ name="system-notification-timeout"
+ appear
+ >
+ <div v-if="!stopTimeout"
+ class="system-notification-timeout"
+ ref="timeout-counter"></div>
+ </transition>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'SystemNotification',
+ props: {
+ allowClosing: {
+ type: Boolean,
+ default: true
+ },
+ appendTo: {
+ type: String,
+ default: null
+ },
+ notification: {
+ type: Object,
+ required: true
+ },
+ visibleFor: {
+ type: Number,
+ default: 5000
+ }
+ },
+ data() {
+ return {
+ stopTimeout: false,
+ timeout: null,
+ windowIsBlurred: false,
+ }
+ },
+ computed: {
+ cssClasses() {
+ const classes = [`system-notification-${this.notification.type}`];
+ if (this.isDisrupted) {
+ classes.push('system-notification-disrupted');
+ }
+ return classes;
+ },
+ hasTimeout() {
+ return !['exception', 'error'].includes(this.notification.type);
+ },
+ icon() {
+ let iconShape = 'info-circle';
+ let iconColor = 'info';
+ switch (this.type) {
+ case 'exception':
+ iconShape = 'exclaim-circle';
+ iconColor = 'info_alt';
+ break;
+ case 'error':
+ iconShape = 'exclaim-circle';
+ iconColor = 'status-red';
+ break;
+ case 'warning':
+ iconShape = 'exclaim-circle';
+ iconColor = 'status-yellow';
+ break;
+ case 'success':
+ iconShape = 'check-circle';
+ iconColor = 'status-green';
+ break;
+ }
+ return {shape: iconShape, color: iconColor};
+ },
+ isDisrupted() {
+ return this.timeout !== null && this.stopTimeout;
+ }
+ },
+ methods: {
+ destroyMe() {
+ this.$emit('destroyMe');
+ },
+ disruptTimeout() {
+ this.stopTimeout = true;
+ clearTimeout(this.timeout);
+ },
+ initTimeout() {
+ if (this.hasTimeout && this.visibleFor > 0) {
+ this.stopTimeout = false;
+ this.timeout = setTimeout(
+ () => this.destroyMe(),
+ this.visibleFor
+ );
+ }
+ }
+ },
+ mounted() {
+ if (this.appendTo !== null) {
+ const target = document.querySelector(this.appendTo);
+
+ // Create a live area for screen reader compatibility.
+ const div = document.createElement('div');
+ div.setAttribute('role', 'alert');
+ div.appendChild(this.$el);
+ if (target) {
+ target.prepend(div);
+ }
+ }
+
+ this.initTimeout();
+
+ this.globalOn('disrupt-system-notifications', this.disruptTimeout);
+ this.globalOn('resume-system-notifications', this.initTimeout);
+
+ if (!STUDIP.config?.PERSONAL_NOTIFICATIONS_AUDIO_DEACTIVATED) {
+ const audio = new Audio(STUDIP.ASSETS_URL + '/sounds/blubb.mp3');
+ audio.play();
+ }
+ },
+ destroyed() {
+ this.globalOff('disrupt-system-notifications', this.disruptTimeout);
+ this.globalOff('resume-system-notifications', this.initTimeout);
+ }
+}
+</script>
+<style scoped>
+[v-cloak] {
+ display: none;
+}
+</style>
diff --git a/resources/vue/components/SystemNotificationManager.vue b/resources/vue/components/SystemNotificationManager.vue
new file mode 100644
index 0000000..0acef24
--- /dev/null
+++ b/resources/vue/components/SystemNotificationManager.vue
@@ -0,0 +1,73 @@
+<template>
+ <transition-group name="system-notification-slide"
+ :class="'system-notifications ' + (placement === 'topcenter' ? 'top-center' : 'bottom-right')"
+ tag="div"
+ role="alert"
+ appear
+ >
+ <system-notification v-for="notification in allNotifications"
+ :key="`message-${notification.key}`"
+ :notification="notification"
+ @destroyMe="destroyNotification(notification)"
+ ></system-notification>
+ </transition-group>
+</template>
+
+<script>
+import SystemNotification from './SystemNotification.vue';
+
+export default {
+ name: 'SystemNotificationManager',
+ components: { SystemNotification },
+ props: {
+ appendAllTo: String,
+ notifications: {
+ type: [Array, Object],
+ default: () => []
+ },
+ placement: {
+ type: String,
+ default: 'topcenter',
+ validator: value => {
+ return ['topcenter', 'bottomright'].includes(value);
+ }
+ }
+ },
+ data() {
+ return {
+ allNotifications: [],
+ counter: 0,
+ stoppedNotifications: false
+ }
+ },
+ methods: {
+ destroyNotification(notification) {
+ this.allNotifications = this.allNotifications.filter(n => n !== notification);
+ }
+ },
+ created() {
+ if (Array.isArray(this.notifications)) {
+ this.allNotifications = [...this.notifications];
+ } else {
+ this.allNotifications = Object.values(this.notifications);
+ }
+ },
+ mounted() {
+ this.globalOn('push-system-notification', notification => {
+ this.allNotifications.push({
+ key: this.counter++,
+ ...notification
+ });
+ });
+
+ window.addEventListener('keydown', evt => {
+ if (evt.altKey && evt.ctrlKey && evt.code === 'KeyT') {
+ this.stoppedNotifications = !this.stoppedNotifications;
+
+ const event = this.stoppedNotifications ? 'disrupt-system-notifications' : 'resume-system-notifications';
+ this.globalEmit(event);
+ }
+ });
+ }
+}
+</script>
diff --git a/resources/vue/components/Timepicker.vue b/resources/vue/components/Timepicker.vue
new file mode 100644
index 0000000..e0b0feb
--- /dev/null
+++ b/resources/vue/components/Timepicker.vue
@@ -0,0 +1,37 @@
+<template>
+ <input type="time"
+ ref="visibleInput"
+ class="hasTimepicker"
+ v-model="timeValue"
+ :placeholder="placeholder"
+ :min="mintime"
+ :max="maxtime"
+ :name="name">
+</template>
+
+<script>
+export default {
+ name: 'Timepicker',
+ inheritAttrs: false,
+ props: {
+ name: {
+ type: String,
+ required: false
+ },
+ value: String,
+ mintime: String,
+ maxtime: String,
+ placeholder: String,
+ },
+ computed: {
+ timeValue: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ }
+ }
+ }
+}
+</script>
diff --git a/resources/vue/components/blubber/Composer.vue b/resources/vue/components/blubber/Composer.vue
index f4b6aff..72a5d1c 100644
--- a/resources/vue/components/blubber/Composer.vue
+++ b/resources/vue/components/blubber/Composer.vue
@@ -1,6 +1,9 @@
<template>
<div class="writer" :style="composerStyle">
<studip-icon shape="blubber" :size="30" role="info"></studip-icon>
+ <label for="blubber-placeholder" class="sr-only">
+ {{ placeholder || $gettext('Schreib was, frag was. Enter zum Abschicken.') }}
+ </label>
<textarea
:placeholder="placeholder || $gettext('Schreib was, frag was. Enter zum Abschicken.')"
v-model="localText"
@@ -10,6 +13,7 @@
@keyup.up.exact="editPreviousComment"
@keyup="saveCommentToSession"
ref="textarea"
+ id="blubber-placeholder"
></textarea>
<a class="send" @click="submit" :title="$gettext('Abschicken')">
<studip-icon shape="arr_2up" :size="30"></studip-icon>
diff --git a/resources/vue/components/courseware/CoursewareContentPermissions.vue b/resources/vue/components/courseware/CoursewareContentPermissions.vue
index d2412ef..60f0964 100644
--- a/resources/vue/components/courseware/CoursewareContentPermissions.vue
+++ b/resources/vue/components/courseware/CoursewareContentPermissions.vue
@@ -42,7 +42,7 @@
<td class="perm">
<input
class="right"
- :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Leserechte für %{ userName }'), { userName: user_perm.username }, true)"
type="radio"
:name="`${user_perm.id}_right`"
value="read"
@@ -53,7 +53,7 @@
<td class="perm">
<input
class="right"
- :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Lese- und Schreibrechte für %{ userName }'), { userName: user_perm.username }, true)"
type="radio"
:name="`${user_perm.id}_right`"
value="write"
@@ -75,7 +75,7 @@
<td class="actions">
<button
class="cw-permission-delete"
- :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username })"
+ :title="$gettextInterpolate($gettext('Entfernen der Rechte von %{ userName }'), { userName: user_perm.username }, true)"
@click.prevent="confirmDeleteUserPerm(index)"
>
</button>
diff --git a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
index c366618..98fab71 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBiographyCareerBlock.vue
@@ -17,8 +17,8 @@
class="cw-timeline-item"
>
<div class="cw-timeline-item-icon cw-timeline-item-icon-color-studip-blue">
- <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" size="32"/>
- <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" size="32"/>
+ <studip-icon v-if="item.type === 'school'" shape="doctoral-cap" role="clickable" :size="32"/>
+ <studip-icon v-if="item.type === 'experience'" shape="tools" role="clickable" :size="32"/>
</div>
<div
class="cw-timeline-item-content cw-timeline-item-content-color-studip-blue"
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
index 89beb0a..eea76fe 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue
@@ -3,6 +3,7 @@
<studip-action-menu
:items="menuItems"
:context="block.attributes.title"
+ :collapseAt="1"
@editBlock="editBlock"
@setVisibility="setVisibility"
@showInfo="showInfo"
diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
index d1ed09f..82d1abc 100644
--- a/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareBlockEdit.vue
@@ -2,7 +2,7 @@
<section class="cw-block-edit">
<header v-if="preview">{{ $gettext('Bearbeiten') }}</header>
<div class="cw-block-features-content">
- <div @click="deactivateToolbar(); exitHandler = true;">
+ <div @click="exitHandler = true;">
<slot name="edit" />
</div>
<div class="cw-button-box">
@@ -31,16 +31,6 @@ export default {
beforeMount() {
this.originalBlock = this.block;
},
- methods: {
- ...mapActions({
- coursewareBlockAdder: 'coursewareBlockAdder',
- coursewareShowToolbar: 'coursewareShowToolbar'
- }),
- deactivateToolbar() {
- this.coursewareBlockAdder({});
- this.coursewareShowToolbar(false);
- },
- },
beforeDestroy() {
if (this.exitHandler) {
this.$emit('store');
diff --git a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
index 669a5b8..24a59f5 100644
--- a/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareCanvasBlock.vue
@@ -127,7 +127,7 @@
<studip-file-chooser
v-model="currentFileId"
selectable="file"
- :courseId="studipContext.id"
+ :courseId="context.id"
:userId="userId"
:isImage="true"
:excludedCourseFolderTypes="excludedCourseFolderTypes"
@@ -187,7 +187,7 @@ export default {
currentUserView: 'own',
currentFile: {},
- context: {},
+ canvasContext: {},
paint: false,
write: false,
clickX: [],
@@ -221,7 +221,6 @@ export default {
},
computed: {
...mapGetters({
- studipContext: 'context',
fileRefById: 'file-refs/byId',
getUserDataById: 'courseware-user-data-fields/byId',
relatedUserData: 'user-data-field/related',
@@ -301,9 +300,7 @@ export default {
return this.currentUploadFolderId !== "";
},
canSwitchView() {
- // this feature is not something to offer in the Arbeitsplatz!
- let context = this.$store.getters.context;
- if (context.type !== 'courses') {
+ if (this.context.type !== 'courses') {
return false;
}
if (this.currentShowUserData === 'off') {
@@ -419,7 +416,7 @@ export default {
} else {
canvas.height = 500;
}
- this.context = canvas.getContext('2d');
+ this.canvasContext = canvas.getContext('2d');
this.setColor('blue');
this.currentSize = this.sizes['normal'];
this.currentTool = this.tools['pen'];
@@ -427,7 +424,7 @@ export default {
},
redraw() {
let view = this;
- let context = view.context;
+ let context = view.canvasContext;
context.clearRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
context.fillStyle = '#ffffff';
context.fillRect(0, 0, context.canvas.width, context.canvas.height); // set background
@@ -658,7 +655,7 @@ export default {
},
async store() {
let user = this.usersById({id: this.userId});
- let imageBase64 = this.context.canvas.toDataURL("image/jpeg", 1.0);
+ let imageBase64 = this.canvasContext.canvas.toDataURL("image/jpeg", 1.0);
let image = await fetch(imageBase64);
let imageBlob = await image.blob();
let file = {};
diff --git a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
index 72cfeb3..55e7b01 100644
--- a/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareDocumentBlock.vue
@@ -131,8 +131,8 @@
</button>
<select v-model="currentScale" :aria-label="$gettext('Zoom')" @change="updateZoom">
<option v-show="false" :value="currentScale">{{ formattedZoom }}%</option>
- <option v-for="(value, index) in scaleValues" :key="index" :value="value">
- {{ value * 100 }}%
+ <option v-for="(value, index) in scaleValues" :key="index" :value="value.scale">
+ {{ value.name }}
</option>
</select>
</div>
@@ -305,7 +305,7 @@ export default {
pdfAnnotationLayer: null,
pdfAnnotation: false,
pdfRotate: 0,
- PdfViewer: null,
+ pdfViewer: null,
pdfEventBus: null,
pdfLinkService: null,
pdfFindController: null,
@@ -322,8 +322,8 @@ export default {
pageNum: 1,
pageCount: 0,
scale: 1,
+ baseScale: 1,
currentScale: 1,
- scaleValues: [0.5, 1, 1.5, 2, 3, 4],
file: null,
srMessage: '',
@@ -361,10 +361,23 @@ export default {
formattedZoom() {
return Number.parseInt(this.scale * 100, 10);
},
+ scaleValues() {
+ const defaultValues = [
+ { name: '25%', scale: 0.25 },
+ { name: '50%', scale: 0.5 },
+ { name: '75%', scale: 0.75 },
+ { name: '100%', scale: 1.0 },
+ { name: '150%', scale: 1.5 },
+ { name: '200%', scale: 2.0 },
+ { name: '300%', scale: 3.0 },
+ ];
+
+ return defaultValues.concat([{ name: this.$gettext('volle Breite'), scale: this.baseScale }]);
+ },
},
watch: {
scale(newValue) {
- let overflow = newValue > 1 ? 'auto' : 'hidden';
+ let overflow = newValue > this.baseScale ? 'auto' : 'hidden';
let container = this.$refs.container;
container.style.overflow = overflow;
this.currentScale = newValue;
@@ -407,7 +420,7 @@ export default {
if (this.currentUrl) {
let view = this;
view.pdfEventBus = new EventBus();
- view.pdfLoadingTask = getDocument(this.currentUrl).promise;
+ view.pdfLoadingTask = getDocument({ url: this.currentUrl, verbosity: 0 }).promise;
view.pdfLoadingTask.__PDFDocumentLoadingTask = true;
// Link Service
view.pdfLinkService = new PDFLinkService({
@@ -473,9 +486,16 @@ export default {
.getPage(parseInt(view.pageNum))
.then((pdfPage) => {
view.pdfPage = pdfPage;
+ const width = outerContainer.offsetWidth;
+ let pdfWidth = pdfPage.view[2];
+ if (pdfPage.rotate === 90 || pdfPage.rotate === 270) {
+ pdfWidth = pdfPage.view[3];
+ }
+ view.baseScale = (width / pdfWidth / 1.33).toFixed(2);
+ view.scale = view.baseScale;
// Creating the page view with default parameters.
let defaultViewport = pdfPage.getViewport({
- scale: 1.35,
+ scale: 1.0,
});
view.pdfBasePage = new PDFViewer({
@@ -512,8 +532,6 @@ export default {
view.pdfViewer.setPdfPage(view.pdfPage);
// Set LinkService viewer
view.pdfLinkService.setViewer(view.pdfViewer);
- // Set outer container height
- outerContainer.style.height = container.offsetHeight + 'px';
view.renderPage();
})
.catch((err) => {
@@ -610,12 +628,12 @@ export default {
this.updateSrMessage(this.$gettext('gedreht'));
},
zoomIn() {
- this.scale = this.scale < 4 ? (this.scale * 10 + 1) / 10 : this.scale;
+ this.scale = this.scale < 4 ? ((this.scale * 10 + 1) / 10).toFixed(1) : this.scale;
this.renderPage();
this.updateSrMessage(this.$gettext('vergrößert'));
},
zoomOut() {
- this.scale = this.scale > 0.1 ? (this.scale * 10 - 1) / 10 : this.scale;
+ this.scale = this.scale > 0.1 ? ((this.scale * 10 - 1) / 10).toFixed(1) : this.scale;
this.renderPage();
this.updateSrMessage(this.$gettext('verkleinert'));
},
diff --git a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
index be56ab4..4ff7e45 100644
--- a/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareIframeBlock.vue
@@ -234,17 +234,16 @@ export default {
this.currentUrlIsValid = this.isValidUrl(this.currentUrl);
},
isValidUrl(urlString) {
- const urlPattern = new RegExp(
- '^(https?:\\/\\/)?' + // validate protocol
- '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // validate domain name
- '((\\d{1,3}\\.){3}\\d{1,3}))' + // validate OR ip (v4) address
- '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // validate port and path
- '(\\?[;&a-z\\d%_.~+=-]*)?' + // validate query string
- '(\\#[-a-z\\d_]*)?$',
- 'i'
- ); // validate fragment locator
+ if (!urlString.startsWith('http')) {
+ urlString = `${location.protocol}//${urlString}`;
+ }
- return !!urlPattern.test(urlString);
+ try {
+ const url = new URL(urlString);
+ return ['http:', 'https:'].includes(url.protocol);
+ } catch (e) {
+ return false;
+ }
},
updateUrl() {
diff --git a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
index 6d12980..4f52d4b 100644
--- a/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareImageMapBlock.vue
@@ -113,7 +113,7 @@
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" size="10"
+ ><studip-icon shape="arr_1down" :size="10"
/></span>
</template>
<template #no-options>
@@ -141,7 +141,7 @@
>
<template #open-indicator="selectAttributes">
<span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" size="10"
+ ><studip-icon shape="arr_1down" :size="10"
/></span>
</template>
<template #no-options>
diff --git a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
index a410740..e52b608 100644
--- a/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
+++ b/resources/vue/components/courseware/blocks/CoursewareKeyPointBlock.vue
@@ -38,7 +38,7 @@
v-model="currentColor"
>
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
<template #no-options>
{{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
@@ -57,7 +57,7 @@
{{ $gettext('Icon') }}
<studip-select :options="icons" :clearable="false" v-model="currentIcon">
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"><studip-icon shape="arr_1down" size="10" /></span>
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
<template #no-options>
{{ $gettext('Es steht keine Auswahl zur Verfügung.') }}
diff --git a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
index 79379bf..875ab1f 100644
--- a/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
+++ b/resources/vue/components/courseware/containers/CoursewareContainerActions.vue
@@ -3,6 +3,7 @@
<studip-action-menu
:items="menuItems"
:context="container.attributes.title"
+ :collapseAt="1"
@editContainer="editContainer"
@changeContainer="changeContainer"
@deleteContainer="deleteContainer"
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
index f617e5a..f26c8d7 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionBox.vue
@@ -1,15 +1,9 @@
-<template>
- <div class="cw-companion-box" :class="[mood]">
- <div>
- <p v-html="msgCompanion"></p>
- <slot name="companionActions"></slot>
- </div>
- </div>
-</template>
-
<script>
export default {
name: 'courseware-companion-box',
+ render(createElement) {
+ return null;
+ },
props: {
msgCompanion: String,
mood: {
@@ -20,5 +14,40 @@ export default {
}
}
},
+ computed: {
+ msgType() {
+ let type = 'info';
+ switch (this.mood) {
+ case 'special':
+ case 'unsure':
+ type = 'warning';
+ break;
+ case 'sad':
+ type = 'error';
+ break;
+ case 'happy':
+ type = 'success';
+ break
+ case 'pointing':
+ case 'curious':
+ }
+ return type;
+ }
+ },
+ watch: {
+ msgCompanion: {
+ handler(current) {
+ if (current.trim().length === 0) {
+ return;
+ }
+ const notification = {
+ type: this.msgType,
+ message: current
+ };
+ this.globalEmit('push-system-notification', notification);
+ },
+ immediate: true
+ }
+ }
};
-</script> \ No newline at end of file
+</script>
diff --git a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
index ff177f7..bfd0a93 100644
--- a/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
+++ b/resources/vue/components/courseware/layouts/CoursewareCompanionOverlay.vue
@@ -1,28 +1,11 @@
-<template>
- <div class="cw-companion-overlay-wrapper">
- <div
- class="cw-companion-overlay"
- :class="[showCompanion ? 'cw-companion-overlay-in' : '', showCompanion ? '' : 'cw-companion-overlay-out', styleCompanion]"
- aria-hidden="true"
- >
- <div class="cw-companion-overlay-content" v-html="msgCompanion"></div>
- <button class="cw-compantion-overlay-close" @click="hideCompanion"></button>
- </div>
- <div
- class="sr-only"
- aria-live="polite"
- role="log"
- >
- <p>{{ msgCompanion }}</p>
- </div>
- </div>
-</template>
-
<script>
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-companion-overlay',
+ render(createElement) {
+ return null;
+ },
computed: {
...mapGetters({
showCompanion: 'showCompanionOverlay',
@@ -30,6 +13,24 @@ export default {
styleCompanion: 'styleCompanionOverlay',
showToolbar: 'showToolbar',
}),
+ msgType() {
+ let type = 'info';
+ switch (this.styleCompanion) {
+ case 'special':
+ case 'unsure':
+ type = 'warning';
+ break;
+ case 'sad':
+ type = 'error';
+ break;
+ case 'happy':
+ type = 'success';
+ break
+ case 'pointing':
+ case 'curious':
+ }
+ return type;
+ }
},
methods: {
...mapActions({
@@ -49,11 +50,24 @@ export default {
}
},
showToolbar(newValue, oldValue) {
- // hide companion when toolbar is closed
+ // hide companion when toolbar is closed
if (oldValue === true && newValue === false) {
this.hideCompanion();
}
+ },
+ msgCompanion: {
+ handler(current) {
+ if (current.trim().length === 0) {
+ return;
+ }
+ const notification = {
+ type: this.msgType,
+ message: current
+ };
+ this.globalEmit('push-system-notification', notification);
+ },
+ immediate: true
}
- },
+ }
};
</script>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
index 1a739a6..1aa8802 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareRootContent.vue
@@ -16,21 +16,21 @@
</div>
<div v-else class="cw-root-content-wrapper">
<div class="cw-root-content" :class="['cw-root-content-' + rootLayout]">
- <div class="cw-root-content-img" :style="image">
+ <div class="cw-root-content-img" :style="bgImage">
<section class="cw-root-content-description" :style="bgColor">
- <img v-if="imageIsSet" class="cw-root-content-description-img" :src="imageURL" />
- <template v-else>
+ <div class="cw-root-content-description-img" :src="imageURL" :style="image"></div>
+ <template v-if="!imageIsSet">
<studip-ident-image
class="cw-root-content-description-img"
- v-model="identImageCanvas"
- :showCanvas="true"
+ v-model="identImage"
:baseColor="bgColorHex"
:pattern="structuralElement.attributes.title"
/>
<studip-ident-image
- v-model="identImage"
- :width="1095"
- :height="withTOC ? 300 : 480"
+ v-model="identBgImage"
+ class="cw-root-content-description-background-img"
+ :width="4380"
+ :height="withTOC ? 1200 : 1920"
:baseColor="bgColorHex"
:pattern="structuralElement.attributes.title"
/>
@@ -46,44 +46,28 @@
</div>
<div v-if="withTOC" class="cw-root-content-toc">
<ul class="cw-tiles">
- <li
- v-for="child in childElements"
- :key="child.id"
- class="tile"
- :class="[child.attributes.payload.color]"
- >
+ <li v-for="child in childElements" :key="child.id">
<router-link :to="'/structural_element/' + child.id" :title="child.attributes.title">
- <div
- v-if="hasImage(child)"
- class="preview-image"
- :style="getChildStyle(child)"
- ></div>
- <studip-ident-image
- v-else
- :baseColor="getColor(child).hex"
- :pattern="child.attributes.title"
- :showCanvas="true"
- />
- <div class="description">
- <header
- :class="[
- child.attributes.purpose !== ''
- ? 'description-icon-' + child.attributes.purpose
- : '',
- ]"
- >
- {{ child.attributes.title || '–' }}
- </header>
- <div class="description-text-wrapper">
- <p>{{ child.attributes.payload.description }}</p>
- </div>
- <footer>
- {{ countChildChildren(child) }}
- <translate :translate-n="countChildChildren(child)" translate-plural="Seiten">
- Seite
- </translate>
- </footer>
- </div>
+ <courseware-tile
+ tag="div"
+ :color="child.attributes.payload.color"
+ :title="child.attributes.title || '–'"
+ :imageUrl="hasImage(child) ? child.relationships?.image?.meta?.['download-url'] : ''"
+ >
+ <template #description>
+ {{ child.attributes.payload.description }}
+ </template>
+ <template #footer>
+ <p class="cw-root-content-toc-tile-footer">
+ {{
+ $gettextInterpolate(
+ $ngettext('%{pages} Seite', '%{pages} Seiten', countChildChildren(child)),
+ { pages: countChildChildren(child) }
+ )
+ }}
+ </p>
+ </template>
+ </courseware-tile>
</router-link>
</li>
</ul>
@@ -93,6 +77,7 @@
<script>
import CoursewareCompanionBox from './../layouts/CoursewareCompanionBox.vue';
+import CoursewareTile from './../layouts/CoursewareTile.vue';
import StudipIdentImage from './../../StudipIdentImage.vue';
import colorMixin from '@/vue/mixins/courseware/colors.js';
import { mapActions, mapGetters } from 'vuex';
@@ -100,18 +85,19 @@ import { mapActions, mapGetters } from 'vuex';
export default {
name: 'courseware-root-content',
mixins: [colorMixin],
- props: {
- structuralElement: Object,
- canEdit: Boolean,
- },
components: {
CoursewareCompanionBox,
+ CoursewareTile,
StudipIdentImage,
},
+ props: {
+ structuralElement: Object,
+ canEdit: Boolean,
+ },
data() {
return {
identImage: '',
- identImageCanvas: '',
+ identBgImage: '',
};
},
computed: {
@@ -130,6 +116,13 @@ export default {
image() {
let style = {};
const backgroundURL = this.imageIsSet ? this.imageURL : this.identImage;
+ style.backgroundImage = 'url(' + backgroundURL + ')';
+
+ return style;
+ },
+ bgImage() {
+ let style = {};
+ const backgroundURL = this.imageIsSet ? this.imageURL : this.identBgImage;
style.backgroundImage = 'url(' + backgroundURL + ')';
style.height = this.withTOC ? '300px' : '480px';
@@ -180,7 +173,7 @@ export default {
},
addPage() {
this.showElementAddDialog(true);
- }
+ },
},
};
</script>
@@ -196,16 +189,20 @@ export default {
}
.cw-root-content-description {
display: flex;
- flex-direction: row;
- margin: 0 8em;
- padding: 2em 4px 2em 2em;
position: relative;
- top: 8em;
+ flex-direction: column;
+ margin: 0 1em;
+ padding: 1em 4px 1em 1em;
+ top: 1em;
+ gap: 10px;
.cw-root-content-description-img {
- width: 240px;
- height: fit-content;
- margin-right: 2em;
+ min-width: 135px;
+ height: 90px;
+ background-color: #fff;
+ background-size: cover;
+ background-position: center;
+ margin-right: 1em;
}
.cw-root-content-description-text {
max-height: calc(480px - 18em);
@@ -233,14 +230,68 @@ export default {
max-width: 1095px;
margin-bottom: 1em;
.cw-root-content-description {
- margin: 0 8em;
- top: 1.5em;
+ height: calc(100% - 4em);
.cw-root-content-description-text {
max-height: calc(300px - 6em);
}
}
+ .cw-root-content-toc-tile-footer {
+ line-height: 4em;
+ }
}
.cw-root-content-hint {
max-width: 1095px;
}
+
+.size-small {
+ .cw-root-content-description {
+ flex-direction: row;
+ padding: 1em 4px 1em 1em;
+
+ .cw-root-content-description-img {
+ min-width: 135px;
+ height: 90px;
+ }
+ }
+
+ .cw-root-content-default {
+ .cw-root-content-description {
+ margin: 0 4em;
+ top: 8em;
+ }
+ }
+ .cw-root-content-toc {
+ .cw-root-content-description {
+ height: calc(100% - 6em);
+ margin: 0 4em;
+ top: 1.5em;
+ }
+ }
+}
+
+.size-large {
+ .cw-root-content-description {
+ flex-direction: row;
+ padding: 2em 4px 2em 2em;
+
+ .cw-root-content-description-img {
+ min-width: 270px;
+ height: 180px;
+ }
+ }
+
+ .cw-root-content-default {
+ .cw-root-content-description {
+ margin: 0 8em;
+ top: 8em;
+ }
+ }
+ .cw-root-content-toc {
+ .cw-root-content-description {
+ height: calc(100% - 7em);
+ margin: 0 8em;
+ top: 1.5em;
+ }
+ }
+}
</style>
diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
index 60b404e..019bbf2 100644
--- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
+++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementDialogImport.vue
@@ -130,7 +130,6 @@ export default {
this.importZipFile = event.target.files[0];
this.setImportFilesProgress(0);
this.setImportStructuresProgress(0);
- this.setImportErrors([]);
},
async importCoursewareArchiv() {
this.importAborted = false;
@@ -199,6 +198,9 @@ export default {
await this.importCourseware(courseware, this.currentElement, files, this.importBehavior, null);
}
+ },
+ mounted() {
+ this.setImportErrors([]);
}
}
</script>
diff --git a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
index 1e076c7..7680167 100644
--- a/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
+++ b/resources/vue/components/courseware/tasks/CoursewareTasksDialogDistribute.vue
@@ -179,7 +179,7 @@
:aria-label="
$gettextInterpolate($gettext('%{userName} auswählen'), {
userName: user.formattedname,
- })
+ }, true)
"
/>
</td>
@@ -217,7 +217,7 @@
:aria-label="
$gettextInterpolate($gettext('%{groupName} auswählen'), {
groupName: group.name,
- })
+ }, true)
"
/>
</td>
diff --git a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
index a843341..d7db6e1 100644
--- a/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
+++ b/resources/vue/components/courseware/tasks/TaskGroupsAddSolversDialog.vue
@@ -44,7 +44,7 @@
:aria-label="
$gettextInterpolate($gettext('%{userName} auswählen'), {
userName: user.formattedname,
- })
+ }, true)
"
/>
</td>
@@ -77,7 +77,7 @@
:aria-label="
$gettextInterpolate($gettext('%{groupName} auswählen'), {
groupName: group.name,
- })
+ }, true)
"
/>
</td>
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
index d31e963..dc70348 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbar.vue
@@ -2,7 +2,7 @@
<div class="cw-toolbar-wrapper">
<div id="cw-toolbar" class="cw-toolbar" :style="toolbarStyle">
<div v-if="showTools" class="cw-toolbar-tools" :class="{ unfold: unfold, hd: isHd, wqhd: isWqhd }">
- <div class="cw-toolbar-button-wrapper">
+ <div id="cw-toolbar-nav" class="cw-toolbar-button-wrapper">
<button
class="cw-toolbar-button"
:class="{ active: activeTool === 'blockAdder' }"
@@ -35,9 +35,19 @@
<studip-icon shape="arr_2right" :size="24" />
</button>
</div>
- <courseware-toolbar-blocks v-if="activeTool === 'blockAdder'" />
- <courseware-toolbar-containers v-if="activeTool === 'containerAdder'" />
- <courseware-toolbar-clipboard v-if="activeTool === 'clipboard'" />
+ <div class="cw-toolbar-tool-wrapper">
+ <CoursewareToolbarBlocks
+ v-if="activeTool === 'blockAdder'"
+ :toolbarContentHeight="toolbarContentHeight"
+ />
+ <CoursewareToolbarContainers
+ v-if="activeTool === 'containerAdder'"
+ />
+ <CoursewareToolbarClipboard
+ v-if="activeTool === 'clipboard'"
+ :toolbarContentHeight="toolbarContentHeight"
+ />
+ </div>
</div>
<div v-else class="cw-toolbar-folded-wrapper">
<button
@@ -97,20 +107,26 @@ export default {
toolbarActive: 'toolbarActive',
hideEditLayout: 'hideEditLayout',
}),
- toolbarStyle() {
- const scrollTopStyles = window.getComputedStyle(document.getElementById('scroll-to-top'));
+ scrollTopStyles() {
+ return window.getComputedStyle(document.getElementById('scroll-to-top'));
+ },
+ toolbarHeight() {
const scrollTopHeight =
- parseInt(scrollTopStyles['height'], 10) +
- parseInt(scrollTopStyles['padding-top'], 10) +
- parseInt(scrollTopStyles['padding-bottom'], 10) +
- parseInt(scrollTopStyles['margin-bottom'], 10);
- let height = parseInt(
+ parseInt(this.scrollTopStyles['height'], 10) +
+ parseInt(this.scrollTopStyles['padding-top'], 10) +
+ parseInt(this.scrollTopStyles['padding-bottom'], 10) +
+ parseInt(this.scrollTopStyles['margin-bottom'], 10);
+ return parseInt(
Math.min(this.windowInnerHeight * 0.9, this.windowInnerHeight - this.toolbarTop - scrollTopHeight)
);
-
+ },
+ toolbarContentHeight() {
+ return this.toolbarHeight - 55;
+ },
+ toolbarStyle() {
return {
- height: height + 'px',
- minHeight: height + 'px',
+ height: this.toolbarHeight + 'px',
+ minHeight: this.toolbarHeight + 'px',
top: this.toolbarTop + 'px',
};
},
@@ -183,8 +199,8 @@ export default {
},
watch: {
- containers(oldValue, newValue) {
- if (oldValue && newValue && oldValue.length !== newValue.length) {
+ containers(newValue, oldValue) {
+ if (newValue) {
this.resetAdderStorage();
}
},
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
index 2795e62..ceda0f0 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarBlocks.vue
@@ -1,85 +1,88 @@
<template>
<div class="cw-toolbar-blocks">
- <form @submit.prevent="loadSearch">
- <div class="input-group files-search search cw-block-search">
- <input
- ref="searchBox"
- type="text"
- v-model="searchInput"
- @click.stop
- :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
- />
- <span class="input-group-append" @click.stop>
- <button
- v-if="searchInput"
- type="button"
- class="button reset-search"
- id="reset-search"
- :title="$gettext('Suche zurücksetzen')"
- @click="resetSearch"
- >
- <studip-icon shape="decline" :size="20"></studip-icon>
- </button>
- <button
- type="submit"
- class="button"
- id="search-btn"
- :title="$gettext('Suche starten')"
- @click="loadSearch"
- >
- <studip-icon shape="search" :size="20"></studip-icon>
- </button>
- </span>
- </div>
- </form>
+ <div id="cw-toolbar-blocks-header" class="cw-toolbar-tool-header">
+ <form @submit.prevent="loadSearch">
+ <div class="input-group files-search search cw-block-search">
+ <input
+ ref="searchBox"
+ type="text"
+ v-model="searchInput"
+ @click.stop
+ :label="$gettext('Geben Sie einen Suchbegriff mit mindestens 3 Zeichen ein.')"
+ />
+ <span class="input-group-append" @click.stop>
+ <button
+ v-if="searchInput"
+ type="button"
+ class="button reset-search"
+ id="reset-search"
+ :title="$gettext('Suche zurücksetzen')"
+ @click="resetSearch"
+ >
+ <studip-icon shape="decline" :size="20"></studip-icon>
+ </button>
+ <button
+ type="submit"
+ class="button"
+ id="search-btn"
+ :title="$gettext('Suche starten')"
+ @click="loadSearch"
+ >
+ <studip-icon shape="search" :size="20"></studip-icon>
+ </button>
+ </span>
+ </div>
+ </form>
- <div class="filterpanel">
- <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span>
- <button
- v-for="category in blockCategories"
- :key="category.type"
- class="button"
- :class="{ 'button-active': category.type === currentFilterCategory }"
- :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'"
- @click="selectCategory(category.type)"
- >
- {{ category.title }}
- </button>
+ <div class="filterpanel">
+ <span class="sr-only">{{ $gettext('Kategorien-Filter') }}</span>
+ <button
+ v-for="category in blockCategories"
+ :key="category.type"
+ class="button"
+ :class="{ 'button-active': category.type === currentFilterCategory }"
+ :aria-pressed="category.type === currentFilterCategory ? 'true' : 'false'"
+ @click="selectCategory(category.type)"
+ >
+ {{ category.title }}
+ </button>
+ </div>
</div>
-
- <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
- <draggable
- v-if="filteredBlockTypes.length > 0"
- class="cw-blockadder-item-list"
- tag="div"
- role="listbox"
- v-model="filteredBlockTypes"
- handle=".cw-sortable-handle-blockadder"
- :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
- :clone="cloneBlock"
- :sort="false"
- :emptyInsertThreshold="20"
- @start="dragBlockStart($event)"
- @end="dropNewBlock($event)"
- ref="sortables"
- sectionId="0"
- >
- <courseware-blockadder-item
- v-for="(block, index) in filteredBlockTypes"
- :key="index"
- :title="block.title"
- :type="block.type"
- :data-blocktype="block.type"
- :description="block.description"
- @blockAdded="$emit('blockAdded')"
- />
- </draggable>
+ <div class="cw-toolbar-tool-content" :style="toolContentStyle">
+ <div v-if="filteredBlockTypes.length > 0" class="cw-blockadder-item-list">
+ <draggable
+ v-if="filteredBlockTypes.length > 0"
+ class="cw-blockadder-item-list"
+ tag="div"
+ role="listbox"
+ v-model="filteredBlockTypes"
+ handle=".cw-sortable-handle-blockadder"
+ :group="{ name: 'blocks', pull: 'clone', put: 'false' }"
+ :clone="cloneBlock"
+ :sort="false"
+ :emptyInsertThreshold="20"
+ @start="dragBlockStart($event)"
+ @end="dropNewBlock($event)"
+ ref="sortables"
+ sectionId="0"
+ >
+ <courseware-blockadder-item
+ v-for="(block, index) in filteredBlockTypes"
+ :key="index"
+ :title="block.title"
+ :type="block.type"
+ :data-blocktype="block.type"
+ :description="block.description"
+ @blockAdded="$emit('blockAdded')"
+ />
+ </draggable>
+ </div>
+ <courseware-companion-box
+ v-else
+ :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
+ mood="pointing"
+ />
</div>
- <courseware-companion-box
- v-else
- :msgCompanion="$gettext('Es wurden keine passenden Blöcke gefunden.')"
- mood="pointing"
- />
</div>
</template>
@@ -99,6 +102,12 @@ export default {
CoursewareCompanionBox,
draggable,
},
+ props: {
+ toolbarContentHeight: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
searchInput: '',
@@ -132,6 +141,13 @@ export default {
{ title: this.$gettext('Biografie'), type: 'biography' },
];
},
+ toolContentStyle() {
+ const height = this.toolbarContentHeight - 115;
+
+ return {
+ height: height + 'px',
+ };
+ },
},
methods: {
...mapActions({
diff --git a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
index 98cd573..c6f1e92 100644
--- a/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
+++ b/resources/vue/components/courseware/toolbar/CoursewareToolbarClipboard.vue
@@ -1,5 +1,5 @@
<template>
- <div class="cw-toolbar-clipboard">
+ <div class="cw-toolbar-clipboard cw-toolbar-tool-content" :style="toolContentStyle">
<courseware-collapsible-box :title="$gettext('Blöcke')" :open="clipboardBlocks.length > 0">
<template v-if="clipboardBlocks.length > 0">
<div class="cw-element-inserter-wrapper">
@@ -101,7 +101,12 @@ export default {
StudipDialog,
draggable,
},
-
+ props: {
+ toolbarContentHeight: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
showDeleteClipboardDialog: false,
@@ -143,6 +148,11 @@ export default {
}
return '';
},
+ toolContentStyle() {
+ return {
+ height: this.toolbarContentHeight + 'px',
+ };
+ },
},
methods: {
...mapActions({
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
index f09601e..dc68225 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItem.vue
@@ -63,7 +63,7 @@
:question="
$gettextInterpolate($gettext('Möchten Sie das Lernmaterial %{ unitTitle } wirklich löschen?'), {
unitTitle: title,
- })
+ }, true)
"
height="200"
@confirm="executeDelete"
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
index f956e47..572e171 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitItemDialogLayout.vue
@@ -1,11 +1,11 @@
<template>
- <studip-dialog
+ <studip-dialog
:title="$gettext('Darstellung')"
:confirmText="$gettext('Speichern')"
confirmClass="accept"
:closeText="$gettext('Schließen')"
closeClass="cancel"
- height="470"
+ height="540"
width="870"
@close="$emit('close')"
@confirm="storeLayout"
@@ -23,12 +23,8 @@
<label v-if="showPreviewImage">
<button class="button" @click="deleteImage">{{ $gettext('Bild löschen') }}</button>
</label>
- <courseware-companion-box
- v-if="uploadFileError"
- :msgCompanion="uploadFileError"
- mood="sad"
- />
- <label v-if="!showPreviewImage">
+ <courseware-companion-box v-if="uploadFileError" :msgCompanion="uploadFileError" mood="sad" />
+ <template v-if="!showPreviewImage">
<img
v-if="currentFile"
:src="uploadImageURL"
@@ -36,14 +32,32 @@
:alt="$gettext('Vorschaubild')"
/>
<div v-else class="cw-structural-element-image-preview-placeholder"></div>
- {{ $gettext('Bild hochladen') }}
- <input class="cw-file-input" ref="upload_image" type="file" accept="image/*" @change="checkUploadFile" />
- </label>
+ <label>
+ {{ $gettext('Bild hochladen') }}
+ <input
+ class="cw-file-input"
+ ref="upload_image"
+ type="file"
+ accept="image/*"
+ @change="checkUploadFile"
+ />
+ </label>
+ {{ $gettext('oder') }}
+ <br />
+ <button class="button" type="button" @click="showStockImageSelector = true">
+ {{ $gettext('Aus dem Bilderpool auswählen') }}
+ </button>
+ <StockImageSelector
+ v-if="showStockImageSelector"
+ @close="showStockImageSelector = false"
+ @select="onSelectStockImage"
+ />
+ </template>
</form>
<form class="default cw-unit-item-dialog-layout-content-settings" @submit.prevent="">
<label>
{{ $gettext('Titel') }}
- <input type="text" v-model="currentElement.attributes.title"/>
+ <input type="text" v-model="currentElement.attributes.title" />
</label>
<label>
{{ $gettext('Beschreibung') }}
@@ -62,13 +76,9 @@
class="cw-vs-select"
>
<template #open-indicator="selectAttributes">
- <span v-bind="selectAttributes"
- ><studip-icon shape="arr_1down" :size="10"
- /></span>
- </template>
- <template #no-options>
- {{ $gettext('Es steht keine Auswahl zur Verfügung') }}.
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10" /></span>
</template>
+ <template #no-options> {{ $gettext('Es steht keine Auswahl zur Verfügung') }}. </template>
<template #selected-option="{ name, hex }">
<span class="vs__option-color" :style="{ 'background-color': hex }"></span
><span>{{ name }}</span>
@@ -96,19 +106,19 @@
<script>
import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue';
-
+import StockImageSelector from '../../stock-images/SelectorDialog.vue';
import colorMixin from '@/vue/mixins/courseware/colors.js';
import { mapActions, mapGetters } from 'vuex';
-
export default {
name: 'courseware-unit-item-dialog-layout',
components: {
- CoursewareCompanionBox
+ CoursewareCompanionBox,
+ StockImageSelector,
},
props: {
unit: Object,
- unitElement: Object
+ unitElement: Object,
},
mixins: [colorMixin],
data() {
@@ -120,18 +130,26 @@ export default {
uploadImageURL: null,
currentRootLayout: 'default',
loadingInstance: false,
- }
+ showStockImageSelector: false,
+ selectedStockImage: null,
+ };
},
computed: {
...mapGetters({
context: 'context',
instanceById: 'courseware-instances/byId',
- userId: 'userId'
+ userId: 'userId',
}),
colors() {
- return this.mixinColors.filter(color => color.darkmode);
+ return this.mixinColors.filter((color) => color.darkmode);
},
image() {
+ if (this.selectedStockImage) {
+ return this.selectedStockImage.attributes['download-urls'].small
+ }
+ if (this.uploadImageURL) {
+ return this.uploadImageURL;
+ }
return this.currentElement.relationships?.image?.meta?.['download-url'] ?? null;
},
@@ -140,15 +158,14 @@ export default {
},
instance() {
if (this.inCourseContext) {
- return this.instanceById({id: 'course_' + this.context.id + '_' + this.unit.id});
+ return this.instanceById({ id: 'course_' + this.context.id + '_' + this.unit.id });
} else {
- return this.instanceById({id: 'user_' + this.context.id + '_' + this.unit.id});
+ return this.instanceById({ id: 'user_' + this.context.id + '_' + this.unit.id });
}
-
},
inCourseContext() {
return this.context.type === 'courses';
- }
+ },
},
methods: {
...mapActions({
@@ -162,9 +179,10 @@ export default {
uploadImageForStructuralElement: 'uploadImageForStructuralElement',
deleteImageForStructuralElement: 'deleteImageForStructuralElement',
storeCoursewareSettings: 'storeCoursewareSettings',
+ setStockImageForStructuralElement: 'setStockImageForStructuralElement',
}),
async loadUnitInstance() {
- const context = {type: this.context.type, id: this.context.id, unit: this.unit.id};
+ const context = { type: this.context.type, id: this.context.id, unit: this.unit.id };
await this.loadInstance(context);
},
initData() {
@@ -192,11 +210,13 @@ export default {
this.$emit('close');
await this.loadStructuralElement(this.currentElement.id);
if (
- this.unitElement.relationships['edit-blocker'].data !== null
- && this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
+ this.unitElement.relationships['edit-blocker'].data !== null &&
+ this.unitElement.relationships['edit-blocker'].data?.id !== this.userId
) {
this.companionWarning({
- info: this.$gettext('Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.')
+ info: this.$gettext(
+ 'Ihre Änderungen konnten nicht gespeichert werden, die Daten werden bereits von einem anderen Nutzer bearbeitet.'
+ ),
});
return false;
} else {
@@ -207,13 +227,20 @@ export default {
this.uploadImageForStructuralElement({
structuralElement: this.currentElement,
file: this.currentFile,
- }).then(() => {
- this.loadStructuralElement(this.currentElement.id)
- }).catch((error) => {
- console.error(error);
- this.companionWarning({
- info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.')
+ })
+ .then(() => {
+ this.loadStructuralElement(this.currentElement.id);
+ })
+ .catch((error) => {
+ console.error(error);
+ this.companionWarning({
+ info: this.$gettext('Beim Hochladen der Bilddatei ist ein Fehler aufgetretten.'),
+ });
});
+ } else if (this.selectedStockImage) {
+ await this.setStockImageForStructuralElement({
+ structuralElement: this.currentElement,
+ stockImage: this.selectedStockImage,
});
} else if (this.deletingPreviewImage) {
await this.deleteImageForStructuralElement(this.currentElement);
@@ -233,13 +260,21 @@ export default {
instance: currentInstance,
});
}
- }
+ },
+ onSelectStockImage(stockImage) {
+ if (this.$refs?.upload_image) {
+ this.$refs.upload_image.value = null;
+ }
+ this.selectedStockImage = stockImage;
+ this.showStockImageSelector = false;
+ this.deletingPreviewImage = false;
+ },
},
async mounted() {
this.loadingInstance = true;
await this.loadUnitInstance();
this.loadingInstance = false;
this.initData();
- }
-}
+ },
+};
</script>
diff --git a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
index 7c9de01..31c6c01 100644
--- a/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
+++ b/resources/vue/components/courseware/unit/CoursewareUnitProgress.vue
@@ -17,7 +17,7 @@
<h1>
<a
:href="chapterUrl"
- :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name })"
+ :title="$gettextInterpolate('%{ pageTitle } öffnen', { pageTitle: selected.name }, true)"
>
{{ selected.name }}
</a>
diff --git a/resources/vue/components/file-chooser/FileChooserDialog.vue b/resources/vue/components/file-chooser/FileChooserDialog.vue
index 2102d1e..2b21f51 100644
--- a/resources/vue/components/file-chooser/FileChooserDialog.vue
+++ b/resources/vue/components/file-chooser/FileChooserDialog.vue
@@ -93,8 +93,8 @@ export default {
},
data() {
return {
- height: 600,
- width: 1000,
+ height: '600',
+ width: '1000',
scope: 'courses',
coursesTree: [],
usersTree: [],
diff --git a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
index a0a76a2..e507adc 100644
--- a/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
+++ b/resources/vue/components/form_inputs/CalendarPermissionsTable.vue
@@ -30,14 +30,16 @@
v-model="user.write_permissions"
:aria-label="$gettextInterpolate(
$gettext('Schreibzugriff für %{name}'),
- {name: user.name}
+ {name: user.name},
+ true
)">
</td>
<td class="actions">
<studip-icon shape="trash" aria-role="button" @click="removeContact(user.id)"
:title="$gettextInterpolate(
$gettext('Kalender nicht mehr mit %{name} teilen'),
- {name: user.name}
+ {name: user.name},
+ true
)"></studip-icon>
</td>
</tr>
diff --git a/resources/vue/components/form_inputs/CaptchaInput.vue b/resources/vue/components/form_inputs/CaptchaInput.vue
new file mode 100644
index 0000000..1409aa8
--- /dev/null
+++ b/resources/vue/components/form_inputs/CaptchaInput.vue
@@ -0,0 +1,70 @@
+<template>
+ <div class="formpart">
+ <altcha-widget :challengeurl="challengeUrl" ref="widget"></altcha-widget>
+ </div>
+</template>
+<script>
+import 'altcha';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
+export default {
+ name: 'CaptchaInput',
+ props: {
+ name: {
+ type: String,
+ default: 'altcha'
+ },
+ challengeUrl: {
+ type: String,
+ requird: true,
+ },
+ auto: {
+ type: String,
+ default: null,
+ validator: (value) => ['onfocus', 'onload', 'onsubmit'].includes(value),
+ }
+ },
+ data() {
+ return {};
+ },
+ methods: {
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.$refs.widget.configure({
+ auto: this.auto,
+ name: this.name,
+ hidefooter: false,
+ hidelogo: false,
+ strings: {
+ error: $gettext('Überprüfung fehlgeschlagen. Versuchen Sie es später erneut.'),
+ footer: $gettext('Geschützt von <a href="https://altcha.org/" target="_blank">ALTCHA</a>'),
+ label: $gettext('Ich bin kein Bot'),
+ verified: $gettext('Überprüft'),
+ verifying: $gettext('Überprüfung...'),
+ waitAlert: $gettext('Überprüfung... Bitte warten.'),
+ },
+ });
+
+ this.$refs.widget.addEventListener('statechange', (ev) => {
+ if (ev.detail.state === 'verified') {
+ this.$emit('input', ev.detail.payload);
+ }
+ })
+ });
+ }
+}
+</script>
+<style>
+:root {
+ --altcha-border-width: 0;
+ --altcha-border-radius: 0;
+ --altcha-color-base: transparent;
+ --altcha-color-border: #a0a0a0;
+ --altcha-color-text: currentColor;
+ --altcha-color-border-focus: currentColor;
+ --altcha-color-error-text: var(--red);
+ --altcha-color-footer-bg: none;
+ --altcha-max-width: auto;
+}
+</style>
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index 05f3c57..d77c993 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -2,11 +2,11 @@
<div class="formpart">
<div class="sr-only" aria-live="polite" ref="list_message_field"></div>
<ul>
- <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="date">
+ <li v-for="date in selected_date_list" v-bind="selected_date_list" :key="getISODate(date)">
<input type="hidden" :name="input_name + '[]'" :value="getISODate(date)">
<studip-date-time :timestamp="Math.floor(date.getTime() / 1000)" :date_only="true"></studip-date-time>
- <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate"
- class="enter-accessible" aria-role="button" tabindex="0"></studip-icon>
+ <studip-icon shape="trash" :title="$gettext('Löschen')" @click="removeDate(date)"
+ class="icon enter-accessible button undecorated" aria-role="button" tabindex="0"></studip-icon>
</li>
</ul>
<label>
@@ -82,13 +82,13 @@ export default {
this.selected_date_list.push(new Date(reformatted_date));
this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} hinzugefügt'), {date: this.selected_date_value});
},
- removeDate(date_key) {
- if (date_key) {
- let date = this.selected_date_list.at(date_key);
- let formatted_date = STUDIP.DateTime.getStudipDate(date, false, true);
- this.selected_date_list.splice(date_key, 1);
- this.$refs.list_message_field.innerText = $gettextInterpolate($gettext('Datum %{date} entfernt'), {date: formatted_date});
- }
+ removeDate(date) {
+ this.selected_date_list = this.selected_date_list.filter(d => d !== date);
+
+ this.$refs.list_message_field.innerText = $gettextInterpolate(
+ $gettext('Datum %{date} entfernt'),
+ {date: STUDIP.DateTime.getStudipDate(date, false, true)}
+ );
},
getISODate(date) {
return STUDIP.DateTime.getISODate(date);
diff --git a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
index d2cc2f1..fe67db5 100644
--- a/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
+++ b/resources/vue/components/form_inputs/MyCoursesColouredTable.vue
@@ -42,7 +42,7 @@
<input type="hidden" :name="`${name}_course_ids[${course.id}]`" value="0">
<input type="checkbox" :name="`${name}_course_ids[${course.id}]`"
value="1" :checked="selected_course_id_list.includes(course.id)"
- :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name})">
+ :title="$gettextInterpolate($gettext('%{course} auswählen'), {course: course.name}, true)">
</td>
</tr>
<tr v-if="loadedSemesters.includes(semester_id) && courses.length === 0">
diff --git a/resources/vue/components/questionnaires/FreetextEdit.vue b/resources/vue/components/questionnaires/FreetextEdit.vue
index 29c6f34..58848ed 100644
--- a/resources/vue/components/questionnaires/FreetextEdit.vue
+++ b/resources/vue/components/questionnaires/FreetextEdit.vue
@@ -2,7 +2,7 @@
<div>
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
<label>
@@ -13,40 +13,19 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'freetext-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: ''
- };
- },
- mounted: function () {
- this.val_clone = this.value;
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
-
}
</script>
diff --git a/resources/vue/components/questionnaires/InputArray.vue b/resources/vue/components/questionnaires/InputArray.vue
index 37fa6fc..896418f 100644
--- a/resources/vue/components/questionnaires/InputArray.vue
+++ b/resources/vue/components/questionnaires/InputArray.vue
@@ -1,177 +1,170 @@
<template>
<div class="input-array">
<span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
- <draggable v-model="options" handle=".dragarea" tag="ol" class="clean options">
- <li v-for="(option, index) in options" :key="index">
- <a class="dragarea"
- v-if="options.length > 1"
- tabindex="0"
- :ref="'draghandle_' + index"
- :title="$gettextInterpolate('Sortierelement für Option %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.', {option: option})"
- @keydown="keyHandler($event, index)">
- <span class="drag-handle"></span>
- </a>
- <input type="text"
- :placeholder="$gettext('Option')"
- :ref="'option_' + index"
- @paste="(ev) => onPaste(ev, index)"
- v-model="options[index]">
- <button class="as-link"
- :title="$gettext('Option löschen')"
- @click.prevent="askForDeletingOption(index)">
- <studip-icon shape="trash" role="clickable" :size="20" alt=""></studip-icon>
- </button>
- <button v-if="index == options.length - 1"
- class="as-link"
- :title="$gettext('Option hinzufügen')"
- @click.prevent="addOption">
- <studip-icon shape="add" role="clickable" :size="20" alt=""></studip-icon>
- </button>
- </li>
- </draggable>
- <studip-dialog
- v-if="askForDeleting"
- :title="$gettext('Bitte bestätigen Sie die Aktion.')"
- :question="$gettext('Wirklich löschen?')"
- :confirmText="$gettext('Ja')"
- :closeText="$gettext('Nein')"
- closeClass="cancel"
- height="180"
- @confirm="deleteOption"
- @close="askForDeleting = false"
- >
- </studip-dialog>
+ <table class="default nohover">
+ <colgroup>
+ <col style="width: 16px">
+ <col>
+ <col v-for="i in additionalColspan" :key="`colspan-${i}`">
+ <col style="width: 24px">
+ </colgroup>
+ <thead>
+ <tr>
+ <th class="dragcolumn"></th>
+ <th>{{ labelPlural }}</th>
+ <slot name="header-cells" />
+ <th class="actions"></th>
+ </tr>
+ </thead>
+ <Draggable v-model="options" handle=".dragarea" tag="tbody" class="statements">
+ <tr v-for="(option, index) in options" :key="index">
+ <td class="dragcolumn">
+ <a class="dragarea"
+ tabindex="0"
+ :title="$gettextInterpolate($gettext(`Sortierelement für %{label} %{option}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.`), {option, label}, true)"
+ @keydown="keyHandler($event, index)"
+ ref="draghandle">
+ <span class="drag-handle"></span>
+ </a>
+ </td>
+ <td>
+ <input type="text"
+ ref="inputs"
+ :placeholder="label"
+ @paste="(ev) => onPaste(ev, index)"
+ v-model="options[index]">
+ </td>
+ <slot name="body-cells" />
+ <td class="actions">
+ <StudipIcon name="delete"
+ shape="trash"
+ :size="20"
+ @click.prevent="deleteOption(index)"
+ :title="$gettextInterpolate($gettext('%{label} löschen'), {label}, true)"
+ />
+ </td>
+ </tr>
+ </Draggable>
+ <tfoot>
+ <tr>
+ <td :colspan="3 + additionalColspan">
+ <button class="as-link"
+ :title="$gettextInterpolate($gettext('%{label} hinzufügen'), {label}, true)"
+ @click.prevent="addOption()">
+ <StudipIcon shape="add" :size="20" alt="" />
+ </button>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
</div>
</template>
<script>
-import StudipIcon from "../StudipIcon.vue";
-import StudipDialog from "../StudipDialog.vue";
-import draggable from 'vuedraggable';
+import Draggable from 'vuedraggable';
+import { $gettext } from '../../../assets/javascripts/lib/gettext';
+
export default {
name: 'input-array',
- components: {
- StudipIcon,
- StudipDialog,
- draggable
- },
+ components: { Draggable },
props: {
- value: {
- type: Array,
- required: false
- }
+ additionalColspan: {
+ type: Number,
+ default: 0,
+ },
+ label: {
+ type: String,
+ default: $gettext('Option'),
+ },
+ labelPlural: {
+ type: String,
+ default: $gettext('Optionen'),
+ },
+ value: Array,
},
- data: function () {
+ data() {
return {
options: [],
- askForDeleting: false,
- indexOfDeletingOption: 0,
- unique_id: null,
- assistiveLive: ''
+ assistiveLive: '',
};
},
methods: {
- addOption: function (val, position) {
- let data = this.value;
- if (val.target) {
- val = '';
- }
- if (typeof position === "undefined") {
- data.push(val || '');
- position = this.value.length - 1
- } else {
- data.splice(position, 0, val || '');
- }
- this.$emit('input', data);
- let v = this;
- this.$nextTick(function () {
- v.$refs['option_' + position][0].focus();
+ addOption(val = '', position = this.options.length) {
+ this.$set(this.options, position, val.trim());
+
+ this.$nextTick(() => {
+ this.$refs.inputs[position].focus();
});
},
- askForDeletingOption: function (index) {
- this.indexOfDeletingOption = index;
- if (this.value[index]) {
- this.askForDeleting = true;
- } else {
- this.deleteOption();
- }
- },
- deleteOption: function () {
- this.$delete(this.value, this.indexOfDeletingOption);
- this.askForDeleting = false;
+ deleteOption(index) {
+ const question = this.options[index] ? this.$gettext('Wirklich löschen?') : true;
+ STUDIP.Dialog.confirm(question).done(() => {
+ this.$delete(this.options, index);
+ });
},
- onPaste: function (ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addOption(data[i], position + i);
- }
- }
+ onPaste(ev, position) {
+ ev.clipboardData
+ .getData('text')
+ .split("\n")
+ .filter(str => str.trim().length > 0)
+ .forEach((value, index) => this.addOption(value, position + index));
+ ev.preventDefault();
},
keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index, listLength: this.options.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.options.length - 1) {
- this.moveDown(index);
- this.$nextTick(function () {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- 'Aktuelle Position in der Liste: %{pos} von %{listLength}.'
- , {pos: index + 2, listLength: this.options.length}
- );
- });
- }
- break;
- }
- },
- moveDown: function (index) {
- if (index == this.options.length - 1) {
+ if (e.keyCode !== 38 && e.keyCode !== 40) {
return;
}
- let option = this.options[index];
- this.options[index] = this.options[index + 1];
- this.options[index + 1] = option;
- this.$forceUpdate();
+
+ e.preventDefault();
+
+ const moveUp = e.keyCode === 38;
+
+ this.moveElement(index, moveUp ? -1 : 1).then((newIndex) => {
+ this.assistiveLive = this.$gettextInterpolate(
+ this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
+ {pos: newIndex + 1, listLength: this.options.length}
+ );
+
+ this.$nextTick(() => {
+ this.$refs['draghandle'][newIndex].focus();
+ });
+ })
},
- moveUp: function (index) {
- if (index === 0) {
- return;
+ moveElement(index, direction) {
+ if (this.options[index + direction] === undefined) {
+ return Promise.resolve(index);
}
- let option = this.options[index];
- this.options[index] = this.options[index - 1];
- this.options[index - 1] = option;
- this.$forceUpdate();
+
+ const indices = [index, index + direction].sort();
+
+ this.options.splice(
+ Math.min(...indices),
+ 2,
+ ...indices.reverse().map(idx => this.options[idx])
+ );
+
+ return Promise.resolve(index + direction);
}
},
- mounted: function () {
- this.options = this.value;
- this.unique_id = 'array_input_' + Math.floor(Math.random() * 100000000);
- },
watch: {
- options (new_data, old_data) {
- if (typeof old_data === 'undefined' || typeof new_data === 'undefined') {
- return;
- }
- this.$emit('input', new_data);
+ options: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
},
- value (new_val) {
- this.options = new_val;
+ value: {
+ handler(current) {
+ this.options = current;
+ },
+ immediate: true
}
}
}
</script>
+<style scoped>
+.input-array input[type="text"] {
+ max-width: unset;
+}
+</style>
diff --git a/resources/vue/components/questionnaires/LikertEdit.vue b/resources/vue/components/questionnaires/LikertEdit.vue
index c87f9fe..736be6b 100644
--- a/resources/vue/components/questionnaires/LikertEdit.vue
+++ b/resources/vue/components/questionnaires/LikertEdit.vue
@@ -1,66 +1,27 @@
<template>
<div class="likert_edit">
-
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext' )}}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="val_clone.options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="(option, index) in val_clone.options" :key="index">{{ option }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="(option, index2) in val_clone.options" :key="index2">
- <input type="radio" disabled :title="option">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.options.length + 3">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <template #body-cells>
+ <td v-for="(option, index) in val_clone.options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -73,17 +34,18 @@
<div>
<div>{{ $gettext('Antwortmöglichkeiten konfigurieren') }}</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
</div>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
-import InputArray from "./InputArray.vue";
import { $gettext } from '../../../assets/javascripts/lib/gettext';
+import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
+// This is necesssar since $gettext does not seem to work in data() or created()
+const default_values = () => ({
description: '',
statements: ['', '', '', ''],
mandatory: 0,
@@ -96,115 +58,16 @@ const default_value = () => ({
$gettext('trifft nicht zu'),
],
});
+
export default {
name: 'likert-edit',
- components: {
- draggable,
- InputArray
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {...default_value()};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.val_clone.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.val_clone.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData("text").split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- )
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- )
- }
- },
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues(default_values());
},
mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
index bc5e829..83d5fa2 100644
--- a/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
+++ b/resources/vue/components/questionnaires/QuestionnaireInfoEdit.vue
@@ -8,39 +8,26 @@
<div class="formpart">
{{ $gettext('Hinweistext (optional)') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
</div>
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'questionnaire-info-edit',
- components: {
- StudipWysiwyg
+ mixins: [ QuestionnaireComponent ],
+ created() {
+ this.setDefaultValues({
+ url: '',
+ description: ''
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return {
- url: '',
- description: ''
- };
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data () {
- return {
- val_clone: this.value,
- };
+ mounted() {
+ this.$refs.infoUrl.focus();
+ this.checkValidity();
},
methods: {
checkValidity() {
@@ -53,15 +40,6 @@ export default {
this.$refs.infoUrl.reportValidity();
}
}
- },
- mounted() {
- this.$refs.infoUrl.focus();
- this.checkValidity();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/components/questionnaires/RangescaleEdit.vue b/resources/vue/components/questionnaires/RangescaleEdit.vue
index 91aec1c..cd7ce3b 100644
--- a/resources/vue/components/questionnaires/RangescaleEdit.vue
+++ b/resources/vue/components/questionnaires/RangescaleEdit.vue
@@ -3,68 +3,25 @@
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Einleitungstext') }}
- <studip-wysiwyg v-model="val_clone.description"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <span aria-live="assertive" class="sr-only">{{ assistiveLive }}</span>
-
- <table class="default nohover">
- <thead>
- <tr>
- <th class="dragcolumn"></th>
- <th>{{ $gettext('Aussagen') }}</th>
- <th v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i" class="number">{{ (val_clone.minimum - 1 + i) }}</th>
- <th v-if="val_clone.alternative_answer.trim().length > 0">{{ val_clone.alternative_answer }}</th>
- <th class="actions"></th>
- </tr>
- </thead>
- <draggable v-model="val_clone.statements" handle=".dragarea" tag="tbody" class="statements">
- <tr v-for="(statement, index) in val_clone.statements" :key="index">
- <td class="dragcolumn">
- <a class="dragarea"
- tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Aussage %{statement}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {statement: statement})"
- @keydown="keyHandler($event, index)"
- :ref="'draghandle_' + index">
- <span class="drag-handle"></span>
- </a>
- </td>
- <td>
- <input type="text"
- :ref="'statement_' + index"
- :placeholder="$gettext('Aussage')"
- @paste="(ev) => onPaste(ev, index)"
- v-model="val_clone.statements[index]">
- </td>
- <td v-for="i in (val_clone.maximum - val_clone.minimum + 1)" :key="i">
- <input type="radio" disabled :title="i + val_clone.minimum - 1">
- </td>
- <td v-if="val_clone.alternative_answer.trim().length > 0">
- <input type="radio" disabled :title="val_clone.alternative_answer">
- </td>
- <td class="actions">
- <studip-icon name="delete"
- shape="trash"
- :size="20"
- @click.prevent="deleteStatement(index)"
- :title="$gettext('Aussage löschen')"
- ></studip-icon>
- </td>
- </tr>
- </draggable>
- <tfoot>
- <tr>
- <td :colspan="val_clone.maximum - val_clone.minimum + 4 + (val_clone.alternative_answer.trim().length > 0 ? 1 : 0)">
- <studip-icon name="add"
- shape="add"
- :size="20"
- @click.prevent="addStatement()"
- :title="$gettext('Aussage hinzufügen')"
- ></studip-icon>
- </td>
- </tr>
- </tfoot>
- </table>
+ <InputArray v-model="val_clone.statements"
+ :label="$gettext('Aussage')"
+ :label-plural="$gettext('Aussagen')"
+ :additional-colspan="options.length"
+ >
+ <template #header-cells>
+ <th v-for="(option, index) in options" class="option-cell" :key="index">
+ {{ option }}
+ </th>
+ </template>
+ <template #body-cells>
+ <td v-for="(option, index) in options" class="option-cell" :key="index">
+ <input type="radio" disabled :title="option">
+ </td>
+ </template>
+ </InputArray>
<label>
<input type="checkbox" v-model.number="val_clone.mandatory" true-value="1" false-value="0">
@@ -82,135 +39,48 @@
<label>
{{ $gettext('Minimum') }}
- <input type="number" v-model.number="val_clone.minimum" min="1">
+ <input type="number" v-model.number="val_clone.minimum" min="1" :max="val_clone.maximum">
</label>
<label>
{{ $gettext('Ausweichantwort (leer lassen für keine)') }}
- <input type="text" v-model="val_clone.alternative_answer">
+ <input type="text" v-model.trim="val_clone.alternative_answer">
</label>
</div>
</template>
<script>
-import draggable from 'vuedraggable';
+import InputArray from './InputArray.vue';
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
-const default_value = () => ({
- description: '',
- statements: ['', '', '', ''],
- mandatory: 0,
- randomize: 0,
- minimum: 1,
- maximum: 5,
- alternative_answer: ''
-});
export default {
- name: 'likert-edit',
- components: {
- draggable,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default() {
- return default_value();
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data() {
- return {
- val_clone: null,
- assistiveLive: ''
- };
- },
- methods: {
- addStatement(val = '', position = null) {
- if (position === null) {
- this.val_clone.statements.push(val || '');
- } else {
- this.val_clone.statements.splice(position, 0, val || '');
- }
- this.$nextTick(() => {
- this.$refs['statement_' + (this.value.statements.length - 1)][0].focus();
- });
- },
- deleteStatement(index) {
- STUDIP.Dialog.confirm(this.$gettext('Wirklich löschen?')).done(() => {
- this.$delete(this.value.statements, index);
- });
- },
- onPaste(ev, position) {
- let data = ev.clipboardData.getData('text').split("\n");
- for (let i = 0; i < data.length; i++) {
- if (data[i].trim()) {
- this.addStatement(data[i], position + i);
- }
- }
- },
- keyHandler(e, index) {
- switch (e.keyCode) {
- case 38: // up
- e.preventDefault();
- if (index > 0) {
- this.moveUp(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index - 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- case 40: // down
- e.preventDefault();
- if (index < this.val_clone.statements.length - 1) {
- this.moveDown(index);
- this.$nextTick(() => {
- this.$refs['draghandle_' + (index + 1)][0].focus();
- this.assistiveLive = this.$gettextInterpolate(
- this.$gettext('Aktuelle Position in der Liste: %{pos} von %{listLength}.'),
- {pos: index + 2, listLength: this.val_clone.statements.length}
- );
- });
- }
- break;
- }
- },
- moveDown(index) {
- this.val_clone.statements.splice(
- index,
- 2,
- this.val_clone.statements[index + 1],
- this.val_clone.statements[index]
- );
- },
- moveUp(index) {
- this.val_clone.statements.splice(
- index - 1,
- 2,
- this.val_clone.statements[index],
- this.val_clone.statements[index - 1]
- );
- },
- },
+ name: 'rangescale-edit',
+ components: { InputArray },
+ mixins: [ QuestionnaireComponent ],
created() {
- this.val_clone = Object.assign({}, default_value(), this.value ?? {});
+ this.setDefaultValues({
+ alternative_answer: '',
+ description: '',
+ mandatory: 0,
+ maximum: 5,
+ minimum: 1,
+ randomize: 0,
+ statements: ['', '', '', '']
+ });
},
mounted() {
this.$refs.autofocus.focus();
},
- watch: {
- val_clone: {
- handler(current) {
- this.$emit('input', current);
- },
- deep: true
+ computed: {
+ options() {
+ let result = [];
+ for (let i = this.val_clone.minimum; i <= this.val_clone.maximum; i += 1) {
+ result.push(i);
+ }
+ if (this.val_clone.alternative_answer.length > 0) {
+ result.push(this.val_clone.alternative_answer);
+ }
+ return result;
}
}
}
diff --git a/resources/vue/components/questionnaires/VoteEdit.vue b/resources/vue/components/questionnaires/VoteEdit.vue
index 9acb01f..1d6d9cf 100644
--- a/resources/vue/components/questionnaires/VoteEdit.vue
+++ b/resources/vue/components/questionnaires/VoteEdit.vue
@@ -2,10 +2,10 @@
<div class="vote_edit">
<div class="formpart" tabindex="0" ref="autofocus">
{{ $gettext('Frage') }}
- <studip-wysiwyg v-model="val_clone.description" :key="question_id"></studip-wysiwyg>
+ <StudipWysiwyg v-model="val_clone.description" />
</div>
- <input-array v-model="val_clone.options"></input-array>
+ <InputArray v-model="val_clone.options" />
<label>
<input type="checkbox" v-model.number="val_clone.multiplechoice" true-value="1" false-value="0">
@@ -24,47 +24,24 @@
</template>
<script>
-import StudipWysiwyg from "../StudipWysiwyg.vue";
import InputArray from "./InputArray.vue";
+import { QuestionnaireComponent } from '../../mixins/QuestionnaireComponent';
export default {
name: 'vote-edit',
- components: {
- StudipWysiwyg,
- InputArray
+ components: { InputArray },
+ mixins: [QuestionnaireComponent],
+ created() {
+ this.setDefaultValues({
+ description: '',
+ mandatory: '0',
+ multiplechoice: '1',
+ options: ['', '', '', ''],
+ randomize: '0',
+ });
},
- props: {
- value: {
- type: Object,
- required: false,
- default: function () {
- return {};
- }
- },
- question_id: {
- type: String,
- required: false
- }
- },
- data: function () {
- return {
- val_clone: {}
- };
- },
- mounted: function () {
- this.val_clone = this.value;
- if (!this.value.description) {
- this.$emit('input', {
- multiplechoice: 1,
- options: ['', '', '', '']
- });
- }
+ mounted() {
this.$refs.autofocus.focus();
- },
- watch: {
- value (new_val) {
- this.val_clone = new_val;
- }
}
}
</script>
diff --git a/resources/vue/components/responsive/ResponsiveContentBar.vue b/resources/vue/components/responsive/ResponsiveContentBar.vue
index d64ac52..eb6dd96 100644
--- a/resources/vue/components/responsive/ResponsiveContentBar.vue
+++ b/resources/vue/components/responsive/ResponsiveContentBar.vue
@@ -131,7 +131,11 @@ export default {
.classList.add('contentbar-wrapper-right');
}
- document.getElementById('responsive-contentbar-container').prepend(this.realContentbar);
+ const contentbarContainer = document.getElementById('responsive-contentbar-container');
+
+ contentbarContainer.prepend(this.realContentbar);
+
+ document.getElementById('content-wrapper').style.marginTop = `${contentbarContainer.clientHeight}px`;
} else {
this.realContentbar.id = 'contentbar';
document.getElementById('toggle-sidebar').remove();
@@ -145,6 +149,8 @@ export default {
}
document.querySelector(this.realContentbarSource).prepend(this.realContentbar);
+
+ document.getElementById('content-wrapper').style.marginTop = 'initial';
}
}
},
diff --git a/resources/vue/components/responsive/ResponsiveNavigation.vue b/resources/vue/components/responsive/ResponsiveNavigation.vue
index 1c07e73..d9c391d 100644
--- a/resources/vue/components/responsive/ResponsiveNavigation.vue
+++ b/resources/vue/components/responsive/ResponsiveNavigation.vue
@@ -130,10 +130,6 @@ export default {
type: String,
default: ''
},
- hasSidebar: {
- type: Boolean,
- default: true
- },
navigation: {
type: Object,
required: true,
@@ -162,6 +158,7 @@ export default {
classObserver: null,
dialogObserver: null,
hasSkiplinks: document.querySelector('#skiplink_list') !== null,
+ hasSidebar: false,
hasContentbar: false,
contentbarTitle: ''
}
@@ -494,6 +491,8 @@ export default {
}
},
mounted() {
+ this.hasSidebar = document.querySelectorAll('#sidebar .sidebar-widget:not(#sidebar-navigation)').length > 0;
+
const cache = STUDIP.Cache.getInstance('responsive.');
const fullscreen = cache.get('fullscreen-mode') ?? false;
const fullscreenDocument = document.documentElement.classList.contains('fullscreen-mode');
@@ -564,18 +563,6 @@ export default {
attributeFilter: ['class']
});
- // Check for closed dialog, re-mounting the Vue component.
- this.dialogObserver = new MutationObserver(mutations => {
- if (mutations[0].removedNodes.length > 0 &&
- mutations[0].removedNodes[0].classList.contains('ui-widget-overlay')) {
- document.getElementById('responsive-menu').replaceChildren(this.$el);
- }
- });
-
- this.dialogObserver.observe(document.body, {
- childList: true
- });
-
this.globalOn('has-contentbar', value => {
this.hasContentbar = value;
if (value && this.isFullscreen) {
@@ -594,6 +581,9 @@ export default {
attributeFilter: ['class']
})
});
+
+ // Check initial state after load
+ this.headerMagic = document.querySelector('body').classList.contains('fixed');
},
beforeDestroy() {
this.classObserver.disconnect();
diff --git a/resources/vue/components/tree/StudipTree.vue b/resources/vue/components/tree/StudipTree.vue
index 136fc95..fc6dc41 100644
--- a/resources/vue/components/tree/StudipTree.vue
+++ b/resources/vue/components/tree/StudipTree.vue
@@ -33,14 +33,21 @@
<MountingPortal v-if="withSearch" mountTo="#search-widget" name="sidebar-search">
<search-widget v-if="currentNode" :min-length="3" ref="searchWidget"></search-widget>
</MountingPortal>
+ <MountingPortal v-if="!editable && !isSearching && !isLoading && currentNode"
+ mountTo="#views-widget"
+ name="sidebar-views">
+ <studip-tree-view-widget :config="viewConfig" />
+ </MountingPortal>
</div>
</template>
<script>
import axios from 'axios';
import { TreeMixin } from '../../mixins/TreeMixin';
+import PageLayout from '../../../assets/javascripts/lib/page_layout';
import StudipProgressIndicator from '../StudipProgressIndicator.vue';
import SearchWidget from '../SearchWidget.vue';
+import StudipTreeViewWidget from './StudipTreeViewWidget.vue';
import StudipTreeList from './StudipTreeList.vue';
import StudipTreeTable from './StudipTreeTable.vue';
import StudipTreeNode from './StudipTreeNode.vue';
@@ -49,7 +56,13 @@ import TreeSearchResult from './TreeSearchResult.vue';
export default {
name: 'StudipTree',
components: {
- TreeSearchResult, SearchWidget, StudipProgressIndicator, StudipTreeList, StudipTreeTable, StudipTreeNode
+ TreeSearchResult,
+ SearchWidget,
+ StudipTreeViewWidget,
+ StudipProgressIndicator,
+ StudipTreeList,
+ StudipTreeTable,
+ StudipTreeNode
},
mixins: [ TreeMixin ],
props: {
@@ -163,12 +176,21 @@ export default {
isLoading: false,
showStructuralNavigation: false,
searchConfig: {},
- isSearching: false
+ isSearching: false,
+ viewConfig: null,
+ pageTitle: document.title
}
},
methods: {
changeCurrentNode(node) {
this.currentNode = node;
+ this.viewConfig = {
+ view: this.viewType,
+ node: this.currentNode,
+ semester: this.semester,
+ semClass: this.semClass
+ };
+ this.setPageTitle(this.currentNode.attributes.name);
this.$nextTick(() => {
document.getElementById('tree-breadcrumb-' + node.attributes.id)?.focus();
});
@@ -187,6 +209,10 @@ export default {
form.appendChild(input);
}
input.setAttribute('value', searchterm);
+ },
+ setPageTitle(nodeTitle) {
+ const title = this.pageTitle.split('-');
+ PageLayout.title = title.slice(0, -1).join('-') + '/ ' + nodeTitle + ' -' + title[title.length - 1];
}
},
mounted() {
@@ -206,6 +232,13 @@ export default {
this.currentNode = this.startNode;
this.loaded = true;
this.isLoading = false;
+ this.viewConfig = {
+ view: this.viewType,
+ node: this.currentNode,
+ semester: this.semester,
+ semClass: this.semClass
+ };
+ this.setPageTitle(this.currentNode.attributes.name);
});
axios.interceptors.request.eject(loadingIndicator);
diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue
index 4ba08bc..6214234 100644
--- a/resources/vue/components/tree/StudipTreeList.vue
+++ b/resources/vue/components/tree/StudipTreeList.vue
@@ -15,7 +15,7 @@
<a v-if="editable && currentNode.attributes.id !== 'root'"
:href="editUrl + '/' + currentNode.attributes.id"
@click.prevent="editNode(editUrl, currentNode.id)" data-dialog="size=medium"
- :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name})">
+ :title="$gettextInterpolate($gettext('%{name} bearbeiten'), {name: currentNode.attributes.name}, true)">
<studip-icon shape="edit" :size="20"></studip-icon>
</a>
@@ -36,7 +36,7 @@
<li v-for="(child, index) in children" :key="index" class="studip-tree-child">
<a v-if="editable && children.length > 1" class="drag-link"
tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})"
+ :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
@keydown="keyHandler($event, index)"
:ref="'draghandle-' + index">
<span class="drag-handle"></span>
@@ -91,9 +91,12 @@
<tbody>
<tr v-for="(course) in courses" :key="course.id" class="studip-tree-child studip-tree-course">
<td>
- <a :href="courseUrl(course.id)"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'),
- { course: course.attributes.title })">
+ <a :href="courseUrl(course.id)" tabindex="0"
+ :title="$gettextInterpolate(
+ $gettext('Zur Veranstaltung %{ title }'),
+ { title: course.attributes.title },
+ true
+ )">
<studip-icon shape="seminar" :size="26"></studip-icon>
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue
index 03a9d7b..093dd9c 100644
--- a/resources/vue/components/tree/StudipTreeTable.vue
+++ b/resources/vue/components/tree/StudipTreeTable.vue
@@ -79,7 +79,7 @@
<td>
<a v-if="editable && children.length > 1" class="drag-link" role="option"
tabindex="0"
- :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name})"
+ :title="$gettextInterpolate($gettext('Sortierelement für Element %{node}. Drücken Sie die Tasten Pfeil-nach-oben oder Pfeil-nach-unten, um dieses Element in der Liste zu verschieben.'), {node: child.attributes.name}, true)"
@keydown="keyHandler($event, index)"
:ref="'draghandle-' + index">
<span class="drag-handle"></span>
@@ -93,7 +93,7 @@
<a :href="nodeUrl(child.id, semester !== 'all' ? semester : null)" tabindex="0"
@click.prevent="openNode(child)"
:title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
- { node: node.attributes.name })">
+ { node: node.attributes.name }, true)">
{{ child.attributes.name }}
</a>
</td>
@@ -112,8 +112,11 @@
</td>
<td>
<a :href="courseUrl(course.id)" tabindex="0"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{ course }'),
- { course: course.attributes.title })">
+ :title="$gettextInterpolate(
+ $gettext('Zur Veranstaltung %{ title }'),
+ { title: course.attributes.title },
+ true
+ )">
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
</template>
diff --git a/resources/vue/components/tree/StudipTreeViewWidget.vue b/resources/vue/components/tree/StudipTreeViewWidget.vue
new file mode 100644
index 0000000..30e2d5c
--- /dev/null
+++ b/resources/vue/components/tree/StudipTreeViewWidget.vue
@@ -0,0 +1,56 @@
+<template>
+ <sidebar-widget id="views-widget" class="sidebar-views" :title="$gettext('Ansicht')">
+ <template #content>
+ <ul class="widget-list widget-links sidebar-views">
+ <li :class="{ active: config.view === 'list' }">
+ <a :href="getUrl('list')"
+ :title="$gettext('Verzeichnis als Liste anzeigen')"
+ tabindex="0">
+ {{ $gettext('Als Liste anzeigen') }}
+ </a>
+ </li>
+ <li :class="{ active: config.view === 'table' }">
+ <a :href="getUrl('table')"
+ :title="$gettext('Verzeichnis als Tabelle anzeigen')"
+ tabindex="0">
+ {{ $gettext('Als Tabelle anzeigen') }}
+ </a>
+ </li>
+ </ul>
+ </template>
+ </sidebar-widget>
+</template>
+
+<script>
+import SidebarWidget from '../SidebarWidget.vue';
+
+export default {
+ name: 'StudipTreeViewWidget',
+ components: {
+ SidebarWidget
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true
+ }
+ },
+ methods: {
+ getUrl(showAs) {
+ const url = new URL(window.location);
+ url.searchParams.set('show_as', showAs);
+ url.searchParams.set('node_id', this.config.node.id);
+
+ if (this.config.semester !== '') {
+ url.searchParams.set('semester', this.config.semester);
+ }
+
+ if (this.config.semClass !== 0) {
+ url.searchParams.set('semclass', this.config.semClass);
+ }
+
+ return url.toString();
+ }
+ }
+}
+</script>
diff --git a/resources/vue/components/tree/TreeBreadcrumb.vue b/resources/vue/components/tree/TreeBreadcrumb.vue
index 33b04b3..a8c3dd5 100644
--- a/resources/vue/components/tree/TreeBreadcrumb.vue
+++ b/resources/vue/components/tree/TreeBreadcrumb.vue
@@ -10,7 +10,7 @@
<a :href="nodeUrl(ancestor.classname + '_' + ancestor.id)" :ref="ancestor.id"
@click.prevent="openNode(ancestor.id, ancestor.classname)" tabindex="0"
:id="'tree-breadcrumb-' + ancestor.id"
- :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name})">
+ :title="$gettextInterpolate($gettext('%{ node } öffnen'), { node: ancestor.name}, true)">
{{ ancestor.name }}
</a>
<template v-if="index !== node.attributes.ancestors.length - 1">
diff --git a/resources/vue/components/tree/TreeCourseDetails.vue b/resources/vue/components/tree/TreeCourseDetails.vue
index 609349d..020cc33 100644
--- a/resources/vue/components/tree/TreeCourseDetails.vue
+++ b/resources/vue/components/tree/TreeCourseDetails.vue
@@ -4,14 +4,16 @@
({{ details.semester }})
</div>
<div class="admission-state" v-if="details.admissionstate">
- <studip-icon :shape="details.admissionstate.icon" :role="details.admissionstate.role"
- :title="details.admissionstate.info"></studip-icon>
+ <studip-icon :shape="details.admissionstate.icon"
+ :role="details.admissionstate.role"
+ :alt="details.admissionstate.info"></studip-icon>
</div>
<div class="course-lecturers">
<span v-for="(lecturer, index) in details.lecturers" :key="index">
<a :href="profileUrl(lecturer.username)"
:title="$gettextInterpolate($gettext('Zum Profil von %{ user }'),
- { user: lecturer.name })">
+ { user: lecturer.name }, true)"
+ tabindex="0">
{{ lecturer.name }}
</a><template v-if="details.lecturers.length > 1 && index < details.lecturers.length - 1">, </template>
</span>
diff --git a/resources/vue/components/tree/TreeNodeCoursePath.vue b/resources/vue/components/tree/TreeNodeCoursePath.vue
index 26ab88e..71f69ea 100644
--- a/resources/vue/components/tree/TreeNodeCoursePath.vue
+++ b/resources/vue/components/tree/TreeNodeCoursePath.vue
@@ -1,6 +1,12 @@
<template>
<div>
- <studip-icon shape="info-circle" @click="togglePathInfo"></studip-icon>
+ <button type="button"
+ @click.prevent="togglePathInfo"
+ :title="showPaths
+ ? $gettext('Pfad im Verzeichnis ausblenden')
+ : $gettext('Pfad im Verzeichnis anzeigen')">
+ <studip-icon shape="info-circle"></studip-icon>
+ </button>
<ul v-if="showPaths" class="studip-tree-course-path">
<li v-for="(path, pindex) in paths" :key="pindex">
<button @click.prevent="openNode(path[path.length - 1].id)">
diff --git a/resources/vue/components/tree/TreeNodeTile.vue b/resources/vue/components/tree/TreeNodeTile.vue
index dab2bdf..698cc25 100644
--- a/resources/vue/components/tree/TreeNodeTile.vue
+++ b/resources/vue/components/tree/TreeNodeTile.vue
@@ -1,6 +1,6 @@
<template>
<a :href="url" @click.prevent="openNode" :title="$gettextInterpolate($gettext('Unterebene %{ node } öffnen'),
- { node: node.attributes.name })">
+ { node: node.attributes.name }, true)">
<p class="studip-tree-child-title">
{{ node.attributes.name }}
</p>
diff --git a/resources/vue/components/tree/TreeSearchResult.vue b/resources/vue/components/tree/TreeSearchResult.vue
index e5093eb..9799dae 100644
--- a/resources/vue/components/tree/TreeSearchResult.vue
+++ b/resources/vue/components/tree/TreeSearchResult.vue
@@ -31,13 +31,14 @@
</td>
<td>
<a :href="courseUrl(course.id)"
- :title="$gettextInterpolate($gettext('Zur Veranstaltung %{name}'), {name: + course.attributes.title})">
+ :title="$gettextInterpolate($gettext('Zur Veranstaltung %{title}'), {title: course.attributes.title}, true)"
+ tabindex="0">
<template v-if="course.attributes['course-number']">
{{ course.attributes['course-number'] }}
</template>
{{ course.attributes.title }}
- <div :id="'course-dates-' + course.id" class="course-dates"></div>
</a>
+ <div :id="'course-dates-' + course.id" class="course-dates"></div>
<tree-node-course-path :node-class="searchConfig.classname"
:course-id="course.id"></tree-node-course-path>
</td>
diff --git a/resources/vue/mixins/QuestionnaireComponent.js b/resources/vue/mixins/QuestionnaireComponent.js
new file mode 100644
index 0000000..277f21c
--- /dev/null
+++ b/resources/vue/mixins/QuestionnaireComponent.js
@@ -0,0 +1,24 @@
+export const QuestionnaireComponent = {
+ props: {
+ value: Object
+ },
+ data () {
+ return {val_clone: this.value};
+ },
+ methods: {
+ setDefaultValues(value) {
+ this.val_clone = Object.assign(value, this.value);
+ }
+ },
+ watch: {
+ val_clone: {
+ handler(current) {
+ this.$emit('input', current);
+ },
+ deep: true
+ },
+ value (new_val) {
+ this.val_clone = new_val;
+ }
+ }
+};
diff --git a/resources/vue/mixins/courseware/import.js b/resources/vue/mixins/courseware/import.js
index a7c9420..547280f 100644
--- a/resources/vue/mixins/courseware/import.js
+++ b/resources/vue/mixins/courseware/import.js
@@ -53,7 +53,6 @@ export default {
this.elementCounter = await this.countImportElements([element]);
this.setImportStructuresState('');
this.importElementCounter = 0;
- this.setImportErrors([]);
if (importBehavior === 'default') {
await this.importStructuralElement([element], rootId, files);
@@ -286,8 +285,14 @@ export default {
let new_file = this.file_mapping[files[i].id].new;
let payload = JSON.stringify(block.attributes.payload);
- payload = payload.replaceAll(old_file.id, new_file.id);
- payload = payload.replaceAll(old_file.folder.id, new_file.relationships.parent.data.id);
+ if (new_file) {
+ payload = payload.replaceAll(old_file.id, new_file.id);
+ payload = payload.replaceAll(old_file.folder.id, new_file.relationships.parent.data.id);
+ } else {
+ payload = payload.replaceAll(old_file.id, '');
+ payload = payload.replaceAll(old_file.folder.id, '');
+ }
+
block.attributes.payload = JSON.parse(payload);
}
@@ -395,13 +400,20 @@ export default {
// create new blob with correct type
let filedata = zip_filedata.slice(0, zip_filedata.size, files[i].attributes['mime-type']);
-
- let file = await this.createFile({
- file: files[i],
- filedata: filedata,
- folder: folders[files[i].folder.id]
- });
- this.setImportFilesState(this.$gettext('Erzeuge Datei') + ': ' + files[i].attributes.name);
+ let file = null;
+ try {
+ file = await this.createFile({
+ file: files[i],
+ filedata: filedata,
+ folder: folders[files[i].folder.id]
+ });
+ } catch (error) {
+ this.currentImportErrors.push(this.$gettext('Import einer Datei fehlgeschlagen.'));
+ this.setImportFilesState(this.$gettext('Fehler beim Anlegen der Datei'));
+ }
+ if (file !== null) {
+ this.setImportFilesState(this.$gettext('Erzeuge Datei') + ': ' + files[i].attributes.name);
+ }
this.setImportFilesProgress(parseInt(i / files.length * 100));
//file mapping
diff --git a/resources/vue/store/ContentModulesStore.js b/resources/vue/store/ContentModulesStore.js
index 9dc4609..141a178 100644
--- a/resources/vue/store/ContentModulesStore.js
+++ b/resources/vue/store/ContentModulesStore.js
@@ -52,7 +52,7 @@ export default {
attributes: { value: view === 'tiles' }
};
- return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } }) ;
+ return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } }) ;
},
exchangeModules({ commit, state }, modules) {
const order = modules.filter(module => module.active)
diff --git a/resources/vue/store/MyCoursesStore.js b/resources/vue/store/MyCoursesStore.js
index 08c0389..af8e0fc 100644
--- a/resources/vue/store/MyCoursesStore.js
+++ b/resources/vue/store/MyCoursesStore.js
@@ -71,7 +71,7 @@ export default {
attributes: { value: configValue[configKey] }
};
- return STUDIP.jsonapi.PATCH(`config-values/${documentId}`, { data: { data } })
+ return STUDIP.jsonapi.withPromises().patch(`config-values/${documentId}`, { data: { data } })
},
toggleOpenGroup ({ state, dispatch }, group) {
let open_groups = [ ...state.config.open_groups ];
diff --git a/resources/vue/store/blubber.js b/resources/vue/store/blubber.js
index 9cc474a..d0bd7f6 100644
--- a/resources/vue/store/blubber.js
+++ b/resources/vue/store/blubber.js
@@ -166,7 +166,7 @@ export default {
// if total is missing, there are more comments to fetch
const total = rootGetters['blubber-comments/lastMeta']?.page?.total;
- const hasMore = !total;
+ const hasMore = total ?? true;
commit('setMoreOlder', { id, hasMore });
},
diff --git a/resources/vue/store/courseware/courseware-shelf.module.js b/resources/vue/store/courseware/courseware-shelf.module.js
index dc92a23..d907ee4 100644
--- a/resources/vue/store/courseware/courseware-shelf.module.js
+++ b/resources/vue/store/courseware/courseware-shelf.module.js
@@ -249,33 +249,23 @@ export const actions = {
}
},
async companionInfo({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'default');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
async companionSuccess({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'happy');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info});
},
async companionError({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'sad');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info});
},
async companionWarning({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'alert');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info});
},
async companionSpecial({ dispatch }, { info }) {
- await dispatch('setStyleCompanionOverlay', 'special');
- await dispatch('setMsgCompanionOverlay', info);
- return dispatch('setShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
coursewareShowCompanionOverlay({dispatch}, { data }) {
return dispatch('setShowCompanionOverlay', data);
@@ -310,7 +300,7 @@ export const actions = {
return dispatch(loadUnits, state.context.id);
},
-
+
async sortUnits({ dispatch, state }, data) {
let loadUnits = null;
if (state.context.type === 'courses') {
@@ -321,7 +311,7 @@ export const actions = {
}
await state.httpClient.post(`${state.context.type}/${state.context.id}/courseware-units/sort`, {data: data});
-
+
return dispatch(loadUnits, state.context.id);
},
diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js
index c44bba1..784b750 100644
--- a/resources/vue/store/courseware/courseware.module.js
+++ b/resources/vue/store/courseware/courseware.module.js
@@ -874,33 +874,23 @@ export const actions = {
},
async companionInfo({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'default');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'info', message: info});
},
async companionSuccess({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'happy');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'success', message: info});
},
async companionError({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'sad');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'error', message: info});
},
async companionWarning({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'alert');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'exception', message: info});
},
async companionSpecial({ dispatch }, { info }) {
- await dispatch('coursewareStyleCompanionOverlay', 'special');
- await dispatch('coursewareMsgCompanionOverlay', info);
- return dispatch('coursewareShowCompanionOverlay', true);
+ STUDIP.eventBus.emit('push-system-notification', { type: 'warning', message: info});
},
// adds a favorite block type using the `type` of the BlockType