frayerjj-frontend 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "frayerjj-frontend",
3
+ "version": "0.1.1",
4
+ "description": "My base frontend for various projects",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "echo 'No build step required'",
9
+ "test": "echo 'No tests yet'"
10
+ },
11
+ "author": "Joshua Frayer",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "@ckeditor/ckeditor5-build-classic": "^44.1.0",
15
+ "@popperjs/core": "^2.11.8",
16
+ "bootstrap": "^5.3.3"
17
+ }
18
+ }
@@ -0,0 +1,55 @@
1
+ class CkeUploadAdapter {
2
+ constructor(loader, uri, token) {
3
+ this.loader = loader;
4
+ this.uri = uri;
5
+ this.token = token;
6
+ }
7
+ upload() {
8
+ return this.loader.file
9
+ .then(file => new Promise((resolve, reject) => {
10
+ this._initRequest();
11
+ this._initListeners(resolve, reject, file);
12
+ this._sendRequest(file);
13
+ }));
14
+ }
15
+ abort() {
16
+ if (this.xhr) this.xhr.abort();
17
+ }
18
+ _initRequest() {
19
+ const xhr = this.xhr = new XMLHttpRequest();
20
+ xhr.open('POST', this.uri, true);
21
+ xhr.setRequestHeader('X-CSRFToken', this.token);
22
+ xhr.responseType = 'json';
23
+ }
24
+ _initListeners(resolve, reject, file) {
25
+ const xhr = this.xhr;
26
+ const loader = this.loader;
27
+ const genericErrorText = `Couldn't upload file: ${ file.name }.`;
28
+ xhr.addEventListener( 'error', () => reject(genericErrorText) );
29
+ xhr.addEventListener( 'abort', () => reject() );
30
+ xhr.addEventListener( 'load', () => {
31
+ const response = xhr.response;
32
+ if (!response || response.error) return reject(response && response.error ? response.error.message : genericErrorText);
33
+ resolve({ default: response.url });
34
+ });
35
+ if (xhr.upload) {
36
+ xhr.upload.addEventListener('progress', evt => {
37
+ if (evt.lengthComputable) {
38
+ loader.uploadTotal = evt.total;
39
+ loader.uploaded = evt.loaded;
40
+ }
41
+ });
42
+ }
43
+ }
44
+ _sendRequest(file) {
45
+ const data = new FormData();
46
+ data.append('file', file);
47
+ this.xhr.send(data);
48
+ }
49
+ };
50
+
51
+ export function CkeUploadAdapterPlugin(editor) {
52
+ editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
53
+ return new CkeUploadAdapter(loader, editor.config._config.extraParams.uri, editor.config._config.extraParams.token);
54
+ };
55
+ }
package/src/helpers.js ADDED
@@ -0,0 +1,94 @@
1
+ function encodeVars(vars) {
2
+ let s = '';
3
+ for (var v in vars) s += v + '=' + vars[v] + "&";
4
+ return s ? s.substring(0, s.length - 1) : s;
5
+ }
6
+
7
+ export function getIntVar(varName, defaultVal) {
8
+ var val = parseInt(sessionStorage.getItem(varName));
9
+ return isNaN(val) ? defaultVal : val;
10
+ }
11
+
12
+ export function ajax(args) {
13
+ if (args.method == 'GET' && args.vars) args.uri += '?' + encodeVars(args.vars);
14
+ message.verbose('Making Request: ' + args.method + ' ' + args.uri);
15
+ let xhr = new XMLHttpRequest();
16
+ xhr.open(args.method, args.uri, true);
17
+ if (args.method == 'POST' && args.vars) xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
18
+ xhr.onreadystatechange = () => {
19
+ if (xhr.readyState === XMLHttpRequest.DONE) {
20
+ const status = xhr.status;
21
+ if (status === 0 || (status >= 200 && status < 400)) {
22
+ message.verbose('AJAX Request Successful');
23
+ if (typeof args.success == 'function') args.success.call(xhr, args.json != false ? JSON.parse(xhr.responseText) : xhr.responseText);
24
+ } else {
25
+ message.warn('AJAX Request Failed (HTTP ' + xhr.status + ': ' + xhr.statusText + ')');
26
+ if (typeof args.failure == 'function') args.failure.call(xhr);
27
+ }
28
+ }
29
+ };
30
+ xhr.send(args.method == 'POST' && args.vars ? encodeVars(args.vars) : null);
31
+ }
32
+
33
+ export function pageInit() {
34
+
35
+ // Updates the id in the form action inside a modal. Used for delete confirm and edit modals.
36
+ document.querySelectorAll('.modal-uuid-update').forEach(el => {
37
+ el.addEventListener('click', ev => {
38
+ ev.preventDefault();
39
+ let uuid = el.getAttribute('inst-uuid'),
40
+ modalSelector = el.getAttribute('data-bs-target'),
41
+ form = document.querySelector(`${modalSelector} form`);
42
+ if (uuid.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i) && form) {
43
+ let action = form.getAttribute('action');
44
+ if (action.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i))
45
+ action = action.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, uuid);
46
+ form.setAttribute('action', action);
47
+ }
48
+ else console.error('Modal UUID Update: No UUID found, make sure to set inst-uuid.');
49
+ });
50
+ });
51
+
52
+ // Automatically submits on change.
53
+ document.querySelectorAll('.change-submit').forEach(el => {
54
+ el.addEventListener('change', () => {
55
+ let form = el.closest('form');
56
+ if (form) form.submit();
57
+ else console.error('Change Submit: No form found, make sure to wrap the input in a form.');
58
+ });
59
+ });
60
+
61
+ // AJAX Modal is a custom plugin for loading AJAX partial views into a modal.
62
+ modal.ajax.init();
63
+
64
+ // Confirm/Delete Modals
65
+ document.querySelectorAll('.confirm-link').forEach(el => {
66
+ el.addEventListener('click', ev => {
67
+ ev.preventDefault();
68
+ modal.confirm(
69
+ el.getAttribute('confirm-msg') || 'Are you sure?',
70
+ () => {
71
+ location.href = el.getAttribute('href');
72
+ },
73
+ () => {},
74
+ el.getAttribute('confirm-title') || 'Confirm',
75
+ el.getAttribute('confirm-yes') || 'Yes',
76
+ el.getAttribute('confirm-no') || 'No'
77
+ );
78
+ });
79
+ });
80
+
81
+ // Scroll to top button
82
+ let toTop = document.getElementById('to_top_btn');
83
+ toTop.style.visibility = "hidden";
84
+ toTop.addEventListener('click', () => {
85
+ scrollTo(0, 0);
86
+ });
87
+ window.addEventListener('scroll', () => {
88
+ if (scrollY > 1500) toTop.style.visibility = "visible";
89
+ else toTop.style.visibility = "hidden";
90
+ });
91
+
92
+ // Initialize form validation
93
+ validate.init();
94
+ }
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import modal from "./modal.js";
2
+ import { ajax, getIntVar, pageInit } from "./helpers.js";
3
+ import { loading } from "./loading.js";
4
+ import { message } from "./message.js";
5
+ import { validate } from "./validate.js";
6
+
7
+ export default { modal, ajax, getIntVar, pageInit, loading, message, validate };
package/src/loading.js ADDED
@@ -0,0 +1,105 @@
1
+
2
+ const loading = {
3
+
4
+ rotation: getIntVar('loadingRotation', 3),
5
+
6
+ build: el => {
7
+ if (loading.interval != null) loading.clear();
8
+ el.classList.add('loading')
9
+ el.insertAdjacentHTML("afterbegin",
10
+ '<div class="loader">' +
11
+ '<div class="loader-circle"></div>' +
12
+ '<div class="loader-line-mask" style="transform:rotate(' + loading.rotation + 'deg)">' +
13
+ '<div class="loader-line"></div>' +
14
+ '</div>' +
15
+ '<div class="loader-logo"></div>' +
16
+ '<div class="loader-text">Loading</div>' +
17
+ '</div>');
18
+ },
19
+
20
+ animation: () => {
21
+ if (document.querySelector('.loading') == null) {
22
+ clearInterval(loading.interval);
23
+ loading.interval = null;
24
+ } else {
25
+ loading.rotation += 3;
26
+ if (loading.rotation >= 360) loading.rotation = 0;
27
+ let mask = document.querySelector('.loader-line-mask');
28
+ if (mask) mask.style.cssText = 'transform:rotate(' + loading.rotation + 'deg)';
29
+ sessionStorage.setItem('loadingRotation', loading.rotation);
30
+ }
31
+ },
32
+
33
+ start: (unload = 0, el) => {
34
+ if (loading.interval != null) return;
35
+ else {
36
+ if (el == null) el = document.querySelector('body');
37
+ loading.build(el);
38
+ if (unload) {
39
+ let opacity = 0;
40
+ let fade = setInterval(() => {
41
+ el.style.opacity = '.' + opacity++;
42
+ if (opacity == 10) {
43
+ clearInterval(fade);
44
+ el.style.opacity = null;
45
+ }
46
+ }, 50);
47
+ }
48
+ loading.interval = setInterval(loading.animation, 10);
49
+ }
50
+ },
51
+
52
+ clear: () => {
53
+ document.querySelector('.loading')?.classList.remove('loading');
54
+ document.querySelector('.loader')?.remove();
55
+ clearInterval(loading.interval);
56
+ loading.interval = null;
57
+ },
58
+
59
+ stop: () => {
60
+ let el = document.querySelector('.loader');
61
+ let opacity = 9;
62
+ let fade = setInterval(() => {
63
+ el.style.opacity = '.' + opacity--;
64
+ if (opacity == 0) {
65
+ clearInterval(fade);
66
+ loading.clear();
67
+ }
68
+ }, 50);
69
+ },
70
+
71
+ init: () => {
72
+ window.addEventListener('DOMContentLoaded', () => {
73
+ message.verbose('DOM Loaded, Starting Animation');
74
+ loading.start();
75
+ });
76
+ window.addEventListener('beforeunload', () => {
77
+ message.verbose('Navigating Away, Starting Animation');
78
+ loading.start(1);
79
+ });
80
+ window.addEventListener('error', () => {
81
+ message.verbose('DOM Error, Clearing Animation');
82
+ loading.clear();
83
+ });
84
+ window.addEventListener('abort', () => {
85
+ message.verbose('Load Aborted, Clearing Animation');
86
+ loading.clear();
87
+ });
88
+ window.addEventListener('unload', () => {
89
+ message.verbose('Page Unloaded, Clearing Animation');
90
+ loading.clear();
91
+ });
92
+ window.addEventListener('load', () => {
93
+ message.verbose('Resources Loaded, Stopping Animation');
94
+ loading.stop();
95
+ });
96
+ window.addEventListener('pageshow', (e) => {
97
+ if (e.originalEvent && e.originalEvent.persisted) {
98
+ message.verbose('Back button detected, Stopping Animation');
99
+ loading.stop();
100
+ }
101
+ });
102
+ }
103
+ };
104
+
105
+ export { loading };
package/src/message.js ADDED
@@ -0,0 +1,15 @@
1
+ const message = {
2
+
3
+ verbose: (msg) => {
4
+ if (window.verbose) console.log(msg);
5
+ else console.debug(msg);
6
+ },
7
+
8
+ warn: (msg) => {
9
+ if (window.verbose) console.warn(msg);
10
+ else console.debug(msg);
11
+ }
12
+
13
+ };
14
+
15
+ export { message };
package/src/modal.js ADDED
@@ -0,0 +1,193 @@
1
+ import bootstrap from "bootstrap";
2
+ import ajax from './helpers.js';
3
+
4
+ const modal = {
5
+
6
+ buildLine: vars => {
7
+ let row = '<div class="row ' + (vars.rowClass ?? '') + '">';
8
+ if (vars.label) row += '<label class="fw-bold text-dark">' + vars.label + "</label>";
9
+ row += '</div>';
10
+ return row;
11
+ },
12
+
13
+ buildInput: vars => {
14
+ let input = '';
15
+ if (vars.type == 'textarea') input += '<textarea class="form-control ';
16
+ if (vars.type == 'select') input += '<select class="form-select ';
17
+ else input += '<input type="' + vars.type + '" value="' + vars.value + '" placeholder="' + (vars.placeholder ?? vars.label) + '" class="form-control ';
18
+ input += (vars.inputClass ?? '') + '" id="' + vars.id + '" aria-label="' + (vars.placeholder ?? vars.label) + '" ' + (vars.checked ? 'checked' : '') + '>';
19
+ if (vars.type == 'textarea') input += vars.value + '</textarea>'
20
+ if (vars.type == 'select') input += '</select>';
21
+ return input;
22
+ },
23
+
24
+ buildInputLine: vars => {
25
+ let row = '<div class="row ' + (vars.rowClass ?? '') + '">';
26
+ if (vars.type == 'checkbox' || vars.type == 'radio') {
27
+ row += modal.buildInput(vars);
28
+ if (vars.label) row += '<label for="' + vars.id + '" class="fw-bold text-dark">' + vars.label + '</label>';
29
+ } else {
30
+ if (vars.label) row += '<label for="' + vars.id + '" class="fw-bold text-dark">' + vars.label + '</label>';
31
+ row += '<div class="input-group">';
32
+ if (vars.prepend) row += '<span class="input-group-text">' + vars.prepend + '</span>';
33
+ row += modal.buildInput(vars);
34
+ if (vars.append) row += '<span class="input-group-text">' + vars.append + '</span>';
35
+ row += '</div></div>';
36
+ }
37
+ return row;
38
+ },
39
+
40
+ build: vars => {
41
+ let html =
42
+ '<div id="' + vars.id + '" class="modal" tabindex="-1">' +
43
+ '<div class="modal-dialog ' + (vars.class ?? '') + '">' +
44
+ '<div class="modal-content">' +
45
+ '<div class="modal-header">' +
46
+ '<h5 class="modal-title">' + vars.title + '</h5>' +
47
+ '<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>' +
48
+ '</div>' +
49
+ '<div class="modal-body">' +
50
+ '<div class="col">' + (vars.body ?? '');
51
+ if (vars.inputs) Object.values(vars.inputs).forEach(i => {
52
+ if (i.type) html += modal.buildInputLine(i);
53
+ else html += modal.buildLine(i);
54
+ });
55
+ html +=
56
+ '</div>' +
57
+ '</div>' +
58
+ '<div class="modal-footer">';
59
+ if (vars.buttons) Object.values(vars.buttons).forEach(b => {
60
+ html += '<button type="button" class="btn btn-outline-secondary me-1 ' + (b.class ?? '') + '" data-bs-dismiss="modal">' + b.text + '</button>';
61
+ });
62
+ html +=
63
+ '</div>' +
64
+ '</div>' +
65
+ '</div>' +
66
+ '</div>';
67
+ return html;
68
+ },
69
+
70
+ alert: (msg, title = "Alert", button = "OK") => {
71
+ document.querySelector('body').insertAdjacentHTML('beforeend', modal.build({
72
+ id: 'alert-modal',
73
+ title: title,
74
+ body: '<p>' + msg + '</p>',
75
+ buttons: [ { text: button } ]
76
+ }));
77
+ let alertModal = document.getElementById('alert-modal');
78
+ alertModal.addEventListener('hidden.bs.modal', ev => {
79
+ ev.target.remove();
80
+ });
81
+ let bsAlertModal = new bootstrap.Modal(alertModal);
82
+ bsAlertModal.show();
83
+ },
84
+
85
+ confirm: (msg, onConfirm, onCancel, title = "Are you sure?", buttonYes = "Yes", buttonNo = "No") => {
86
+ document.querySelector('body').insertAdjacentHTML('beforeend', modal.build({
87
+ id: 'confirm-modal',
88
+ title: title,
89
+ body: '<p>' + msg + '</p>',
90
+ buttons: [
91
+ { text: buttonYes, class: 'btn-confirm' },
92
+ { text: buttonNo, class: 'btn-outline-danger' } ]
93
+ }));
94
+ let confirmModal = document.getElementById('confirm-modal');
95
+ confirmModal.addEventListener('hidden.bs.modal', ev => {
96
+ ev.target.remove();
97
+ });
98
+ confirmModal.querySelector('.btn-confirm').addEventListener('click', () => {
99
+ onConfirm();
100
+ });
101
+ confirmModal.querySelector('.btn-outline-danger').addEventListener('click', () => {
102
+ onCancel();
103
+ });
104
+ let bsConfirmModal = new bootstrap.Modal(confirmModal);
105
+ bsConfirmModal.show();
106
+ },
107
+
108
+ ajax: {
109
+
110
+ error: {
111
+ load: { title: 'Unable to Load Data', msg: 'There was a problem trying to load data. Please try again.' },
112
+ save: { title: 'Unable to Save Data', msg: 'There was a problem trying to save data. Please try again' }
113
+ },
114
+
115
+ init: () => {
116
+ // Cycle through all AJAX Modals in DOM
117
+ document.querySelectorAll('.ajax-modal').forEach(el => {
118
+ // Add Click Handler
119
+ el.addEventListener('click', () => {
120
+ // Build Modal
121
+ let buttons;
122
+ if (el.getAttribute('modal-info')) buttons = [{ text: 'OK' }];
123
+ else buttons = [{ text: 'Save', class: 'btn-save' }, { text: 'Cancel' }];
124
+ document.querySelector('body').insertAdjacentHTML('beforeend', modal.build({
125
+ id: el.getAttribute('data-bs-target').substring(1),
126
+ title: el.getAttribute('modal-title'),
127
+ body: '',
128
+ class: (el.getAttribute('modal-class') ?? ''),
129
+ buttons: buttons
130
+ }));
131
+ let ajaxModal = document.getElementById(el.getAttribute('data-bs-target').substring(1));
132
+ ajaxModal.addEventListener('hidden.bs.modal', ev => {
133
+ ev.target.remove();
134
+ });
135
+ let bsAjaxModal = new bootstrap.Modal(ajaxModal);
136
+ let ajaxModalBody = ajaxModal.querySelector('.modal-body');
137
+ bsAjaxModal.show();
138
+ loading.start(ajaxModalBody);
139
+ // Make request to load partial view
140
+ ajax({
141
+ method: 'GET',
142
+ uri: el.getAttribute('modal-load-uri'),
143
+ json: false,
144
+ success: html => {
145
+ // Insert partial view into view
146
+ ajaxModalBody.insertAdjacentHTML('afterbegin', html);
147
+ let images = ajaxModalBody.querySelectorAll('img')
148
+ // Ensure all images are loaded prior to lifting loading animation
149
+ if (images.length) {
150
+ let counter = images.length;
151
+ images.forEach(image => {
152
+ if (image.complete && --counter == 0) loading.stop();
153
+ else image.addEventListener('load', () => {
154
+ if (--counter == 0) loading.stop();
155
+ });
156
+ });
157
+ } else loading.stop();
158
+ // Set save handler
159
+ if (!el.getAttribute('modal-info')) {
160
+ let saveButton = ajaxModal.querySelector('.btn-save');
161
+ saveButton.removeAttribute('data-bs-dismiss').addEventListener('click', () => {
162
+ loading.start(ajaxModal.querySelector('.modal-body'));
163
+ let form = ajaxModal.querySelector('form');
164
+ ajax({
165
+ method: 'POST',
166
+ uri: form.getAttribute('action'),
167
+ vars: serialize(form),
168
+ success: () => {
169
+ bsAjaxModal.hide();
170
+ ajaxModal.remove();
171
+ },
172
+ failure: () => {
173
+ modal.alert(modal.ajax.error.save.msg, modal.ajax.error.save.title);
174
+ loading.stop();
175
+ }
176
+ });
177
+ });
178
+ }
179
+ },
180
+ failure: () => {
181
+ loading.stop();
182
+ bsAjaxModal.hide();
183
+ ajaxModal.remove();
184
+ modal.alert(modal.ajax.error.load.msg, modal.ajax.error.load.title);
185
+ }
186
+ });
187
+ });
188
+ });
189
+ }
190
+ }
191
+ };
192
+
193
+ export default modal;
package/src/ready.js ADDED
File without changes
@@ -0,0 +1,55 @@
1
+ const validate = {
2
+ methods: {
3
+ required: input => {
4
+ return input.value.length > 0;
5
+ },
6
+ identifier: input => {
7
+ return /^[a-z0-9_-]*$/i.test(input.value) && input.value.length > 0 && input.value.length <= 50;
8
+ },
9
+ email: input => {
10
+ return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(input.value);
11
+ },
12
+ uri: input => {
13
+ return /^\/[0-9a-z?=&#~:@!$+,;%()*\/\-[\]\.]*$/i.test(input.value);
14
+ },
15
+ url: input => {
16
+ return /^((http|https):\/\/[a-z0-9_]+([\-\.]{1}[a-z_0-9]+)*\.[_a-z]{2,5}(:[0-9]{1,5})?)?\/[0-9a-z?=&#~:@!$+,;%()*\/\-[\]\.]*$/i.test(input.value);
17
+ },
18
+ zip: input => {
19
+ return /^[0-9]{5}(-)?([0-9]{4})?$/.test(input.value);
20
+ }
21
+ },
22
+ check: form => {
23
+ let blurEvent = new Event('blur');
24
+ form.querySelectorAll('input,select,textarea').forEach(input => {
25
+ input.dispatchEvent(blurEvent);
26
+ });
27
+ return form.querySelectorAll('input.is-invalid,select.is-invalid,textarea.is-invalid').length == 0;
28
+ },
29
+ init: () => {
30
+ Object.entries(validate.methods).forEach(([ className, validationMethod ]) => {
31
+ document.querySelectorAll('input.' + className + ',select.' + className + ',textarea.' + className).forEach(input => {
32
+ input.addEventListener('blur', () => {
33
+ if (validationMethod(input)) {
34
+ input.classList.add('is-valid');
35
+ input.classList.remove('is-invalid');
36
+ } else {
37
+ input.classList.add('is-invalid');
38
+ input.classList.remove('is-valid');
39
+ }
40
+ });
41
+ });
42
+ });
43
+ document.querySelectorAll('form:not(.skip-validation)').forEach(form => {
44
+ form.addEventListener('submit', ev => {
45
+ if (!validate.check(form)) {
46
+ ev.preventDefault();
47
+ form.querySelector('input.is-invalid,select.is-invalid,textarea.is-invalid').focus();
48
+ message.warn('Form failed validation. Submited cancelled.');
49
+ }
50
+ });
51
+ });
52
+ }
53
+ };
54
+
55
+ export { validate };