vueform-plugin-checkbox-select-all 1.0.0
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 +22 -0
- package/readme.md +238 -0
- package/src/index.js +144 -0
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vueform-plugin-checkbox-select-all",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bi-directional Select All logic for Vueform's Checkbox and Checkboxgroup elements",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"vue": "^3.0.0",
|
|
12
|
+
"@vueform/vueform": "^1.0.0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"vueform",
|
|
16
|
+
"plugin",
|
|
17
|
+
"checkbox",
|
|
18
|
+
"select-all"
|
|
19
|
+
],
|
|
20
|
+
"author": "Mohieb Al-Hesan",
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Vueform Select All Checkboxes Plugin
|
|
2
|
+
|
|
3
|
+
This plugin enables complex "Select All" logic for Vueform. It handles bi-directional synchronization (Parent $\leftrightarrow$ Child) and prevents infinite loops using an internal locking mechanism. It supports standard checkboxes, checkbox groups, and nested structures.
|
|
4
|
+
|
|
5
|
+
## 📦 Installation
|
|
6
|
+
|
|
7
|
+
### 1. Create the Plugin File
|
|
8
|
+
|
|
9
|
+
Create a file named `CheckboxSelectAll.js` inside your plugins directory (e.g., `./vueform/plugins/CheckboxSelectAll.js`) and paste the plugin code into it.
|
|
10
|
+
|
|
11
|
+
### 2. Register in `vueform.config.js`
|
|
12
|
+
|
|
13
|
+
Import the plugin and add it to the `plugins` array in your Vueform configuration file.
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// vueform.config.js
|
|
17
|
+
import en from "@vueform/vueform/locales/en";
|
|
18
|
+
import vueform from "@vueform/vueform/dist/vueform";
|
|
19
|
+
import { defineConfig } from "@vueform/vueform";
|
|
20
|
+
import "@vueform/vueform/dist/vueform.css";
|
|
21
|
+
|
|
22
|
+
// 1. Import the custom plugin
|
|
23
|
+
import CheckboxSelectAll from "./vueform/plugins/CheckboxSelectAll";
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
theme: vueform,
|
|
27
|
+
locales: { en },
|
|
28
|
+
locale: "en",
|
|
29
|
+
|
|
30
|
+
// 2. Add it to the plugins array
|
|
31
|
+
plugins: [CheckboxSelectAll],
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## ⚙️ Configuration & Concepts
|
|
36
|
+
|
|
37
|
+
To set up the logic, you only need to configure three properties in your schema.
|
|
38
|
+
|
|
39
|
+
### A. Downstream (Parent $\rightarrow$ Children)
|
|
40
|
+
|
|
41
|
+
**Used on the Parent (Controller).**
|
|
42
|
+
When the parent is clicked, it forces all defined children to match its state.
|
|
43
|
+
|
|
44
|
+
- **Prop:** `controls`
|
|
45
|
+
- **Type:** `Array<String>`
|
|
46
|
+
- **Value:** A list of **full paths** to the child elements.
|
|
47
|
+
|
|
48
|
+
### B. Upstream (Child $\rightarrow$ Parent)
|
|
49
|
+
|
|
50
|
+
**Used on the Child (Controlled).**
|
|
51
|
+
When a child is changed, it checks if all its siblings are selected. If yes, it checks the parent. If no, it unchecks the parent.
|
|
52
|
+
|
|
53
|
+
- **Prop:** `controller`
|
|
54
|
+
- **Type:** `String`
|
|
55
|
+
- **Value:** The **full path** to the parent element.
|
|
56
|
+
|
|
57
|
+
### C. Internal Group "All"
|
|
58
|
+
|
|
59
|
+
**Used strictly inside a `checkboxgroup`.**
|
|
60
|
+
It designates one specific option within the `items` array as the "Select All" for that specific group.
|
|
61
|
+
|
|
62
|
+
- **Prop:** `groupController`
|
|
63
|
+
- **Type:** `Boolean`
|
|
64
|
+
- **Value:** `true`
|
|
65
|
+
- **Location:** Inside an object in the `items` array.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 🚀 Usage Examples
|
|
70
|
+
|
|
71
|
+
### Example 1: Standard Checkbox Controlling Others
|
|
72
|
+
|
|
73
|
+
A standalone "Select All" checkbox controlling separate checkbox elements.
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
{
|
|
77
|
+
// 1. The Parent
|
|
78
|
+
selectAll: {
|
|
79
|
+
type: 'checkbox',
|
|
80
|
+
text: 'Select All',
|
|
81
|
+
// Tell parent who to control (Downstream)
|
|
82
|
+
// NOTE: Must be the full path to find the element via form$.el(fullPath)
|
|
83
|
+
controls: ['container.option1', 'container.option2']
|
|
84
|
+
},
|
|
85
|
+
container: {
|
|
86
|
+
type: 'group',
|
|
87
|
+
schema: {
|
|
88
|
+
// 2. The Children
|
|
89
|
+
option1: {
|
|
90
|
+
type: 'checkbox',
|
|
91
|
+
text: 'Option 1',
|
|
92
|
+
// Tell child who controls it (Upstream)
|
|
93
|
+
controller: 'selectAll'
|
|
94
|
+
},
|
|
95
|
+
option2: {
|
|
96
|
+
type: 'checkbox',
|
|
97
|
+
text: 'Option 2',
|
|
98
|
+
// Tell child who controls it (Upstream)
|
|
99
|
+
controller: 'selectAll'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Example 2: Parent Controlling a CheckboxGroup
|
|
107
|
+
|
|
108
|
+
A standalone checkbox controlling a whole group list.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
{
|
|
112
|
+
// 1. The Parent
|
|
113
|
+
selectAll: {
|
|
114
|
+
type: 'checkbox',
|
|
115
|
+
controls: ['myGroup'] // Points to the group element
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// 2. The Child (Group)
|
|
119
|
+
myGroup: {
|
|
120
|
+
type: 'checkboxgroup',
|
|
121
|
+
controller: 'selectAll', // Points back to parent
|
|
122
|
+
items: [
|
|
123
|
+
{ value: 'a', label: 'A' },
|
|
124
|
+
{ value: 'b', label: 'B' }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example 3: CheckboxGroup with Internal "Select All"
|
|
131
|
+
|
|
132
|
+
A group that contains its own "All" option inside the list.
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
{
|
|
136
|
+
myGroup: {
|
|
137
|
+
type: 'checkboxgroup',
|
|
138
|
+
items: [
|
|
139
|
+
// 1. The Internal Controller
|
|
140
|
+
{
|
|
141
|
+
value: 'all',
|
|
142
|
+
label: 'Select All',
|
|
143
|
+
groupController: true // <--- The Magic Prop
|
|
144
|
+
},
|
|
145
|
+
// 2. The Siblings
|
|
146
|
+
{ value: 'a', label: 'Item A' },
|
|
147
|
+
{ value: 'b', label: 'Item B' }
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Example 4: Complex Nested Structure
|
|
154
|
+
|
|
155
|
+
A group that contains its own "All" option inside the list, and a main checkbox controlling the group.
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
{
|
|
159
|
+
selectAllText: {
|
|
160
|
+
type: "static",
|
|
161
|
+
tag: "p",
|
|
162
|
+
content: "<div>Select Fields to be Exported</div>",
|
|
163
|
+
columns: { container: 9 },
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// --- MAIN CONTROLLER ---
|
|
167
|
+
selectAll: {
|
|
168
|
+
type: "checkbox",
|
|
169
|
+
text: "All Fields",
|
|
170
|
+
columns: { container: 3 },
|
|
171
|
+
align: "left",
|
|
172
|
+
// EXTERNAL CONTROL: Controls the two groups
|
|
173
|
+
controls: [
|
|
174
|
+
"container.reqDetailsCheckboxes",
|
|
175
|
+
"container.reqFieldsCheckboxes",
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
container: {
|
|
180
|
+
type: "group",
|
|
181
|
+
class: "dimBackground_50 scrollable",
|
|
182
|
+
schema: {
|
|
183
|
+
reqDetailsLabel: {
|
|
184
|
+
type: "static",
|
|
185
|
+
tag: "p",
|
|
186
|
+
content: "<p>Requisition Details</p>",
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// --- GROUP 1 ---
|
|
190
|
+
reqDetailsCheckboxes: {
|
|
191
|
+
type: "checkboxgroup",
|
|
192
|
+
class: "flex-container",
|
|
193
|
+
// EXTERNAL LINK: Points to Main Controller
|
|
194
|
+
controller: "selectAll",
|
|
195
|
+
items: [
|
|
196
|
+
{
|
|
197
|
+
// INTERNAL CONTROL: Marks this as the group's "All" button
|
|
198
|
+
value: "0",
|
|
199
|
+
label: "All",
|
|
200
|
+
groupController: true,
|
|
201
|
+
},
|
|
202
|
+
{ value: "1", label: "Requisition ID" },
|
|
203
|
+
{ value: "2", label: "Date Created" },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
reqFieldsLabel: {
|
|
208
|
+
type: "static",
|
|
209
|
+
tag: "p",
|
|
210
|
+
content: "<p>Requisition Fields</p>",
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
// --- GROUP 2 ---
|
|
214
|
+
reqFieldsCheckboxes: {
|
|
215
|
+
type: "checkboxgroup",
|
|
216
|
+
class: "flex-container",
|
|
217
|
+
// EXTERNAL LINK: Points to Main Controller
|
|
218
|
+
controller: "selectAll",
|
|
219
|
+
items: [
|
|
220
|
+
{
|
|
221
|
+
// INTERNAL CONTROL
|
|
222
|
+
value: "0",
|
|
223
|
+
label: "All",
|
|
224
|
+
groupController: true,
|
|
225
|
+
},
|
|
226
|
+
{ value: "1", label: "Field ID" },
|
|
227
|
+
{ value: "2", label: "Field Created" },
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
saveSelections: {
|
|
234
|
+
type: "checkbox",
|
|
235
|
+
text: "Save My Selections",
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
```
|
package/src/index.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { watch, onMounted, nextTick } from "vue";
|
|
2
|
+
|
|
3
|
+
export default function CheckboxController() {
|
|
4
|
+
return {
|
|
5
|
+
apply: ["CheckboxElement", "CheckboxgroupElement"],
|
|
6
|
+
props: {
|
|
7
|
+
controls: { type: Array, default: null }, // External: Downstream
|
|
8
|
+
controller: { type: String, default: null }, // External: Upstream
|
|
9
|
+
},
|
|
10
|
+
setup(props, context, component) {
|
|
11
|
+
const { form$, el$ } = component;
|
|
12
|
+
|
|
13
|
+
// --- Helpers ---
|
|
14
|
+
const isComplete = (el) => {
|
|
15
|
+
if (!el) return false;
|
|
16
|
+
if (el.type === "checkbox") return el.value === true;
|
|
17
|
+
if (el.type === "checkboxgroup") {
|
|
18
|
+
// Complete if selected count equals total options count
|
|
19
|
+
return el.value?.length > 0 && el.value?.length === el.items?.length;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const setElementState = (el, makeChecked) => {
|
|
25
|
+
if (!el) return;
|
|
26
|
+
if (el.type === "checkbox") {
|
|
27
|
+
if (makeChecked && el.value !== true) el.check();
|
|
28
|
+
if (!makeChecked && el.value !== false) el.uncheck();
|
|
29
|
+
} else if (el.type === "checkboxgroup") {
|
|
30
|
+
if (makeChecked && !isComplete(el)) el.checkAll();
|
|
31
|
+
else if (!makeChecked && el.value?.length > 0) el.uncheckAll();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
nextTick(() => {
|
|
37
|
+
// ---------------------------------------------------------
|
|
38
|
+
// 1. EXTERNAL DOWNSTREAM (Main Controller -> This Element)
|
|
39
|
+
// ---------------------------------------------------------
|
|
40
|
+
if (props.controls && props.controls.length > 0) {
|
|
41
|
+
watch(component.value, () => {
|
|
42
|
+
if (el$.value.__locked) {
|
|
43
|
+
el$.value.__locked = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const shouldCheck = isComplete(el$.value);
|
|
47
|
+
props.controls.forEach((path) => {
|
|
48
|
+
const child = form$.value.el$(path);
|
|
49
|
+
if (child) setElementState(child, shouldCheck);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------
|
|
55
|
+
// 2. EXTERNAL UPSTREAM (This Element -> Main Controller)
|
|
56
|
+
// ---------------------------------------------------------
|
|
57
|
+
if (props.controller) {
|
|
58
|
+
watch(component.value, () => {
|
|
59
|
+
if (el$.value.__locked) {
|
|
60
|
+
el$.value.__locked = false;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const parent = form$.value.el$(props.controller);
|
|
64
|
+
if (!parent || !parent.controls) return;
|
|
65
|
+
|
|
66
|
+
const allSiblingsChecked = parent.controls.every((path) => {
|
|
67
|
+
const sibling = form$.value.el$(path);
|
|
68
|
+
return sibling ? isComplete(sibling) : false;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const parentIsChecked = isComplete(parent);
|
|
72
|
+
if (allSiblingsChecked !== parentIsChecked) {
|
|
73
|
+
parent.__locked = true;
|
|
74
|
+
setElementState(parent, allSiblingsChecked);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------
|
|
80
|
+
// 3. INTERNAL GROUP LOGIC (CheckboxGroup Only)
|
|
81
|
+
// Handles the "All" option inside the items array
|
|
82
|
+
// ---------------------------------------------------------
|
|
83
|
+
if (el$.value.type === "checkboxgroup") {
|
|
84
|
+
// Find the "All" option based on the custom property 'groupController'
|
|
85
|
+
const controllerItem = el$.value.items.find(
|
|
86
|
+
(option) => option.groupController
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (controllerItem) {
|
|
90
|
+
const ctrlVal = controllerItem.value;
|
|
91
|
+
const otherVals = el$.value.items
|
|
92
|
+
.filter((i) => i.value !== ctrlVal)
|
|
93
|
+
.map((i) => i.value);
|
|
94
|
+
|
|
95
|
+
watch(component.value, (newVal, oldVal) => {
|
|
96
|
+
// If locked by external logic, skip internal calculation
|
|
97
|
+
if (el$.value.__lockedInternal) {
|
|
98
|
+
el$.value.__lockedInternal = false;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Determine what happened
|
|
103
|
+
const wasChecked = oldVal.includes(ctrlVal);
|
|
104
|
+
const isChecked = newVal.includes(ctrlVal);
|
|
105
|
+
|
|
106
|
+
// Scenario A: User clicked "All" (Checked it)
|
|
107
|
+
if (!wasChecked && isChecked) {
|
|
108
|
+
// Lock to prevent infinite recursion
|
|
109
|
+
el$.value.__lockedInternal = true;
|
|
110
|
+
el$.value.checkAll();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Scenario B: User clicked "All" (Unchecked it)
|
|
115
|
+
if (wasChecked && !isChecked) {
|
|
116
|
+
el$.value.__lockedInternal = true;
|
|
117
|
+
el$.value.uncheckAll();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Scenario C: User clicked a Sibling (Check logic)
|
|
122
|
+
const othersComplete = otherVals.every((v) =>
|
|
123
|
+
newVal.includes(v)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (isChecked && !othersComplete) {
|
|
127
|
+
// "All" is checked, but a sibling is missing -> Uncheck "All"
|
|
128
|
+
el$.value.__lockedInternal = true;
|
|
129
|
+
el$.value.value = newVal.filter((v) => v !== ctrlVal);
|
|
130
|
+
} else if (!isChecked && othersComplete) {
|
|
131
|
+
// "All" is unchecked, but all siblings are there -> Check "All"
|
|
132
|
+
el$.value.__lockedInternal = true;
|
|
133
|
+
el$.value.value = [...newVal, ctrlVal];
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return component;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|