@appius-fr/apx 2.2.2

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.
@@ -0,0 +1,211 @@
1
+ // Importing the CSS and HTML templates
2
+ import './css/dialog.css';
3
+ import dialogTemplate from './html/dialog.html?raw';
4
+
5
+ const createDialog = (options) => {
6
+ const dialogDefaults = {
7
+ title: '',
8
+ content: '',
9
+ contentURI: '',
10
+ buttons: [],
11
+ size: 'medium',
12
+ showLoadingIndicator: false
13
+ };
14
+
15
+ const { title, content, contentURI, buttons, size, showLoadingIndicator } = { ...dialogDefaults, ...options };
16
+
17
+ const wrapper = document.createElement('div');
18
+ wrapper.innerHTML = dialogTemplate.trim();
19
+ const dialogElement = wrapper.firstChild;
20
+
21
+ // Lifecycle event management
22
+ const lifecycleResolvers = {};
23
+ const lifecycleEvents = {};
24
+
25
+ // Define lifecycle promises as properties
26
+ ['opening', 'opened', 'closing', 'closed', 'contentLoading', 'contentLoaded'].forEach((event) => {
27
+ lifecycleEvents[event] = new Promise((resolve) => {
28
+ lifecycleResolvers[event] = resolve;
29
+ });
30
+ });
31
+
32
+ const triggerEvent = (event) => {
33
+ if (lifecycleResolvers[event]) {
34
+ lifecycleResolvers[event]();
35
+ } else {
36
+ console.warn(`No resolver found for lifecycle event: ${event}`);
37
+ }
38
+ };
39
+
40
+ const dialog = {
41
+ element: dialogElement,
42
+ buttons: {},
43
+ isClosed: false,
44
+ lifecycle: lifecycleEvents, // Lifecycle promises directly exposed here
45
+ open: async () => {
46
+ try {
47
+ triggerEvent('opening');
48
+ await dialog.lifecycle.opening;
49
+
50
+ // If loading indicator is enabled and we're fetching content, show dialog first
51
+ if (showLoadingIndicator && contentURI) {
52
+ // Append dialog to the DOM and display it first
53
+ document.body.appendChild(dialogElement);
54
+ dialogElement.style.display = "flex";
55
+
56
+ // Show loading indicator
57
+ dialogElement.querySelector(".APX-dialog-dynamic-content").innerHTML = '<div class="APX-dialog-loading-indicator"></div>';
58
+
59
+ // Load content after dialog is visible
60
+ await fetchAndInsertContent();
61
+ } else {
62
+ // Default behavior: load content first, then show dialog
63
+ await fetchAndInsertContent();
64
+
65
+ // Append dialog to the DOM and display it
66
+ document.body.appendChild(dialogElement);
67
+ dialogElement.style.display = "flex";
68
+ }
69
+
70
+ triggerEvent('opened');
71
+ await dialog.lifecycle.opened;
72
+ } catch (error) {
73
+ console.error('Error opening dialog:', error);
74
+ }
75
+ return dialog; // Return for daisy-chaining
76
+ },
77
+ close: async () => {
78
+ try {
79
+ triggerEvent('closing');
80
+ await dialog.lifecycle.closing;
81
+
82
+ // Cleanup and remove dialog
83
+ closeDialogAndCleanup();
84
+
85
+ triggerEvent('closed');
86
+ await dialog.lifecycle.closed;
87
+ } catch (error) {
88
+ console.error('Error during dialog closing:', error);
89
+ }
90
+ return dialog; // Return for daisy-chaining
91
+ }
92
+ };
93
+
94
+ const closeDialogAndCleanup = () => {
95
+ if (dialog.isClosed) return;
96
+
97
+ dialog.isClosed = true;
98
+ dialogElement.style.display = "none";
99
+ if (document.body.contains(dialogElement)) {
100
+ document.body.removeChild(dialogElement);
101
+ }
102
+ };
103
+
104
+ const setDialogSize = () => {
105
+ const dialogContent = dialogElement.querySelector(".APX-dialog-content");
106
+ const sizes = {
107
+ small: { width: '300px', height: '200px' },
108
+ medium: { width: '500px', height: '400px' },
109
+ large: { width: '800px', height: '600px' }
110
+ };
111
+
112
+ if (size === 'auto') return;
113
+
114
+ const selectedSize = sizes[size] || (typeof size === 'object' && size);
115
+ if (selectedSize) {
116
+ dialogContent.style.minWidth = selectedSize.width || '500px';
117
+ dialogContent.style.minHeight = selectedSize.height || '400px';
118
+ }
119
+ };
120
+
121
+ const addButtons = () => {
122
+ const buttonContainer = dialogElement.querySelector(".APX-dialog-button-container");
123
+
124
+ buttons.forEach(({ key, label, order = 0, align = 'flex-end', closeOnClick = true }) => {
125
+ const button = document.createElement('button');
126
+ button.textContent = label;
127
+ button.style.order = order;
128
+
129
+ if (closeOnClick) {
130
+ // Handle as a one-time Promise
131
+ let firstClickResolved = false;
132
+
133
+ const firstClickPromise = new Promise((resolve) => {
134
+ button.onclick = () => {
135
+ if (!firstClickResolved) {
136
+ firstClickResolved = true;
137
+ resolve(dialog); // Resolve the Promise
138
+ dialog.close(); // Close the dialog
139
+ }
140
+ };
141
+ });
142
+
143
+ // Assign the promise for chaining with `.then`
144
+ dialog.buttons[key] = firstClickPromise;
145
+ } else {
146
+ // Handle as a repeated event handler
147
+ let handlers = []; // Store handlers for `.then()`
148
+
149
+ const clickPromise = {
150
+ then: (callback) => {
151
+ if (typeof callback === 'function') {
152
+ handlers.push(callback); // Add to handlers
153
+ }
154
+ return clickPromise; // Allow chaining
155
+ },
156
+ };
157
+
158
+ button.onclick = () => {
159
+ handlers.forEach((handler) => handler(dialog)); // Call all registered handlers
160
+ };
161
+
162
+ dialog.buttons[key] = clickPromise; // Assign the "fake" promise
163
+ }
164
+
165
+ buttonContainer.appendChild(button);
166
+ buttonContainer.style.justifyContent = align;
167
+ });
168
+ };
169
+
170
+ const fetchAndInsertContent = async () => {
171
+ const originalCursor = document.body.style.cursor;
172
+ document.body.style.cursor = 'wait';
173
+ const dynamicContent = dialogElement.querySelector(".APX-dialog-dynamic-content");
174
+
175
+ try {
176
+ triggerEvent('contentLoading');
177
+ await dialog.lifecycle.contentLoading;
178
+
179
+ const contentHtml = contentURI
180
+ ? await fetchContent(contentURI)
181
+ : content;
182
+
183
+ dynamicContent.innerHTML = contentHtml;
184
+
185
+ triggerEvent('contentLoaded');
186
+ await dialog.lifecycle.contentLoaded;
187
+ } catch (error) {
188
+ console.error('Error loading content:', error);
189
+ } finally {
190
+ document.body.style.cursor = originalCursor;
191
+ }
192
+ };
193
+
194
+ const fetchContent = async (uri) => {
195
+ const response = await fetch(uri);
196
+ if (!response.ok) throw new Error(`Failed to fetch content: ${response.statusText}`);
197
+ return response.text();
198
+ };
199
+
200
+ // Initialize dialog
201
+ setDialogSize();
202
+ addButtons();
203
+
204
+ // Set title and close button handler
205
+ dialogElement.querySelector(".APX-dialog-title").textContent = title;
206
+ dialogElement.querySelector('.APX-close-dialog').onclick = dialog.close;
207
+
208
+ return dialog;
209
+ };
210
+
211
+ export default createDialog;
@@ -0,0 +1,8 @@
1
+ <div class="APX-dialog">
2
+ <div class="APX-dialog-content">
3
+ <span class="APX-close-dialog">&times;</span>
4
+ <h2 class="APX-dialog-title"></h2>
5
+ <div class="APX-dialog-dynamic-content"></div>
6
+ <div class="APX-dialog-button-container"></div>
7
+ </div>
8
+ </div>
@@ -0,0 +1,166 @@
1
+ let counter = 0;
2
+ const doCallbacks = {};
3
+ const timeouts = {};
4
+
5
+ /**
6
+ * This function adds an event listener to the elements and returns an object with a do method to add callback functions.
7
+ * @param {Object} apx - The apx object to augment with the listen function.
8
+ * @returns {Object} - The augmented apx object.
9
+ */
10
+ export default function (apx) {
11
+ let elements = apx.elements;
12
+
13
+ /**
14
+ * Adds an event listener to the elements and returns an object with a do method to add callback functions.
15
+ * @param {string} eventType - The type of event to listen for (e.g. 'click', 'keydown', etc.).
16
+ * @param {string} [selector] - The CSS selector to use for event delegation (optional).
17
+ * @returns {Object} - An object with a do method to add callback functions.
18
+ * @example
19
+ * apx.listen('click', '.my-button').do((event) => {
20
+ * console.log('Button clicked!', event.target);
21
+ * });
22
+ */
23
+ apx.listen = function (eventTypes, secondArgument, thirdArgument) {
24
+ let selector;
25
+ let options = {};
26
+ // Determine if the second argument is a selector or options
27
+ if (typeof secondArgument === 'string') {
28
+ selector = secondArgument;
29
+ options = thirdArgument || {};
30
+ } else {
31
+ options = secondArgument || {};
32
+ }
33
+
34
+ const uniqueId = generateUniqueId();
35
+ const timeoutDuration = options.timeout || 0;
36
+
37
+ if (!Array.isArray(eventTypes)) {
38
+ eventTypes = [eventTypes]; // Ensure eventTypes is an array
39
+ }
40
+
41
+ eventTypes.forEach((eventType) => {
42
+ elements.forEach((element) => {
43
+ element.addEventListener(eventType, (event) => {
44
+ executeCallbacks(event, uniqueId, timeoutDuration, selector, element);
45
+ });
46
+ });
47
+ });
48
+
49
+ return {
50
+ /**
51
+ * Adds a callback function to the doCallbacks object for the current event listener.
52
+ * @param {Function} doCallback - The callback function to execute when the event is triggered.
53
+ * @returns {Object} - Returns the current object to allow for chained calls.
54
+ * @example
55
+ * apx.listen('click', '.my-button').do((event) => {
56
+ * console.log('Button clicked!', event.target);
57
+ * }).do((event) => {
58
+ * console.log('Another callback function!');
59
+ * });
60
+ */
61
+ do: function (doCallback) {
62
+ if (!doCallbacks[uniqueId]) {
63
+ doCallbacks[uniqueId] = [];
64
+ }
65
+ doCallbacks[uniqueId].push(doCallback);
66
+ return this; // Return the object to allow for chained calls
67
+ },
68
+ };
69
+ };
70
+
71
+ /**
72
+ * Manually triggers the registered callbacks for a given event type on all elements or creates a new event if a string is provided.
73
+ * @param {string|Event} event - The event to trigger. This can be an event type string (like 'click') or an actual Event object.
74
+ */
75
+ apx.trigger = function (event) {
76
+ let eventType;
77
+ if (typeof event === 'string') {
78
+ eventType = event;
79
+ event = new Event(event, { bubbles: true, cancelable: true });
80
+ } else if (event instanceof Event) {
81
+ eventType = event.type;
82
+ } else {
83
+ console.error('Invalid event type provided to apx.trigger. It must be either a string or Event object.');
84
+ return;
85
+ }
86
+
87
+ apx.elements.forEach((element) => {
88
+ element.dispatchEvent(event); // This will allow the event to bubble up naturally.
89
+ });
90
+
91
+ // Handle event delegation manually by executing callbacks if the target matches the selector
92
+ Object.keys(doCallbacks).forEach((uniqueId) => {
93
+ const callbackDetails = doCallbacks[uniqueId];
94
+ if (callbackDetails && callbackDetails.eventType === eventType) {
95
+ // Check if the event target matches the selector used during listener registration
96
+ const { selector } = callbackDetails;
97
+ if (!selector || event.target.matches(selector)) {
98
+ executeCallbacks(event, uniqueId, 0, selector, event.target); // Immediate execution, no timeout
99
+ }
100
+ }
101
+ });
102
+ };
103
+
104
+ return apx;
105
+ };
106
+
107
+
108
+ function executeCallbacks(event, uniqueId, timeoutDuration, selector = '', element) {
109
+ const matchedElement = selector ? closest(event.target, selector) : element;
110
+ if (!matchedElement) {
111
+ return;
112
+ }
113
+
114
+ // Clear any previous timeout for this uniqueId to prevent unintended behavior
115
+ clearTimeout(timeouts[uniqueId]);
116
+
117
+ // Function to handle the execution of callbacks
118
+ function handleCallbackExecution() {
119
+ if (doCallbacks[uniqueId]) {
120
+ doCallbacks[uniqueId].reduce((prevPromise, doCallback) => {
121
+ return prevPromise.then(() => {
122
+ return new Promise((resolve, reject) => {
123
+ try {
124
+ const result = doCallback.bind(matchedElement)(event);
125
+ if (result instanceof Promise) {
126
+ result.then(resolve).catch(reject);
127
+ } else {
128
+ resolve(result);
129
+ }
130
+ } catch (error) {
131
+ reject(error);
132
+ }
133
+ });
134
+ });
135
+ }, Promise.resolve()).catch(error => {
136
+ console.error('Error in callback chain:', error);
137
+ });
138
+ }
139
+ }
140
+
141
+ // Check if timeoutDuration is zero to decide whether to delay the callback execution or not
142
+ if (timeoutDuration === 0) {
143
+ handleCallbackExecution();
144
+ } else {
145
+ timeouts[uniqueId] = setTimeout(handleCallbackExecution, timeoutDuration);
146
+ }
147
+ }
148
+
149
+
150
+ function generateUniqueId() {
151
+ const randomPart = Math.random().toString(36).substr(2, 9); // More randomness
152
+ const uniqueId = `${Date.now()}_${randomPart}_${counter}`;
153
+ counter = (counter + 1) % 1000; // Reset counter to avoid overflow
154
+
155
+ return uniqueId;
156
+ }
157
+
158
+ function closest(element, selector) {
159
+ while (element && element.nodeType === 1) {
160
+ if (element.matches(selector)) {
161
+ return element;
162
+ }
163
+ element = element.parentNode;
164
+ }
165
+ return null;
166
+ }
@@ -0,0 +1,95 @@
1
+ # APX Tri-State Checkbox Module
2
+
3
+ The APX Tri-State Checkbox module provides an augmentation to the `APX` object. This functionality allows you to easily transform standard checkboxes within an `APX` object into tri-state checkboxes.
4
+
5
+ ## Installation
6
+
7
+ Ensure you have the core `APX` object integrated into your project.
8
+
9
+ Next, to use this augmentation, make sure you've imported and added the `tristate` module to your project:
10
+
11
+ ```javascript
12
+ import APX from 'path-to-APX';
13
+ // Assuming tristate is part of APX's module structure
14
+ ```
15
+
16
+ Also, link the required CSS:
17
+
18
+ ```html
19
+ <link rel="stylesheet" href="path-to-css/tristate.css">
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ To transform a group of checkboxes into tri-state checkboxes using the `APX` object, first create an `APX` object containing the checkboxes you wish to transform, then simply call the `tristate` method:
25
+
26
+ ```javascript
27
+ const myApxCheckboxes = APX('.my-checkbox-class');
28
+ myApxCheckboxes.tristate();
29
+ ```
30
+
31
+ ### Parameters
32
+
33
+ The `tristate` method accepts an `options` parameter which can have the following properties:
34
+
35
+ - `size`: An object that defines the `width` and `height` of the tri-state checkbox.
36
+ - `width`: Width of the checkbox in pixels (e.g., `50` for 50px width).
37
+ - `height`: Height of the checkbox in pixels.
38
+
39
+ - `classes`: A string or an array of strings representing classes to be added to the custom checkbox. The string can contain class names separated by spaces or commas.
40
+
41
+ - `defaultState`: A string that defines the default state of the tri-state checkbox. Possible values are 'checked', 'unchecked', and 'crossed'.
42
+
43
+ - `callbacks`: An object that contains callback functions.
44
+ - `after`: A function that is called after the tri-state checkbox is created.
45
+ - `change`: A function that is called when the tri-state checkbox state changes.
46
+
47
+ - `bubbleEvents`: A boolean that determines whether events (`click`, `keyup`, `change`) on the custom checkbox should bubble up to the original checkbox.
48
+
49
+ Example:
50
+
51
+ ```javascript
52
+ const options = {
53
+ size: {
54
+ width: 50,
55
+ height: 30
56
+ },
57
+ classes: 'myClass1 myClass2',
58
+ defaultState: 'checked',
59
+ callbacks: {
60
+ after: function(originalCheckbox, tristateDom, hiddenInput) {
61
+ console.log("Checkbox created!");
62
+ },
63
+ change: function(originalCheckbox, tristateDom, hiddenInput) {
64
+ console.log("Checkbox state changed!");
65
+ }
66
+ },
67
+ bubbleEvents: true
68
+ };
69
+
70
+ const myApxCheckboxes = APX('.my-checkbox-class');
71
+ myApxCheckboxes.tristate(options);
72
+ ```
73
+
74
+ ## Features
75
+
76
+ ### Tri-State Logic
77
+
78
+ The checkbox will cycle through three states:
79
+
80
+ 1. Checked (represented by a tick)
81
+ 2. Crossed (represented by a cross)
82
+ 3. Unchecked (default checkbox state)
83
+
84
+ ### Accessibility
85
+
86
+ The tri-state checkbox retains the `tabIndex` property of the original checkbox, ensuring the accessibility of the control.
87
+
88
+ ### Event Bubbling
89
+
90
+ Any `click`, `keyup`, or `change` events attached to the original checkbox will be bubbled up from the custom checkbox if the `bubbleEvents` option is set to true. This ensures that your existing event listeners will still work as expected and that custom callbacks can also be executed.
91
+
92
+
93
+ ## License
94
+
95
+ Copyright Appius SARL
@@ -0,0 +1,25 @@
1
+ .apx-tristate {
2
+ display: inline-block;
3
+ vertical-align: middle;
4
+ text-align: center;
5
+ cursor: pointer;
6
+ position: relative;
7
+ min-width:13px;
8
+ min-height:13px;
9
+ background-size: contain;
10
+ background-color: white;
11
+ border-radius:3px;
12
+ }
13
+ .apx-tristate.unchecked {
14
+ background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2732%27%20height%3D%2732%27%3E%3Cpath%20%20fill%3D%22%23cccccc%22%20d%3D%22M25.107%2032.030h-18.214c-3.456%200-6.268-2.81-6.268-6.264v-19.529c0-3.456%202.812-6.268%206.268-6.268h18.214c3.456%200%206.268%202.812%206.268%206.268v19.529c0%203.452-2.812%206.264-6.268%206.264zM6.893%201.85c-2.419%200-4.386%201.967-4.386%204.386v19.529c0%202.417%201.967%204.382%204.386%204.382h18.214c2.419%200%204.386-1.965%204.386-4.382v-19.529c0-2.419-1.967-4.386-4.386-4.386h-18.214z%22%3E%3C/path%3E%3C/svg%3E');
15
+ }
16
+ .apx-tristate.checked {
17
+ background-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20width%3D%2732%27%20height%3D%2732%27%3E%3Cpath%20fill%3D%22%230654ba%22%20d%3D%22M0.543%205.647c0-3.119%202.531-5.647%205.65-5.647h19.309c3.12%200%205.65%202.511%205.65%205.647v20.705c0%203.119-2.531%205.647-5.65%205.647h-19.309c-3.12%200-5.65-2.511-5.65-5.647v-20.705zM5.313%2017.587l7.039%206.839%2013.831-13.439-2.636-2.561-10.929%2010.62-4.442-4.317-2.862%202.858z%22%3E%3C/path%3E%3C/svg%3E');
18
+ background-repeat: no-repeat;
19
+ background-position: center;
20
+ }
21
+ .apx-tristate.crossed {
22
+ background-image: url('data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%3Csvg%20viewBox%3D%220%200%2032%2032%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22m0.543%205.647c0-3.119%202.531-5.647%205.65-5.647h19.309c3.12%200%205.65%202.511%205.65%205.647v20.705c0%203.119-2.531%205.647-5.65%205.647h-19.309c-3.12%200-5.65-2.511-5.65-5.647v-20.705z%22%20fill%3D%22%23f00%22%2F%3E%3Cpath%20d%3D%22m8%208%2016%2016%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3Cpath%20d%3D%22M24%208L8%2024%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222%22%2F%3E%3C%2Fsvg%3E');
23
+ background-repeat: no-repeat;
24
+ background-position: center;
25
+ }
@@ -0,0 +1,172 @@
1
+ import './css/tristate.css';
2
+
3
+ /**
4
+ * Creates a tri-state checkbox for each checkbox element in the apx.elements array.
5
+ * @param {Array<HTMLElement>} apx - An array of HTMLElements which is returned by APX()
6
+ * @example
7
+ * // Call the tristate function on an array of checkbox elements
8
+ * apx.tristate();
9
+ */
10
+ export default function (apx) {
11
+ apx.tristate = function (options = {}){
12
+ apx.elements.forEach((element) => {
13
+ if (element.type === 'checkbox') {
14
+ const [originalCheckbox, tristateDom, hiddenInput] = createTriStateCheckbox(element,options);
15
+ //If there is a "options.callbacks.after" callback in options call it
16
+ if(options.callbacks && options.callbacks.after && typeof options.callbacks.after === 'function') {
17
+ options.callbacks.after(originalCheckbox, tristateDom, hiddenInput);
18
+ }
19
+ }
20
+ });
21
+ }
22
+ }
23
+
24
+ function toggleTriStateCheckboxState(customCheckbox, actualCheckbox, hiddenInput) {
25
+ if (customCheckbox.classList.contains('checked')) {
26
+ customCheckbox.classList.remove('checked');
27
+ customCheckbox.classList.add('crossed');
28
+ actualCheckbox.checked = false;
29
+ hiddenInput.value = 'false';
30
+ } else if (customCheckbox.classList.contains('crossed')) {
31
+ customCheckbox.classList.remove('crossed');
32
+ customCheckbox.classList.add('unchecked');
33
+ hiddenInput.removeAttribute('name'); // Ensure the hidden input doesn't post any value
34
+ hiddenInput.removeAttribute('value'); // Ensure the hidden input doesn't post any value
35
+ } else {
36
+ customCheckbox.classList.remove('unchecked');
37
+ customCheckbox.classList.add('checked');
38
+ actualCheckbox.checked = true;
39
+ hiddenInput.value = 'true';
40
+ hiddenInput.setAttribute('name', actualCheckbox.dataset.name); // Restore the name for posting
41
+ }
42
+ }
43
+
44
+ function createTriStateCheckbox(checkboxElement, options) {
45
+
46
+ //get the checkbox margins including browser default
47
+ const checkboxStyle = window.getComputedStyle(checkboxElement);
48
+ // Hide the original checkbox
49
+ checkboxElement.style.display = 'none';
50
+
51
+ // Create the custom checkbox div and set its initial state
52
+ const tristateDom = document.createElement('div');
53
+ tristateDom.classList.add('apx-tristate');
54
+ if (options.size) {
55
+ if (options.size.width) {
56
+ tristateDom.style.width = options.size.width + 'px';
57
+ }
58
+ if (options.size.height) {
59
+ tristateDom.style.height = options.size.height + 'px';
60
+ }
61
+ }
62
+ if(options.classes) tristateDom.classList.add(...computeClasses(options.classes));
63
+
64
+ tristateDom.style.margin = checkboxStyle.margin;
65
+
66
+ if (checkboxElement.checked) {
67
+ tristateDom.classList.add('checked');
68
+ } else {
69
+ tristateDom.classList.add('unchecked');
70
+ }
71
+
72
+ // Create the hidden input field
73
+ const hiddenInput = document.createElement('input');
74
+ hiddenInput.type = 'hidden';
75
+ hiddenInput.name = checkboxElement.name; // Copy the name from the original checkbox
76
+ checkboxElement.dataset.name = checkboxElement.name; // Store the name in a data attribute for later use
77
+ checkboxElement.removeAttribute('name'); // Remove the name from the original checkbox to avoid double posting
78
+
79
+ if (options.defaultState) setDefaultState(options, tristateDom, hiddenInput);
80
+ else{
81
+ // Set the initial value for the hidden input based on the checkbox's state
82
+ if (checkboxElement.checked) {
83
+ hiddenInput.value = 'true';
84
+ } else {
85
+ hiddenInput.name = ''; // Ensure the hidden input doesn't post any value
86
+ hiddenInput.value = '';
87
+ }
88
+ }
89
+
90
+ //handle accessibility, set the tabindex to the same as the checkbox
91
+ tristateDom.tabIndex = checkboxElement.tabIndex;
92
+
93
+ // Insert the hidden input inside the custom checkbox div
94
+ tristateDom.appendChild(hiddenInput);
95
+
96
+ // Insert it next to the actual checkbox
97
+ checkboxElement.parentNode.insertBefore(tristateDom, checkboxElement.nextSibling);
98
+
99
+ // Add event listener
100
+ tristateDom.addEventListener('click', function (e) {
101
+ toggleTriStateCheckboxState(tristateDom, checkboxElement, hiddenInput);
102
+ bubbleEventsToOriginalCheckbox(options, checkboxElement, tristateDom, hiddenInput, e);
103
+ });
104
+ tristateDom.addEventListener('keyup', onkeyup.bind(null, checkboxElement));
105
+
106
+ return [checkboxElement, tristateDom, hiddenInput];
107
+ }
108
+
109
+ function setDefaultState(options, tristateDom, hiddenInput) {
110
+ //for all values possible for defaultState, set the initial state of the checkbox
111
+ if (options.defaultState === 'checked') {
112
+ tristateDom.classList.remove('unchecked');
113
+ tristateDom.classList.add('checked');
114
+ hiddenInput.value = 'true';
115
+ }
116
+ else if (options.defaultState === 'unchecked') {
117
+ tristateDom.classList.remove('checked');
118
+ tristateDom.classList.add('unchecked');
119
+ hiddenInput.removeAttribute('name'); // Ensure the hidden input doesn't post any value
120
+ hiddenInput.removeAttribute('value'); // Ensure the hidden input doesn't post any value
121
+ }
122
+ else if (options.defaultState === 'crossed') {
123
+ tristateDom.classList.remove('checked');
124
+ tristateDom.classList.add('crossed');
125
+ hiddenInput.value = 'false';
126
+ }
127
+ }
128
+
129
+ function computeClasses(classes) {
130
+ let computedClasses = [];
131
+ if(typeof classes === 'string') {
132
+ //replace multiple white spaces with a single space
133
+ classes = classes.replace(/\s\s+/g, ' ');
134
+ if(classes.indexOf(' ') > -1)
135
+ computedClasses = classes.split(' ');
136
+ else if(classes.indexOf(',') > -1)
137
+ computedClasses = classes.split(',');
138
+ else
139
+ computedClasses = [classes];
140
+ }
141
+ else if(Array.isArray(classes)) {
142
+ computedClasses = classes;
143
+ }
144
+ return computedClasses;
145
+ }
146
+
147
+ function bubbleEventsToOriginalCheckbox(options, checkboxElement, tristateDom, hiddenInput, event) {
148
+ //if options.callbacks.change is set, call it
149
+ if(options.callbacks && options.callbacks.change && typeof options.callbacks.change === 'function') {
150
+ options.callbacks.change(checkboxElement, tristateDom, hiddenInput);
151
+ }
152
+
153
+ if(!options.bubbleEvents) return;
154
+
155
+ if(event.type === 'click') {
156
+ checkboxElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
157
+ }
158
+ else if(event.type === 'keyup') {
159
+ checkboxElement.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', bubbles: true, cancelable: true }));
160
+ }
161
+ else if(event.type === 'change') {
162
+ checkboxElement.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
163
+ }
164
+ }
165
+
166
+ function onkeyup(checkboxElement, event) {
167
+ if (event.keyCode === 32) {
168
+ bubbleEventsToOriginalCheckbox(checkboxElement, event);
169
+ event.preventDefault();
170
+ event.target.click();
171
+ }
172
+ }