@kevlid/discordmenus 0.0.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,347 @@
1
+ const { ComponentTypes, ButtonStyles } = require("./types");
2
+ const { OptionTypes } = require("../utils/types");
3
+ const {
4
+ renderButton,
5
+ renderSeparator,
6
+ renderTextDisplay,
7
+ renderActionRow,
8
+ renderHeader,
9
+ renderSubOptionSection,
10
+ validateString,
11
+ validateNumber,
12
+ } = require("./utils");
13
+ const { encodeId, Presets } = require("../utils/customIds");
14
+ const { ComponentsV2MessageFlags } = require("../utils/constants");
15
+ const ModalInputPrefix = "m";
16
+
17
+
18
+ function renderListPage(menuKey, option, items, listPageIndex, userId, category, page) {
19
+ const itemsPerPage = 1;
20
+ let safeItems;
21
+ if (Array.isArray(items)) {
22
+ safeItems = items;
23
+ } else {
24
+ safeItems = [];
25
+ }
26
+ const totalPages = Math.max(1, Math.ceil(safeItems.length / itemsPerPage));
27
+ const clampedPage = Math.min(Math.max(0, listPageIndex), totalPages - 1);
28
+ const start = clampedPage * itemsPerPage;
29
+ const pageItems = safeItems.slice(start, start + itemsPerPage);
30
+ const ctx = { u: userId, cat: category, page, sub: clampedPage };
31
+ const genId = (...v) => encodeId(menuKey, v, ctx);
32
+ const headerText = renderHeader(option.title, option.description);
33
+ const headerDisplay = renderTextDisplay(headerText);
34
+ const divider = renderSeparator(true);
35
+ const containerComponents = [headerDisplay, divider];
36
+ if (pageItems.length === 0) {
37
+ containerComponents.push(renderTextDisplay("`(no items)`"));
38
+ } else {
39
+ for (const [i, item] of pageItems.entries()) {
40
+ const itemComponent = renderListItem(menuKey, option, item, start + i, ctx);
41
+ containerComponents.push(itemComponent);
42
+ }
43
+ }
44
+ const hasPrev = clampedPage > 0;
45
+ const hasNext = clampedPage < totalPages - 1;
46
+ const isLastItem = clampedPage >= totalPages - 1;
47
+ const currentAbsIndex = start;
48
+
49
+ const jumpItems = Math.min(10, Math.max(1, Math.ceil(safeItems.length * 0.1)));
50
+ const jumpPages = Math.max(1, Math.ceil(jumpItems / itemsPerPage));
51
+ const fastPrevPage = Math.max(0, clampedPage - jumpPages);
52
+ const fastNextPage = Math.min(totalPages - 1, clampedPage + jumpPages);
53
+
54
+ containerComponents.push(renderSeparator(true));
55
+
56
+ const addButton = renderButton(genId(option.key, OptionTypes.List, Presets.add), "Add", ButtonStyles.Success, !isLastItem);
57
+ const deleteButton = renderButton(genId(option.key, OptionTypes.List, Presets.delete, String(currentAbsIndex)), "Delete", ButtonStyles.Danger, safeItems.length <= 0);
58
+ containerComponents.push(renderActionRow(addButton, deleteButton));
59
+
60
+ const navRow = renderActionRow(
61
+ renderButton(genId(option.key, OptionTypes.List, Presets.nav, String(fastPrevPage), "fastl"), "<<", ButtonStyles.Secondary, fastPrevPage === clampedPage),
62
+ renderButton(genId(option.key, OptionTypes.List, Presets.nav, String(clampedPage - 1)), "<", ButtonStyles.Secondary, !hasPrev),
63
+ renderButton(genId(option.key, OptionTypes.List, Presets.done), "Done", ButtonStyles.Secondary),
64
+ renderButton(genId(option.key, OptionTypes.List, Presets.nav, String(clampedPage + 1)), ">", ButtonStyles.Secondary, !hasNext),
65
+ renderButton(genId(option.key, OptionTypes.List, Presets.nav, String(fastNextPage), "fastr"), ">>", ButtonStyles.Secondary, fastNextPage === clampedPage),
66
+ );
67
+
68
+ const visible = containerComponents.filter(c => c && typeof c === "object" && "type" in c);
69
+ return {
70
+ flags: ComponentsV2MessageFlags,
71
+ components: [{ type: ComponentTypes.Container, components: visible }, navRow],
72
+ };
73
+ }
74
+
75
+ function renderListItem(menuKey, option, value, absIndex, ctx) {
76
+ let displayValue;
77
+ if (value != null) {
78
+ if (typeof value === "object") {
79
+ const props = option.itemType === OptionTypes.Object && Array.isArray(option.itemConfig?.properties)
80
+ ? option.itemConfig.properties
81
+ : null;
82
+ const maxPreviewLines = 4;
83
+ if (props && props.length > 0) {
84
+ const lines = [];
85
+ const slice = props.slice(0, maxPreviewLines);
86
+ for (const p of slice) {
87
+ const entryValue = value[p.key];
88
+ let printable;
89
+ if (entryValue == null) {
90
+ printable = "(not set)";
91
+ } else if (typeof entryValue === "object") {
92
+ printable = JSON.stringify(entryValue);
93
+ } else {
94
+ printable = String(entryValue);
95
+ }
96
+ lines.push(`${p.key}: ${printable}`);
97
+ }
98
+ if (props.length > maxPreviewLines) {
99
+ lines.push(`... +${props.length - maxPreviewLines} more`);
100
+ }
101
+ displayValue = lines.join("\n");
102
+ } else {
103
+ const entries = Object.entries(value);
104
+ if (entries.length <= 0) {
105
+ displayValue = "(empty object)";
106
+ } else {
107
+ const lines = [];
108
+ const previewEntries = entries.slice(0, maxPreviewLines);
109
+ for (const [key, entryValue] of previewEntries) {
110
+ let printable;
111
+ if (entryValue == null) {
112
+ printable = "(not set)";
113
+ } else if (typeof entryValue === "object") {
114
+ printable = JSON.stringify(entryValue);
115
+ } else {
116
+ printable = String(entryValue);
117
+ }
118
+ lines.push(`${key}: ${printable}`);
119
+ }
120
+ if (entries.length > maxPreviewLines) {
121
+ lines.push(`... +${entries.length - maxPreviewLines} more`);
122
+ }
123
+ displayValue = lines.join("\n");
124
+ }
125
+ }
126
+ } else {
127
+ displayValue = String(value);
128
+ }
129
+ } else {
130
+ displayValue = "(not set)";
131
+ }
132
+ const genId = (...v) => encodeId(menuKey, v, ctx);
133
+ const modifyButton = renderButton(genId(option.key, OptionTypes.List, Presets.edit, String(absIndex)), "Modify", ButtonStyles.Secondary);
134
+ const textContent = renderTextDisplay(displayValue.split("\n").map(line => `> ${line}`).join("\n"));
135
+ return renderSubOptionSection(textContent, modifyButton);
136
+ }
137
+
138
+ function renderListTextInput(option, currentValue, itemIndex) {
139
+ const isAdd = itemIndex == null;
140
+ let idValues;
141
+ if (isAdd) {
142
+ idValues = [option.key, OptionTypes.List, Presets.add];
143
+ } else {
144
+ idValues = [option.key, OptionTypes.List, Presets.edit, String(itemIndex)];
145
+ }
146
+ const genId = encodeId(ModalInputPrefix, idValues);
147
+ const cfg = option.itemConfig;
148
+ let component;
149
+ if (option.itemType === OptionTypes.String) {
150
+ const minLen = cfg.minLength ?? 0;
151
+ const maxLen = cfg.maxLength ?? 4000;
152
+ let value;
153
+ if (isAdd) {
154
+ value = "";
155
+ } else if (currentValue != null) {
156
+ value = validateString(String(currentValue), minLen, maxLen);
157
+ } else {
158
+ value = "";
159
+ }
160
+ component = {
161
+ type: ComponentTypes.TextInput,
162
+ custom_id: genId,
163
+ style: 2,
164
+ placeholder: cfg.description ?? "Enter a value...",
165
+ min_length: minLen,
166
+ max_length: maxLen,
167
+ required: minLen > 0,
168
+ };
169
+ if (value !== "") {
170
+ component.value = value;
171
+ }
172
+ } else if (option.itemType === OptionTypes.Number) {
173
+ let value;
174
+ if (isAdd) {
175
+ value = "";
176
+ } else if (currentValue != null) {
177
+ value = validateNumber(String(currentValue), cfg.minValue ?? -Infinity, cfg.maxValue ?? Infinity);
178
+ } else {
179
+ value = "";
180
+ }
181
+ component = {
182
+ type: ComponentTypes.TextInput,
183
+ custom_id: genId,
184
+ style: 1,
185
+ placeholder: cfg.description ?? "Enter a number...",
186
+ required: true,
187
+ };
188
+ if (value !== "") {
189
+ component.value = value;
190
+ }
191
+ } else {
192
+ throw new Error(`Item type "${option.itemType}" does not support modal input`);
193
+ }
194
+ let labelText;
195
+ if (isAdd) {
196
+ labelText = "Add item";
197
+ } else {
198
+ labelText = "Edit item";
199
+ }
200
+ return {
201
+ type: ComponentTypes.Label,
202
+ label: labelText,
203
+ description: null,
204
+ component: component,
205
+ };
206
+ }
207
+
208
+ function renderObjectListItemPage(menuKey, option, itemIndex, currentItem, userId, category, page, listPage, totalItems = 0) {
209
+ const ctx = { u: userId, cat: category, page, sub: listPage, item: itemIndex };
210
+ const genId = (...v) => encodeId(menuKey, v, ctx);
211
+ let safeItem;
212
+ if (currentItem != null && typeof currentItem === "object") {
213
+ safeItem = currentItem;
214
+ } else {
215
+ safeItem = {};
216
+ }
217
+ const shownTotalItems = Math.max(totalItems, 1);
218
+ const headerText = renderHeader(option.title, `Editing item ${itemIndex + 1} of ${shownTotalItems}`);
219
+ const headerDisplay = renderTextDisplay(headerText);
220
+ const divider = renderSeparator(true);
221
+ const containerComponents = [headerDisplay, divider];
222
+ const properties = option.itemConfig.properties;
223
+ const propertyList = Array.isArray(properties) ? properties : [];
224
+ for (const [propertyIndex, property] of propertyList.entries()) {
225
+ const currentValue = safeItem[property.key] ?? null;
226
+ const propertySection = renderObjectListItemProperty(menuKey, option.key, property, propertyIndex, itemIndex, currentValue, ctx);
227
+ containerComponents.push(propertySection, renderSeparator());
228
+ }
229
+
230
+ const hasPreviousItem = itemIndex > 0;
231
+ const hasNextItem = totalItems > 0 && itemIndex < totalItems - 1;
232
+ const isLastItem = totalItems <= 0 || itemIndex >= totalItems - 1;
233
+
234
+ containerComponents.push(renderSeparator(true));
235
+
236
+ const addButton = renderButton(genId(option.key, OptionTypes.List, Presets.add), "Add", ButtonStyles.Success, !isLastItem);
237
+ const removeButton = renderButton(genId(option.key, OptionTypes.List, Presets.delete, String(itemIndex)), "Remove", ButtonStyles.Danger, totalItems <= 0);
238
+ containerComponents.push(renderActionRow(addButton, removeButton));
239
+
240
+ const navButtons = [
241
+ renderButton(genId(option.key, OptionTypes.List, Presets.previousCategory), "<<", ButtonStyles.Secondary, !hasPreviousItem),
242
+ renderButton(genId(option.key, OptionTypes.List, Presets.listItemView, String(itemIndex - 1)), "<", ButtonStyles.Secondary, !hasPreviousItem),
243
+ renderButton(genId(option.key, OptionTypes.List, Presets.listItemDone), "Done", ButtonStyles.Secondary),
244
+ renderButton(genId(option.key, OptionTypes.List, Presets.listItemView, String(itemIndex + 1)), ">", ButtonStyles.Secondary, !hasNextItem),
245
+ renderButton(genId(option.key, OptionTypes.List, Presets.nextCategory), ">>", ButtonStyles.Secondary, !hasNextItem)
246
+ ];
247
+
248
+ const doneRow = renderActionRow(...navButtons);
249
+ return {
250
+ flags: ComponentsV2MessageFlags,
251
+ components: [
252
+ { type: ComponentTypes.Container, components: containerComponents },
253
+ doneRow,
254
+ ],
255
+ };
256
+ }
257
+
258
+ function renderObjectListItemProperty(menuKey, optionKey, property, propertyIndex, itemIndex, currentValue, ctx) {
259
+ const genId = (...v) => encodeId(menuKey, v, ctx);
260
+ const titleSuffix = property.required ? " (required)" : "";
261
+ const lines = [`**${property.title}**${titleSuffix}`];
262
+ if (property.description) {
263
+ lines.push(`-# ${property.description}`);
264
+ }
265
+ let button;
266
+ if (property.type === OptionTypes.Boolean) {
267
+ const isTrue = Boolean(currentValue);
268
+ const newValue = isTrue ? "false" : "true";
269
+ const label = isTrue ? (property.trueLabel ?? "True") : (property.falseLabel ?? "False");
270
+ lines.push(`\`${label}\``);
271
+ button = renderButton(genId(optionKey, OptionTypes.List, Presets.listItemBoolToggle, String(itemIndex), String(propertyIndex), newValue), label, ButtonStyles.Secondary);
272
+ } else {
273
+ if (property.preview !== false) {
274
+ if (currentValue != null) {
275
+ lines.push(`\`${currentValue}\``);
276
+ } else if (!property.required) {
277
+ lines.push("`(not set)`");
278
+ }
279
+ }
280
+ button = renderButton(genId(optionKey, OptionTypes.List, Presets.listItemField, String(itemIndex), String(propertyIndex)), "Edit", ButtonStyles.Secondary);
281
+ }
282
+ const textContent = renderTextDisplay(lines.join("\n"));
283
+ return renderSubOptionSection(textContent, button);
284
+ }
285
+
286
+ function renderObjectListItemTextInput(option, property, currentValue) {
287
+ const propertyIndex = option.itemConfig.properties.findIndex(p => p.key === property.key);
288
+ let propertyToken;
289
+ if (propertyIndex >= 0) {
290
+ propertyToken = String(propertyIndex);
291
+ } else {
292
+ propertyToken = property.key;
293
+ }
294
+ const genId = encodeId(ModalInputPrefix, [option.key, OptionTypes.List, Presets.listItemModal, propertyToken]);
295
+ const cfg = property.itemConfig;
296
+ let component;
297
+ if (property.itemType === OptionTypes.String) {
298
+ const minLen = cfg.minLength ?? 0;
299
+ const maxLen = cfg.maxLength ?? 4000;
300
+ let prefill;
301
+ if (currentValue != null) {
302
+ prefill = validateString(String(currentValue), minLen, maxLen);
303
+ } else {
304
+ prefill = "";
305
+ }
306
+ component = {
307
+ type: ComponentTypes.TextInput,
308
+ custom_id: genId,
309
+ style: 2,
310
+ placeholder: property.description ?? "Enter a value...",
311
+ min_length: minLen,
312
+ max_length: maxLen,
313
+ required: property.required || minLen > 0,
314
+ };
315
+ if (prefill !== "") {
316
+ component.value = prefill;
317
+ }
318
+ } else if (property.itemType === OptionTypes.Number) {
319
+ let prefill;
320
+ if (currentValue != null) {
321
+ prefill = validateNumber(String(currentValue), cfg.minValue ?? -Infinity, cfg.maxValue ?? Infinity);
322
+ } else {
323
+ prefill = "";
324
+ }
325
+ component = {
326
+ type: ComponentTypes.TextInput,
327
+ custom_id: genId,
328
+ style: 1,
329
+ placeholder: property.description ?? "Enter a number...",
330
+ required: property.required,
331
+ };
332
+ if (prefill !== "") {
333
+ component.value = prefill;
334
+ }
335
+ } else {
336
+ throw new Error(`Property type "${property.itemType}" does not support modal input`);
337
+ }
338
+ return {
339
+ type: ComponentTypes.Label,
340
+ label: property.title,
341
+ description: property.description,
342
+ component: component,
343
+ };
344
+ }
345
+
346
+
347
+ module.exports = { renderListPage, renderListItem, renderListTextInput, renderObjectListItemPage, renderObjectListItemTextInput };
@@ -0,0 +1,76 @@
1
+ const { ComponentTypes } = require("./types");
2
+ const { OptionTypes } = require("../utils/types");
3
+ const { validateString, validateNumber } = require("./utils");
4
+ const { encodeId, Presets } = require("../utils/customIds");
5
+ const ModalInputPrefix = "m";
6
+
7
+
8
+ function renderModal(menuKey, title, components, { category, page, sub, item, op } = {}) {
9
+ const opts = { cat: category, page, sub };
10
+ if (item != null) {
11
+ opts.item = item;
12
+ }
13
+ if (op != null) {
14
+ opts.op = op;
15
+ }
16
+ return {
17
+ custom_id: encodeId(menuKey, [], opts),
18
+ title: title,
19
+ components: components,
20
+ };
21
+ }
22
+
23
+ function renderTextInput(option, currentValue) {
24
+ const genId = encodeId(ModalInputPrefix, [option.key, option.type, Presets.modal]);
25
+ let component;
26
+ if (option.type === OptionTypes.String) {
27
+ const minLen = option.minLength ?? 0;
28
+ const maxLen = option.maxLength ?? 4000;
29
+ let prefill;
30
+ if (currentValue != null) {
31
+ prefill = validateString(String(currentValue), minLen, maxLen);
32
+ } else {
33
+ prefill = "";
34
+ }
35
+ component = {
36
+ type: ComponentTypes.TextInput,
37
+ custom_id: genId,
38
+ style: 2,
39
+ placeholder: option.description ?? "Enter a value...",
40
+ min_length: minLen,
41
+ max_length: maxLen,
42
+ required: minLen > 0,
43
+ };
44
+ if (prefill !== "") {
45
+ component.value = prefill;
46
+ }
47
+ } else if (option.type === OptionTypes.Number) {
48
+ let prefill;
49
+ if (currentValue != null) {
50
+ prefill = validateNumber(String(currentValue), option.minValue ?? -Infinity, option.maxValue ?? Infinity);
51
+ } else {
52
+ prefill = "";
53
+ }
54
+ component = {
55
+ type: ComponentTypes.TextInput,
56
+ custom_id: genId,
57
+ style: 1,
58
+ placeholder: option.description ?? "Enter a number...",
59
+ required: false,
60
+ };
61
+ if (prefill !== "") {
62
+ component.value = prefill;
63
+ }
64
+ } else {
65
+ throw new Error(`Option type "${option.type}" does not support modal input`);
66
+ }
67
+ return {
68
+ type: ComponentTypes.Label,
69
+ label: option.title,
70
+ description: option.description,
71
+ component: component,
72
+ };
73
+ }
74
+
75
+
76
+ module.exports = { renderModal, renderTextInput };