WIP: Implement frontend
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user