Transpiled version (24071L) is out of date.
1 | // one instance should only be used for one page at a time |
2 | sclass HCRUD extends HAbstractRenderable { |
3 | HCRUD_Data data; // normally an instance of HCRUD_Concepts (#1026002) |
4 | |
5 | // yeah these fields are a mess... |
6 | bool mutationRights = true; |
7 | bool allowCreateOrDelete = true; |
8 | bool allowCreate = true; |
9 | bool allowEdit = true; |
10 | bool singleton; |
11 | |
12 | bool cmdsLeft; // put commands on the left instead of the right |
13 | bool showEntryCountInTitle; |
14 | bool allowFieldRenaming; |
15 | int defaultTextFieldCols = 80; |
16 | int valueDisplayLength = 1000; |
17 | S tableClass; // CSS class for table |
18 | S formTableClass; // CSS class for form |
19 | S checkBoxClass = "crud_chkbox"; |
20 | S customTitle; |
21 | bool showTextFieldsAsAutoExpandingTextAreas; |
22 | Set<S> unshownFields; // not shown in table or form |
23 | Set<S> uneditableFields; // not shown in form |
24 | Set<S> unlistedFields; // not shown in table |
25 | bool showCheckBoxes; |
26 | bool cleanItemIDs; |
27 | |
28 | bool haveJQuery, haveSelectizeJS, haveSelectizeClickable; |
29 | bool needsJQuery; // unused |
30 | bool paginate; |
31 | bool sortable; |
32 | bool buttonsOnTop, buttonsOnBottom = true; |
33 | bool duplicateInNewTab; |
34 | S formID = "crudForm"; |
35 | bool showQuickSaveButton; // needs hnotificationPopups() |
36 | bool enableMultiSelect = true; // enables shift+click on check boxes. needs haveJQuery |
37 | bool cellColumnToolTips; // give each table cell a tooltip showing its column name |
38 | bool showSearchField; |
39 | S searchQuery; |
40 | |
41 | SS params; |
42 | new HTMLPaginator paginator; |
43 | |
44 | // sort options |
45 | S sortByField = "id"; |
46 | S sortParameter = "sort"; |
47 | bool descending; |
48 | |
49 | int flexibleLengthListLeeway = 5; // how many empty rows to display at the end of each flexible length list |
50 | |
51 | // set internally after update/create or from "selectObj" param to highlight an item in the list (or show only that item) |
52 | O objectIDToHighlight; |
53 | bool showOnlySelected; |
54 | |
55 | S fieldPrefix = "f_"; |
56 | |
57 | int entryCount; |
58 | |
59 | *() {} |
60 | *(HCRUD_Data *data) {} |
61 | *(S *baseLink, HCRUD_Data *data) {} |
62 | |
63 | S newLink() { ret appendQueryToURL(baseLink, cmd := "new"); } |
64 | S deleteLink(O id) { ret appendQueryToURL(baseLink, "delete_" + id, 1); } |
65 | S editLink(O id) { ret appendQueryToURL(baseLink, edit := id); } |
66 | S duplicateLink(O id) { ret appendQueryToURL(baseLink, duplicate := id); } |
67 | |
68 | void setParams(SS params) { |
69 | this.params = params; |
70 | |
71 | if (objectIDToHighlight == null) objectIDToHighlight = params.get("selectObj"); |
72 | if (eq("1", params.get("showOnlySelected"))) |
73 | set showOnlySelected; |
74 | } |
75 | |
76 | // also handles commands if withCmds=true |
77 | // you probably want to call renderPage() instead to handle all commands |
78 | S render(bool withCmds, SS params) { |
79 | //print("HCRUD render"); |
80 | setParams(params); |
81 | |
82 | if (!withCmds) ret renderTable(false); |
83 | |
84 | try answer handleCommands(params); |
85 | |
86 | ret renderMsgs(params) |
87 | + divUnlessEmpty(nav()) |
88 | + renderTable(withCmds); |
89 | } |
90 | |
91 | swappable S nav() { |
92 | new LS l; |
93 | if (actuallyAllowCreate()) |
94 | l.add(ahref(newLink(), "New " + itemName())); |
95 | if (showSearchField) |
96 | l.add(hInlineSearchForm("search", searchQuery, "")); |
97 | ret joinWithVBar(l); |
98 | } |
99 | |
100 | S handleCommands(SS params) { |
101 | new LS msgs; |
102 | |
103 | if (eqGet(params, "action", "create")) { |
104 | if (!actuallyAllowCreate()) fail("Creating objects not allowed"); |
105 | processRenames(params); |
106 | O id = data.createObject(preprocessUpdateParams(params), fieldPrefix); |
107 | msgs.add(itemName() + " created (ID: " + id + ")"); |
108 | objectIDToHighlight = id; |
109 | } |
110 | |
111 | if (eqGet(params, "action", "update")) { |
112 | if (!actuallyAllowEdit()) fail("Editing objects not allowed"); |
113 | S id = params.get("id"); |
114 | processRenames(params); |
115 | msgs.add(data.updateObject(id, preprocessUpdateParams(params), fieldPrefix)); |
116 | objectIDToHighlight = id; |
117 | } |
118 | |
119 | LS toDeleteList = keysDeprefixNemptyValue(params, "delete_"); |
120 | if (eq(params.get("bulkAction"), "deleteSelected")) |
121 | toDeleteList.addAll(keysDeprefixNemptyValue(params, "obj_")); |
122 | |
123 | for (S toDelete : toDeleteList) { |
124 | if (!actuallyAllowDelete()) fail("Deleting objects not allowed"); |
125 | msgs.add(data.deleteObject(toDelete)); |
126 | } |
127 | |
128 | ret empty(msgs) ? "" : refreshAfterCommand(params, msgs); |
129 | } |
130 | |
131 | swappable S refreshAfterCommand(SS params, LS msgs) { |
132 | S redirectAfterSave = mapGet(params, "redirectAfterSave"); |
133 | if (nempty(redirectAfterSave)) ret hrefresh(redirectAfterSave); |
134 | |
135 | ret refreshWithMsgs(msgs, |
136 | anchor := objectIDToHighlight == null ? null : "obj" + objectIDToHighlight, |
137 | params := objectIDToHighlight == null ? null : litmap(selectObj := objectIDToHighlight)); |
138 | } |
139 | |
140 | S encodeField(S s) { |
141 | ret or(data.fieldNameToHTML(s), s); |
142 | } |
143 | |
144 | // in table |
145 | swappable S renderValue(S field, O value) { |
146 | if (value cast HTML) ret value.html; |
147 | value = deref(value); |
148 | if (value cast SecretValue) |
149 | ret hhiddenStuff(renderValue_inner(value!)); |
150 | ret renderValue_inner(value); |
151 | } |
152 | |
153 | swappable S renderValue_inner(O value) { |
154 | if (value cast Bool) |
155 | ret yesNo_short(value); |
156 | ret htmlEncode_nlToBr_withIndents(shorten(valueDisplayLength, strOrEmpty(value))); |
157 | } |
158 | |
159 | S renderTable(bool withCmds) { |
160 | ret renderTable(withCmds, data.list()); |
161 | } |
162 | |
163 | S valueToSortable(O value) { |
164 | if (value cast HTML) |
165 | ret value!; |
166 | ret strOrNull(value); |
167 | } |
168 | |
169 | // l = list of maps as it comes from data object |
170 | S renderTable(bool withCmds, L<MapSO> l) { |
171 | //print("HCRUD renderTable"); |
172 | entryCount = l(l); |
173 | if (empty(l)) ret p("No entries"); |
174 | if (!eq(data.defaultSortField(), pair(sortByField, descending))) { |
175 | if (nempty(sortByField)) { |
176 | print("Sorting " + nEntries(l) + " by " + sortByField); |
177 | l = sortByTransformedMapKey_alphaNum valueToSortable(l, sortByField); |
178 | } |
179 | if (descending) l = reversed(l); |
180 | } |
181 | |
182 | // l2 is the rendered map. use keyEncoding to get from l keys to l2 keys |
183 | new SS keyEncoding; |
184 | L<MapSO> l2 = lazyMap(l, _map -> { |
185 | O id = itemID(_map); |
186 | ret data.new Item(id) { |
187 | public MapSO calcFullMap() { |
188 | MapSO map = _map; |
189 | map = mapMinusKeys(map, joinSets(unshownFields, unlistedFields)); |
190 | MapSO map2 = postProcessTableRow(map, mapToMap( |
191 | (key, value) -> pair(mapPut_returnValue(keyEncoding, key, encodeField(key)), renderValue(key, value)), |
192 | map)); |
193 | if (singleton) |
194 | map2.remove(encodeField(data.idField())); // don't show ID in table in singleton mode |
195 | if (withCmds) |
196 | map2 = addCmdsToTableRow(map, map2); |
197 | // add anchor to row |
198 | map2.put(firstKey(map2), aname("obj" + id, firstValue(map2))); |
199 | ret map2; |
200 | } |
201 | }; |
202 | }); |
203 | |
204 | new LS out; |
205 | |
206 | if (paginate) { |
207 | paginator.processParams(params); |
208 | paginator.baseLink = addParamsToURL(baseLink, |
209 | filterKeys keepParamInPagination(params)); |
210 | paginator.max = l(l2); |
211 | out.add(divUnlessEmpty(paginator.renderNav())); |
212 | |
213 | L<MapSO> l3 = subListOrFull(l2, paginator.visibleRange()); |
214 | |
215 | //printVars_str(+objectIDToHighlight, visible := l(l3), first := first(l3), firstID := mapToID(first(l3))); |
216 | |
217 | // if highlighted object is not on page or user wants to see only this object, show only this object |
218 | if (objectIDToHighlight != null && (showOnlySelected || !any isHighlighted(l3))) { |
219 | l3 = llNonNulls(firstThat isHighlighted(l2)); |
220 | //printVars_str(+objectIDToHighlight, total := l(l2), found := l(l3)); |
221 | } |
222 | |
223 | l2 = l3; |
224 | } |
225 | |
226 | new SS replaceHeaders; |
227 | if (sortable && !singleton) |
228 | for (S key, html : keyEncoding) { |
229 | bool sortedByField = eq(sortByField, key); |
230 | bool showDescendingLink = sortedByField && !descending; |
231 | S htmlOld = html; |
232 | if (sortedByField) { |
233 | S title = showDescendingLink ? "Click here to sort descending" : "Click here to sort ascending"; |
234 | S titleSorted = "Sorted by this field (" |
235 | + (descending ? "descending" : "ascending") + ")"; |
236 | title = titleSorted + ". " + title; |
237 | html = span_title(title, unicode_downOrUpPointingTriangle(descending)) + " " + html; |
238 | } |
239 | S sortLink = appendQueryToURL(baseLink, sortParameter, showDescendingLink ? "-" + key : key); |
240 | replaceHeaders.put(htmlOld, |
241 | /*html + " " + ahref(sortLink, |
242 | unicode_smallDownOrUpPointingTriangle(showDescendingLink), +title)*/ |
243 | ahref(sortLink, html)); |
244 | } |
245 | |
246 | Map<S, O[]> paramsByColName = null; |
247 | if (cellColumnToolTips) { |
248 | paramsByColName = new Map; |
249 | for (MapSO map : l2) |
250 | for (S key : keys(map)) |
251 | if (!paramsByColName.containsKey(key)) |
252 | paramsByColName.put(key, litobjectarray(title := nullIfEmpty(htmldecode_dropTagsAndComments(key)))); |
253 | } |
254 | |
255 | out.add(hpostform( |
256 | htmlTable2_noHtmlEncode(l2, paramsPlus(tableParams(), +replaceHeaders, +paramsByColName)) |
257 | + (!withCmds || !showCheckBoxes ? "" : "\n" + divUnlessEmpty(renderBulkCmds())), |
258 | action := baseLink)); |
259 | |
260 | if (showCheckBoxes && haveJQuery && enableMultiSelect) |
261 | out.add(hCheckBoxMultiSelect_v2()); |
262 | |
263 | ret lines_rtrim(out); |
264 | } |
265 | |
266 | O mapToID(MapSO item) { |
267 | ret item == null ?: dropAllTags(strOrNull(item.get(encodeField(idField())))); |
268 | } |
269 | |
270 | // item is after encodeField |
271 | bool isHighlighted(MapSO item) { |
272 | O id = mapToID(item); |
273 | //print("isHighlighted ID: " + toStringWithClass(id) + " / " + toStringWithClass(objectIDToHighlight)); |
274 | ret eq(id, objectIDToHighlight); |
275 | } |
276 | |
277 | swappable S renderBulkCmds() { |
278 | ret "Bulk action: " + hselect("bulkAction", litorderedmap("" := "", "deleteSelected" := "Delete selected")) |
279 | + " " + hsubmit("OK", onclick := "return confirm('Are you sure?')"); |
280 | } |
281 | |
282 | MapSO addCmdsToTableRow(MapSO map, MapSO map2) { |
283 | if (showCheckBoxes) { |
284 | O id = itemID(map); |
285 | map2.put(checkBoxKey(), hcheckbox("obj_" + id, false, title := "Select this object for a bulk action", class := checkBoxClass)); |
286 | map2 = putKeysFirst(map2, checkBoxKey()); |
287 | } |
288 | map2.put(cmdsKey(), renderCmds(map)); |
289 | if (cmdsLeft) |
290 | map2 = putKeysFirst(map2, cmdsKey()); |
291 | ret map2; |
292 | } |
293 | |
294 | /*swappable*/ O[] tableParams() { |
295 | ret litparams( |
296 | tdParams := litparams(valign := "top"), |
297 | tableParams := litparams(class := tableClass)); |
298 | } |
299 | |
300 | S renderForm(MapSO map) { |
301 | temp tempSetTL(htmlencode_forParams_useV2, true); |
302 | //print("renderForm: filteredFields=" + data.filteredFields()); |
303 | map = mapMinusKeys(map, joinSets(unshownFields, uneditableFields, data.filteredFields())); |
304 | MapSO mapWithoutID = mapWithoutKey(map, data.idField()); |
305 | |
306 | LLS matrix = map(mapWithoutID, (field, value) -> { |
307 | S help = data.fieldHelp(field); |
308 | ret ll( |
309 | allowFieldRenaming ? hinputfield("rename_" + field, field, class := "field-rename", style := "border: none; text-align: right", title := "Edit this to rename field " + quote(field) + " or clear to delete field") : encodeField(field), |
310 | addHelpText(help, renderInput(field, value)) |
311 | ); |
312 | }); |
313 | massageFormMatrix(map, matrix); |
314 | |
315 | ret htableRaw_valignTop(matrix |
316 | , empty(formTableClass) ? litparams(border := 1, cellpadding := 4) : litparams(class := formTableClass)); |
317 | } |
318 | |
319 | S renderInput(S field, O value) { |
320 | S name = fieldPrefix + field; |
321 | ret renderInput(name, data.getRenderer(field, value), value); |
322 | } |
323 | |
324 | S renderInput(S name, HCRUD_Data.Renderer r, O value) { |
325 | //print("Renderer for " + name + ": " + r); |
326 | if (r != null) value = r.preprocessValue(value); |
327 | |
328 | S meta = r == null ? "" : renderMetaInfo(r.metaInfo, name); |
329 | |
330 | // switch by renderer type |
331 | |
332 | if (r cast HCRUD_Data.AceEditor) { |
333 | //ret meta + hAceEditor(strOrEmpty(value), style := "width: " + r.cols + "ch; height: " + r.rows + "em", +name); |
334 | HTMLAceEditor ace = new(strOrEmpty(value)); |
335 | ace.name = name; |
336 | ace.divParams.put(style := "width: " + r.cols + "ch; height: " + r.rows + "em"); |
337 | customizeACEEditor(ace); |
338 | ret meta + ace.headStuff() + ace.html(); |
339 | } |
340 | |
341 | if (r cast HCRUD_Data.TextArea) |
342 | ret meta + htextarea(strOrEmpty(value), +name, cols := r.cols, rows := r.rows); |
343 | |
344 | if (r cast HCRUD_Data.TextField) |
345 | ret meta + renderTextField(name, strOrEmpty(value), r.cols); |
346 | |
347 | if (r cast HCRUD_Data.ComboBox) |
348 | ret meta |
349 | + renderComboBox(name, r.valueToEntry(value), r.entries, r.editable); |
350 | |
351 | if (r cast HCRUD_Data.DynamicComboBox) |
352 | ret meta |
353 | + renderDynamicComboBox(name, r.valueToEntry(value), r.info, r.editable, r.url); |
354 | |
355 | if (r cast HCRUD_Data.CheckBox) |
356 | //ret hcheckbox(name, isTrue(value)); |
357 | ret meta + htrickcheckboxWithText(name, "", isTrue(value)); |
358 | |
359 | if (r cast HCRUD_Data.FlexibleLengthList) { |
360 | L list = cast value; |
361 | new LS rows; |
362 | |
363 | int n = l(list)+flexibleLengthListLeeway; |
364 | for i to n: { |
365 | O item = _get(list, i); |
366 | //print("Item: " + item); |
367 | rows.add(tr(td(i+1 + ".", align := "right") |
368 | + td(renderInput(name + "_" + i, r.itemRenderer, item)))); |
369 | } |
370 | ret meta + htag table(lines(rows)); |
371 | } |
372 | |
373 | if (r instanceof HCRUD_Data.NotEditable) |
374 | ret "Not editable"; |
375 | |
376 | ret renderInput_default(name, value); |
377 | } |
378 | |
379 | swappable void customizeACEEditor(HTMLAceEditor ace) {} |
380 | |
381 | S renderMetaInfo(S metaInfo, S name) { |
382 | if (empty(metaInfo)) ret ""; |
383 | ret hhidden("metaInfo_" + dropPrefix(fieldPrefix, name), metaInfo); |
384 | } |
385 | |
386 | S renderInput_default(S name, O value) { |
387 | ret renderTextField(name, strOrEmpty(value), defaultTextFieldCols); |
388 | } |
389 | |
390 | S renderTextField(S name, S value, int cols) { |
391 | if (showTextFieldsAsAutoExpandingTextAreas) { |
392 | ret htextarea(value, +name, class := "auto-expand", |
393 | style := "width: " + cols + "ch", |
394 | autofocus := eq(mapGet(params, "autofocus"), name) ? html_valueLessParam() : null, |
395 | onkeydown := jquery_submitFormOnCtrlEnter()); |
396 | } |
397 | |
398 | ret htextfield(name, value, size := cols, style := "font-family: monospace"); |
399 | } |
400 | |
401 | S renderNewForm() { |
402 | ret renderNewForm(data.emptyObject()); |
403 | } |
404 | |
405 | // pre-populate fields from request parameters |
406 | S renderNewFormWithParams(SS params) { |
407 | SS filteredMap = subMapStartingWith_dropPrefix(params, fieldPrefix); |
408 | MapSO map = joinMaps(data.emptyObject(), (Map) filteredMap); |
409 | data.rawFormValues = params; |
410 | |
411 | // pre-populate list fields |
412 | for (S key, value : filteredMap) { |
413 | LS l = splitAt(key, "_"); |
414 | if (l(l) == 2) { |
415 | S field = first(l); |
416 | int idx = parseInt(second(l)); |
417 | LS list = cast map.get(field); |
418 | if (!list instanceof ArrayList) map.put(field, list = new L); |
419 | listPut(list, idx, value); |
420 | } |
421 | } |
422 | |
423 | ret renderNewForm(map); |
424 | } |
425 | |
426 | S renderNewForm(MapSO map) { |
427 | //printStruct("renderNewForm", map); |
428 | S buttons = p(hsubmit("Create")); |
429 | ret hpostform( |
430 | hhidden("action", "create") |
431 | + formExtraHiddens() |
432 | + stringIf(buttonsOnTop, buttons) |
433 | + renderForm(map) |
434 | + stringIf(buttonsOnBottom, buttons), |
435 | paramsPlus(formParameters(), action := baseLink)); |
436 | } |
437 | |
438 | swappable O[] formParameters() { ret litparams(id := formID); } |
439 | |
440 | S idField() { ret data.idField(); } |
441 | |
442 | S renderEditForm(S id) { |
443 | if (!actuallyAllowEdit()) |
444 | ret "Can't edit objects in this table"; |
445 | |
446 | if (!data.objectCanBeEdited(id)) |
447 | ret htmlEncode2("Object " + id + " can't be edited"); |
448 | |
449 | MapSO map = data.getObjectForEdit(id); |
450 | if (map == null) ret htmlEncode2("Entry " + id + " not found"); |
451 | |
452 | S onlyFields = mapGet(params, "onlyFields"); |
453 | if (nempty(onlyFields)) |
454 | map = onlyKeys(map, itemPlus(idField(), tok_identifiersOnly(onlyFields))); |
455 | |
456 | S buttons = p_vbar( |
457 | hsubmit("Save changes"), |
458 | !showQuickSaveButton ? "" : |
459 | hbuttonOnClick_noSubmit("Save & keep editing", [[ |
460 | $.ajax({ |
461 | type: 'POST', |
462 | url: $('#crudForm').attr('action'), |
463 | data: $('#crudForm').serialize(), |
464 | success: function(response) { successNotification("Saved"); }, |
465 | }).error(function() { errorNotification("Couldn't save"); }); |
466 | ]]), |
467 | deleteObjectHTML(id)); |
468 | ret hpostform( |
469 | hhidden("action", "update") + |
470 | formExtraHiddens() + |
471 | hhidden(+id) + |
472 | p("Object ID: " + htmlEncode2(id)) + |
473 | + stringIf(buttonsOnTop, buttons) |
474 | + renderForm(map) |
475 | + stringIf(buttonsOnBottom, buttons), |
476 | paramsPlus(formParameters(), action := baseLink + "#obj" + id)); |
477 | } |
478 | |
479 | S renderPage(SS params) { |
480 | //print("HCRUD renderPage"); |
481 | setParams(params); |
482 | |
483 | try answer handleComboSearch(params); |
484 | |
485 | if (eqGet(params, "cmd", "new")) { |
486 | if (!actuallyAllowCreate()) |
487 | ret "Can't create objects in ths table"; |
488 | ret frame(customTitleOr("New " + itemName()), renderNewFormWithParams(params)); |
489 | } |
490 | |
491 | if (nempty(params.get("edit"))) |
492 | ret frame("Edit " + itemName(), renderEditForm(params.get("edit"))); |
493 | |
494 | if (nempty(params.get("duplicate"))) |
495 | ret frame("New " + itemName(), renderNewForm(data.getObjectForDuplication(params.get("duplicate")))); |
496 | |
497 | S rendered = render(mutationRights, params); |
498 | |
499 | // handle commands, render list |
500 | S title = null; |
501 | if (singleton) |
502 | title = ahref(baseLink, firstToUpper(data.itemName())); |
503 | else { |
504 | if (objectIDToHighlight != null) |
505 | title = data.titleForObjectID(objectIDToHighlight); |
506 | if (empty(title)) |
507 | title = (showEntryCountInTitle ? n2(entryCount) + " " : "") |
508 | + ahref(baseLink, firstToUpper(data.itemNamePlural())); |
509 | } |
510 | |
511 | ret frame(customTitleOr(title), rendered); |
512 | } |
513 | |
514 | HCRUD makeFrame(MakeFrame makeFrame) { super.makeFrame(makeFrame); this; } |
515 | |
516 | S cmdsKey() { ret "<!-- cmds -->"; } |
517 | S checkBoxKey() { ret "<!-- checkbox -->"; } |
518 | |
519 | S itemName() { ret data.itemName(); } |
520 | |
521 | swappable MapSO postProcessTableRow(MapSO data, MapSO rendered) { ret rendered; } |
522 | |
523 | O itemID(MapSO item) { |
524 | O id = mapGet(item, data.idField()); |
525 | // getVarOpt decodes HTML record |
526 | if (cleanItemIDs) id = htmlDecode_dropTags(strOrNull(getVarOpt(id))); |
527 | ret id; |
528 | } |
529 | |
530 | long itemIDAsLong(MapSO item) { |
531 | ret parseLong(itemID(item)); |
532 | } |
533 | |
534 | // return list of HTMLs for commands in pop down button |
535 | swappable LS additionalCmds(MapSO item) { null; } |
536 | |
537 | swappable S renderCmds(MapSO item) { |
538 | O id = itemID(item); |
539 | LS additionalCmds = additionalCmds(item); |
540 | ret joinNemptiesWithVBar( |
541 | !actuallyAllowEdit() || !data.objectCanBeEdited(id) ? null : ahref(editLink(id), "EDIT"), |
542 | deleteObjectHTML(id), |
543 | !actuallyAllowCreate() ? null : targetBlankIf(duplicateInNewTab, duplicateLink(id), "dup", title := "duplicate"), |
544 | empty(additionalCmds) ? null : hPopDownButton(additionalCmds) |
545 | ); |
546 | } |
547 | |
548 | bool actuallyAllowCreate() { |
549 | ret !singleton && allowCreateOrDelete && allowCreate; |
550 | } |
551 | |
552 | bool actuallyAllowEdit() { |
553 | ret allowCreateOrDelete && allowEdit; |
554 | } |
555 | |
556 | bool actuallyAllowDelete() { |
557 | ret !singleton && allowCreateOrDelete; |
558 | } |
559 | |
560 | // e.g. for adding rows to edit/create form |
561 | swappable void massageFormMatrix(MapSO map, LLS matrix) { |
562 | } |
563 | |
564 | S renderComboBox(S name, S value, LS entries, bool editable) { |
565 | if (haveSelectizeJS) { |
566 | // coolest option - use selectize.js |
567 | S id = aGlobalID(); |
568 | ret hselect_list(entries, value, +name, +id) |
569 | + hjs("$('#" + id + "').selectize" + [[ |
570 | ({ |
571 | searchField: 'text', |
572 | openOnFocus: true, |
573 | dropdownParent: 'body', |
574 | create: ]] + jsBool(editable) + [[ |
575 | /*allowEmptyOption: true*/ |
576 | ]] |
577 | + unnull(moreSelectizeOptions2(name)) + [[ |
578 | }); |
579 | ]]) |
580 | + selectizeLayoutFix(); |
581 | } |
582 | |
583 | if (haveJQuery) { |
584 | // make searchable list if JQuery is available |
585 | // (functional, but looks quite poor on Firefox) |
586 | // TODO: this seems to always be editable |
587 | S id = aGlobalID(); |
588 | ret tag datalist(mapToLines hoption(entries), +id) |
589 | + tag input("", +name, list := id); |
590 | } |
591 | |
592 | // standard non-searchable list |
593 | if (editable) |
594 | ret hinputfield(name, value); // no editable combo box possible without selectize.js |
595 | ret hselect_list(entries, value, +name); |
596 | } |
597 | |
598 | S renderDynamicComboBox(S name, S value, S info, bool editable, S url default null) { |
599 | assertTrue(+haveSelectizeJS); |
600 | S id = aGlobalID(); |
601 | S ajaxURL = or2(url, baseLink); |
602 | ret hselect_list(llNempties(value), value, +name, +id) |
603 | + hjs("$('#" + id + "').selectize" + [[ |
604 | ({ |
605 | searchField: 'text', |
606 | valueField: 'text', |
607 | labelField: 'text', |
608 | openOnFocus: true, |
609 | dropdownParent: 'body', |
610 | create: ]] + jsBool(editable) + [[, |
611 | load: function(query, callback) { |
612 | if (!query.length) return callback(); |
613 | var data = { |
614 | comboSearchInfo: ]] + jsQuote(info) + [[, |
615 | comboSearchQuery: query |
616 | }; |
617 | console.log("Loading " + ]] + jsQuote(baseLink) + [[ + " with " + JSON.stringify(data)); |
618 | $.ajax({ |
619 | url: ]] + jsQuote(ajaxURL) + [[, |
620 | type: 'GET', |
621 | dataType: 'json', |
622 | data: data, |
623 | error: function() { |
624 | console.log("Got error"); |
625 | callback(); |
626 | }, |
627 | success: function(res) { |
628 | //console.log("Got data: " + res); |
629 | var converted = res.map(x => { return {text: x}; }); |
630 | //console.log("Converted: " + converted); |
631 | callback(converted); |
632 | } |
633 | }); |
634 | } |
635 | /*allowEmptyOption: true*/ |
636 | ]] + moreSelectizeOptions2(name) + [[ |
637 | }); |
638 | ]]) |
639 | + selectizeLayoutFix(); |
640 | } |
641 | |
642 | void processSortParameter(SS params) { |
643 | S sort = mapGet(params, sortParameter); |
644 | //sortByField = null; |
645 | if (nempty(sort)) |
646 | if (startsWith(sort, "-")) { |
647 | descending = true; |
648 | sortByField = substring(sort, 1); |
649 | } else { |
650 | descending = false; |
651 | sortByField = sort; |
652 | } |
653 | } |
654 | |
655 | S deleteObjectHTML(O id) { |
656 | ret !actuallyAllowDelete() ? null : |
657 | !data.objectCanBeDeleted(id) ? |
658 | // TODO: custom msg! |
659 | span_title("Object can't be deleted, either there are references to it or you are not authorized", htmlEncode2(unicode_DEL())) |
660 | : ahrefWithConfirm( |
661 | "Really delete item " + id + "?", deleteLink(id), htmlEncode2(unicode_DEL()), title := "delete"); |
662 | } |
663 | |
664 | // helpText is also HTML |
665 | S addHelpText(S helpText, S html) { |
666 | ret empty(helpText) ? html : html + p(small(helpText), style := "text-align: right"); |
667 | } |
668 | |
669 | S moreSelectizeOptions2(S name) { |
670 | ret unnull(moreSelectizeOptions(name)) |
671 | + (!haveSelectizeClickable ? "" : [[ |
672 | , plugins: ['clickable'] |
673 | , render: { |
674 | option: function(item) { |
675 | var id = item.text.match(/\d+/)[0]; |
676 | return '<div><span>'+item.text+'</span>' |
677 | + '<div style="float: right">' |
678 | + '<a title="Go to object" class="clickable" href="/' + id + '" target="_blank">↗</a>' |
679 | + '</div></div>'; |
680 | } |
681 | } |
682 | ]]); |
683 | } |
684 | |
685 | swappable S moreSelectizeOptions(S name) { ret ""; } |
686 | |
687 | // deliver content for dynamic search in combo boxes |
688 | S handleComboSearch(SS params) { |
689 | S query = params.get("comboSearchQuery"); |
690 | if (nempty(query)) { |
691 | S info = params.get("comboSearchInfo"); |
692 | ret jsonEncode_shallowLineBreaks(data.comboBoxSearch(info, query)); |
693 | } |
694 | null; |
695 | } |
696 | |
697 | // for update or create |
698 | swappable SS preprocessUpdateParams(SS params) { ret params; } |
699 | |
700 | void disableAllMutationRights() { |
701 | mutationRights = allowCreateOrDelete = allowCreate |
702 | = allowEdit = false; |
703 | } |
704 | |
705 | swappable bool keepParamInPagination(S name) { |
706 | ret eq(name, "search"); |
707 | } |
708 | |
709 | S customTitleOr(S title) { |
710 | ret or2(customTitle, or2(mapGet(params, "title"), title)); |
711 | } |
712 | |
713 | swappable S selectizeLayoutFix() { |
714 | // dirty CSS quick-fix (TODO: send only once) |
715 | ret hcss(".selectize-input, .selectize-control { min-width: 300px }"); |
716 | } |
717 | |
718 | S formExtraHiddens() { |
719 | S redirectAfterSave = mapGet(params, "redirectAfterSave"); |
720 | ret empty(redirectAfterSave) ? "" : hhidden(+redirectAfterSave); |
721 | } |
722 | |
723 | void processRenames(SS params) { |
724 | if (!allowFieldRenaming) ret; |
725 | for (S key1 : keysList(params)) { |
726 | S field = dropPrefixOrNull("rename_", key1); |
727 | if (field == null) continue; |
728 | |
729 | S newName = trim(params.get(key1)); |
730 | if (newName == null || eq(field, newName)) continue; |
731 | print("Renaming " + field + " to " + or2(newName, "<deleted>")); |
732 | params.remove("rename_" + field); |
733 | |
734 | // Try to catch f_myField, metaInfo_myField, f_myField_1... |
735 | S re = "^([^_]+_)" + regexpQuote(field) + "(_[^_]+)?$"; |
736 | for (S key : keysList(params)) { |
737 | LS groups = regexpGroups(re, key); |
738 | if (groups != null) { |
739 | S newKey = empty(newName) ? null : first(groups) + newName + unnull(second(groups)); |
740 | mapPut(params, newKey, params.get(key)); |
741 | params.put(key, ""); // this should delete stuff (if empty strings are converted to null) |
742 | print("Renaming key: " + key + " => " + newKey); |
743 | } |
744 | } |
745 | } |
746 | } |
747 | } |
download show line numbers debug dex old transpilations
Travelled to 8 computer(s): bhatertpkbcr, mqqgnosmbjvj, onxytkatvevr, pyentgdyhuwx, pzhvpgtvlbxg, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1026001 |
Snippet name: | HCRUD - CRUD in HTML with pluggable data handler |
Eternal ID of this version: | #1026001/318 |
Text MD5: | 5c56764e06575b9f3977bee2c9449b9c |
Author: | stefan |
Category: | javax / html |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2021-10-06 02:58:04 |
Source code size: | 26208 bytes / 747 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 1353 / 2903 |
Version history: | 317 change(s) |
Referenced in: | [show references] |