UNPKG

@digital-blueprint/formalize-app

Version:

[GitHub Repository](https://github.com/digital-blueprint/formalize-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/formalize-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/formalize-app/) | [Formalize Bundle](https:

313 lines (266 loc) • 10.9 kB
import {html} from 'lit'; import * as commonUtils from '@dbp-toolkit/common/utils.js'; import {ScopedElementsMixin} from '@dbp-toolkit/common'; import {DbpBaseElement} from '@dbp-toolkit/form-elements/src/base-element.js'; import {ResourceSelect} from '@dbp-toolkit/resource-select'; import {createInstance} from '../../i18n.js'; export class CourseSelect extends ResourceSelect { constructor() { super(); this._courseI18n = createInstance(); this.resourcePath = '/base/courses'; this.fetchMode = 'search'; this.placeholder = this._getCoursePlaceholder(); } update(changedProperties) { if (changedProperties.has('lang')) { void this._courseI18n.changeLanguage(this.lang); this.placeholder = this._getCoursePlaceholder(); } super.update(changedProperties); } _getCoursePlaceholder() { return this._courseI18n.t('render-form.course-select.placeholder'); } getCollectionQueryParameters(select) { return [ ['includeLocal', 'semesterKey,typeKey,lecturers'], ['filter[localData.semesterKey][operator]', 'IN'], ...CourseSelect.getSemesterKeys().map((semesterKey) => [ 'filter[localData.semesterKey][value][]', semesterKey, ]), ]; } getSearchQueryParameters(select, searchTerm) { return { // Course codes do not contain dots, but CAMPUSonline displays them with dots. search: searchTerm.replaceAll('.', '').trim(), }; } getItemQueryParameters(select) { return { includeLocal: 'semesterKey,typeKey,lecturers', }; } static getSemesterKeys() { const now = new Date(); const currentMonth = now.getMonth() + 1; const currentYear = now.getFullYear(); if (currentMonth >= 10 || currentMonth <= 2) { const winterStartYear = currentMonth >= 10 && currentMonth <= 12 ? currentYear : currentYear - 1; const summerYear = winterStartYear + 1; return [`"${winterStartYear}S"`, `"${winterStartYear}W"`, `"${summerYear}S"`]; } const yearBefore = currentYear - 1; return [`"${yearBefore}W"`, `"${currentYear}S"`, `"${currentYear}W"`]; } formatResource(select, course) { return this.formatCourse(course); } formatCourse(course) { const courseCode = course.code ?? ''; const courseName = course.name ?? ''; const courseType = course.localData?.typeKey ?? ''; let courseTerm = course.localData?.semesterKey ?? ''; const courseTermParts = this.parseSemesterKey(courseTerm); if (courseTermParts) { courseTerm = courseTermParts.term === 'W' ? this._courseI18n.t('render-form.course-select.winter-term', { year: courseTermParts.year, nextYear: courseTermParts.year + 1, }) : this._courseI18n.t('render-form.course-select.summer-term', { year: courseTermParts.year, }); } return `${courseCode}: ${courseName} (${courseType}, ${courseTerm})`; } parseSemesterKey(semesterKey) { if (typeof semesterKey !== 'string') return null; const match = semesterKey.match(/^(\d{4})([WS])$/i); if (!match) return null; return {year: Number(match[1]), term: match[2].toUpperCase()}; } } export class DbpCourseSelectElement extends ScopedElementsMixin(DbpBaseElement) { getToken() { if (!this.auth?.token) { throw new Error('Authentication token is not available'); } return this.auth.token; } constructor() { super(); this.entryPointUrl = null; this.courseIdentifier = null; // True while the course is being set programmatically (e.g. presetting a // loaded submission). Used to suppress the `dbp-course-changed` event so // it is only dispatched for genuine user selections. this._presettingCourse = false; this._userSelectedValue = null; } static get properties() { return { ...super.properties, entryPointUrl: {type: String, attribute: 'entry-point-url'}, courseIdentifier: {type: String, attribute: 'course-identifier'}, }; } static get scopedElements() { return { 'dbp-course-select': CourseSelect, }; } async fetchGermanCourseData(idPath) { const response = await fetch(this.entryPointUrl + idPath, { headers: { 'Content-Type': 'application/ld+json', 'Accept-Language': 'de', Authorization: 'Bearer ' + this.getToken(), }, }); if (!response.ok) { return null; } else { try { return await response.json(); } catch { return null; } } } async handleInputValue(e) { e.stopPropagation(); let courseDataObject = e.detail.object; const originalCourseObject = courseDataObject; // Specify the value to be included in the form submission if (courseDataObject != null) { // We get the course type and term from the original data object // because we will not get the localData with the API request let courseType = courseDataObject['localData']['typeKey']; let courseTerm = courseDataObject['localData']['semesterKey']; // If the language wasn't German, try to fetch German data if (this.lang !== 'de') { const data = await this.fetchGermanCourseData(courseDataObject['@id']); if (data != null) { courseDataObject = data; } } let courseCode = courseDataObject['code']; let courseName = courseDataObject['name']; const value = `${courseCode}: ${courseName} (${courseType}, ${courseTerm})`; if (!this._presettingCourse) { this._userSelectedValue = value; } this.value = value; } else { if (!this._presettingCourse) { this._userSelectedValue = ''; } this.value = ''; } // Suppress the event for programmatic presets (e.g. loading a submission) // so it only fires for genuine user selections. if (this._presettingCourse) { this._presettingCourse = false; return; } this.dispatchEvent( new CustomEvent('dbp-course-changed', { detail: {course: originalCourseObject}, bubbles: true, composed: true, }), ); } _extractCourseCode(courseName) { if (!courseName) return null; const separatorIndex = courseName.indexOf(':'); return separatorIndex === -1 ? null : courseName.slice(0, separatorIndex).trim(); } /** * Presets the inner CourseSelect with the course from a loaded submission. * Uses the saved identifier when available. For older submissions, looks up * candidates by code and uses all saved course details to find a unique match. * * @param {string} courseName - The formatted course name string. * @param {string|null} courseIdentifier - The unique course identifier. */ async _presetCourse(courseName, courseIdentifier) { /** @type {CourseSelect | null} */ const picker = this.renderRoot.querySelector('#' + this.name + '-picker'); if (!picker) return; if (courseIdentifier) { const courseId = `/base/courses/${encodeURIComponent(courseIdentifier)}`; if (picker.value === courseId) return; this._presettingCourse = true; picker.value = courseId; return; } const courseCode = this._extractCourseCode(courseName); if (!courseCode) return; // Look up the course by code to get its @id (code !== identifier) const params = new URLSearchParams({ includeLocal: 'semesterKey,typeKey,lecturers', 'filter[foo][condition][path]': 'code', 'filter[foo][condition][operator]': 'EQUALS', 'filter[foo][condition][value]': `"${courseCode}"`, }); const resp = await fetch(`${this.entryPointUrl}/base/courses?${params.toString()}`, { headers: { Authorization: 'Bearer ' + this.getToken(), 'Accept-Language': 'de', }, }); if (!resp.ok) return; const data = await resp.json(); const matchingCourses = (data['hydra:member'] ?? []).filter( (course) => `${course.code}: ${course.name} (${course.localData?.typeKey}, ${ course.localData?.semesterKey })` === courseName, ); if (matchingCourses.length !== 1) return; const course = matchingCourses[0]; if (!course?.['@id']) return; // Skip if the picker already holds this course to avoid redundant // change events on re-renders (e.g. when `auth` gets a new reference). if (picker.value === course['@id']) return; // Mark this as a programmatic change so `handleInputValue` does not // dispatch `dbp-course-changed` (which would reset dependent fields). this._presettingCourse = true; picker.value = course['@id']; } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('value') && this._userSelectedValue === this.value) { this._userSelectedValue = null; return; } if ( (changedProperties.has('value') || changedProperties.has('courseIdentifier') || changedProperties.has('auth')) && typeof this.value === 'string' && this.value && this.auth?.token ) { void this._presetCourse(this.value, this.courseIdentifier); } } renderInput() { return html` <div class="control"> <dbp-course-select id="${this.name}-picker" name="${this.name}Picker" subscribe="lang,auth,entry-point-url" @change="${(e) => this.handleInputValue(e)}"></dbp-course-select> </div> `; } } commonUtils.defineCustomElement('dbp-course-select-element', DbpCourseSelectElement);