You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
7.8 KiB
242 lines
7.8 KiB
// dtp-app.js
|
|
// Copyright (C) 2022 DTP Technologies, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
import { Upload } from 'upload';
|
|
|
|
import DtpLog from './dtp-log';
|
|
import DtpSocket from './dtp-socket';
|
|
import DtpDisplayEngine from './dtp-display-engine';
|
|
|
|
export default class DtpApp {
|
|
|
|
constructor (component, user) {
|
|
this.user = user;
|
|
this.log = new DtpLog(component);
|
|
|
|
this.log.debug('constructor', 'creating DisplayEngine instance');
|
|
this.displayEngine = new DtpDisplayEngine();
|
|
|
|
this.updateTimestamps();
|
|
}
|
|
|
|
async connect (options) {
|
|
try {
|
|
this.log.debug('connect', 'creating WebSocket interface');
|
|
this.socket = new DtpSocket();
|
|
|
|
this.log.debug('connect', 'connecting WebSocket platform');
|
|
await this.socket.connect(options);
|
|
|
|
this.log.info('connect', 'DTP framework online');
|
|
} catch (error) {
|
|
this.log.error('connect', 'failed to connect', { error });
|
|
UIkit.modal.alert(`Failed to connect to server: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
async showForm (event, formUrl) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
try {
|
|
const response = await fetch(formUrl, { method: 'GET' });
|
|
const html = await response.text();
|
|
this.currentModal = UIkit.modal.dialog(html);
|
|
} catch (error) {
|
|
UIkit.modal.alert(`Failed to display form: ${error.message}`);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async submitForm (event, userAction) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
try {
|
|
const formElement = event.currentTarget || event.target;
|
|
const form = new FormData(formElement);
|
|
|
|
// include the submitter if we have one and it presents all required data
|
|
if (event.submitter && event.submitter.name && event.submitter.value) {
|
|
form.append(event.submitter.name, event.submitter.value);
|
|
}
|
|
|
|
this.log.info('submitForm', userAction, { event, action: formElement.action });
|
|
const response = await fetch(formElement.action, {
|
|
method: formElement.method,
|
|
body: form,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
let json;
|
|
try {
|
|
json = await response.json();
|
|
} catch (error) {
|
|
throw new Error('Server error');
|
|
}
|
|
throw new Error(json.message || 'Server error');
|
|
}
|
|
|
|
await this.processResponse(response);
|
|
} catch (error) {
|
|
UIkit.modal.alert(`Failed to ${userAction}: ${error.message}`);
|
|
} finally {
|
|
if (this.currentModal) {
|
|
this.currentModal.hide();
|
|
delete this.currentModal;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
async submitFormWithProgress (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
const formElement = event.currentTarget || event.target;
|
|
const form = new FormData(formElement);
|
|
|
|
const progressDialog = document.querySelector(formElement.getAttribute('data-progress-dialog') || '#upload-progress-dialog');
|
|
const progressBar = document.querySelector(formElement.getAttribute('data-progress-element') || 'progress#upload-progress');
|
|
const progressPrompt = document.querySelector(formElement.getAttribute('data-progress-prompt') || '#upload-progress-prompt');
|
|
|
|
const upload = new Upload({
|
|
url: formElement.getAttribute('action'),
|
|
form,
|
|
});
|
|
|
|
upload.on('progress', (progress) => {
|
|
this.log.info('submitSemitism', 'upload progress', { progress });
|
|
progressBar.value = Math.round(progress * 100.0);
|
|
});
|
|
|
|
progressDialog.removeAttribute('hidden');
|
|
const response = await upload.upload();
|
|
|
|
progressPrompt.textContent = 'Upload complete...';
|
|
this.log.info('submitSemitism', 'upload response', { response });
|
|
|
|
const json = JSON.parse(response.data);
|
|
await this.processResponseJSON(json);
|
|
|
|
return true;
|
|
}
|
|
|
|
async processResponse (response) {
|
|
const json = await response.json();
|
|
return this.processResponseJSON(json);
|
|
}
|
|
|
|
async processResponseJSON (json) {
|
|
if (!json.success) {
|
|
this.log.error('processResponse', json.message);
|
|
throw new Error(json.message);
|
|
}
|
|
if (json.displayList) {
|
|
this.displayEngine.executeDisplayList(json.displayList);
|
|
}
|
|
}
|
|
|
|
async validateTextElement (element, elementName, options) {
|
|
const DEFAULT_MAX_CHARS = 1000;
|
|
const DEFAULT_OPTIONS = {
|
|
maxLength: DEFAULT_MAX_CHARS,
|
|
minLength: 1,
|
|
warnLengths: [80, 150],
|
|
feedback: {
|
|
textEmpty: `The ${elementName} can't be empty`,
|
|
textTooShort: "Keep going...",
|
|
textValid: "You're off to a great start!",
|
|
textLengthWarn: [
|
|
"This is getting a little crazy",
|
|
`Only ${Math.max(0, options.maxLength - element.value.length)} characters left`,
|
|
],
|
|
textMaxLength: `The ${elementName} is as long as it can be.`,
|
|
textTooLong: `${elementName} is too long by ${element.value.length - options.maxLength} characters`,
|
|
}
|
|
};
|
|
options = Object.assign(DEFAULT_OPTIONS, options);
|
|
|
|
if (options.prompt) {
|
|
options.prompt.textContent = `${element.value.length} of ${options.maxLength} max`;
|
|
}
|
|
|
|
let isValid = true;
|
|
|
|
if (element.value.length === 0 || element.value.length > options.maxLength) {
|
|
element.classList.remove('uk-form-success');
|
|
element.classList.remove('uk-form-warning');
|
|
element.classList.add('uk-form-danger');
|
|
isValid = false;
|
|
} else if (element.value.length > options.warnLengths[1]) {
|
|
element.classList.remove('uk-form-success');
|
|
element.classList.remove('uk-form-danger');
|
|
element.classList.add('uk-form-warning');
|
|
} else {
|
|
element.classList.remove('uk-form-danger');
|
|
element.classList.remove('uk-form-warning');
|
|
element.classList.remove('uk-form-success');
|
|
}
|
|
|
|
if (element.value.length === 0) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textEmpty;
|
|
isValid = false;
|
|
} else if (options.minLength && (element.value.length < options.minLength)) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textTooShort;
|
|
isValid = false;
|
|
} else if (element.value.length < options.warnLengths[0]) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textValid;
|
|
} else if (element.value.length < options.warnLengths[1]) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textLengthWarn[0];
|
|
} else if (element.value.length < options.maxLength) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textLengthWarn[1];
|
|
window.dtp.submit.prompt.classList.add('uk-text-bold');
|
|
} else if (element.value.length === options.maxLength) {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textMaxLength;
|
|
window.dtp.submit.prompt.classList.add('uk-text-bold');
|
|
} else {
|
|
window.dtp.submit.prompt.textContent = options.feedback.textTooLong;
|
|
window.dtp.submit.prompt.classList.add('uk-text-bold');
|
|
isValid = false;
|
|
}
|
|
|
|
return isValid;
|
|
}
|
|
|
|
/**
|
|
* Finds every element with the `data-dtp-timestamp` attribute, and sets the
|
|
* text content of that element to the client-local representation of that
|
|
* timestamp using the formatting provided by the `data-dtp-timestamp-format`
|
|
* attribute.
|
|
*/
|
|
updateTimestamps ( ) {
|
|
const nodeList = document.querySelectorAll("[data-dtp-timestamp]");
|
|
for (const ts of nodeList) {
|
|
const date = ts.getAttribute('data-dtp-timestamp');
|
|
if (!date) {
|
|
continue;
|
|
}
|
|
|
|
const format = ts.getAttribute('data-dtp-timestamp-format');
|
|
switch (format) {
|
|
case 'date':
|
|
ts.textContent = moment(date).format('MMM DD, YYYY');
|
|
break;
|
|
case 'datetime':
|
|
ts.textContent = moment(date).format('MMMM D, h:mm a');
|
|
break;
|
|
case 'fuzzy':
|
|
ts.textContent = moment(date).fromNow();
|
|
break;
|
|
|
|
case 'timestamp':
|
|
default:
|
|
ts.textContent = moment(date).format('hh:mm:ss a');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|