// dtp-app.js // Copyright (C) 2024 DTP Technologies, LLC // All Rights Reserved 'use strict'; import DtpDisplayEngine from './dtp-display-engine.js'; import DtpSocket from './dtp-socket.js'; import DtpLog from './dtp-log.js'; import UIkit from 'uikit'; import numeral from 'numeral'; export default class DtpApp { constructor (appName, user) { this.user = user; this.name = appName; this.log = new DtpLog(appName); this.log.debug('constructor', 'creating DisplayEngine instance'); this.displayEngine = new DtpDisplayEngine(); this.domParser = new DOMParser(); this.properties = { }; } async onDtpLoad ( ) { } 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 confirmNavigation (event) { const target = event.currentTarget || event.target; event.preventDefault(); event.stopPropagation(); const href = target.getAttribute('href'); const hrefTarget = target.getAttribute('target'); const text = target.textContent; const whitelist = [ 'digitaltelepresence.com', 'www.digitaltelepresence.com', 'sites.digitaltelepresence.com', 'ourvoice.stream', 'www.ourvoice.stream', ]; try { const url = new URL(href); if (!whitelist.includes(url.hostname)) { await UIkit.modal.confirm(`

You are navigating to ${href}, a link or button that was displayed as:

${text}

Please only open links to destinations you trust and want to visit.
`); } window.open(href, hrefTarget); } catch (error) { this.log.info('confirmNavigation', 'navigation canceled', { error }); } return true; } async showForm (event, formUrl, formId, initRoutine) { event.preventDefault(); event.stopPropagation(); try { const response = await fetch(formUrl, { method: 'GET' }); const html = await response.text(); this.currentModal = UIkit.modal.dialog(html); if (formId) { this.currentModal.$el.id = formId; } if (initRoutine) { this[initRoutine](); } } catch (error) { UIkit.modal.alert(`Failed to display form: ${error.message}`); } return true; } async submitForm (event, userAction) { event.preventDefault(); event.stopPropagation(); const formElement = event.currentTarget || event.target; const submitButton = formElement.querySelector('button[type="submit"]'); try { const form = new FormData(formElement); if (submitButton) { submitButton.setAttribute('disabled', ''); } // 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; } if (submitButton) { submitButton.removeAttribute('disabled'); } } return; } async checkResponse (response) { if (!response.ok) { let json; try { json = await response.json(); } catch (error) { throw new Error('Server error'); } throw new Error(json.message); } } async processResponse (response) { const json = await response.json(); return this.processResponseJSON(json); } async processResponseJSON (json) { if (!json.success) { this.log.error('processResponseJSON', 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; } async parseHtml (html) { return this.domParser.parseFromString(html, 'text/html'); } async copyToClipboard (event) { const target = event.currentTarget || event.target; const text = target.getAttribute('data-text'); const message = target.getAttribute('data-message'); try { await navigator.clipboard.writeText(text); UIkit.modal.alert(message); } catch (error) { UIkit.modal.alert(`Failed to copy to clipboard: ${error.message}`); } } formatCount (value) { return numeral(value).format((value > 1000) ? '0,0.0a' : '0,0'); } randomString ( ) { return Math.random().toString().slice(2); } }