create-template-html-css 1.4.3 → 1.6.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/CHANGELOG.md +92 -0
- package/INSERT-QUICK-REFERENCE.md +147 -0
- package/README.md +33 -2
- package/RELEASE-NOTES-v1.6.1.md +129 -0
- package/RELEASE-STATUS.md +203 -0
- package/RELEASE-v1.6.2.md +169 -0
- package/SECURITY-AUDIT.md +157 -0
- package/TEST-REPORT.md +110 -0
- package/VERIFICATION-REPORT.md +162 -0
- package/bin/cli.js +416 -247
- package/demo/css/accordion-component.css +135 -0
- package/demo/css/button-component.css +110 -0
- package/demo/css/card-component.css +381 -0
- package/demo/index.html +169 -0
- package/demo/js/accordion-component.js +31 -0
- package/demo/js/button-component.js +17 -0
- package/demo/js/card-component.js +124 -0
- package/package.json +6 -3
- package/src/generator.js +165 -64
- package/src/index.js +1 -1
- package/src/inserter.js +352 -146
- package/templates/accordion/index.html +67 -0
- package/templates/accordion/script.js +29 -0
- package/templates/accordion/style.css +133 -0
- package/templates/counter/index.html +46 -0
- package/templates/counter/script.js +88 -0
- package/templates/counter/style.css +164 -0
- package/templates/tabs/index.html +83 -0
- package/templates/tabs/script.js +46 -0
- package/templates/tabs/style.css +173 -0
- package/templates/todo-list/index.html +45 -0
- package/templates/todo-list/script.js +69 -0
- package/templates/todo-list/style.css +138 -0
- package/v1.6.2-IMPROVEMENTS.md +185 -0
- package/CONTRIBUTING.md +0 -62
- package/INSERT-DEMO.md +0 -171
- package/QUICKSTART.md +0 -195
- package/SECURITY.md +0 -95
- package/SHOWCASE.html +0 -342
- package/test-insert.html +0 -54
package/src/inserter.js
CHANGED
|
@@ -1,146 +1,352 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const path = require(
|
|
3
|
-
|
|
4
|
-
// Security: Validate component name against whitelist
|
|
5
|
-
const VALID_COMPONENTS = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
1
|
+
const fs = require("fs").promises;
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
// Security: Validate component name against whitelist
|
|
5
|
+
const VALID_COMPONENTS = [
|
|
6
|
+
"button",
|
|
7
|
+
"card",
|
|
8
|
+
"form",
|
|
9
|
+
"navigation",
|
|
10
|
+
"modal",
|
|
11
|
+
"footer",
|
|
12
|
+
"hero",
|
|
13
|
+
"slider",
|
|
14
|
+
"table",
|
|
15
|
+
"spinner",
|
|
16
|
+
"animated-card",
|
|
17
|
+
"typing-effect",
|
|
18
|
+
"fade-gallery",
|
|
19
|
+
"grid-layout",
|
|
20
|
+
"masonry-grid",
|
|
21
|
+
"dashboard-grid",
|
|
22
|
+
"flex-layout",
|
|
23
|
+
"flex-cards",
|
|
24
|
+
"flex-dashboard",
|
|
25
|
+
"todo-list",
|
|
26
|
+
"counter",
|
|
27
|
+
"accordion",
|
|
28
|
+
"tabs",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extracts indentation from a line
|
|
33
|
+
*/
|
|
34
|
+
function getIndentation(line) {
|
|
35
|
+
const match = line.match(/^(\s*)/);
|
|
36
|
+
return match ? match[1] : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks if a component is already inserted in the HTML
|
|
41
|
+
*/
|
|
42
|
+
function isComponentAlreadyInserted(htmlContent, component) {
|
|
43
|
+
const commentPattern = new RegExp(
|
|
44
|
+
`<!-- ${component.toUpperCase()} Component -->`,
|
|
45
|
+
"i",
|
|
46
|
+
);
|
|
47
|
+
return commentPattern.test(htmlContent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Formats HTML content with prettier
|
|
52
|
+
*/
|
|
53
|
+
async function formatHtml(htmlContent) {
|
|
54
|
+
const prettier = require("prettier");
|
|
55
|
+
try {
|
|
56
|
+
return await prettier.format(htmlContent, { parser: "html" });
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return htmlContent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Formats CSS content with prettier
|
|
64
|
+
*/
|
|
65
|
+
async function formatCss(cssContent) {
|
|
66
|
+
const prettier = require("prettier");
|
|
67
|
+
try {
|
|
68
|
+
return await prettier.format(cssContent, { parser: "css" });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return cssContent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Formats JavaScript content with prettier
|
|
76
|
+
*/
|
|
77
|
+
async function formatJs(jsContent) {
|
|
78
|
+
const prettier = require("prettier");
|
|
79
|
+
try {
|
|
80
|
+
return await prettier.format(jsContent, { parser: "babel" });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return jsContent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gets the indentation level used in an HTML file
|
|
88
|
+
*/
|
|
89
|
+
function getHtmlIndentation(htmlContent) {
|
|
90
|
+
// Look for any indented line to determine the standard indentation
|
|
91
|
+
const match = htmlContent.match(/\n(\s+)\S/);
|
|
92
|
+
return match ? match[1] : " "; // default to 4 spaces
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates that HTML file has proper structure
|
|
97
|
+
* @param {string} htmlContent - The HTML content to validate
|
|
98
|
+
* @returns {Object} Object with valid property and any errors
|
|
99
|
+
*/
|
|
100
|
+
function validateHtmlStructure(htmlContent) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
|
|
103
|
+
if (!htmlContent.includes("<!DOCTYPE")) {
|
|
104
|
+
errors.push("Missing DOCTYPE declaration");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!htmlContent.includes("<html")) {
|
|
108
|
+
errors.push("Missing <html> tag");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!htmlContent.includes("<head>") && !htmlContent.includes("<head ")) {
|
|
112
|
+
errors.push("Missing <head> tag");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!htmlContent.includes("</head>")) {
|
|
116
|
+
errors.push("Missing closing </head> tag");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!htmlContent.includes("<body>") && !htmlContent.includes("<body ")) {
|
|
120
|
+
errors.push("Missing <body> tag");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!htmlContent.includes("</body>")) {
|
|
124
|
+
errors.push("Missing closing </body> tag");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
valid: errors.length === 0,
|
|
129
|
+
errors,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates a backup of the original file before insertion
|
|
135
|
+
* @param {string} targetPath - Path to the original file
|
|
136
|
+
* @returns {Promise<string>} Path to the backup file
|
|
137
|
+
*/
|
|
138
|
+
async function createBackup(targetPath) {
|
|
139
|
+
const backupPath = `${targetPath}.backup`;
|
|
140
|
+
const content = await fs.readFile(targetPath, "utf-8");
|
|
141
|
+
await fs.writeFile(backupPath, content, "utf-8");
|
|
142
|
+
return backupPath;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function insertComponent(options) {
|
|
146
|
+
const {
|
|
147
|
+
component,
|
|
148
|
+
targetFile,
|
|
149
|
+
styleMode,
|
|
150
|
+
scriptMode,
|
|
151
|
+
createBackup: shouldBackup = false,
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
// Security: Validate component name
|
|
155
|
+
if (!VALID_COMPONENTS.includes(component)) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Invalid component: ${component}. Must be one of: ${VALID_COMPONENTS.join(", ")}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if target file exists
|
|
162
|
+
const targetPath = path.resolve(process.cwd(), targetFile);
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(targetPath);
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(`Target file not found: ${targetFile}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Read target HTML file
|
|
170
|
+
let htmlContent = await fs.readFile(targetPath, "utf-8");
|
|
171
|
+
|
|
172
|
+
// Validate HTML structure
|
|
173
|
+
const validation = validateHtmlStructure(htmlContent);
|
|
174
|
+
if (!validation.valid) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Invalid HTML structure:\n - ${validation.errors.join("\n - ")}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check if component is already inserted
|
|
181
|
+
if (isComponentAlreadyInserted(htmlContent, component)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Component "${component}" is already inserted in this file`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Create backup if requested
|
|
188
|
+
let backupPath = null;
|
|
189
|
+
if (shouldBackup) {
|
|
190
|
+
backupPath = await createBackup(targetPath);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!htmlContent.includes("</head>")) {
|
|
194
|
+
throw new Error("Target HTML file does not have a </head> tag");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Get component templates
|
|
198
|
+
const templateDir = path.join(__dirname, "..", "templates", component);
|
|
199
|
+
const componentHtml = await fs.readFile(
|
|
200
|
+
path.join(templateDir, "index.html"),
|
|
201
|
+
"utf-8",
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Try to read CSS from css/ subfolder, fall back to root
|
|
205
|
+
let componentCss;
|
|
206
|
+
try {
|
|
207
|
+
componentCss = await fs.readFile(
|
|
208
|
+
path.join(templateDir, "css", "style.css"),
|
|
209
|
+
"utf-8",
|
|
210
|
+
);
|
|
211
|
+
} catch {
|
|
212
|
+
componentCss = await fs.readFile(
|
|
213
|
+
path.join(templateDir, "style.css"),
|
|
214
|
+
"utf-8",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Extract component body content (only the inner content, not the body tags)
|
|
219
|
+
const bodyMatch = componentHtml.match(/<body[^>]*>\s*([\s\S]*?)\s*<\/body>/i);
|
|
220
|
+
if (!bodyMatch) {
|
|
221
|
+
throw new Error("Invalid component template structure");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let componentBody = bodyMatch[1].trim();
|
|
225
|
+
|
|
226
|
+
// Remove any script and style tags that might be in the body
|
|
227
|
+
componentBody = componentBody
|
|
228
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
229
|
+
.trim();
|
|
230
|
+
|
|
231
|
+
// Get indentation used in the HTML file
|
|
232
|
+
const baseIndent = getHtmlIndentation(htmlContent);
|
|
233
|
+
|
|
234
|
+
// Normalize component body indentation
|
|
235
|
+
const lines = componentBody
|
|
236
|
+
.split("\n")
|
|
237
|
+
.map((line) => {
|
|
238
|
+
if (line.trim() === "") return "";
|
|
239
|
+
return baseIndent + line.trim();
|
|
240
|
+
})
|
|
241
|
+
.join("\n");
|
|
242
|
+
componentBody = lines;
|
|
243
|
+
|
|
244
|
+
// Insert component HTML before closing </body> tag
|
|
245
|
+
htmlContent = htmlContent.replace(
|
|
246
|
+
"</body>",
|
|
247
|
+
`${baseIndent}<!-- ${component.toUpperCase()} Component -->\n${componentBody}\n\n</body>`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Handle CSS
|
|
251
|
+
if (styleMode === "inline") {
|
|
252
|
+
// Normalize CSS indentation
|
|
253
|
+
const normalizedCss = componentCss
|
|
254
|
+
.split("\n")
|
|
255
|
+
.map((line) => {
|
|
256
|
+
if (line.trim() === "") return "";
|
|
257
|
+
return baseIndent + " " + line.trim();
|
|
258
|
+
})
|
|
259
|
+
.join("\n");
|
|
260
|
+
|
|
261
|
+
htmlContent = htmlContent.replace(
|
|
262
|
+
"</head>",
|
|
263
|
+
`${baseIndent}<style id="${component}-styles">\n${baseIndent} /* ${component.toUpperCase()} Component Styles */\n${normalizedCss}\n${baseIndent}</style>\n</head>`,
|
|
264
|
+
);
|
|
265
|
+
} else if (styleMode === "separate") {
|
|
266
|
+
// Create css directory if needed
|
|
267
|
+
const cssDir = path.join(path.dirname(targetPath), "css");
|
|
268
|
+
await fs.mkdir(cssDir, { recursive: true });
|
|
269
|
+
|
|
270
|
+
// Create separate CSS file in css/ folder
|
|
271
|
+
const cssFileName = `${component}-component.css`;
|
|
272
|
+
const cssPath = path.join(cssDir, cssFileName);
|
|
273
|
+
const formattedCss = await formatCss(
|
|
274
|
+
`/* ${component.toUpperCase()} Component Styles */\n\n${componentCss}`,
|
|
275
|
+
);
|
|
276
|
+
await fs.writeFile(cssPath, formattedCss);
|
|
277
|
+
|
|
278
|
+
// Add link to CSS file
|
|
279
|
+
htmlContent = htmlContent.replace(
|
|
280
|
+
"</head>",
|
|
281
|
+
`${baseIndent}<link rel="stylesheet" href="css/${cssFileName}">\n</head>`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Handle JavaScript
|
|
286
|
+
try {
|
|
287
|
+
// Try to read JS from js/ subfolder, fall back to root
|
|
288
|
+
let componentJs;
|
|
289
|
+
try {
|
|
290
|
+
componentJs = await fs.readFile(
|
|
291
|
+
path.join(templateDir, "js", "script.js"),
|
|
292
|
+
"utf-8",
|
|
293
|
+
);
|
|
294
|
+
} catch {
|
|
295
|
+
componentJs = await fs.readFile(
|
|
296
|
+
path.join(templateDir, "script.js"),
|
|
297
|
+
"utf-8",
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (scriptMode === "inline") {
|
|
302
|
+
// Normalize JS indentation
|
|
303
|
+
const normalizedJs = componentJs
|
|
304
|
+
.split("\n")
|
|
305
|
+
.map((line) => {
|
|
306
|
+
if (line.trim() === "") return "";
|
|
307
|
+
return baseIndent + " " + line.trim();
|
|
308
|
+
})
|
|
309
|
+
.join("\n");
|
|
310
|
+
|
|
311
|
+
htmlContent = htmlContent.replace(
|
|
312
|
+
"</body>",
|
|
313
|
+
`${baseIndent}<script id="${component}-script">\n${baseIndent} // ${component.toUpperCase()} Component Script\n${normalizedJs}\n${baseIndent}</script>\n</body>`,
|
|
314
|
+
);
|
|
315
|
+
} else if (scriptMode === "separate") {
|
|
316
|
+
// Create js directory if needed
|
|
317
|
+
const jsDir = path.join(path.dirname(targetPath), "js");
|
|
318
|
+
await fs.mkdir(jsDir, { recursive: true });
|
|
319
|
+
|
|
320
|
+
// Create separate JS file in js/ folder
|
|
321
|
+
const jsFileName = `${component}-component.js`;
|
|
322
|
+
const jsPath = path.join(jsDir, jsFileName);
|
|
323
|
+
const formattedJs = await formatJs(
|
|
324
|
+
`// ${component.toUpperCase()} Component Script\n\n${componentJs}`,
|
|
325
|
+
);
|
|
326
|
+
await fs.writeFile(jsPath, formattedJs);
|
|
327
|
+
|
|
328
|
+
// Add script tag
|
|
329
|
+
htmlContent = htmlContent.replace(
|
|
330
|
+
"</body>",
|
|
331
|
+
`${baseIndent}<script src="js/${jsFileName}" id="${component}-script"></script>\n</body>`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// No JavaScript file for this component, skip
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Write updated HTML with prettier formatting
|
|
339
|
+
const formattedHtml = await formatHtml(htmlContent);
|
|
340
|
+
await fs.writeFile(targetPath, formattedHtml);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
targetFile: targetPath,
|
|
344
|
+
component,
|
|
345
|
+
styleMode,
|
|
346
|
+
scriptMode,
|
|
347
|
+
backupPath: backupPath || null,
|
|
348
|
+
success: true,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = { insertComponent, validateHtmlStructure, createBackup };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{name}} - Accordion Component</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="container">
|
|
11
|
+
<div class="accordion-wrapper">
|
|
12
|
+
<h1>Frequently Asked Questions</h1>
|
|
13
|
+
|
|
14
|
+
<div class="accordion">
|
|
15
|
+
<div class="accordion-item">
|
|
16
|
+
<button class="accordion-header">
|
|
17
|
+
<span>What is this accordion component?</span>
|
|
18
|
+
<span class="icon">+</span>
|
|
19
|
+
</button>
|
|
20
|
+
<div class="accordion-content">
|
|
21
|
+
<div class="accordion-body">
|
|
22
|
+
This is a fully functional accordion component that uses DOM manipulation to toggle content visibility. Click on any header to expand or collapse the content.
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="accordion-item">
|
|
28
|
+
<button class="accordion-header">
|
|
29
|
+
<span>How does it work?</span>
|
|
30
|
+
<span class="icon">+</span>
|
|
31
|
+
</button>
|
|
32
|
+
<div class="accordion-content">
|
|
33
|
+
<div class="accordion-body">
|
|
34
|
+
The accordion uses JavaScript to toggle classes on DOM elements. When you click a header, it adds or removes the 'active' class to expand or collapse the content section.
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="accordion-item">
|
|
40
|
+
<button class="accordion-header">
|
|
41
|
+
<span>Can I customize the content?</span>
|
|
42
|
+
<span class="icon">+</span>
|
|
43
|
+
</button>
|
|
44
|
+
<div class="accordion-content">
|
|
45
|
+
<div class="accordion-body">
|
|
46
|
+
Absolutely! You can easily modify the HTML to add your own questions and answers. The styling and functionality will work with any content you add.
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="accordion-item">
|
|
52
|
+
<button class="accordion-header">
|
|
53
|
+
<span>Is it mobile responsive?</span>
|
|
54
|
+
<span class="icon">+</span>
|
|
55
|
+
</button>
|
|
56
|
+
<div class="accordion-content">
|
|
57
|
+
<div class="accordion-body">
|
|
58
|
+
Yes! This accordion is fully responsive and works great on all screen sizes - desktop, tablet, and mobile devices.
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<script src="script.js"></script>
|
|
66
|
+
</body>
|
|
67
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Accordion Component
|
|
2
|
+
|
|
3
|
+
const accordionHeaders = document.querySelectorAll('.accordion-header');
|
|
4
|
+
|
|
5
|
+
// Toggle accordion
|
|
6
|
+
function toggleAccordion(e) {
|
|
7
|
+
const header = e.currentTarget;
|
|
8
|
+
const item = header.parentElement;
|
|
9
|
+
|
|
10
|
+
// Close all other items
|
|
11
|
+
document.querySelectorAll('.accordion-item').forEach(accordionItem => {
|
|
12
|
+
if (accordionItem !== item) {
|
|
13
|
+
accordionItem.classList.remove('active');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Toggle current item
|
|
18
|
+
item.classList.toggle('active');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add event listeners
|
|
22
|
+
accordionHeaders.forEach(header => {
|
|
23
|
+
header.addEventListener('click', toggleAccordion);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Optional: Open first item by default
|
|
27
|
+
if (accordionHeaders.length > 0) {
|
|
28
|
+
accordionHeaders[0].parentElement.classList.add('active');
|
|
29
|
+
}
|