@yuneta/gobj-ui 7.23.193 → 7.23.195
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/dist/gobj-ui.cjs.js +99 -20
- package/dist/gobj-ui.es.js +99 -20
- package/package.json +1 -1
- package/src/c_yui_form.js +10 -16
- package/src/c_yui_treedb_topic_with_form.js +17 -15
- package/src/c_yui_treedb_topics.js +6 -0
- package/src/form_time_value.js +46 -0
- package/src/form_time_value.test.js +23 -0
- package/src/table_cell_text.js +53 -0
- package/src/table_cell_text.test.js +39 -0
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -7310,6 +7310,39 @@ function json_diff_rows(doc_a, doc_b) {
|
|
|
7310
7310
|
return rows;
|
|
7311
7311
|
}
|
|
7312
7312
|
//#endregion
|
|
7313
|
+
//#region src/form_time_value.js
|
|
7314
|
+
/****************************************************************************
|
|
7315
|
+
* form_time_value.js
|
|
7316
|
+
*
|
|
7317
|
+
* A treedb `time` / `now` column is an epoch in SECONDS, and a form
|
|
7318
|
+
* shows it in a `datetime-local` input. These two convert between
|
|
7319
|
+
* them, and they must be each other's inverse: a writable time
|
|
7320
|
+
* column travels back on EVERY save of the record.
|
|
7321
|
+
*
|
|
7322
|
+
* Copyright (c) 2026, ArtGins.
|
|
7323
|
+
* All Rights Reserved.
|
|
7324
|
+
****************************************************************************/
|
|
7325
|
+
/************************************************************
|
|
7326
|
+
* A Date -> the value of a `datetime-local` input, in local time,
|
|
7327
|
+
* WITH seconds: the input is built with `step: 1`, which is what
|
|
7328
|
+
* lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
|
|
7329
|
+
* every save of any field (M26 of the 2026-09-21 review).
|
|
7330
|
+
************************************************************/
|
|
7331
|
+
function date_to_datetime_local(value) {
|
|
7332
|
+
if (!(value instanceof Date) || isNaN(value.getTime())) return "";
|
|
7333
|
+
let pad = (n) => String(n).padStart(2, "0");
|
|
7334
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
|
|
7335
|
+
}
|
|
7336
|
+
/************************************************************
|
|
7337
|
+
* The value of a `datetime-local` input -> epoch in seconds
|
|
7338
|
+
************************************************************/
|
|
7339
|
+
function datetime_local_to_epoch(value) {
|
|
7340
|
+
if (!value) return null;
|
|
7341
|
+
const date = new Date(value);
|
|
7342
|
+
if (isNaN(date.getTime())) return null;
|
|
7343
|
+
return Math.floor(date.getTime() / 1e3);
|
|
7344
|
+
}
|
|
7345
|
+
//#endregion
|
|
7313
7346
|
//#region src/c_yui_form.js
|
|
7314
7347
|
/***********************************************************************
|
|
7315
7348
|
* c_yui_form.js
|
|
@@ -8322,6 +8355,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
|
|
|
8322
8355
|
type: inputType,
|
|
8323
8356
|
placeholder
|
|
8324
8357
|
};
|
|
8358
|
+
if (inputType === "datetime-local") attrs.step = 1;
|
|
8325
8359
|
Object.assign(attrs, extras);
|
|
8326
8360
|
if (readonly) attrs.readonly = "";
|
|
8327
8361
|
else if (required) attrs.required = "";
|
|
@@ -9022,8 +9056,7 @@ function get_form_values(gobj, $form) {
|
|
|
9022
9056
|
case "input":
|
|
9023
9057
|
switch (input_type) {
|
|
9024
9058
|
case "datetime-local":
|
|
9025
|
-
|
|
9026
|
-
value = Math.floor(date.getTime() / 1e3);
|
|
9059
|
+
value = datetime_local_to_epoch($input.value);
|
|
9027
9060
|
break;
|
|
9028
9061
|
default: value = getInputValue($input);
|
|
9029
9062
|
}
|
|
@@ -9186,10 +9219,7 @@ function set_form_values(gobj, template, $form, record) {
|
|
|
9186
9219
|
case "input":
|
|
9187
9220
|
switch (input_type) {
|
|
9188
9221
|
case "datetime-local":
|
|
9189
|
-
|
|
9190
|
-
let pad = (n) => String(n).padStart(2, "0");
|
|
9191
|
-
$input.value = `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
9192
|
-
} else $input.value = "";
|
|
9222
|
+
$input.value = date_to_datetime_local(value);
|
|
9193
9223
|
break;
|
|
9194
9224
|
case "color":
|
|
9195
9225
|
$input.value = convertColor(value);
|
|
@@ -25240,6 +25270,7 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
25240
25270
|
if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
|
|
25241
25271
|
list_dict: true,
|
|
25242
25272
|
create: true,
|
|
25273
|
+
create_only: true,
|
|
25243
25274
|
autolink: true
|
|
25244
25275
|
}, form_write) < 0) {
|
|
25245
25276
|
answer_form_write(gobj, src, form_write, false);
|
|
@@ -26480,6 +26511,58 @@ function delete_impact(desc, records) {
|
|
|
26480
26511
|
return out;
|
|
26481
26512
|
}
|
|
26482
26513
|
//#endregion
|
|
26514
|
+
//#region src/table_cell_text.js
|
|
26515
|
+
/****************************************************************************
|
|
26516
|
+
* table_cell_text.js
|
|
26517
|
+
*
|
|
26518
|
+
* What a treedb table cell hands Tabulator.
|
|
26519
|
+
*
|
|
26520
|
+
* Tabulator assigns a formatter's STRING result with `innerHTML`
|
|
26521
|
+
* (only its own `plaintext` formatter escapes), so a record's text
|
|
26522
|
+
* returned as a string is parsed as markup: `a<b and c>d` showed
|
|
26523
|
+
* as `ad`, and a field holding `<img src=x onerror=...>` ran it in
|
|
26524
|
+
* the operator's browser (M31 of the 2026-09-21 review). Record
|
|
26525
|
+
* data goes in as a TEXT NODE, and a cell built around it as DOM,
|
|
26526
|
+
* never as an HTML string with the data interpolated.
|
|
26527
|
+
*
|
|
26528
|
+
* Pure but for the `document` it is handed, so it is tested
|
|
26529
|
+
* without one.
|
|
26530
|
+
*
|
|
26531
|
+
* Copyright (c) 2026, ArtGins.
|
|
26532
|
+
* All Rights Reserved.
|
|
26533
|
+
****************************************************************************/
|
|
26534
|
+
/************************************************************
|
|
26535
|
+
* A value shown as text, whatever characters it holds
|
|
26536
|
+
************************************************************/
|
|
26537
|
+
function cell_text(value, doc = globalThis.document) {
|
|
26538
|
+
if (value === null || value === void 0) return "";
|
|
26539
|
+
return doc.createTextNode(String(value));
|
|
26540
|
+
}
|
|
26541
|
+
/************************************************************
|
|
26542
|
+
* The hook cell, as a createElement2 spec: the row id is an
|
|
26543
|
+
* attribute VALUE, never text spliced into markup (an id holding
|
|
26544
|
+
* a `"` cut the attribute and lost the rest of the id)
|
|
26545
|
+
************************************************************/
|
|
26546
|
+
function hook_cell_spec(row_id, col_id, items, title, title_key) {
|
|
26547
|
+
return [
|
|
26548
|
+
"a",
|
|
26549
|
+
{
|
|
26550
|
+
class: "HOOK_CELL hook_cell",
|
|
26551
|
+
title,
|
|
26552
|
+
"data-i18n-title": title_key,
|
|
26553
|
+
"aria-label": title,
|
|
26554
|
+
"data-i18n-aria-label": title_key,
|
|
26555
|
+
"data-row_id": String(row_id),
|
|
26556
|
+
"data-col_id": String(col_id)
|
|
26557
|
+
},
|
|
26558
|
+
[["span", { class: "icon yi-eye" }], [
|
|
26559
|
+
"u",
|
|
26560
|
+
{ class: "HOOK_CELL_COUNT" },
|
|
26561
|
+
`[ ${items} ]`
|
|
26562
|
+
]]
|
|
26563
|
+
];
|
|
26564
|
+
}
|
|
26565
|
+
//#endregion
|
|
26483
26566
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
26484
26567
|
/***********************************************************************
|
|
26485
26568
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -27906,18 +27989,8 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27906
27989
|
break;
|
|
27907
27990
|
case "hook":
|
|
27908
27991
|
let items = (0, _yuneta_gobj_js.treedb_hook_data_size)(value);
|
|
27909
|
-
if (items > 0)
|
|
27910
|
-
|
|
27911
|
-
value = [
|
|
27912
|
-
"<a class=\"hook_cell\" ",
|
|
27913
|
-
`title="${hook_title}" aria-label="${hook_title}" `,
|
|
27914
|
-
`data-row_id="${row.id}" `,
|
|
27915
|
-
`data-col_id="${col.id}" > `,
|
|
27916
|
-
"<span style=\"\" class=\"icon yi-eye\"></span>",
|
|
27917
|
-
`<span>[ <u>${items}</u> ]</span>`,
|
|
27918
|
-
"</a>"
|
|
27919
|
-
].join("");
|
|
27920
|
-
} else value = "";
|
|
27992
|
+
if (items > 0) value = (0, _yuneta_gobj_js.createElement2)(hook_cell_spec(row.id, col.id, items, (0, i18next.t)("show linked records"), "show linked records"));
|
|
27993
|
+
else value = "";
|
|
27921
27994
|
break;
|
|
27922
27995
|
case "file":
|
|
27923
27996
|
case "fkey":
|
|
@@ -27943,7 +28016,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27943
28016
|
value = /* @__PURE__ */ new Date(value * 1e3);
|
|
27944
28017
|
const rawLocale = navigator.language;
|
|
27945
28018
|
const userLocale = typeof rawLocale === "string" && rawLocale && rawLocale !== "undefined" ? rawLocale : void 0;
|
|
27946
|
-
|
|
28019
|
+
const formatter = new Intl.DateTimeFormat(userLocale, {
|
|
27947
28020
|
day: "numeric",
|
|
27948
28021
|
month: "short",
|
|
27949
28022
|
year: "numeric",
|
|
@@ -27951,7 +28024,12 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27951
28024
|
minute: "2-digit",
|
|
27952
28025
|
second: "2-digit",
|
|
27953
28026
|
hour12: true
|
|
27954
|
-
})
|
|
28027
|
+
});
|
|
28028
|
+
value = (0, _yuneta_gobj_js.createElement2)([
|
|
28029
|
+
"span",
|
|
28030
|
+
{ class: "is-size-7" },
|
|
28031
|
+
formatter.format(value)
|
|
28032
|
+
]);
|
|
27955
28033
|
}
|
|
27956
28034
|
break;
|
|
27957
28035
|
case "color": break;
|
|
@@ -27963,6 +28041,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27963
28041
|
(0, _yuneta_gobj_js.log_error)(`transform__treedb_value_2_table_value() unexpected object value for field '${field}' (type='${field_desc.type}', real_type='${field_desc.real_type}'): ${JSON.stringify(value)}, topic ${priv.topic_name}`);
|
|
27964
28042
|
value = JSON.stringify(value);
|
|
27965
28043
|
}
|
|
28044
|
+
if (typeof value === "string" && value !== "") value = cell_text(value);
|
|
27966
28045
|
return value;
|
|
27967
28046
|
}
|
|
27968
28047
|
/************************************************************
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -7305,6 +7305,39 @@ function json_diff_rows(doc_a, doc_b) {
|
|
|
7305
7305
|
return rows;
|
|
7306
7306
|
}
|
|
7307
7307
|
//#endregion
|
|
7308
|
+
//#region src/form_time_value.js
|
|
7309
|
+
/****************************************************************************
|
|
7310
|
+
* form_time_value.js
|
|
7311
|
+
*
|
|
7312
|
+
* A treedb `time` / `now` column is an epoch in SECONDS, and a form
|
|
7313
|
+
* shows it in a `datetime-local` input. These two convert between
|
|
7314
|
+
* them, and they must be each other's inverse: a writable time
|
|
7315
|
+
* column travels back on EVERY save of the record.
|
|
7316
|
+
*
|
|
7317
|
+
* Copyright (c) 2026, ArtGins.
|
|
7318
|
+
* All Rights Reserved.
|
|
7319
|
+
****************************************************************************/
|
|
7320
|
+
/************************************************************
|
|
7321
|
+
* A Date -> the value of a `datetime-local` input, in local time,
|
|
7322
|
+
* WITH seconds: the input is built with `step: 1`, which is what
|
|
7323
|
+
* lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
|
|
7324
|
+
* every save of any field (M26 of the 2026-09-21 review).
|
|
7325
|
+
************************************************************/
|
|
7326
|
+
function date_to_datetime_local(value) {
|
|
7327
|
+
if (!(value instanceof Date) || isNaN(value.getTime())) return "";
|
|
7328
|
+
let pad = (n) => String(n).padStart(2, "0");
|
|
7329
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`;
|
|
7330
|
+
}
|
|
7331
|
+
/************************************************************
|
|
7332
|
+
* The value of a `datetime-local` input -> epoch in seconds
|
|
7333
|
+
************************************************************/
|
|
7334
|
+
function datetime_local_to_epoch(value) {
|
|
7335
|
+
if (!value) return null;
|
|
7336
|
+
const date = new Date(value);
|
|
7337
|
+
if (isNaN(date.getTime())) return null;
|
|
7338
|
+
return Math.floor(date.getTime() / 1e3);
|
|
7339
|
+
}
|
|
7340
|
+
//#endregion
|
|
7308
7341
|
//#region src/c_yui_form.js
|
|
7309
7342
|
/***********************************************************************
|
|
7310
7343
|
* c_yui_form.js
|
|
@@ -8317,6 +8350,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
|
|
|
8317
8350
|
type: inputType,
|
|
8318
8351
|
placeholder
|
|
8319
8352
|
};
|
|
8353
|
+
if (inputType === "datetime-local") attrs.step = 1;
|
|
8320
8354
|
Object.assign(attrs, extras);
|
|
8321
8355
|
if (readonly) attrs.readonly = "";
|
|
8322
8356
|
else if (required) attrs.required = "";
|
|
@@ -9017,8 +9051,7 @@ function get_form_values(gobj, $form) {
|
|
|
9017
9051
|
case "input":
|
|
9018
9052
|
switch (input_type) {
|
|
9019
9053
|
case "datetime-local":
|
|
9020
|
-
|
|
9021
|
-
value = Math.floor(date.getTime() / 1e3);
|
|
9054
|
+
value = datetime_local_to_epoch($input.value);
|
|
9022
9055
|
break;
|
|
9023
9056
|
default: value = getInputValue($input);
|
|
9024
9057
|
}
|
|
@@ -9181,10 +9214,7 @@ function set_form_values(gobj, template, $form, record) {
|
|
|
9181
9214
|
case "input":
|
|
9182
9215
|
switch (input_type) {
|
|
9183
9216
|
case "datetime-local":
|
|
9184
|
-
|
|
9185
|
-
let pad = (n) => String(n).padStart(2, "0");
|
|
9186
|
-
$input.value = `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
9187
|
-
} else $input.value = "";
|
|
9217
|
+
$input.value = date_to_datetime_local(value);
|
|
9188
9218
|
break;
|
|
9189
9219
|
case "color":
|
|
9190
9220
|
$input.value = convertColor(value);
|
|
@@ -25235,6 +25265,7 @@ function ac_create_record(gobj, event, kw, src) {
|
|
|
25235
25265
|
if (treedb_update_node$1(gobj, treedb_name, topic_name, record, {
|
|
25236
25266
|
list_dict: true,
|
|
25237
25267
|
create: true,
|
|
25268
|
+
create_only: true,
|
|
25238
25269
|
autolink: true
|
|
25239
25270
|
}, form_write) < 0) {
|
|
25240
25271
|
answer_form_write(gobj, src, form_write, false);
|
|
@@ -26475,6 +26506,58 @@ function delete_impact(desc, records) {
|
|
|
26475
26506
|
return out;
|
|
26476
26507
|
}
|
|
26477
26508
|
//#endregion
|
|
26509
|
+
//#region src/table_cell_text.js
|
|
26510
|
+
/****************************************************************************
|
|
26511
|
+
* table_cell_text.js
|
|
26512
|
+
*
|
|
26513
|
+
* What a treedb table cell hands Tabulator.
|
|
26514
|
+
*
|
|
26515
|
+
* Tabulator assigns a formatter's STRING result with `innerHTML`
|
|
26516
|
+
* (only its own `plaintext` formatter escapes), so a record's text
|
|
26517
|
+
* returned as a string is parsed as markup: `a<b and c>d` showed
|
|
26518
|
+
* as `ad`, and a field holding `<img src=x onerror=...>` ran it in
|
|
26519
|
+
* the operator's browser (M31 of the 2026-09-21 review). Record
|
|
26520
|
+
* data goes in as a TEXT NODE, and a cell built around it as DOM,
|
|
26521
|
+
* never as an HTML string with the data interpolated.
|
|
26522
|
+
*
|
|
26523
|
+
* Pure but for the `document` it is handed, so it is tested
|
|
26524
|
+
* without one.
|
|
26525
|
+
*
|
|
26526
|
+
* Copyright (c) 2026, ArtGins.
|
|
26527
|
+
* All Rights Reserved.
|
|
26528
|
+
****************************************************************************/
|
|
26529
|
+
/************************************************************
|
|
26530
|
+
* A value shown as text, whatever characters it holds
|
|
26531
|
+
************************************************************/
|
|
26532
|
+
function cell_text(value, doc = globalThis.document) {
|
|
26533
|
+
if (value === null || value === void 0) return "";
|
|
26534
|
+
return doc.createTextNode(String(value));
|
|
26535
|
+
}
|
|
26536
|
+
/************************************************************
|
|
26537
|
+
* The hook cell, as a createElement2 spec: the row id is an
|
|
26538
|
+
* attribute VALUE, never text spliced into markup (an id holding
|
|
26539
|
+
* a `"` cut the attribute and lost the rest of the id)
|
|
26540
|
+
************************************************************/
|
|
26541
|
+
function hook_cell_spec(row_id, col_id, items, title, title_key) {
|
|
26542
|
+
return [
|
|
26543
|
+
"a",
|
|
26544
|
+
{
|
|
26545
|
+
class: "HOOK_CELL hook_cell",
|
|
26546
|
+
title,
|
|
26547
|
+
"data-i18n-title": title_key,
|
|
26548
|
+
"aria-label": title,
|
|
26549
|
+
"data-i18n-aria-label": title_key,
|
|
26550
|
+
"data-row_id": String(row_id),
|
|
26551
|
+
"data-col_id": String(col_id)
|
|
26552
|
+
},
|
|
26553
|
+
[["span", { class: "icon yi-eye" }], [
|
|
26554
|
+
"u",
|
|
26555
|
+
{ class: "HOOK_CELL_COUNT" },
|
|
26556
|
+
`[ ${items} ]`
|
|
26557
|
+
]]
|
|
26558
|
+
];
|
|
26559
|
+
}
|
|
26560
|
+
//#endregion
|
|
26478
26561
|
//#region src/c_yui_treedb_topic_with_form.js
|
|
26479
26562
|
/***********************************************************************
|
|
26480
26563
|
* c_yui_treedb_topic_with_form.js
|
|
@@ -27901,18 +27984,8 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27901
27984
|
break;
|
|
27902
27985
|
case "hook":
|
|
27903
27986
|
let items = treedb_hook_data_size(value);
|
|
27904
|
-
if (items > 0)
|
|
27905
|
-
|
|
27906
|
-
value = [
|
|
27907
|
-
"<a class=\"hook_cell\" ",
|
|
27908
|
-
`title="${hook_title}" aria-label="${hook_title}" `,
|
|
27909
|
-
`data-row_id="${row.id}" `,
|
|
27910
|
-
`data-col_id="${col.id}" > `,
|
|
27911
|
-
"<span style=\"\" class=\"icon yi-eye\"></span>",
|
|
27912
|
-
`<span>[ <u>${items}</u> ]</span>`,
|
|
27913
|
-
"</a>"
|
|
27914
|
-
].join("");
|
|
27915
|
-
} else value = "";
|
|
27987
|
+
if (items > 0) value = createElement2(hook_cell_spec(row.id, col.id, items, t("show linked records"), "show linked records"));
|
|
27988
|
+
else value = "";
|
|
27916
27989
|
break;
|
|
27917
27990
|
case "file":
|
|
27918
27991
|
case "fkey":
|
|
@@ -27938,7 +28011,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27938
28011
|
value = /* @__PURE__ */ new Date(value * 1e3);
|
|
27939
28012
|
const rawLocale = navigator.language;
|
|
27940
28013
|
const userLocale = typeof rawLocale === "string" && rawLocale && rawLocale !== "undefined" ? rawLocale : void 0;
|
|
27941
|
-
|
|
28014
|
+
const formatter = new Intl.DateTimeFormat(userLocale, {
|
|
27942
28015
|
day: "numeric",
|
|
27943
28016
|
month: "short",
|
|
27944
28017
|
year: "numeric",
|
|
@@ -27946,7 +28019,12 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27946
28019
|
minute: "2-digit",
|
|
27947
28020
|
second: "2-digit",
|
|
27948
28021
|
hour12: true
|
|
27949
|
-
})
|
|
28022
|
+
});
|
|
28023
|
+
value = createElement2([
|
|
28024
|
+
"span",
|
|
28025
|
+
{ class: "is-size-7" },
|
|
28026
|
+
formatter.format(value)
|
|
28027
|
+
]);
|
|
27950
28028
|
}
|
|
27951
28029
|
break;
|
|
27952
28030
|
case "color": break;
|
|
@@ -27958,6 +28036,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field) {
|
|
|
27958
28036
|
log_error(`transform__treedb_value_2_table_value() unexpected object value for field '${field}' (type='${field_desc.type}', real_type='${field_desc.real_type}'): ${JSON.stringify(value)}, topic ${priv.topic_name}`);
|
|
27959
28037
|
value = JSON.stringify(value);
|
|
27960
28038
|
}
|
|
28039
|
+
if (typeof value === "string" && value !== "") value = cell_text(value);
|
|
27961
28040
|
return value;
|
|
27962
28041
|
}
|
|
27963
28042
|
/************************************************************
|
package/package.json
CHANGED
package/src/c_yui_form.js
CHANGED
|
@@ -83,6 +83,7 @@ import "tabulator-tables/dist/css/tabulator_bulma.css";
|
|
|
83
83
|
|
|
84
84
|
import "./tabulator.css";
|
|
85
85
|
import {is_time_field} from "./json_view_helpers.js";
|
|
86
|
+
import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value.js";
|
|
86
87
|
import { TabulatorFull as Tabulator } from "tabulator-tables"; // Import Full Tabulator JS
|
|
87
88
|
// import { Tabulator } from "tabulator-tables"; // Import light Tabulator JS
|
|
88
89
|
|
|
@@ -1247,6 +1248,12 @@ function create_form_field(
|
|
|
1247
1248
|
type: inputType,
|
|
1248
1249
|
placeholder: placeholder
|
|
1249
1250
|
};
|
|
1251
|
+
if(inputType === 'datetime-local') {
|
|
1252
|
+
/* A time column is an epoch in SECONDS: without `step: 1`
|
|
1253
|
+
* the input holds minutes, and a save wrote the seconds
|
|
1254
|
+
* away (see form_time_value.js). */
|
|
1255
|
+
attrs.step = 1;
|
|
1256
|
+
}
|
|
1250
1257
|
Object.assign(attrs, extras);
|
|
1251
1258
|
|
|
1252
1259
|
if(readonly) {
|
|
@@ -2321,10 +2328,7 @@ function get_form_values(gobj, $form)
|
|
|
2321
2328
|
case "input":
|
|
2322
2329
|
switch(input_type) {
|
|
2323
2330
|
case "datetime-local":
|
|
2324
|
-
|
|
2325
|
-
const date = new Date($input.value);
|
|
2326
|
-
// Convert the Date object to epoch time (in seconds)
|
|
2327
|
-
value = Math.floor(date.getTime() / 1000);
|
|
2331
|
+
value = datetime_local_to_epoch($input.value);
|
|
2328
2332
|
break;
|
|
2329
2333
|
default:
|
|
2330
2334
|
value = getInputValue($input);
|
|
@@ -2588,18 +2592,8 @@ function set_form_values(gobj, template, $form, record)
|
|
|
2588
2592
|
case "input":
|
|
2589
2593
|
switch(input_type) {
|
|
2590
2594
|
case "datetime-local":
|
|
2591
|
-
/* value is a Date (see treedb_value_2_form_value)
|
|
2592
|
-
|
|
2593
|
-
* "YYYY-MM-DDTHH:mm" (no seconds). */
|
|
2594
|
-
if(value instanceof Date && !isNaN(value.getTime())) {
|
|
2595
|
-
let pad = (n) => String(n).padStart(2, "0");
|
|
2596
|
-
$input.value =
|
|
2597
|
-
`${value.getFullYear()}-${pad(value.getMonth() + 1)}` +
|
|
2598
|
-
`-${pad(value.getDate())}T` +
|
|
2599
|
-
`${pad(value.getHours())}:${pad(value.getMinutes())}`;
|
|
2600
|
-
} else {
|
|
2601
|
-
$input.value = "";
|
|
2602
|
-
}
|
|
2595
|
+
/* value is a Date (see treedb_value_2_form_value) */
|
|
2596
|
+
$input.value = date_to_datetime_local(value);
|
|
2603
2597
|
break;
|
|
2604
2598
|
case "color":
|
|
2605
2599
|
$input.value = convertColor(value);
|
|
@@ -111,6 +111,7 @@ import {delete_impact} from "./delete_impact.js";
|
|
|
111
111
|
|
|
112
112
|
import "./c_yui_treedb_topic_with_form.css";
|
|
113
113
|
import "./tabulator.css";
|
|
114
|
+
import {cell_text, hook_cell_spec} from "./table_cell_text.js";
|
|
114
115
|
|
|
115
116
|
import { TabulatorFull as Tabulator } from "tabulator-tables";
|
|
116
117
|
|
|
@@ -2147,20 +2148,12 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
|
|
|
2147
2148
|
let items = treedb_hook_data_size(value);
|
|
2148
2149
|
|
|
2149
2150
|
if(items > 0) {
|
|
2150
|
-
/*
|
|
2151
|
-
*
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
.
|
|
2155
|
-
|
|
2156
|
-
'<a class="hook_cell" ',
|
|
2157
|
-
`title="${hook_title}" aria-label="${hook_title}" `,
|
|
2158
|
-
`data-row_id="${row.id}" `,
|
|
2159
|
-
`data-col_id="${col.id}" > `,
|
|
2160
|
-
'<span style="" class="icon yi-eye"></span>',
|
|
2161
|
-
`<span>[ <u>${items}</u> ]</span>`,
|
|
2162
|
-
'</a>'
|
|
2163
|
-
].join('');
|
|
2151
|
+
/* DOM, not an HTML string: the row id is record data (M31
|
|
2152
|
+
* of the 2026-09-21 review). A language change re-runs
|
|
2153
|
+
* setColumns(), which re-runs this. */
|
|
2154
|
+
value = createElement2(hook_cell_spec(
|
|
2155
|
+
row.id, col.id, items, t("show linked records"), "show linked records"
|
|
2156
|
+
));
|
|
2164
2157
|
} else {
|
|
2165
2158
|
value = "";
|
|
2166
2159
|
}
|
|
@@ -2217,7 +2210,7 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
|
|
|
2217
2210
|
};
|
|
2218
2211
|
|
|
2219
2212
|
const formatter = new Intl.DateTimeFormat(userLocale, opts);
|
|
2220
|
-
value = '
|
|
2213
|
+
value = createElement2(['span', {class: 'is-size-7'}, formatter.format(value)]);
|
|
2221
2214
|
break;
|
|
2222
2215
|
}
|
|
2223
2216
|
break;
|
|
@@ -2249,6 +2242,15 @@ function transform__treedb_value_2_table_value(gobj, col, value, row, field)
|
|
|
2249
2242
|
value = JSON.stringify(value);
|
|
2250
2243
|
}
|
|
2251
2244
|
|
|
2245
|
+
/*
|
|
2246
|
+
* A string is record data, and Tabulator would parse it as markup
|
|
2247
|
+
* (innerHTML): `a<b` lost its text, a field holding an `<img onerror>`
|
|
2248
|
+
* ran it (M31 of the 2026-09-21 review). Shown as a text node.
|
|
2249
|
+
*/
|
|
2250
|
+
if(typeof value === "string" && value !== "") {
|
|
2251
|
+
value = cell_text(value);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2252
2254
|
return value;
|
|
2253
2255
|
}
|
|
2254
2256
|
|
|
@@ -2523,9 +2523,15 @@ function ac_create_record(gobj, event, kw, src)
|
|
|
2523
2523
|
let topic_name = gobj_read_attr(src, "topic_name");
|
|
2524
2524
|
let record = kw.record;
|
|
2525
2525
|
|
|
2526
|
+
/* `create_only`: +New makes a NEW record. With `create` alone a taken
|
|
2527
|
+
* id was an update -- the record overwritten and, through autolink
|
|
2528
|
+
* with the selects empty, unlinked, answered "Node update!" (M28 of
|
|
2529
|
+
* the 2026-09-21 review). A backend older than the option ignores it
|
|
2530
|
+
* and keeps the old upsert. */
|
|
2526
2531
|
let options = {
|
|
2527
2532
|
list_dict: true,
|
|
2528
2533
|
create: true,
|
|
2534
|
+
create_only: true,
|
|
2529
2535
|
autolink: true
|
|
2530
2536
|
};
|
|
2531
2537
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/****************************************************************************
|
|
2
|
+
* form_time_value.js
|
|
3
|
+
*
|
|
4
|
+
* A treedb `time` / `now` column is an epoch in SECONDS, and a form
|
|
5
|
+
* shows it in a `datetime-local` input. These two convert between
|
|
6
|
+
* them, and they must be each other's inverse: a writable time
|
|
7
|
+
* column travels back on EVERY save of the record.
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) 2026, ArtGins.
|
|
10
|
+
* All Rights Reserved.
|
|
11
|
+
****************************************************************************/
|
|
12
|
+
|
|
13
|
+
/************************************************************
|
|
14
|
+
* A Date -> the value of a `datetime-local` input, in local time,
|
|
15
|
+
* WITH seconds: the input is built with `step: 1`, which is what
|
|
16
|
+
* lets it hold them. Written as "YYYY-MM-DDTHH:mm" it lost them on
|
|
17
|
+
* every save of any field (M26 of the 2026-09-21 review).
|
|
18
|
+
************************************************************/
|
|
19
|
+
function date_to_datetime_local(value)
|
|
20
|
+
{
|
|
21
|
+
if(!(value instanceof Date) || isNaN(value.getTime())) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
let pad = (n) => String(n).padStart(2, "0");
|
|
25
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}` +
|
|
26
|
+
`-${pad(value.getDate())}T` +
|
|
27
|
+
`${pad(value.getHours())}:${pad(value.getMinutes())}` +
|
|
28
|
+
`:${pad(value.getSeconds())}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/************************************************************
|
|
32
|
+
* The value of a `datetime-local` input -> epoch in seconds
|
|
33
|
+
************************************************************/
|
|
34
|
+
function datetime_local_to_epoch(value)
|
|
35
|
+
{
|
|
36
|
+
if(!value) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const date = new Date(value);
|
|
40
|
+
if(isNaN(date.getTime())) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return Math.floor(date.getTime() / 1000);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {date_to_datetime_local, datetime_local_to_epoch};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {date_to_datetime_local, datetime_local_to_epoch} from "./form_time_value.js";
|
|
3
|
+
|
|
4
|
+
describe("form_time_value", () => {
|
|
5
|
+
|
|
6
|
+
it("an epoch with seconds survives a round trip through the input", () => {
|
|
7
|
+
/* M26 of the 2026-09-21 review: the input was written without
|
|
8
|
+
* seconds, so every save of ANY field moved a writable time
|
|
9
|
+
* column back up to 59 s. */
|
|
10
|
+
const epoch = 1790000037; // :37 seconds
|
|
11
|
+
const shown = date_to_datetime_local(new Date(epoch * 1000));
|
|
12
|
+
expect(datetime_local_to_epoch(shown)).toBe(epoch);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("an empty input is null, not NaN", () => {
|
|
16
|
+
expect(datetime_local_to_epoch("")).toBe(null);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("no date shows as an empty input", () => {
|
|
20
|
+
expect(date_to_datetime_local(null)).toBe("");
|
|
21
|
+
expect(date_to_datetime_local(new Date(NaN))).toBe("");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/****************************************************************************
|
|
2
|
+
* table_cell_text.js
|
|
3
|
+
*
|
|
4
|
+
* What a treedb table cell hands Tabulator.
|
|
5
|
+
*
|
|
6
|
+
* Tabulator assigns a formatter's STRING result with `innerHTML`
|
|
7
|
+
* (only its own `plaintext` formatter escapes), so a record's text
|
|
8
|
+
* returned as a string is parsed as markup: `a<b and c>d` showed
|
|
9
|
+
* as `ad`, and a field holding `<img src=x onerror=...>` ran it in
|
|
10
|
+
* the operator's browser (M31 of the 2026-09-21 review). Record
|
|
11
|
+
* data goes in as a TEXT NODE, and a cell built around it as DOM,
|
|
12
|
+
* never as an HTML string with the data interpolated.
|
|
13
|
+
*
|
|
14
|
+
* Pure but for the `document` it is handed, so it is tested
|
|
15
|
+
* without one.
|
|
16
|
+
*
|
|
17
|
+
* Copyright (c) 2026, ArtGins.
|
|
18
|
+
* All Rights Reserved.
|
|
19
|
+
****************************************************************************/
|
|
20
|
+
|
|
21
|
+
/************************************************************
|
|
22
|
+
* A value shown as text, whatever characters it holds
|
|
23
|
+
************************************************************/
|
|
24
|
+
function cell_text(value, doc = globalThis.document)
|
|
25
|
+
{
|
|
26
|
+
if(value === null || value === undefined) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
return doc.createTextNode(String(value));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/************************************************************
|
|
33
|
+
* The hook cell, as a createElement2 spec: the row id is an
|
|
34
|
+
* attribute VALUE, never text spliced into markup (an id holding
|
|
35
|
+
* a `"` cut the attribute and lost the rest of the id)
|
|
36
|
+
************************************************************/
|
|
37
|
+
function hook_cell_spec(row_id, col_id, items, title, title_key)
|
|
38
|
+
{
|
|
39
|
+
return ['a', {
|
|
40
|
+
class: 'HOOK_CELL hook_cell',
|
|
41
|
+
title: title,
|
|
42
|
+
'data-i18n-title': title_key,
|
|
43
|
+
'aria-label': title,
|
|
44
|
+
'data-i18n-aria-label': title_key,
|
|
45
|
+
'data-row_id': String(row_id),
|
|
46
|
+
'data-col_id': String(col_id)
|
|
47
|
+
}, [
|
|
48
|
+
['span', {class: 'icon yi-eye'}],
|
|
49
|
+
['u', {class: 'HOOK_CELL_COUNT'}, `[ ${items} ]`]
|
|
50
|
+
]];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {cell_text, hook_cell_spec};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {describe, it, expect} from "vitest";
|
|
2
|
+
import {cell_text, hook_cell_spec} from "./table_cell_text.js";
|
|
3
|
+
|
|
4
|
+
/* A document double: the library has no DOM test (see yui_clipboard.test.js) */
|
|
5
|
+
const doc = {
|
|
6
|
+
createTextNode: (text) => ({nodeType: 3, textContent: text})
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe("table_cell_text", () => {
|
|
10
|
+
|
|
11
|
+
it("record text is a text node, markup included", () => {
|
|
12
|
+
/* M31: Tabulator puts a string through innerHTML */
|
|
13
|
+
const node = cell_text("limit a<b and c>d", doc);
|
|
14
|
+
expect(node.nodeType).toBe(3);
|
|
15
|
+
expect(node.textContent).toBe("limit a<b and c>d");
|
|
16
|
+
expect(cell_text('<img src=x onerror=alert(1)>', doc).textContent)
|
|
17
|
+
.toBe('<img src=x onerror=alert(1)>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("numbers and booleans are shown as text too, nothing is empty", () => {
|
|
21
|
+
expect(cell_text(0, doc).textContent).toBe("0");
|
|
22
|
+
expect(cell_text(false, doc).textContent).toBe("false");
|
|
23
|
+
expect(cell_text(null, doc)).toBe("");
|
|
24
|
+
expect(cell_text(undefined, doc)).toBe("");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("the hook cell carries the row id whole, quote included", () => {
|
|
28
|
+
const spec = hook_cell_spec(
|
|
29
|
+
'es"madrid', "users", 3, "ver registros enlazados", "show linked records"
|
|
30
|
+
);
|
|
31
|
+
expect(spec[0]).toBe("a");
|
|
32
|
+
expect(spec[1]["data-row_id"]).toBe('es"madrid');
|
|
33
|
+
expect(spec[1]["data-col_id"]).toBe("users");
|
|
34
|
+
expect(spec[1].title).toBe("ver registros enlazados");
|
|
35
|
+
expect(spec[1]["aria-label"]).toBe("ver registros enlazados");
|
|
36
|
+
expect(spec[1]["data-i18n-title"]).toBe("show linked records");
|
|
37
|
+
expect(spec[1]["data-i18n-aria-label"]).toBe("show linked records");
|
|
38
|
+
});
|
|
39
|
+
});
|