@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.
- package/APX.mjs +117 -0
- package/README.md +115 -0
- package/dist/APX.dev.mjs +1637 -0
- package/dist/APX.mjs +1 -0
- package/dist/APX.prod.mjs +1 -0
- package/modules/common.mjs +18 -0
- package/modules/dialog/README.md +195 -0
- package/modules/dialog/css/dialog.css +118 -0
- package/modules/dialog/dialog.mjs +211 -0
- package/modules/dialog/html/dialog.html +8 -0
- package/modules/listen/listen.mjs +166 -0
- package/modules/tristate/README.md +95 -0
- package/modules/tristate/css/tristate.css +25 -0
- package/modules/tristate/tristate.mjs +172 -0
- package/package.json +80 -0
|
@@ -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,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
|
+
}
|