@yuneta/gobj-ui 7.23.43 → 7.23.44
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 +680 -655
- package/dist/gobj-ui.es.js +680 -655
- package/package.json +1 -1
- package/src/c_yui_form.js +6 -5
- package/src/json_view_helpers.js +23 -7
- package/src/json_view_helpers.test.js +33 -0
package/dist/gobj-ui.es.js
CHANGED
|
@@ -6035,173 +6035,460 @@ function yui_watch_theme(gobj, event = "EV_THEME") {
|
|
|
6035
6035
|
}
|
|
6036
6036
|
} };
|
|
6037
6037
|
}
|
|
6038
|
-
//#endregion
|
|
6039
|
-
//#region src/c_yui_form.js
|
|
6040
|
-
/***********************************************************************
|
|
6041
|
-
* c_yui_form.js
|
|
6042
|
-
*
|
|
6043
|
-
* Form (in Window or not)
|
|
6044
|
-
*
|
|
6045
|
-
* Copyright (c) 2024-2026, ArtGins.
|
|
6046
|
-
* All Rights Reserved.
|
|
6047
|
-
***********************************************************************/
|
|
6048
|
-
/***************************************************************
|
|
6049
|
-
* Constants
|
|
6050
|
-
***************************************************************/
|
|
6051
|
-
var GCLASS_NAME$16 = "C_YUI_FORM";
|
|
6052
|
-
/***************************************************************
|
|
6053
|
-
* Data
|
|
6054
|
-
***************************************************************/
|
|
6055
|
-
var attrs_table$16 = [
|
|
6056
|
-
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of publishing messages"),
|
|
6057
|
-
SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else do it manually"),
|
|
6058
|
-
SDATA(data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
|
|
6059
|
-
SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
|
|
6060
|
-
SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
|
|
6061
|
-
SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
|
|
6062
|
-
SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
|
|
6063
|
-
SDATA(data_type_t.DTP_STRING, "render_mode", 0, "exec", "'exec' interprets structured cols (template->sub-form, table->grid, coordinates->picker); 'edit' shows them as raw JSON editors (record-editing, e.g. the treedb topic form)"),
|
|
6064
|
-
SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
|
|
6065
|
-
SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
|
|
6066
|
-
SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
|
|
6067
|
-
SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
|
|
6068
|
-
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
|
|
6069
|
-
SDATA(data_type_t.DTP_BOOLEAN, "with_rownum", 0, false, "Print first column with row number"),
|
|
6070
|
-
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_row_btn", 0, false, "Print last column with delete row button"),
|
|
6071
|
-
SDATA(data_type_t.DTP_JSON, "toolbar", 0, [
|
|
6072
|
-
"save",
|
|
6073
|
-
"undo",
|
|
6074
|
-
"clear",
|
|
6075
|
-
"copy",
|
|
6076
|
-
"paste"
|
|
6077
|
-
], "Toolbar buttons to show, in order: save, undo, clear, copy, paste. [] hides the toolbar"),
|
|
6078
|
-
SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
|
|
6079
|
-
layout: "fitDataFill",
|
|
6080
|
-
validationMode: "highlight",
|
|
6081
|
-
columnDefaults: { resizable: true },
|
|
6082
|
-
maxHeight: "100%"
|
|
6083
|
-
}, "Default settings for Tabulator"),
|
|
6084
|
-
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Container element"),
|
|
6085
|
-
SDATA(data_type_t.DTP_STRING, "form_id", 0, "", "Form identifier"),
|
|
6086
|
-
SDATA(data_type_t.DTP_BOOLEAN, "initialized", 0, false, "Indicates if it is initialized"),
|
|
6087
|
-
SDATA(data_type_t.DTP_BOOLEAN, "changes", 0, false, "Indicates if changes have been made"),
|
|
6088
|
-
SDATA_END()
|
|
6089
|
-
];
|
|
6090
|
-
var PRIVATE_DATA$16 = {};
|
|
6091
|
-
var __gclass__$16 = null;
|
|
6092
|
-
/***************************
|
|
6093
|
-
* Framework Methods
|
|
6094
|
-
***************************/
|
|
6095
6038
|
/************************************************************
|
|
6096
|
-
*
|
|
6039
|
+
* JSON type discriminator (viewer vocabulary)
|
|
6097
6040
|
************************************************************/
|
|
6098
|
-
function
|
|
6099
|
-
|
|
6100
|
-
if (
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6041
|
+
function json_type(value) {
|
|
6042
|
+
if (value === null) return "null";
|
|
6043
|
+
if (Array.isArray(value)) return "array";
|
|
6044
|
+
switch (typeof value) {
|
|
6045
|
+
case "string": return "string";
|
|
6046
|
+
case "number": return "number";
|
|
6047
|
+
case "boolean": return "boolean";
|
|
6048
|
+
case "object": return "object";
|
|
6049
|
+
default: return "unknown";
|
|
6050
|
+
}
|
|
6105
6051
|
}
|
|
6106
6052
|
/************************************************************
|
|
6107
|
-
*
|
|
6108
|
-
*
|
|
6109
|
-
*
|
|
6053
|
+
* Detect a kw_collapse() sentinel.
|
|
6054
|
+
*
|
|
6055
|
+
* The kernel replaces an over-limit dict with:
|
|
6056
|
+
* { "__collapsed__": { "path": ..., "size": N } }
|
|
6057
|
+
* and an over-limit array with:
|
|
6058
|
+
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
6059
|
+
*
|
|
6060
|
+
* Returns { size, path, is_array } or null.
|
|
6110
6061
|
************************************************************/
|
|
6111
|
-
function
|
|
6112
|
-
|
|
6062
|
+
function is_collapsed(value) {
|
|
6063
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
6064
|
+
let keys = Object.keys(value);
|
|
6065
|
+
if (keys.length === 1 && keys[0] === "__collapsed__") {
|
|
6066
|
+
let c = value.__collapsed__ || {};
|
|
6067
|
+
return {
|
|
6068
|
+
size: c.size,
|
|
6069
|
+
path: c.path,
|
|
6070
|
+
is_array: false
|
|
6071
|
+
};
|
|
6072
|
+
}
|
|
6073
|
+
return null;
|
|
6074
|
+
}
|
|
6075
|
+
if (Array.isArray(value) && value.length === 1) {
|
|
6076
|
+
let e = value[0];
|
|
6077
|
+
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
6078
|
+
let c = e.__collapsed__ || {};
|
|
6079
|
+
return {
|
|
6080
|
+
size: c.size,
|
|
6081
|
+
path: c.path,
|
|
6082
|
+
is_array: true
|
|
6083
|
+
};
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
return null;
|
|
6113
6087
|
}
|
|
6114
6088
|
/************************************************************
|
|
6115
|
-
*
|
|
6089
|
+
* Join / split absolute path segments (kernel convention)
|
|
6116
6090
|
************************************************************/
|
|
6117
|
-
function
|
|
6118
|
-
|
|
6119
|
-
|
|
6091
|
+
function seg_join(segments) {
|
|
6092
|
+
return segments.map(function(s) {
|
|
6093
|
+
return String(s);
|
|
6094
|
+
}).join("`");
|
|
6095
|
+
}
|
|
6096
|
+
function seg_split(path) {
|
|
6097
|
+
if (path === "" || path === null || path === void 0) return [];
|
|
6098
|
+
return path.split("`");
|
|
6120
6099
|
}
|
|
6121
6100
|
/************************************************************
|
|
6122
|
-
*
|
|
6101
|
+
* Walk a JSON tree by absolute segments; undefined if the
|
|
6102
|
+
* path does not resolve.
|
|
6123
6103
|
************************************************************/
|
|
6124
|
-
function
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6104
|
+
function get_by_segments(root, segments) {
|
|
6105
|
+
let v = root;
|
|
6106
|
+
for (let seg of segments) {
|
|
6107
|
+
if (v === null || v === void 0) return;
|
|
6108
|
+
if (Array.isArray(v)) v = v[Number(seg)];
|
|
6109
|
+
else if (typeof v === "object") v = v[seg];
|
|
6110
|
+
else return;
|
|
6111
|
+
}
|
|
6112
|
+
return v;
|
|
6113
|
+
}
|
|
6128
6114
|
/************************************************************
|
|
6115
|
+
* Set a value at absolute segments (mutates root).
|
|
6129
6116
|
*
|
|
6117
|
+
* segments == [] means "replace the whole root" — the caller
|
|
6118
|
+
* must use the RETURNED value in that case (a primitive/array
|
|
6119
|
+
* root cannot be mutated in place).
|
|
6130
6120
|
************************************************************/
|
|
6131
|
-
function
|
|
6132
|
-
|
|
6133
|
-
|
|
6121
|
+
function set_by_segments(root, segments, new_value) {
|
|
6122
|
+
if (segments.length === 0) return new_value;
|
|
6123
|
+
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
6124
|
+
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
6125
|
+
let last = segments[segments.length - 1];
|
|
6126
|
+
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
6127
|
+
else parent[last] = new_value;
|
|
6128
|
+
return root;
|
|
6134
6129
|
}
|
|
6135
6130
|
/************************************************************
|
|
6131
|
+
* Does `value` (or any loaded descendant / its own key)
|
|
6132
|
+
* contain the lower-cased search term?
|
|
6136
6133
|
*
|
|
6134
|
+
* Collapsed (not-yet-loaded) subtrees can't be searched, so
|
|
6135
|
+
* they never match on content — only their key can match,
|
|
6136
|
+
* which the renderer checks separately.
|
|
6137
6137
|
************************************************************/
|
|
6138
|
-
function
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
gobj_write_bool_attr(gobj, "changes", true);
|
|
6153
|
-
enable($button_save, true, "MediumSeaGreen");
|
|
6154
|
-
enable($button_undo, true, "Magenta");
|
|
6155
|
-
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
6156
|
-
if (isEventSet($table.tabulator, "dataChanged")) $table.tabulator.off("dataChanged");
|
|
6157
|
-
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
6158
|
-
if (isEventSet($table_nested.tabulator, "dataChanged")) $table_nested.tabulator.off("dataChanged");
|
|
6159
|
-
});
|
|
6160
|
-
});
|
|
6161
|
-
} else {
|
|
6162
|
-
gobj_write_bool_attr(gobj, "changes", false);
|
|
6163
|
-
enable($button_save, false, OFF_COLOR);
|
|
6164
|
-
enable($button_undo, false, OFF_COLOR);
|
|
6165
|
-
setTimeout(function() {
|
|
6166
|
-
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
6167
|
-
if (isEventSet($table.tabulator, "dataChanged")) $table.tabulator.off("dataChanged");
|
|
6168
|
-
$table.tabulator.on("dataChanged", function(data) {
|
|
6169
|
-
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
6170
|
-
});
|
|
6171
|
-
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
6172
|
-
if (isEventSet($table_nested.tabulator, "dataChanged")) $table_nested.tabulator.off("dataChanged");
|
|
6173
|
-
$table_nested.tabulator.on("dataChanged", function(data) {
|
|
6174
|
-
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
6175
|
-
});
|
|
6176
|
-
});
|
|
6177
|
-
});
|
|
6178
|
-
}, 200);
|
|
6138
|
+
function subtree_matches(value, term) {
|
|
6139
|
+
if (!term) return true;
|
|
6140
|
+
if (is_collapsed(value)) return false;
|
|
6141
|
+
let type = json_type(value);
|
|
6142
|
+
if (type === "object") {
|
|
6143
|
+
for (let [k, v] of Object.entries(value)) {
|
|
6144
|
+
if (String(k).toLowerCase().includes(term)) return true;
|
|
6145
|
+
if (subtree_matches(v, term)) return true;
|
|
6146
|
+
}
|
|
6147
|
+
return false;
|
|
6148
|
+
}
|
|
6149
|
+
if (type === "array") {
|
|
6150
|
+
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
6151
|
+
return false;
|
|
6179
6152
|
}
|
|
6153
|
+
return String(value).toLowerCase().includes(term);
|
|
6180
6154
|
}
|
|
6181
6155
|
/************************************************************
|
|
6156
|
+
* Fields whose numeric value is an epoch timestamp.
|
|
6182
6157
|
*
|
|
6158
|
+
* THE ONE LIST. C_YUI_FORM's jsoneditor takes its
|
|
6159
|
+
* `timestampTag` from here: it used to carry its own copy,
|
|
6160
|
+
* and a copy is how the two stopped agreeing.
|
|
6161
|
+
*
|
|
6162
|
+
* TWO SPELLINGS, AND LAS DOS SON DEL BACKEND. timeranger2
|
|
6163
|
+
* writes `from_t`/`from_tm` in the match_cond of a query and
|
|
6164
|
+
* `fr_t`/`fr_tm` in the metadata of a topic and of a file --
|
|
6165
|
+
* which is what a Raw JSON of a treedb actually shows. With
|
|
6166
|
+
* only the long pair listed, `to_t` got its date and the
|
|
6167
|
+
* `fr_t` right above it did not, which reads as a broken
|
|
6168
|
+
* annotator rather than a missing name.
|
|
6169
|
+
************************************************************/
|
|
6170
|
+
var TIME_FIELDS = [
|
|
6171
|
+
"__t__",
|
|
6172
|
+
"__tm__",
|
|
6173
|
+
"t",
|
|
6174
|
+
"tm",
|
|
6175
|
+
"time",
|
|
6176
|
+
"from_t",
|
|
6177
|
+
"to_t",
|
|
6178
|
+
"from_tm",
|
|
6179
|
+
"to_tm",
|
|
6180
|
+
"fr_t",
|
|
6181
|
+
"fr_tm",
|
|
6182
|
+
"t_input",
|
|
6183
|
+
"t_output"
|
|
6184
|
+
];
|
|
6185
|
+
function is_time_field(field) {
|
|
6186
|
+
return TIME_FIELDS.indexOf(field) >= 0;
|
|
6187
|
+
}
|
|
6188
|
+
/************************************************************
|
|
6189
|
+
* Format an epoch value as a local wall-clock string.
|
|
6190
|
+
* Accepts seconds or milliseconds; returns null when the
|
|
6191
|
+
* value is not a usable positive timestamp (0 == "unset").
|
|
6183
6192
|
************************************************************/
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
log_error(`Failed to read clipboard contents: ${error}`);
|
|
6191
|
-
}
|
|
6193
|
+
function format_epoch(value) {
|
|
6194
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
6195
|
+
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
6196
|
+
let d = new Date(ms);
|
|
6197
|
+
if (isNaN(d.getTime())) return null;
|
|
6198
|
+
return d.toLocaleString();
|
|
6192
6199
|
}
|
|
6193
6200
|
/************************************************************
|
|
6194
|
-
*
|
|
6201
|
+
* The text view's payload: the document as text, indented
|
|
6202
|
+
* four characters, cut at `max_chars`.
|
|
6195
6203
|
*
|
|
6196
|
-
*
|
|
6197
|
-
*
|
|
6198
|
-
*
|
|
6199
|
-
* always had, so nothing moves for a caller that says nothing.
|
|
6204
|
+
* Returns {text, capped}. `capped` is what the caller
|
|
6205
|
+
* announces under the dump — a cut that is not announced
|
|
6206
|
+
* reads as a document that ends there.
|
|
6200
6207
|
************************************************************/
|
|
6201
|
-
function
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6208
|
+
function json_text_dump(root, max_chars) {
|
|
6209
|
+
let text;
|
|
6210
|
+
try {
|
|
6211
|
+
text = JSON.stringify(root, null, 4);
|
|
6212
|
+
} catch (e) {
|
|
6213
|
+
return {
|
|
6214
|
+
text: "",
|
|
6215
|
+
capped: false,
|
|
6216
|
+
error: String(e)
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
if (text === void 0) return {
|
|
6220
|
+
text: "",
|
|
6221
|
+
capped: false,
|
|
6222
|
+
error: "not representable as JSON"
|
|
6223
|
+
};
|
|
6224
|
+
if (typeof max_chars === "number" && max_chars > 0 && text.length > max_chars) return {
|
|
6225
|
+
text: text.slice(0, max_chars),
|
|
6226
|
+
capped: true
|
|
6227
|
+
};
|
|
6228
|
+
return {
|
|
6229
|
+
text,
|
|
6230
|
+
capped: false
|
|
6231
|
+
};
|
|
6232
|
+
}
|
|
6233
|
+
/************************************************************
|
|
6234
|
+
* Which view a freshly mounted viewer opens on.
|
|
6235
|
+
*
|
|
6236
|
+
* Three answers, in order of authority: what the HOST asked
|
|
6237
|
+
* for, what the READER chose last time, and `fallback`.
|
|
6238
|
+
*
|
|
6239
|
+
* The host wins because a viewer mounted to show a graph has
|
|
6240
|
+
* to show one. The memory comes next, because reopening a
|
|
6241
|
+
* document in the view you just left is the whole point of
|
|
6242
|
+
* remembering it. `fallback` is the floor.
|
|
6243
|
+
*
|
|
6244
|
+
* Anything not in `modes` is treated as unsaid, which covers
|
|
6245
|
+
* an absent value, an empty one, and a stored string from an
|
|
6246
|
+
* older release that named a view no longer offered.
|
|
6247
|
+
************************************************************/
|
|
6248
|
+
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
6249
|
+
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
6250
|
+
if (known(asked)) return asked;
|
|
6251
|
+
if (known(remembered)) return remembered;
|
|
6252
|
+
return fallback;
|
|
6253
|
+
}
|
|
6254
|
+
/************************************************************
|
|
6255
|
+
* The short label a container row carries beside its size.
|
|
6256
|
+
*
|
|
6257
|
+
* An array of dicts that carry an `id` is a list of RECORDS,
|
|
6258
|
+
* and a row that says only `2: {15}` makes you open all
|
|
6259
|
+
* fifteen fields of every one of them to find out WHICH
|
|
6260
|
+
* record it is. The id is the one field that answers that,
|
|
6261
|
+
* so the row says it without being opened.
|
|
6262
|
+
*
|
|
6263
|
+
* Only a SCALAR id, and only when it is really there: an `id`
|
|
6264
|
+
* that is itself a dict or a list is not a name, it is more
|
|
6265
|
+
* document, and printing `[object Object]` beside the size
|
|
6266
|
+
* would be worse than printing nothing.
|
|
6267
|
+
*
|
|
6268
|
+
* Cut at `max_chars`, because this rides on a row whose job
|
|
6269
|
+
* is to stay one line.
|
|
6270
|
+
************************************************************/
|
|
6271
|
+
function container_label(value, max_chars) {
|
|
6272
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
|
6273
|
+
if (!Object.prototype.hasOwnProperty.call(value, "id")) return "";
|
|
6274
|
+
let id = value.id;
|
|
6275
|
+
let t = typeof id;
|
|
6276
|
+
if (t !== "string" && t !== "number" && t !== "boolean") return "";
|
|
6277
|
+
let text = String(id).trim();
|
|
6278
|
+
if (text === "") return "";
|
|
6279
|
+
let cap = typeof max_chars === "number" && max_chars > 0 ? max_chars : 0;
|
|
6280
|
+
if (cap && text.length > cap) return text.slice(0, cap) + "…";
|
|
6281
|
+
return text;
|
|
6282
|
+
}
|
|
6283
|
+
/************************************************************
|
|
6284
|
+
* A dict, in the JSON sense: not an array and not null.
|
|
6285
|
+
************************************************************/
|
|
6286
|
+
function is_plain_object(v) {
|
|
6287
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
6288
|
+
}
|
|
6289
|
+
/************************************************************
|
|
6290
|
+
* Does this container hold ONLY other containers?
|
|
6291
|
+
*
|
|
6292
|
+
* Such a value is not a thing, it is a LIST of things -- and a
|
|
6293
|
+
* list is fully described by the row that names it in its
|
|
6294
|
+
* parent, plus the edges to what it holds. Drawing it as a
|
|
6295
|
+
* card of its own put a node in the middle of the graph with no
|
|
6296
|
+
* data in it, saying nothing the parent's row did not already
|
|
6297
|
+
* say, and pushed everything below it one level down.
|
|
6298
|
+
*
|
|
6299
|
+
* An EMPTY container inside counts as content: `{0}` is
|
|
6300
|
+
* something the card has to show, and there is no child card
|
|
6301
|
+
* to show it instead.
|
|
6302
|
+
************************************************************/
|
|
6303
|
+
function is_pure_collection(value) {
|
|
6304
|
+
let entries = is_plain_object(value) ? Object.entries(value) : Array.isArray(value) ? value.map((v, i) => [i, v]) : [];
|
|
6305
|
+
if (entries.length === 0) return false;
|
|
6306
|
+
for (let [, v] of entries) if (!has_branch(v)) return false;
|
|
6307
|
+
return true;
|
|
6308
|
+
}
|
|
6309
|
+
/************************************************************
|
|
6310
|
+
* A value that gets a card, or children, of its own.
|
|
6311
|
+
************************************************************/
|
|
6312
|
+
function has_branch(value) {
|
|
6313
|
+
return is_plain_object(value) && Object.keys(value).length > 0 || Array.isArray(value) && value.length > 0;
|
|
6314
|
+
}
|
|
6315
|
+
/************************************************************
|
|
6316
|
+
* How many branches a container has, which is what the fold
|
|
6317
|
+
* chip counts.
|
|
6318
|
+
************************************************************/
|
|
6319
|
+
function count_branches(value) {
|
|
6320
|
+
let entries = is_plain_object(value) ? Object.values(value) : Array.isArray(value) ? value : [];
|
|
6321
|
+
let n = 0;
|
|
6322
|
+
for (let v of entries) if (has_branch(v)) n++;
|
|
6323
|
+
return n;
|
|
6324
|
+
}
|
|
6325
|
+
//#endregion
|
|
6326
|
+
//#region src/c_yui_form.js
|
|
6327
|
+
/***********************************************************************
|
|
6328
|
+
* c_yui_form.js
|
|
6329
|
+
*
|
|
6330
|
+
* Form (in Window or not)
|
|
6331
|
+
*
|
|
6332
|
+
* Copyright (c) 2024-2026, ArtGins.
|
|
6333
|
+
* All Rights Reserved.
|
|
6334
|
+
***********************************************************************/
|
|
6335
|
+
/***************************************************************
|
|
6336
|
+
* Constants
|
|
6337
|
+
***************************************************************/
|
|
6338
|
+
var GCLASS_NAME$16 = "C_YUI_FORM";
|
|
6339
|
+
/***************************************************************
|
|
6340
|
+
* Data
|
|
6341
|
+
***************************************************************/
|
|
6342
|
+
var attrs_table$16 = [
|
|
6343
|
+
SDATA(data_type_t.DTP_POINTER, "subscriber", 0, null, "Subscriber of publishing messages"),
|
|
6344
|
+
SDATA(data_type_t.DTP_POINTER, "$parent", 0, null, "$container will be appended to $parent if not null, else do it manually"),
|
|
6345
|
+
SDATA(data_type_t.DTP_POINTER, "template", 0, "<div>You must supply a template for form</div>", "Template for form, object -> TEMPLATE, array -> DESC"),
|
|
6346
|
+
SDATA(data_type_t.DTP_JSON, "record", 0, "{}", "Data to form"),
|
|
6347
|
+
SDATA(data_type_t.DTP_JSON, "placeholders", 0, "{}", "Placeholder values for form fields"),
|
|
6348
|
+
SDATA(data_type_t.DTP_JSON, "fkey_options", 0, "{}", "Options for fkey fields: {topic_name: [ids or {id} records]}, read at build time"),
|
|
6349
|
+
SDATA(data_type_t.DTP_STRING, "form_mode", 0, "", "'create' (pkey editable+required) or 'update' (pkey readonly); empty = no pkey handling"),
|
|
6350
|
+
SDATA(data_type_t.DTP_STRING, "render_mode", 0, "exec", "'exec' interprets structured cols (template->sub-form, table->grid, coordinates->picker); 'edit' shows them as raw JSON editors (record-editing, e.g. the treedb topic form)"),
|
|
6351
|
+
SDATA(data_type_t.DTP_STRING, "pkey", 0, "id", "Primary key field name, used by form_mode"),
|
|
6352
|
+
SDATA(data_type_t.DTP_STRING, "topic_name", 0, "", "Treedb topic name: labels use the i18n cascade '<topic>.<col>' -> '<col>' -> header (like table headers)"),
|
|
6353
|
+
SDATA(data_type_t.DTP_BOOLEAN, "editable", 0, false, "Default is editable, set false to readonly"),
|
|
6354
|
+
SDATA(data_type_t.DTP_BOOLEAN, "selectable", 0, true, "Rows selectable with 'click' (BAD with editable=true)"),
|
|
6355
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_checkbox", 0, false, "Show a first column with checkbox to select rows"),
|
|
6356
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_rownum", 0, false, "Print first column with row number"),
|
|
6357
|
+
SDATA(data_type_t.DTP_BOOLEAN, "with_delete_row_btn", 0, false, "Print last column with delete row button"),
|
|
6358
|
+
SDATA(data_type_t.DTP_JSON, "toolbar", 0, [
|
|
6359
|
+
"save",
|
|
6360
|
+
"undo",
|
|
6361
|
+
"clear",
|
|
6362
|
+
"copy",
|
|
6363
|
+
"paste"
|
|
6364
|
+
], "Toolbar buttons to show, in order: save, undo, clear, copy, paste. [] hides the toolbar"),
|
|
6365
|
+
SDATA(data_type_t.DTP_JSON, "tabulator_settings", 0, {
|
|
6366
|
+
layout: "fitDataFill",
|
|
6367
|
+
validationMode: "highlight",
|
|
6368
|
+
columnDefaults: { resizable: true },
|
|
6369
|
+
maxHeight: "100%"
|
|
6370
|
+
}, "Default settings for Tabulator"),
|
|
6371
|
+
SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "Container element"),
|
|
6372
|
+
SDATA(data_type_t.DTP_STRING, "form_id", 0, "", "Form identifier"),
|
|
6373
|
+
SDATA(data_type_t.DTP_BOOLEAN, "initialized", 0, false, "Indicates if it is initialized"),
|
|
6374
|
+
SDATA(data_type_t.DTP_BOOLEAN, "changes", 0, false, "Indicates if changes have been made"),
|
|
6375
|
+
SDATA_END()
|
|
6376
|
+
];
|
|
6377
|
+
var PRIVATE_DATA$16 = {};
|
|
6378
|
+
var __gclass__$16 = null;
|
|
6379
|
+
/***************************
|
|
6380
|
+
* Framework Methods
|
|
6381
|
+
***************************/
|
|
6382
|
+
/************************************************************
|
|
6383
|
+
* Framework Method create
|
|
6384
|
+
************************************************************/
|
|
6385
|
+
function mt_create$16(gobj) {
|
|
6386
|
+
let subscriber = gobj_read_pointer_attr(gobj, "subscriber");
|
|
6387
|
+
if (!subscriber) subscriber = gobj_parent(gobj);
|
|
6388
|
+
gobj_subscribe_event(gobj, null, {}, subscriber);
|
|
6389
|
+
let name = clean_name(gobj_name(gobj));
|
|
6390
|
+
gobj_write_attr(gobj, "form_id", "form" + name);
|
|
6391
|
+
build_ui$14(gobj);
|
|
6392
|
+
}
|
|
6393
|
+
/************************************************************
|
|
6394
|
+
* Framework Method destroy
|
|
6395
|
+
* In this point, all children
|
|
6396
|
+
* and subscriptions are already deleted.
|
|
6397
|
+
************************************************************/
|
|
6398
|
+
function mt_destroy$16(gobj) {
|
|
6399
|
+
destroy_ui$10(gobj);
|
|
6400
|
+
}
|
|
6401
|
+
/************************************************************
|
|
6402
|
+
* Framework Method start
|
|
6403
|
+
************************************************************/
|
|
6404
|
+
function mt_start$15(gobj) {
|
|
6405
|
+
let record = gobj_read_attr(gobj, "record");
|
|
6406
|
+
if (record) gobj_send_event(gobj, "EV_LOAD_RECORD", record, gobj);
|
|
6407
|
+
}
|
|
6408
|
+
/************************************************************
|
|
6409
|
+
* Framework Method stop
|
|
6410
|
+
************************************************************/
|
|
6411
|
+
function mt_stop$15(gobj) {}
|
|
6412
|
+
/***************************
|
|
6413
|
+
* Local Methods
|
|
6414
|
+
***************************/
|
|
6415
|
+
/************************************************************
|
|
6416
|
+
*
|
|
6417
|
+
************************************************************/
|
|
6418
|
+
function isEventSet(table, event) {
|
|
6419
|
+
let eventListeners = table.externalEvents.events;
|
|
6420
|
+
return eventListeners && eventListeners[event] && eventListeners[event].length > 0;
|
|
6421
|
+
}
|
|
6422
|
+
/************************************************************
|
|
6423
|
+
*
|
|
6424
|
+
************************************************************/
|
|
6425
|
+
function set_changed_stated(gobj, changed) {
|
|
6426
|
+
let $container = gobj_read_attr(gobj, "$container");
|
|
6427
|
+
let $form = $container.querySelector("form");
|
|
6428
|
+
const enable = ($btn, on, color) => {
|
|
6429
|
+
if (!$btn) return;
|
|
6430
|
+
if (on) $btn.removeAttribute("disabled");
|
|
6431
|
+
else $btn.setAttribute("disabled", true);
|
|
6432
|
+
const $i = $btn.querySelector("span i");
|
|
6433
|
+
if ($i) $i.style.color = color;
|
|
6434
|
+
};
|
|
6435
|
+
const OFF_COLOR = "hsl(var(--bulma-button-h), var(--bulma-button-s), var(--bulma-button-color-l))";
|
|
6436
|
+
let $button_save = $container.querySelector(".button-save");
|
|
6437
|
+
let $button_undo = $container.querySelector(".button-undo");
|
|
6438
|
+
if (changed) {
|
|
6439
|
+
gobj_write_bool_attr(gobj, "changes", true);
|
|
6440
|
+
enable($button_save, true, "MediumSeaGreen");
|
|
6441
|
+
enable($button_undo, true, "Magenta");
|
|
6442
|
+
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
6443
|
+
if (isEventSet($table.tabulator, "dataChanged")) $table.tabulator.off("dataChanged");
|
|
6444
|
+
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
6445
|
+
if (isEventSet($table_nested.tabulator, "dataChanged")) $table_nested.tabulator.off("dataChanged");
|
|
6446
|
+
});
|
|
6447
|
+
});
|
|
6448
|
+
} else {
|
|
6449
|
+
gobj_write_bool_attr(gobj, "changes", false);
|
|
6450
|
+
enable($button_save, false, OFF_COLOR);
|
|
6451
|
+
enable($button_undo, false, OFF_COLOR);
|
|
6452
|
+
setTimeout(function() {
|
|
6453
|
+
$form.querySelectorAll(".yui-tabulator-table").forEach(($table) => {
|
|
6454
|
+
if (isEventSet($table.tabulator, "dataChanged")) $table.tabulator.off("dataChanged");
|
|
6455
|
+
$table.tabulator.on("dataChanged", function(data) {
|
|
6456
|
+
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
6457
|
+
});
|
|
6458
|
+
$table.querySelectorAll(".yui-tabulator-table-nested").forEach(($table_nested) => {
|
|
6459
|
+
if (isEventSet($table_nested.tabulator, "dataChanged")) $table_nested.tabulator.off("dataChanged");
|
|
6460
|
+
$table_nested.tabulator.on("dataChanged", function(data) {
|
|
6461
|
+
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
6462
|
+
});
|
|
6463
|
+
});
|
|
6464
|
+
});
|
|
6465
|
+
}, 200);
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
/************************************************************
|
|
6469
|
+
*
|
|
6470
|
+
************************************************************/
|
|
6471
|
+
async function readClipboard$1(gobj) {
|
|
6472
|
+
try {
|
|
6473
|
+
const text = await navigator.clipboard.readText();
|
|
6474
|
+
let kw = JSON.parse(text);
|
|
6475
|
+
gobj_send_event(gobj, "EV_PASTE_RECORD", kw, gobj);
|
|
6476
|
+
} catch (error) {
|
|
6477
|
+
log_error(`Failed to read clipboard contents: ${error}`);
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
/************************************************************
|
|
6481
|
+
* Bottom toolbar, from the `toolbar` attr.
|
|
6482
|
+
*
|
|
6483
|
+
* The five buttons it can show, and their order, are the
|
|
6484
|
+
* caller's: a dialog with a single action asks for ["save"],
|
|
6485
|
+
* and [] leaves no toolbar at all. Default is the five it
|
|
6486
|
+
* always had, so nothing moves for a caller that says nothing.
|
|
6487
|
+
************************************************************/
|
|
6488
|
+
function toolbar_button$1(gobj, name) {
|
|
6489
|
+
switch (name) {
|
|
6490
|
+
case "save": return [
|
|
6491
|
+
"button",
|
|
6205
6492
|
{
|
|
6206
6493
|
class: "button p-1 button-save",
|
|
6207
6494
|
title: "save",
|
|
@@ -7163,7 +7450,7 @@ function create_form_field(gobj, $form, prefix, field_desc, { tag, inputType, na
|
|
|
7163
7450
|
gobj_send_event(gobj, "EV_RECORD_CHANGED", {}, gobj);
|
|
7164
7451
|
},
|
|
7165
7452
|
timestampTag: function({ field, value, path }) {
|
|
7166
|
-
return field
|
|
7453
|
+
return is_time_field(field);
|
|
7167
7454
|
}
|
|
7168
7455
|
}
|
|
7169
7456
|
});
|
|
@@ -10033,559 +10320,297 @@ var ZoomCanvasPinch = class extends ZoomCanvas {
|
|
|
10033
10320
|
}
|
|
10034
10321
|
let dx = touches[0].clientX - touches[1].clientX;
|
|
10035
10322
|
let dy = touches[0].clientY - touches[1].clientY;
|
|
10036
|
-
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
10037
|
-
if (dist <= 0) return;
|
|
10038
|
-
if (this._pinch_dist0 === 0) {
|
|
10039
|
-
this._pinch_dist0 = dist;
|
|
10040
|
-
this._pinch_zoom0 = graph.getZoom();
|
|
10041
|
-
return;
|
|
10042
|
-
}
|
|
10043
|
-
this._pinch_to(dist / this._pinch_dist0, touches, event);
|
|
10044
|
-
};
|
|
10045
|
-
state.container.addEventListener("touchmove", this._on_touch_move, { passive: true });
|
|
10046
|
-
}
|
|
10047
|
-
/************************************************************
|
|
10048
|
-
* Zoom to `scale` times the zoom the pinch started at,
|
|
10049
|
-
* about the midpoint between the two fingers.
|
|
10050
|
-
*
|
|
10051
|
-
* ABSOLUTE against the start of the gesture, where G6's
|
|
10052
|
-
* own pinch feeds `zoom()` an increment per frame: an
|
|
10053
|
-
* increment drifts away from the fingers over a long
|
|
10054
|
-
* gesture, and cannot come back when they close again.
|
|
10055
|
-
************************************************************/
|
|
10056
|
-
_pinch_to(scale, touches, event) {
|
|
10057
|
-
if (!this.validate(event)) return;
|
|
10058
|
-
let graph = this.context.graph;
|
|
10059
|
-
let state = touch_state_of(graph);
|
|
10060
|
-
let origin = null;
|
|
10061
|
-
let rect = state.container?.getBoundingClientRect();
|
|
10062
|
-
if (rect) origin = [(touches[0].clientX + touches[1].clientX) / 2 - rect.left, (touches[0].clientY + touches[1].clientY) / 2 - rect.top];
|
|
10063
|
-
graph.zoomTo(this._pinch_zoom0 * scale, false, origin);
|
|
10064
|
-
this.options.onFinish?.();
|
|
10065
|
-
}
|
|
10066
|
-
destroy() {
|
|
10067
|
-
if (this._on_touch_move) {
|
|
10068
|
-
__touch_state__.get(this.context.graph)?.container?.removeEventListener("touchmove", this._on_touch_move);
|
|
10069
|
-
this._on_touch_move = null;
|
|
10070
|
-
}
|
|
10071
|
-
super.destroy();
|
|
10072
|
-
}
|
|
10073
|
-
};
|
|
10074
|
-
/************************************************************
|
|
10075
|
-
* Register once, over the built-in `zoom-canvas`, so every
|
|
10076
|
-
* graph (tree, json, treedb, editor) pinches without
|
|
10077
|
-
* touching its behaviors list.
|
|
10078
|
-
************************************************************/
|
|
10079
|
-
var __zoom_canvas_patched__ = false;
|
|
10080
|
-
function ensure_pinch_zoom_patch() {
|
|
10081
|
-
if (__zoom_canvas_patched__) return;
|
|
10082
|
-
register(ExtensionCategory.BEHAVIOR, "zoom-canvas", ZoomCanvasPinch);
|
|
10083
|
-
__zoom_canvas_patched__ = true;
|
|
10084
|
-
}
|
|
10085
|
-
/***************************
|
|
10086
|
-
* Long press
|
|
10087
|
-
***************************/
|
|
10088
|
-
/************************************************************
|
|
10089
|
-
* Is the click being dispatched right now the tail of a long
|
|
10090
|
-
* press? Answers true ONCE, to the first asker.
|
|
10091
|
-
*
|
|
10092
|
-
* @antv/g does not take `click` from the DOM either: it makes
|
|
10093
|
-
* one out of the pointerdown/pointerup pair itself, inside
|
|
10094
|
-
* the same `onPointerUp` that just fed us the release. So
|
|
10095
|
-
* the press that opened the menu goes on to click whatever it
|
|
10096
|
-
* opened the menu ON -- selecting the node, or worse, dropping
|
|
10097
|
-
* a selection of several -- unless the handler asks.
|
|
10098
|
-
************************************************************/
|
|
10099
|
-
function consume_long_press_click(graph) {
|
|
10100
|
-
if (!graph) return false;
|
|
10101
|
-
let state = __touch_state__.get(graph);
|
|
10102
|
-
if (!state || !state.long_press_click) return false;
|
|
10103
|
-
state.long_press_click = false;
|
|
10104
|
-
return true;
|
|
10105
|
-
}
|
|
10106
|
-
/************************************************************
|
|
10107
|
-
* Make a long press open the context menu.
|
|
10108
|
-
*
|
|
10109
|
-
* Returns the function that uninstalls it.
|
|
10110
|
-
*
|
|
10111
|
-
* The menu the press opens is whatever the `contextmenu`
|
|
10112
|
-
* plugin builds: we re-emit G6's own forwarded event under
|
|
10113
|
-
* the name the plugin listens for, so `getItems(e)` and
|
|
10114
|
-
* `enable(e)` see the same object a right-click would give
|
|
10115
|
-
* them -- same `target`, same `targetType`, same `client`.
|
|
10116
|
-
*
|
|
10117
|
-
* `options.enable(event)` decides, at the DOWN, whether this
|
|
10118
|
-
* press is even a candidate. Pass the plugin's own predicate:
|
|
10119
|
-
* a press on the empty canvas would otherwise arm a menu the
|
|
10120
|
-
* plugin then refuses to show, and swallow the click that
|
|
10121
|
-
* would have cleared the selection.
|
|
10122
|
-
************************************************************/
|
|
10123
|
-
function install_long_press_contextmenu(graph, options) {
|
|
10124
|
-
if (!graph) return () => {};
|
|
10125
|
-
let enable = options?.enable;
|
|
10126
|
-
let state = touch_state_of(graph);
|
|
10127
|
-
let pending = null;
|
|
10128
|
-
let disarm = () => {
|
|
10129
|
-
if (pending) {
|
|
10130
|
-
clearTimeout(pending.tick);
|
|
10131
|
-
pending = null;
|
|
10132
|
-
}
|
|
10133
|
-
};
|
|
10134
|
-
let announce = () => {
|
|
10135
|
-
if (!pending) return;
|
|
10136
|
-
if (state.multi) return;
|
|
10137
|
-
navigator.vibrate?.(15);
|
|
10138
|
-
};
|
|
10139
|
-
let fire = (event) => {
|
|
10140
|
-
state.long_press_click = true;
|
|
10141
|
-
setTimeout(() => {
|
|
10142
|
-
state.long_press_click = false;
|
|
10143
|
-
}, 0);
|
|
10144
|
-
let swallow = (evt) => {
|
|
10145
|
-
evt.stopPropagation();
|
|
10146
|
-
document.removeEventListener("click", swallow, true);
|
|
10147
|
-
};
|
|
10148
|
-
document.addEventListener("click", swallow, true);
|
|
10149
|
-
setTimeout(() => {
|
|
10150
|
-
document.removeEventListener("click", swallow, true);
|
|
10151
|
-
}, 1e3);
|
|
10152
|
-
graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
|
|
10153
|
-
graph.emit(CommonEvent.CONTEXT_MENU, event);
|
|
10154
|
-
};
|
|
10155
|
-
let on_down = (event) => {
|
|
10156
|
-
disarm();
|
|
10157
|
-
if (event.pointerType !== "touch") return;
|
|
10158
|
-
if (state.count > 1 || state.multi) return;
|
|
10159
|
-
if (enable && !enable(event)) return;
|
|
10160
|
-
let x = event.client?.x;
|
|
10161
|
-
let y = event.client?.y;
|
|
10162
|
-
if (x === void 0 || y === void 0) return;
|
|
10163
|
-
pending = {
|
|
10164
|
-
x,
|
|
10165
|
-
y,
|
|
10166
|
-
t0: Date.now(),
|
|
10167
|
-
event: Object.assign({}, event),
|
|
10168
|
-
tick: setTimeout(announce, 500)
|
|
10169
|
-
};
|
|
10170
|
-
};
|
|
10171
|
-
let on_move = (event) => {
|
|
10172
|
-
if (!pending) return;
|
|
10173
|
-
if (state.count > 1 || state.multi) {
|
|
10174
|
-
disarm();
|
|
10175
|
-
return;
|
|
10176
|
-
}
|
|
10177
|
-
let x = event.client?.x;
|
|
10178
|
-
let y = event.client?.y;
|
|
10179
|
-
if (x === void 0 || y === void 0) return;
|
|
10180
|
-
if (press_moved(pending, x, y)) disarm();
|
|
10181
|
-
};
|
|
10182
|
-
let on_up = (event) => {
|
|
10183
|
-
let press = pending;
|
|
10184
|
-
if (press) clearTimeout(press.tick);
|
|
10185
|
-
pending = null;
|
|
10186
|
-
if (!press) return;
|
|
10187
|
-
if (state.multi) return;
|
|
10188
|
-
let x = event.client?.x;
|
|
10189
|
-
let y = event.client?.y;
|
|
10190
|
-
if (x === void 0) x = press.x;
|
|
10191
|
-
if (y === void 0) y = press.y;
|
|
10192
|
-
if (classify_press(press, x, y, Date.now()) !== "long") return;
|
|
10193
|
-
fire(press.event);
|
|
10194
|
-
};
|
|
10195
|
-
let on_touch_cancel = () => {
|
|
10196
|
-
disarm();
|
|
10197
|
-
};
|
|
10198
|
-
state.container?.addEventListener("touchcancel", on_touch_cancel);
|
|
10199
|
-
graph.on(CommonEvent.POINTER_DOWN, on_down);
|
|
10200
|
-
graph.on(CommonEvent.POINTER_MOVE, on_move);
|
|
10201
|
-
graph.on(CommonEvent.POINTER_UP, on_up);
|
|
10202
|
-
return () => {
|
|
10203
|
-
disarm();
|
|
10204
|
-
state.container?.removeEventListener("touchcancel", on_touch_cancel);
|
|
10205
|
-
graph.off(CommonEvent.POINTER_DOWN, on_down);
|
|
10206
|
-
graph.off(CommonEvent.POINTER_MOVE, on_move);
|
|
10207
|
-
graph.off(CommonEvent.POINTER_UP, on_up);
|
|
10208
|
-
};
|
|
10209
|
-
}
|
|
10210
|
-
//#endregion
|
|
10211
|
-
//#region src/g6_drag_canvas_touch.js
|
|
10212
|
-
/***********************************************************************
|
|
10213
|
-
* g6_drag_canvas_touch.js
|
|
10214
|
-
*
|
|
10215
|
-
* Drop-in replacement for G6 v5 built-in 'drag-canvas'.
|
|
10216
|
-
*
|
|
10217
|
-
* Why this exists
|
|
10218
|
-
* ----------------
|
|
10219
|
-
* G6 v5.1.0 drag-canvas computes the pan delta as:
|
|
10220
|
-
*
|
|
10221
|
-
* const x = event.movement?.x ?? event.dx;
|
|
10222
|
-
*
|
|
10223
|
-
* but @antv/g always attaches a `movement` Point to every
|
|
10224
|
-
* federated pointer event (g-lite: `this.movement = new
|
|
10225
|
-
* Point()`), and fills it from the native
|
|
10226
|
-
* `PointerEvent.movementX/Y` (g-lite: `event.movement.x =
|
|
10227
|
-
* nativeEvent.movementX`). So `event.movement?.x` is never
|
|
10228
|
-
* nullish and the `?? event.dx` fallback never runs.
|
|
10229
|
-
*
|
|
10230
|
-
* Native `movementX/Y`:
|
|
10231
|
-
* - on touch pointers most mobile browsers leave it at 0
|
|
10232
|
-
* (it is a mouse / pointer-lock feature), so the canvas
|
|
10233
|
-
* barely pans on mobile;
|
|
10234
|
-
* - on desktop it is skewed by OS pointer acceleration and
|
|
10235
|
-
* devicePixelRatio, so the graph lags the cursor.
|
|
10236
|
-
*
|
|
10237
|
-
* The fix tracks `event.viewport` (the federated pointer
|
|
10238
|
-
* position already mapped to canvas/viewport space by
|
|
10239
|
-
* @antv/g, the exact units `translateBy` expects) and pans
|
|
10240
|
-
* by its frame-to-frame delta. This is:
|
|
10241
|
-
* - reliable on mouse AND touch (derived from clientX/Y,
|
|
10242
|
-
* never from native movementX);
|
|
10243
|
-
* - correct for ANY canvas scale (it is post-`getScale()`,
|
|
10244
|
-
* so a configured-vs-displayed size mismatch can no
|
|
10245
|
-
* longer make panning over- or under-shoot the cursor);
|
|
10246
|
-
* - the same source G6's own `zoom-canvas` uses for its
|
|
10247
|
-
* origin (`event.viewport`).
|
|
10248
|
-
* The subclass reuses all of the built-in clamp/cursor logic
|
|
10249
|
-
* and only swaps the delta source.
|
|
10250
|
-
*
|
|
10251
|
-
* Registered globally over the built-in 'drag-canvas' id so
|
|
10252
|
-
* every graph (tree, json, treedb, editor) is fixed without
|
|
10253
|
-
* touching each consumer's behaviors list.
|
|
10254
|
-
*
|
|
10255
|
-
* It also has to keep the ONE guard the base class has and
|
|
10256
|
-
* this replacement dropped: G6's `onDrag` refuses to pan
|
|
10257
|
-
* while a pinch is in progress. See `is_pinching` in
|
|
10258
|
-
* `g6_touch_gestures.js`, which is where the pinch that
|
|
10259
|
-
* matters is now recognised.
|
|
10260
|
-
*
|
|
10261
|
-
* Copyright (c) 2026, ArtGins.
|
|
10262
|
-
* All Rights Reserved.
|
|
10263
|
-
***********************************************************************/
|
|
10264
|
-
/************************************************************
|
|
10265
|
-
* Custom drag-canvas: same behavior, reliable delta source
|
|
10266
|
-
************************************************************/
|
|
10267
|
-
var DragCanvasTouch = class extends DragCanvas {
|
|
10268
|
-
constructor(context, options) {
|
|
10269
|
-
super(context, options);
|
|
10270
|
-
this._lastVP = null;
|
|
10271
|
-
this._onDragTouch = (event) => {
|
|
10272
|
-
if (!this.isDragging) {
|
|
10273
|
-
this._lastVP = null;
|
|
10274
|
-
return;
|
|
10275
|
-
}
|
|
10276
|
-
if (is_pinching(this.context.graph)) {
|
|
10277
|
-
this._lastVP = null;
|
|
10278
|
-
return;
|
|
10279
|
-
}
|
|
10280
|
-
let vp = event.viewport;
|
|
10281
|
-
if (!vp || typeof vp.x !== "number" || typeof vp.y !== "number") return;
|
|
10282
|
-
if (this._lastVP === null) {
|
|
10283
|
-
this._lastVP = {
|
|
10284
|
-
x: vp.x,
|
|
10285
|
-
y: vp.y
|
|
10286
|
-
};
|
|
10323
|
+
let dist = Math.sqrt(dx * dx + dy * dy);
|
|
10324
|
+
if (dist <= 0) return;
|
|
10325
|
+
if (this._pinch_dist0 === 0) {
|
|
10326
|
+
this._pinch_dist0 = dist;
|
|
10327
|
+
this._pinch_zoom0 = graph.getZoom();
|
|
10287
10328
|
return;
|
|
10288
10329
|
}
|
|
10289
|
-
|
|
10290
|
-
let y = vp.y - this._lastVP.y;
|
|
10291
|
-
this._lastVP.x = vp.x;
|
|
10292
|
-
this._lastVP.y = vp.y;
|
|
10293
|
-
if (x !== 0 || y !== 0) this.translate([x, y], false);
|
|
10294
|
-
};
|
|
10295
|
-
this._onDragEndReset = () => {
|
|
10296
|
-
this._lastVP = null;
|
|
10330
|
+
this._pinch_to(dist / this._pinch_dist0, touches, event);
|
|
10297
10331
|
};
|
|
10298
|
-
|
|
10299
|
-
graph.off(CommonEvent.DRAG, this.onDrag);
|
|
10300
|
-
graph.on(CommonEvent.DRAG, this._onDragTouch);
|
|
10301
|
-
graph.on(CommonEvent.DRAG_END, this._onDragEndReset);
|
|
10332
|
+
state.container.addEventListener("touchmove", this._on_touch_move, { passive: true });
|
|
10302
10333
|
}
|
|
10303
|
-
|
|
10304
|
-
|
|
10334
|
+
/************************************************************
|
|
10335
|
+
* Zoom to `scale` times the zoom the pinch started at,
|
|
10336
|
+
* about the midpoint between the two fingers.
|
|
10337
|
+
*
|
|
10338
|
+
* ABSOLUTE against the start of the gesture, where G6's
|
|
10339
|
+
* own pinch feeds `zoom()` an increment per frame: an
|
|
10340
|
+
* increment drifts away from the fingers over a long
|
|
10341
|
+
* gesture, and cannot come back when they close again.
|
|
10342
|
+
************************************************************/
|
|
10343
|
+
_pinch_to(scale, touches, event) {
|
|
10344
|
+
if (!this.validate(event)) return;
|
|
10305
10345
|
let graph = this.context.graph;
|
|
10306
|
-
graph
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
graph.
|
|
10346
|
+
let state = touch_state_of(graph);
|
|
10347
|
+
let origin = null;
|
|
10348
|
+
let rect = state.container?.getBoundingClientRect();
|
|
10349
|
+
if (rect) origin = [(touches[0].clientX + touches[1].clientX) / 2 - rect.left, (touches[0].clientY + touches[1].clientY) / 2 - rect.top];
|
|
10350
|
+
graph.zoomTo(this._pinch_zoom0 * scale, false, origin);
|
|
10351
|
+
this.options.onFinish?.();
|
|
10311
10352
|
}
|
|
10312
10353
|
destroy() {
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10354
|
+
if (this._on_touch_move) {
|
|
10355
|
+
__touch_state__.get(this.context.graph)?.container?.removeEventListener("touchmove", this._on_touch_move);
|
|
10356
|
+
this._on_touch_move = null;
|
|
10357
|
+
}
|
|
10316
10358
|
super.destroy();
|
|
10317
10359
|
}
|
|
10318
10360
|
};
|
|
10319
10361
|
/************************************************************
|
|
10320
|
-
*
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
function ensure_drag_canvas_patch() {
|
|
10324
|
-
if (__drag_canvas_patched__) return;
|
|
10325
|
-
register(ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
|
|
10326
|
-
__drag_canvas_patched__ = true;
|
|
10327
|
-
}
|
|
10328
|
-
/************************************************************
|
|
10329
|
-
* JSON type discriminator (viewer vocabulary)
|
|
10362
|
+
* Register once, over the built-in `zoom-canvas`, so every
|
|
10363
|
+
* graph (tree, json, treedb, editor) pinches without
|
|
10364
|
+
* touching its behaviors list.
|
|
10330
10365
|
************************************************************/
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
if (
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
case "number": return "number";
|
|
10337
|
-
case "boolean": return "boolean";
|
|
10338
|
-
case "object": return "object";
|
|
10339
|
-
default: return "unknown";
|
|
10340
|
-
}
|
|
10366
|
+
var __zoom_canvas_patched__ = false;
|
|
10367
|
+
function ensure_pinch_zoom_patch() {
|
|
10368
|
+
if (__zoom_canvas_patched__) return;
|
|
10369
|
+
register(ExtensionCategory.BEHAVIOR, "zoom-canvas", ZoomCanvasPinch);
|
|
10370
|
+
__zoom_canvas_patched__ = true;
|
|
10341
10371
|
}
|
|
10372
|
+
/***************************
|
|
10373
|
+
* Long press
|
|
10374
|
+
***************************/
|
|
10342
10375
|
/************************************************************
|
|
10343
|
-
*
|
|
10344
|
-
*
|
|
10345
|
-
* The kernel replaces an over-limit dict with:
|
|
10346
|
-
* { "__collapsed__": { "path": ..., "size": N } }
|
|
10347
|
-
* and an over-limit array with:
|
|
10348
|
-
* [ { "__collapsed__": { "path": ..., "size": N } } ]
|
|
10376
|
+
* Is the click being dispatched right now the tail of a long
|
|
10377
|
+
* press? Answers true ONCE, to the first asker.
|
|
10349
10378
|
*
|
|
10350
|
-
*
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
let c = value.__collapsed__ || {};
|
|
10357
|
-
return {
|
|
10358
|
-
size: c.size,
|
|
10359
|
-
path: c.path,
|
|
10360
|
-
is_array: false
|
|
10361
|
-
};
|
|
10362
|
-
}
|
|
10363
|
-
return null;
|
|
10364
|
-
}
|
|
10365
|
-
if (Array.isArray(value) && value.length === 1) {
|
|
10366
|
-
let e = value[0];
|
|
10367
|
-
if (e && typeof e === "object" && !Array.isArray(e) && Object.keys(e).length === 1 && e.__collapsed__) {
|
|
10368
|
-
let c = e.__collapsed__ || {};
|
|
10369
|
-
return {
|
|
10370
|
-
size: c.size,
|
|
10371
|
-
path: c.path,
|
|
10372
|
-
is_array: true
|
|
10373
|
-
};
|
|
10374
|
-
}
|
|
10375
|
-
}
|
|
10376
|
-
return null;
|
|
10377
|
-
}
|
|
10378
|
-
/************************************************************
|
|
10379
|
-
* Join / split absolute path segments (kernel convention)
|
|
10380
|
-
************************************************************/
|
|
10381
|
-
function seg_join(segments) {
|
|
10382
|
-
return segments.map(function(s) {
|
|
10383
|
-
return String(s);
|
|
10384
|
-
}).join("`");
|
|
10385
|
-
}
|
|
10386
|
-
function seg_split(path) {
|
|
10387
|
-
if (path === "" || path === null || path === void 0) return [];
|
|
10388
|
-
return path.split("`");
|
|
10389
|
-
}
|
|
10390
|
-
/************************************************************
|
|
10391
|
-
* Walk a JSON tree by absolute segments; undefined if the
|
|
10392
|
-
* path does not resolve.
|
|
10379
|
+
* @antv/g does not take `click` from the DOM either: it makes
|
|
10380
|
+
* one out of the pointerdown/pointerup pair itself, inside
|
|
10381
|
+
* the same `onPointerUp` that just fed us the release. So
|
|
10382
|
+
* the press that opened the menu goes on to click whatever it
|
|
10383
|
+
* opened the menu ON -- selecting the node, or worse, dropping
|
|
10384
|
+
* a selection of several -- unless the handler asks.
|
|
10393
10385
|
************************************************************/
|
|
10394
|
-
function
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
else return;
|
|
10401
|
-
}
|
|
10402
|
-
return v;
|
|
10386
|
+
function consume_long_press_click(graph) {
|
|
10387
|
+
if (!graph) return false;
|
|
10388
|
+
let state = __touch_state__.get(graph);
|
|
10389
|
+
if (!state || !state.long_press_click) return false;
|
|
10390
|
+
state.long_press_click = false;
|
|
10391
|
+
return true;
|
|
10403
10392
|
}
|
|
10404
10393
|
/************************************************************
|
|
10405
|
-
*
|
|
10394
|
+
* Make a long press open the context menu.
|
|
10406
10395
|
*
|
|
10407
|
-
*
|
|
10408
|
-
* must use the RETURNED value in that case (a primitive/array
|
|
10409
|
-
* root cannot be mutated in place).
|
|
10410
|
-
************************************************************/
|
|
10411
|
-
function set_by_segments(root, segments, new_value) {
|
|
10412
|
-
if (segments.length === 0) return new_value;
|
|
10413
|
-
let parent = get_by_segments(root, segments.slice(0, -1));
|
|
10414
|
-
if (parent === null || parent === void 0 || typeof parent !== "object") return root;
|
|
10415
|
-
let last = segments[segments.length - 1];
|
|
10416
|
-
if (Array.isArray(parent)) parent[Number(last)] = new_value;
|
|
10417
|
-
else parent[last] = new_value;
|
|
10418
|
-
return root;
|
|
10419
|
-
}
|
|
10420
|
-
/************************************************************
|
|
10421
|
-
* Does `value` (or any loaded descendant / its own key)
|
|
10422
|
-
* contain the lower-cased search term?
|
|
10396
|
+
* Returns the function that uninstalls it.
|
|
10423
10397
|
*
|
|
10424
|
-
*
|
|
10425
|
-
*
|
|
10426
|
-
*
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
if (!term) return true;
|
|
10430
|
-
if (is_collapsed(value)) return false;
|
|
10431
|
-
let type = json_type(value);
|
|
10432
|
-
if (type === "object") {
|
|
10433
|
-
for (let [k, v] of Object.entries(value)) {
|
|
10434
|
-
if (String(k).toLowerCase().includes(term)) return true;
|
|
10435
|
-
if (subtree_matches(v, term)) return true;
|
|
10436
|
-
}
|
|
10437
|
-
return false;
|
|
10438
|
-
}
|
|
10439
|
-
if (type === "array") {
|
|
10440
|
-
for (let v of value) if (subtree_matches(v, term)) return true;
|
|
10441
|
-
return false;
|
|
10442
|
-
}
|
|
10443
|
-
return String(value).toLowerCase().includes(term);
|
|
10444
|
-
}
|
|
10445
|
-
/************************************************************
|
|
10446
|
-
* Fields whose numeric value is an epoch timestamp.
|
|
10447
|
-
* Mirrors the timestampTag list wired into C_YUI_FORM's
|
|
10448
|
-
* jsoneditor control, so the two viewers agree.
|
|
10449
|
-
************************************************************/
|
|
10450
|
-
function is_time_field(field) {
|
|
10451
|
-
return field === "__t__" || field === "__tm__" || field === "tm" || field === "t" || field === "time" || field === "from_t" || field === "to_t" || field === "t_input" || field === "t_output" || field === "from_tm" || field === "to_tm";
|
|
10452
|
-
}
|
|
10453
|
-
/************************************************************
|
|
10454
|
-
* Format an epoch value as a local wall-clock string.
|
|
10455
|
-
* Accepts seconds or milliseconds; returns null when the
|
|
10456
|
-
* value is not a usable positive timestamp (0 == "unset").
|
|
10457
|
-
************************************************************/
|
|
10458
|
-
function format_epoch(value) {
|
|
10459
|
-
if (typeof value !== "number" || !isFinite(value) || value <= 0) return null;
|
|
10460
|
-
let ms = Math.abs(value) < 0xe8d4a51000 ? value * 1e3 : value;
|
|
10461
|
-
let d = new Date(ms);
|
|
10462
|
-
if (isNaN(d.getTime())) return null;
|
|
10463
|
-
return d.toLocaleString();
|
|
10464
|
-
}
|
|
10465
|
-
/************************************************************
|
|
10466
|
-
* The text view's payload: the document as text, indented
|
|
10467
|
-
* four characters, cut at `max_chars`.
|
|
10398
|
+
* The menu the press opens is whatever the `contextmenu`
|
|
10399
|
+
* plugin builds: we re-emit G6's own forwarded event under
|
|
10400
|
+
* the name the plugin listens for, so `getItems(e)` and
|
|
10401
|
+
* `enable(e)` see the same object a right-click would give
|
|
10402
|
+
* them -- same `target`, same `targetType`, same `client`.
|
|
10468
10403
|
*
|
|
10469
|
-
*
|
|
10470
|
-
*
|
|
10471
|
-
*
|
|
10404
|
+
* `options.enable(event)` decides, at the DOWN, whether this
|
|
10405
|
+
* press is even a candidate. Pass the plugin's own predicate:
|
|
10406
|
+
* a press on the empty canvas would otherwise arm a menu the
|
|
10407
|
+
* plugin then refuses to show, and swallow the click that
|
|
10408
|
+
* would have cleared the selection.
|
|
10472
10409
|
************************************************************/
|
|
10473
|
-
function
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10410
|
+
function install_long_press_contextmenu(graph, options) {
|
|
10411
|
+
if (!graph) return () => {};
|
|
10412
|
+
let enable = options?.enable;
|
|
10413
|
+
let state = touch_state_of(graph);
|
|
10414
|
+
let pending = null;
|
|
10415
|
+
let disarm = () => {
|
|
10416
|
+
if (pending) {
|
|
10417
|
+
clearTimeout(pending.tick);
|
|
10418
|
+
pending = null;
|
|
10419
|
+
}
|
|
10420
|
+
};
|
|
10421
|
+
let announce = () => {
|
|
10422
|
+
if (!pending) return;
|
|
10423
|
+
if (state.multi) return;
|
|
10424
|
+
navigator.vibrate?.(15);
|
|
10425
|
+
};
|
|
10426
|
+
let fire = (event) => {
|
|
10427
|
+
state.long_press_click = true;
|
|
10428
|
+
setTimeout(() => {
|
|
10429
|
+
state.long_press_click = false;
|
|
10430
|
+
}, 0);
|
|
10431
|
+
let swallow = (evt) => {
|
|
10432
|
+
evt.stopPropagation();
|
|
10433
|
+
document.removeEventListener("click", swallow, true);
|
|
10482
10434
|
};
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10435
|
+
document.addEventListener("click", swallow, true);
|
|
10436
|
+
setTimeout(() => {
|
|
10437
|
+
document.removeEventListener("click", swallow, true);
|
|
10438
|
+
}, 1e3);
|
|
10439
|
+
graph.emit(`${event.targetType}:${CommonEvent.CONTEXT_MENU}`, event);
|
|
10440
|
+
graph.emit(CommonEvent.CONTEXT_MENU, event);
|
|
10488
10441
|
};
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10442
|
+
let on_down = (event) => {
|
|
10443
|
+
disarm();
|
|
10444
|
+
if (event.pointerType !== "touch") return;
|
|
10445
|
+
if (state.count > 1 || state.multi) return;
|
|
10446
|
+
if (enable && !enable(event)) return;
|
|
10447
|
+
let x = event.client?.x;
|
|
10448
|
+
let y = event.client?.y;
|
|
10449
|
+
if (x === void 0 || y === void 0) return;
|
|
10450
|
+
pending = {
|
|
10451
|
+
x,
|
|
10452
|
+
y,
|
|
10453
|
+
t0: Date.now(),
|
|
10454
|
+
event: Object.assign({}, event),
|
|
10455
|
+
tick: setTimeout(announce, 500)
|
|
10456
|
+
};
|
|
10492
10457
|
};
|
|
10493
|
-
|
|
10494
|
-
|
|
10495
|
-
|
|
10458
|
+
let on_move = (event) => {
|
|
10459
|
+
if (!pending) return;
|
|
10460
|
+
if (state.count > 1 || state.multi) {
|
|
10461
|
+
disarm();
|
|
10462
|
+
return;
|
|
10463
|
+
}
|
|
10464
|
+
let x = event.client?.x;
|
|
10465
|
+
let y = event.client?.y;
|
|
10466
|
+
if (x === void 0 || y === void 0) return;
|
|
10467
|
+
if (press_moved(pending, x, y)) disarm();
|
|
10468
|
+
};
|
|
10469
|
+
let on_up = (event) => {
|
|
10470
|
+
let press = pending;
|
|
10471
|
+
if (press) clearTimeout(press.tick);
|
|
10472
|
+
pending = null;
|
|
10473
|
+
if (!press) return;
|
|
10474
|
+
if (state.multi) return;
|
|
10475
|
+
let x = event.client?.x;
|
|
10476
|
+
let y = event.client?.y;
|
|
10477
|
+
if (x === void 0) x = press.x;
|
|
10478
|
+
if (y === void 0) y = press.y;
|
|
10479
|
+
if (classify_press(press, x, y, Date.now()) !== "long") return;
|
|
10480
|
+
fire(press.event);
|
|
10481
|
+
};
|
|
10482
|
+
let on_touch_cancel = () => {
|
|
10483
|
+
disarm();
|
|
10484
|
+
};
|
|
10485
|
+
state.container?.addEventListener("touchcancel", on_touch_cancel);
|
|
10486
|
+
graph.on(CommonEvent.POINTER_DOWN, on_down);
|
|
10487
|
+
graph.on(CommonEvent.POINTER_MOVE, on_move);
|
|
10488
|
+
graph.on(CommonEvent.POINTER_UP, on_up);
|
|
10489
|
+
return () => {
|
|
10490
|
+
disarm();
|
|
10491
|
+
state.container?.removeEventListener("touchcancel", on_touch_cancel);
|
|
10492
|
+
graph.off(CommonEvent.POINTER_DOWN, on_down);
|
|
10493
|
+
graph.off(CommonEvent.POINTER_MOVE, on_move);
|
|
10494
|
+
graph.off(CommonEvent.POINTER_UP, on_up);
|
|
10496
10495
|
};
|
|
10497
10496
|
}
|
|
10498
|
-
|
|
10499
|
-
|
|
10497
|
+
//#endregion
|
|
10498
|
+
//#region src/g6_drag_canvas_touch.js
|
|
10499
|
+
/***********************************************************************
|
|
10500
|
+
* g6_drag_canvas_touch.js
|
|
10500
10501
|
*
|
|
10501
|
-
*
|
|
10502
|
-
* for, what the READER chose last time, and `fallback`.
|
|
10502
|
+
* Drop-in replacement for G6 v5 built-in 'drag-canvas'.
|
|
10503
10503
|
*
|
|
10504
|
-
*
|
|
10505
|
-
*
|
|
10506
|
-
*
|
|
10507
|
-
* remembering it. `fallback` is the floor.
|
|
10504
|
+
* Why this exists
|
|
10505
|
+
* ----------------
|
|
10506
|
+
* G6 v5.1.0 drag-canvas computes the pan delta as:
|
|
10508
10507
|
*
|
|
10509
|
-
*
|
|
10510
|
-
* an absent value, an empty one, and a stored string from an
|
|
10511
|
-
* older release that named a view no longer offered.
|
|
10512
|
-
************************************************************/
|
|
10513
|
-
function pick_view_mode(asked, remembered, modes, fallback) {
|
|
10514
|
-
let known = (m) => Array.isArray(modes) && modes.indexOf(m) >= 0;
|
|
10515
|
-
if (known(asked)) return asked;
|
|
10516
|
-
if (known(remembered)) return remembered;
|
|
10517
|
-
return fallback;
|
|
10518
|
-
}
|
|
10519
|
-
/************************************************************
|
|
10520
|
-
* The short label a container row carries beside its size.
|
|
10508
|
+
* const x = event.movement?.x ?? event.dx;
|
|
10521
10509
|
*
|
|
10522
|
-
*
|
|
10523
|
-
*
|
|
10524
|
-
*
|
|
10525
|
-
*
|
|
10526
|
-
*
|
|
10510
|
+
* but @antv/g always attaches a `movement` Point to every
|
|
10511
|
+
* federated pointer event (g-lite: `this.movement = new
|
|
10512
|
+
* Point()`), and fills it from the native
|
|
10513
|
+
* `PointerEvent.movementX/Y` (g-lite: `event.movement.x =
|
|
10514
|
+
* nativeEvent.movementX`). So `event.movement?.x` is never
|
|
10515
|
+
* nullish and the `?? event.dx` fallback never runs.
|
|
10527
10516
|
*
|
|
10528
|
-
*
|
|
10529
|
-
*
|
|
10530
|
-
*
|
|
10531
|
-
*
|
|
10517
|
+
* Native `movementX/Y`:
|
|
10518
|
+
* - on touch pointers most mobile browsers leave it at 0
|
|
10519
|
+
* (it is a mouse / pointer-lock feature), so the canvas
|
|
10520
|
+
* barely pans on mobile;
|
|
10521
|
+
* - on desktop it is skewed by OS pointer acceleration and
|
|
10522
|
+
* devicePixelRatio, so the graph lags the cursor.
|
|
10532
10523
|
*
|
|
10533
|
-
*
|
|
10534
|
-
*
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
return text;
|
|
10547
|
-
}
|
|
10548
|
-
/************************************************************
|
|
10549
|
-
* A dict, in the JSON sense: not an array and not null.
|
|
10550
|
-
************************************************************/
|
|
10551
|
-
function is_plain_object(v) {
|
|
10552
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
10553
|
-
}
|
|
10554
|
-
/************************************************************
|
|
10555
|
-
* Does this container hold ONLY other containers?
|
|
10524
|
+
* The fix tracks `event.viewport` (the federated pointer
|
|
10525
|
+
* position already mapped to canvas/viewport space by
|
|
10526
|
+
* @antv/g, the exact units `translateBy` expects) and pans
|
|
10527
|
+
* by its frame-to-frame delta. This is:
|
|
10528
|
+
* - reliable on mouse AND touch (derived from clientX/Y,
|
|
10529
|
+
* never from native movementX);
|
|
10530
|
+
* - correct for ANY canvas scale (it is post-`getScale()`,
|
|
10531
|
+
* so a configured-vs-displayed size mismatch can no
|
|
10532
|
+
* longer make panning over- or under-shoot the cursor);
|
|
10533
|
+
* - the same source G6's own `zoom-canvas` uses for its
|
|
10534
|
+
* origin (`event.viewport`).
|
|
10535
|
+
* The subclass reuses all of the built-in clamp/cursor logic
|
|
10536
|
+
* and only swaps the delta source.
|
|
10556
10537
|
*
|
|
10557
|
-
*
|
|
10558
|
-
*
|
|
10559
|
-
*
|
|
10560
|
-
* card of its own put a node in the middle of the graph with no
|
|
10561
|
-
* data in it, saying nothing the parent's row did not already
|
|
10562
|
-
* say, and pushed everything below it one level down.
|
|
10538
|
+
* Registered globally over the built-in 'drag-canvas' id so
|
|
10539
|
+
* every graph (tree, json, treedb, editor) is fixed without
|
|
10540
|
+
* touching each consumer's behaviors list.
|
|
10563
10541
|
*
|
|
10564
|
-
*
|
|
10565
|
-
*
|
|
10566
|
-
*
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
}
|
|
10542
|
+
* It also has to keep the ONE guard the base class has and
|
|
10543
|
+
* this replacement dropped: G6's `onDrag` refuses to pan
|
|
10544
|
+
* while a pinch is in progress. See `is_pinching` in
|
|
10545
|
+
* `g6_touch_gestures.js`, which is where the pinch that
|
|
10546
|
+
* matters is now recognised.
|
|
10547
|
+
*
|
|
10548
|
+
* Copyright (c) 2026, ArtGins.
|
|
10549
|
+
* All Rights Reserved.
|
|
10550
|
+
***********************************************************************/
|
|
10574
10551
|
/************************************************************
|
|
10575
|
-
*
|
|
10552
|
+
* Custom drag-canvas: same behavior, reliable delta source
|
|
10576
10553
|
************************************************************/
|
|
10577
|
-
|
|
10578
|
-
|
|
10579
|
-
|
|
10554
|
+
var DragCanvasTouch = class extends DragCanvas {
|
|
10555
|
+
constructor(context, options) {
|
|
10556
|
+
super(context, options);
|
|
10557
|
+
this._lastVP = null;
|
|
10558
|
+
this._onDragTouch = (event) => {
|
|
10559
|
+
if (!this.isDragging) {
|
|
10560
|
+
this._lastVP = null;
|
|
10561
|
+
return;
|
|
10562
|
+
}
|
|
10563
|
+
if (is_pinching(this.context.graph)) {
|
|
10564
|
+
this._lastVP = null;
|
|
10565
|
+
return;
|
|
10566
|
+
}
|
|
10567
|
+
let vp = event.viewport;
|
|
10568
|
+
if (!vp || typeof vp.x !== "number" || typeof vp.y !== "number") return;
|
|
10569
|
+
if (this._lastVP === null) {
|
|
10570
|
+
this._lastVP = {
|
|
10571
|
+
x: vp.x,
|
|
10572
|
+
y: vp.y
|
|
10573
|
+
};
|
|
10574
|
+
return;
|
|
10575
|
+
}
|
|
10576
|
+
let x = vp.x - this._lastVP.x;
|
|
10577
|
+
let y = vp.y - this._lastVP.y;
|
|
10578
|
+
this._lastVP.x = vp.x;
|
|
10579
|
+
this._lastVP.y = vp.y;
|
|
10580
|
+
if (x !== 0 || y !== 0) this.translate([x, y], false);
|
|
10581
|
+
};
|
|
10582
|
+
this._onDragEndReset = () => {
|
|
10583
|
+
this._lastVP = null;
|
|
10584
|
+
};
|
|
10585
|
+
let graph = this.context.graph;
|
|
10586
|
+
graph.off(CommonEvent.DRAG, this.onDrag);
|
|
10587
|
+
graph.on(CommonEvent.DRAG, this._onDragTouch);
|
|
10588
|
+
graph.on(CommonEvent.DRAG_END, this._onDragEndReset);
|
|
10589
|
+
}
|
|
10590
|
+
update(options) {
|
|
10591
|
+
super.update(options);
|
|
10592
|
+
let graph = this.context.graph;
|
|
10593
|
+
graph.off(CommonEvent.DRAG, this.onDrag);
|
|
10594
|
+
graph.off(CommonEvent.DRAG, this._onDragTouch);
|
|
10595
|
+
graph.off(CommonEvent.DRAG_END, this._onDragEndReset);
|
|
10596
|
+
graph.on(CommonEvent.DRAG, this._onDragTouch);
|
|
10597
|
+
graph.on(CommonEvent.DRAG_END, this._onDragEndReset);
|
|
10598
|
+
}
|
|
10599
|
+
destroy() {
|
|
10600
|
+
let graph = this.context.graph;
|
|
10601
|
+
graph.off(CommonEvent.DRAG, this._onDragTouch);
|
|
10602
|
+
graph.off(CommonEvent.DRAG_END, this._onDragEndReset);
|
|
10603
|
+
super.destroy();
|
|
10604
|
+
}
|
|
10605
|
+
};
|
|
10580
10606
|
/************************************************************
|
|
10581
|
-
*
|
|
10582
|
-
* chip counts.
|
|
10607
|
+
* Register once, replacing the built-in 'drag-canvas'
|
|
10583
10608
|
************************************************************/
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10609
|
+
var __drag_canvas_patched__ = false;
|
|
10610
|
+
function ensure_drag_canvas_patch() {
|
|
10611
|
+
if (__drag_canvas_patched__) return;
|
|
10612
|
+
register(ExtensionCategory.BEHAVIOR, "drag-canvas", DragCanvasTouch);
|
|
10613
|
+
__drag_canvas_patched__ = true;
|
|
10589
10614
|
}
|
|
10590
10615
|
//#endregion
|
|
10591
10616
|
//#region src/c_yui_json_graph.js
|