WIP: Implement frontend

This commit is contained in:
Björn Fromme
2023-09-18 13:10:20 +02:00
parent bfc5e5e242
commit ce97017ac4
70 changed files with 8751 additions and 337 deletions
@@ -0,0 +1,88 @@
import { Controller } from '@hotwired/stimulus'
import { useFetch } from '../mixins/use_fetch'
export default class extends Controller {
static targets = [ 'title', 'content', 'spinner', 'backbutton' ]
static values = { loading: Boolean }
connect() {
useFetch(this)
}
show({ title, url }) {
this.titleTarget.innerHTML = title
this.element.classList.remove('hidden')
this.loadContent(url)
}
hide() {
this.element.classList.add('hidden')
}
loadContent(url) {
this.loadingValue = true
fetch(url, this.getInitObject())
.then(this.checkStatus)
.then(this.parseJSON)
.then(response => {
this.handleResponse(response)
})
.catch(error => {
this.handleResponse(this.createErrorResponse(error))
})
.finally(() => {
this.loadingValue = false
})
}
submitForm(event) {
event.preventDefault()
this.loadingValue = true
const form = event.target
const formData = new FormData(form)
if (this.hasBackbuttonTarget && event.submitter === this.backbuttonTarget) {
formData.append(this.backbuttonTarget.name, this.backbuttonTarget.value)
}
fetch(form.action, this.getInitObject('post', formData))
.then(this.checkStatus)
.then(this.parseJSON)
.then(response => {
this.handleResponse(response)
})
.catch(error => {
this.handleResponse(this.createErrorResponse(error))
})
.finally(() => {
this.loadingValue = false
})
}
handleResponse(modalResponse) {
if (modalResponse.redirect) {
window.location = modalResponse.redirect
return
}
if (true === modalResponse.close) {
this.element.classList.add('hidden')
return
}
this.contentTarget.innerHTML = modalResponse.content
}
createErrorResponse(error) {
return {
close: false,
redirect: null,
content: error.message,
}
}
loadingValueChanged() {
this.spinnerTarget.classList.toggle('hidden', false === this.loadingValue)
this.contentTarget.classList.toggle('hidden', true === this.loadingValue)
}
}
@@ -0,0 +1,78 @@
import { Controller } from '@hotwired/stimulus'
import { useFetch } from '../mixins/use_fetch'
import Autocomplete from '@trevoreyre/autocomplete-js'
export default class extends Controller {
static targets = [ 'input', 'field', 'resetButton', 'searchIcon' ]
static values = { url: String, choices: Array, selected: Boolean }
initialize() {
useFetch(this)
}
connect() {
const searchHandler = input => {
return new Promise(resolve => {
// Check input minimum length
if (input.length < 2) {
return resolve([])
}
// Search choices if provided
if (this.choicesValue.length > 0) {
return resolve(this.choicesValue.filter(choice => {
return -1 !== choice.text.toLowerCase().search(input.toLowerCase())
}))
}
// Use API otherwise
let data = { search: input }
fetch(this.urlValue, { method: 'POST', body: JSON.stringify(data), credentials: 'include' })
.then(this.checkStatus)
.then(this.parseJSON)
.then(data => {
resolve(data)
})
})
}
const submitHandler = result => {
this.fieldTarget.value = result.value
this.selectedValue = true
this.element.dispatchEvent(new CustomEvent('select', {
detail: { result }
}))
}
new Autocomplete(this.element, {
search: searchHandler,
getResultValue: result => result.text,
onSubmit: submitHandler,
debounceTime: 250,
})
if (this.fieldTarget.value) {
this.selectedValue = true
}
this.element.addEventListener('keypress', e => {
if (e.key === 'Enter') {
e.preventDefault()
}
})
}
reset() {
this.inputTarget.value = null
this.fieldTarget.value = null
this.selectedValue = false
this.element.dispatchEvent(new CustomEvent('reset'))
}
selectedValueChanged() {
this.resetButtonTarget.classList.toggle('hidden', false === this.selectedValue)
this.searchIconTarget.classList.toggle('hidden', this.selectedValue)
}
}
@@ -0,0 +1,17 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'form', 'title', 'content' ]
show({ action, title, content }) {
this.formTarget.action = action
this.titleTarget.innerText = title
this.contentTarget.innerHTML = content
this.element.classList.remove('hidden')
}
hide() {
this.element.classList.add('hidden')
}
}
@@ -0,0 +1,57 @@
import { Controller } from '@hotwired/stimulus'
import flatpickr from 'flatpickr';
import { German } from 'flatpickr/dist/l10n/de';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [ 'field' ]
static values = { format: String, minDate: String, maxDate: String, disableWeekends: Boolean }
initialize() {
flatpickr.localize(German);
flatpickr.setDefaults({
dateFormat: 'd.m.Y',
allowInput: true
});
}
connect() {
const dateFormat = this.formatValue || 'd.m.Y'
const minDate = this.minDateValue
const maxDate = this.maxDateValue
const options = {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: dateFormat,
minDate,
maxDate,
allowInput: true,
onClose(dates, currentdatestring, instance) {
instance.setDate(instance.altInput.value, true, instance.config.altFormat)
this.element.dispatchEvent(new CustomEvent('datepicker:closed'))
},
onChange(dates, currentdatestring, instance) {
this.element.dispatchEvent(new CustomEvent('datepicker:picked', {
detail: {
date: instance.altInput.value
}
}))
},
}
if (true === this.disableWeekendsValue) {
options['disable'] = [
function(date) {
return (date.getDay() === 0 || date.getDay() === 6);
}
]
}
this.picker = flatpickr(this.fieldTarget, options)
}
disconnect() {
this.picker.destroy()
}
}
@@ -0,0 +1,41 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'fields', 'field', 'addButton' ]
static values = {
prototype: String,
maxItems: Number,
itemsCount: Number,
}
connect () {
this.index = this.itemsCountValue = this.fieldTargets.length
}
addItem() {
let prototype = JSON.parse(this.prototypeValue)
const newField = prototype.replace(/__name__/g, this.index)
this.fieldsTarget.insertAdjacentHTML('beforeend', newField)
this.index++
this.itemsCountValue++
}
removeItem(event) {
this.fieldTargets.forEach(element => {
if (element.contains(event.target)) {
element.remove()
this.itemsCountValue--
}
})
}
itemsCountValueChanged() {
if (false === this.hasAddButtonTarget || 0 === this.maxItemsValue) {
return
}
const maxItemsReached = this.itemsCountValue >= this.maxItemsValue
this.addButtonTarget.classList.toggle('hidden', maxItemsReached)
}
}
@@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static outlets = [ 'mobilenav' ]
static classes = [ 'closed' ]
trigger({ params: { mode } }) {
this.mobilenavOutlet.toggle(mode)
}
toggle(mode) {
// Add animation classes only now to avoid fouc
this.element.classList.add('transition-transform', 'duration-300')
this.element.classList.toggle(this.closedClass, 'close' === mode)
}
}
@@ -0,0 +1,20 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static outlets = [ 'confirmation-modal', 'ajax-modal' ]
confirmation({ params: { title, action, content } }) {
this.confirmationModalOutlet.show({
title: title,
action: action,
content: content,
})
}
ajax({ params: { title, url } }) {
this.ajaxModalOutlet.show({
title: title,
url: url,
})
}
}
+32
View File
@@ -0,0 +1,32 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static classes = [ 'buttonActive' ]
static targets = [ 'button', 'tab' ]
static values = { activeTab: Number }
select({ params: { tab } }) {
let selectedTab = tab
this.activeTabValue = selectedTab
this.element.dispatchEvent(new CustomEvent('select', {
detail: {
tab: selectedTab
}
}))
}
activeTabValueChanged(activeTab) {
this.tabTargets.forEach((element, index) => {
element.classList.toggle('hidden', activeTab !== index)
})
this.buttonTargets.forEach((element, index) => {
if (this.hasButtonActiveClass) {
element.classList.toggle(this.buttonActiveClass, activeTab === index)
}
})
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Controller } from '@hotwired/stimulus'
import Toastify from 'toastify-js'
export default class extends Controller {
static values = { text: String, class: String }
connect() {
Toastify({
duration: 5000,
text: this.textValue,
gravity: 'top',
position: 'right',
className: this.classValue,
close: true,
}).showToast()
}
}
+152
View File
@@ -0,0 +1,152 @@
import { Controller } from '@hotwired/stimulus'
import Dropzone from 'dropzone'
Dropzone.autoDiscover = false
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [
'dropzone',
'queue',
'previewTemplate',
'fieldsTemplate',
'fields',
'error',
'errorMessage',
'upload',
]
static values = {
endpoint: String,
maxFiles: Number,
maxFilesize: Number,
acceptedFiles: String,
chunking: Boolean,
language: Object,
params: Object,
uploads: Array,
error: String,
formName: String,
}
initialize() {
this.fieldsTemplate = this.fieldsTemplateTarget.innerHTML
}
connect () {
const dropzone = new Dropzone(this.dropzoneTarget, {
url: this.endpointValue,
withCredentials: true,
maxFiles: this.maxFilesValue,
maxFilesize: this.maxFilesizeValue,
acceptedFiles: this.acceptedFilesValue,
chunking: this.chunkingValue,
chunkSize: 10000000,
parallelUploads: 3,
retryChunks: true,
previewsContainer: this.queueTarget,
previewTemplate: this.previewTemplateTarget.innerHTML,
createImageThumbnails: true,
dictFileTooBig: this.languageValue.fileTooBig,
dictInvalidFileType: this.languageValue.invalidFileType,
dictMaxFilesExceeded: this.languageValue.maxFilesExceeded,
})
dropzone.on('addedfile', file => {
if (this.maxFilesValue > this.uploadsValue.length) {
this._removeFile(file.id)
return
}
this.errorValue = ''
const uploads = this.uploadsValue;
uploads.pop()
this.uploadsValue = uploads
})
dropzone.on('removedfile', file => {
this._removeFile(file.upload.uuid)
})
dropzone.on('maxfilesexceeded', file => {
this.errorValue = dropzone.options.dictMaxFilesExceeded
dropzone.removeFile(file);
})
dropzone.on('error', (file, errorMessage) => {
this.errorValue = errorMessage;
dropzone.removeFile(file)
})
dropzone.on('dragenter', () => {
this.errorValue = ''
})
dropzone.on('success', file => {
const response = JSON.parse(file.xhr.response)
const uploads = this.uploadsValue
uploads.push({
filename: response.filename,
originalFilename: response.originalFilename,
uploaderType: response.uploaderType,
size: response.size,
mimeType: response.mimeType,
fileId: file.upload.uuid
})
this.uploadsValue = uploads
window.dispatchEvent(new CustomEvent('dropzone', {
detail: 'success'
}))
})
dropzone.on('sending', () => {
window.dispatchEvent(new CustomEvent('dropzone', {
detail: 'uploading'
}))
})
}
errorValueChanged () {
if ('' === this.errorValue) {
this.errorTarget.classList.add('hidden')
} else {
this.errorMessageTarget.innerText = this.errorValue
this.errorTarget.classList.remove('hidden')
}
}
uploadsValueChanged (uploads) {
this.fieldsTarget.innerHTML = ''
uploads.forEach((upload, index) => {
const field = this.fieldsTemplate
.replace(/__formName__/g, this.formNameValue)
.replace(/__index__/g, index)
.replace(/__fileId__/g, upload.fileId)
.replace(/__originalFilename__/g, upload.originalFilename)
.replace(/__filename__/g, upload.filename)
.replace(/__mimeType__/g, upload.mimeType)
.replace(/__size__/g, upload.size)
this.fieldsTarget.insertAdjacentHTML('beforeend', field)
})
}
removeFile ({ params }) {
const fileId = params.fileid
this._removeFile(fileId)
}
_removeFile(fileId) {
this.uploadsValue = this.uploadsValue.filter(upload => {
return upload.fileId !== fileId
})
}
removeUpload (event) {
event.preventDefault()
this.uploadTargets.forEach(element => {
if (element.contains(event.target)) {
element.remove()
}
})
}
}