@yuneta/gobj-ui 7.23.157 → 7.23.159

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.23.157",
3
+ "version": "7.23.159",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -543,8 +543,9 @@ let PRIVATE_DATA = {
543
543
  // it exists to diff the repaint)
544
544
  _pending_focus_topic: null, // focus requested before data was loaded
545
545
  _pending_focus_all: false, // ...and whether it reveals the whole topic
546
- _find_hidden_matches: 0, // matches of the last find in HIDDEN topics
547
- _pending_find: null, // find requested before data was loaded
546
+ _find_term: "", // the find box's term, repainted after every reconcile
547
+ _find_current: "", // node id the last Enter of the find centred on
548
+ _find_found: null, // ...and its last count (see find_matches)
548
549
  _layout_asked: "", // layout the host asked for at create (see mt_create)
549
550
  _nodes_total: 0, // records of the treedb (auto_layout)
550
551
  _nodes_placed: 0, // ...of which carry a saved position
@@ -3654,6 +3655,18 @@ function graph_focus_topic(gobj, topic, reveal_all)
3654
3655
  let model = priv._fold_model;
3655
3656
  let state = priv._fold_state;
3656
3657
 
3658
+ /* The two share the highlight slot. A topic takes it from the
3659
+ * find; an EMPTY topic (nothing focused) leaves a live find
3660
+ * alone, or clearing the legend's mark would wipe the find. */
3661
+ if(empty_string(topic) && priv._find_term) {
3662
+ priv._focus_topic = null;
3663
+ return;
3664
+ }
3665
+ if(!empty_string(topic)) {
3666
+ priv._find_term = "";
3667
+ priv._find_current = "";
3668
+ }
3669
+
3657
3670
  /* "Show me these": the ones on screen are highlighted, and the
3658
3671
  * hidden ones are opened up to. How many depends on who asks.
3659
3672
  * A click on the LEGEND (`reveal_all`) means every record of the
@@ -3689,18 +3702,18 @@ function graph_focus_topic(gobj, topic, reveal_all)
3689
3702
  priv._focus_topic = topic || null;
3690
3703
 
3691
3704
  reconcile_fold(gobj, {}).then(() => {
3692
- apply_focus_ids(gobj, keys, "focus topic");
3705
+ apply_focus_ids(gobj, keys, "focus topic", true);
3693
3706
  });
3694
3707
  }
3695
3708
 
3696
3709
  /************************************************************
3697
- * Paint the highlight on these records (by model key) and put
3698
- * the first of them in the middle. Shared by the find and the
3699
- * topic focus, after the reconcile that brought them on screen.
3700
- * Keys that are still off screen are skipped: a match beyond the
3701
- * page a reveal opened is counted, not painted.
3710
+ * Paint the highlight on these records (by model key) and, with
3711
+ * `center`, put the first of them in the middle. Shared by the
3712
+ * find and the topic focus. Keys that are not on screen are
3713
+ * skipped: a match beyond the page a reveal opened is counted,
3714
+ * not painted.
3702
3715
  ************************************************************/
3703
- function apply_focus_ids(gobj, keys, what)
3716
+ function apply_focus_ids(gobj, keys, what, center)
3704
3717
  {
3705
3718
  let priv = gobj.priv;
3706
3719
  let graph = priv.graph;
@@ -3734,7 +3747,7 @@ function apply_focus_ids(gobj, keys, what)
3734
3747
  graph.setElementState(states);
3735
3748
  }
3736
3749
  apply_node_highlight(gobj, prev_ids, ids);
3737
- if(ids.length) {
3750
+ if(center && ids.length) {
3738
3751
  yui_graph_center_on(graph, ids[0]);
3739
3752
  }
3740
3753
  } catch(e) {
@@ -3753,94 +3766,186 @@ function node_id_of_key(gobj, key)
3753
3766
  }
3754
3767
 
3755
3768
  /************************************************************
3756
- * Find the nodes whose name, id or topic contains `term`, highlight
3757
- * them with the same amber 'active' state the topic focus uses, and
3758
- * centre the viewport on them. Returns how many matched, which is the
3759
- * half the toolbar needs: a search that finds nothing must SAY so,
3760
- * because a graph that did not move is also what a graph looks like
3761
- * when the match is off-screen.
3769
+ * The find box: highlight the cards ON SCREEN whose name, id or
3770
+ * topic contains `term`, and change nothing else -- no group is
3771
+ * opened, no layout runs, the camera does not move. The graph the
3772
+ * reader arranged is the graph they are searching; emptying the
3773
+ * box takes the amber off and leaves it exactly as it was.
3774
+ *
3775
+ * It used to search the whole treedb and unfold a page of what it
3776
+ * found, which re-laid the graph out on every keystroke and left
3777
+ * the unfolded groups behind when the box was cleared. Opening up
3778
+ * to a topic is the legend's focus (graph_focus_topic); this only
3779
+ * LOOKS.
3780
+ *
3781
+ * The term is kept and repainted after every reconcile (see
3782
+ * reconcile_fold_now): a group opened by hand shows its matches
3783
+ * already lit, and the count follows what is on screen.
3762
3784
  *
3763
3785
  * It shares the highlight slot with graph_focus_topic on purpose —
3764
- * two amber sets at once would say nothing about either. An empty
3765
- * term clears it. Matching reads the node's LABEL and not only its
3766
- * id: on a topic keyed by rowid or by a qualified path, the id is a
3767
- * counter and the name a human knows is elsewhere (see node_label).
3786
+ * two amber sets at once would say nothing about either. Matching
3787
+ * reads the node's LABEL and not only its id: on a topic keyed by
3788
+ * rowid or by a qualified path, the id is a counter and the name a
3789
+ * human knows is elsewhere (see node_label).
3768
3790
  ************************************************************/
3769
3791
  function graph_find_nodes(gobj, term)
3770
3792
  {
3771
3793
  let priv = gobj.priv;
3772
- let graph = priv.graph;
3773
- if(!graph || !priv.graph_rendered || !priv._fold_model) {
3774
- priv._pending_find = term;
3775
- return 0;
3794
+ let next = empty_string(term)? "" : String(term);
3795
+ let was = priv._find_term;
3796
+
3797
+ priv._find_term = next;
3798
+ priv._find_current = "";
3799
+ priv._find_found = null;
3800
+
3801
+ let ready = priv.graph && priv.graph_rendered && priv._fold_model;
3802
+ if(!next) {
3803
+ /* Only a find that HELD the slot is taken off: an empty box
3804
+ * that was already empty must not wipe a topic focus. */
3805
+ if(was && ready && !priv._focus_topic) {
3806
+ apply_focus_ids(gobj, [], "find", false);
3807
+ }
3808
+ publish_find_result(gobj);
3809
+ return;
3810
+ }
3811
+
3812
+ priv._focus_topic = null; /* a find takes over the highlight */
3813
+ if(!ready) {
3814
+ /* Painted by the first reconcile, when the data arrives. */
3815
+ return;
3816
+ }
3817
+ refresh_find(gobj, fold_visible_set(priv._fold_model, priv._fold_state));
3818
+ }
3819
+
3820
+ /************************************************************
3821
+ * Repaint the find's highlight over the `visible` set and publish
3822
+ * the count. A no-op while no term is live, so the topic focus
3823
+ * keeps the slot.
3824
+ ************************************************************/
3825
+ function refresh_find(gobj, visible)
3826
+ {
3827
+ let priv = gobj.priv;
3828
+ if(!priv._find_term) {
3829
+ return;
3830
+ }
3831
+
3832
+ let found = find_matches(gobj, priv._find_term, visible);
3833
+ apply_focus_ids(gobj, found.keys, "find", false);
3834
+ if(priv._find_current && (priv._focus_ids || []).indexOf(priv._find_current) < 0) {
3835
+ priv._find_current = "";
3776
3836
  }
3837
+ priv._find_found = found;
3838
+ publish_find_result(gobj);
3839
+ }
3777
3840
 
3841
+ /************************************************************
3842
+ * The records matching `term`: the keys ON SCREEN, and how many
3843
+ * more match that are NOT -- folded away in the fold model, or in
3844
+ * a hidden topic. Those two are only counted: "0" alone would read
3845
+ * as "does not exist" when it means "is not on screen".
3846
+ ************************************************************/
3847
+ function find_matches(gobj, term, visible)
3848
+ {
3849
+ let priv = gobj.priv;
3778
3850
  let model = priv._fold_model;
3779
- let state = priv._fold_state;
3851
+ let found = {keys: [], folded: 0, hidden: 0};
3852
+ if(!model || empty_string(term)) {
3853
+ return found;
3854
+ }
3780
3855
 
3781
- /* Searched over the RECORDS, not the cards: most of the treedb is
3782
- * folded away, and a find that only read what was on screen would
3783
- * answer "nothing" for a device three levels down. */
3784
- let keys = [];
3785
- let hidden_matches = 0;
3786
- if(!empty_string(term)) {
3787
- let needle = String(term).toLowerCase();
3788
- let matches = (topic_name, record) => {
3789
- let label = "";
3790
- try {
3791
- label = node_label(priv.descs[topic_name] || {}, record) || "";
3792
- } catch(e) {
3793
- label = "";
3794
- }
3795
- let haystack = [
3796
- label,
3797
- record.id,
3798
- topic_name
3799
- ].filter((v) => typeof v === "string").join(" ").toLowerCase();
3800
- return haystack.includes(needle);
3801
- };
3802
- for(let node of model.nodes.values()) {
3803
- if(matches(node.topic_name, node.record || {})) {
3804
- keys.push(node.key);
3856
+ let needle = String(term).toLowerCase();
3857
+ let matches = (topic_name, record) => {
3858
+ let label = "";
3859
+ try {
3860
+ label = node_label(priv.descs[topic_name] || {}, record) || "";
3861
+ } catch(e) {
3862
+ label = "";
3863
+ }
3864
+ let haystack = [
3865
+ label,
3866
+ record.id,
3867
+ topic_name
3868
+ ].filter((v) => typeof v === "string").join(" ").toLowerCase();
3869
+ return haystack.includes(needle);
3870
+ };
3871
+
3872
+ for(let node of model.nodes.values()) {
3873
+ if(matches(node.topic_name, node.record || {})) {
3874
+ if(visible.has(node.key)) {
3875
+ found.keys.push(node.key);
3876
+ } else {
3877
+ found.folded++;
3805
3878
  }
3806
3879
  }
3807
- /* A hidden topic is searched too, and COUNTED apart: a term
3808
- * that only matches there would otherwise answer "0", which
3809
- * reads as "does not exist" when it means "is hidden". */
3810
- for(let topic_name of model.hidden) {
3811
- for(let record of (priv.records[topic_name] || [])) {
3812
- if(is_object(record) && matches(topic_name, record)) {
3813
- hidden_matches++;
3814
- }
3880
+ }
3881
+ for(let topic_name of model.hidden) {
3882
+ for(let record of (priv.records[topic_name] || [])) {
3883
+ if(is_object(record) && matches(topic_name, record)) {
3884
+ found.hidden++;
3815
3885
  }
3816
3886
  }
3817
3887
  }
3818
- priv._find_hidden_matches = hidden_matches;
3888
+ return found;
3889
+ }
3819
3890
 
3820
- /* Hidden matches are opened up to, one page of them at most: a
3821
- * single letter matches half the treedb, and unfolding half the
3822
- * treedb on a keystroke is the pile this whole thing exists to
3823
- * avoid. The count reported is of all the matches. */
3824
- let visible = fold_visible_set(model, state);
3825
- let revealed = 0;
3826
- for(let key of keys) {
3827
- if(visible.has(key)) {
3828
- continue;
3829
- }
3830
- if(revealed >= state.page_size) {
3831
- break;
3832
- }
3833
- fold_reveal(model, state, key);
3834
- revealed++;
3891
+ /************************************************************
3892
+ * The lit cards in reading order: top to bottom, then left to
3893
+ * right. The order Enter walks, and the one the "k/N" counts in.
3894
+ ************************************************************/
3895
+ function find_order(gobj)
3896
+ {
3897
+ let priv = gobj.priv;
3898
+ let graph = priv.graph;
3899
+ let ids = (priv._focus_ids || []).slice();
3900
+ if(!graph) {
3901
+ return ids;
3835
3902
  }
3836
3903
 
3837
- priv._focus_topic = null; /* a find takes over the highlight */
3904
+ let pos = {};
3905
+ for(let id of ids) {
3906
+ let p = null;
3907
+ try {
3908
+ p = graph.getElementPosition(id);
3909
+ } catch(e) {
3910
+ p = null;
3911
+ }
3912
+ pos[id] = p || [0, 0];
3913
+ }
3914
+ ids.sort((a, b) => (pos[a][1] - pos[b][1]) || (pos[a][0] - pos[b][0]));
3915
+ return ids;
3916
+ }
3838
3917
 
3839
- reconcile_fold(gobj, {}).then(() => {
3840
- apply_focus_ids(gobj, keys, "find");
3841
- });
3918
+ /************************************************************
3919
+ * Enter in the find box: centre the next lit card (`back`: the
3920
+ * previous one). The one camera move of the find, and the reader
3921
+ * asks for it: the highlight alone cannot take anybody to a match
3922
+ * that is outside the frame.
3923
+ ************************************************************/
3924
+ function graph_find_step(gobj, back)
3925
+ {
3926
+ let priv = gobj.priv;
3927
+ let graph = priv.graph;
3928
+ if(!graph || !priv._find_term) {
3929
+ return;
3930
+ }
3931
+ let ids = find_order(gobj);
3932
+ if(ids.length === 0) {
3933
+ return;
3934
+ }
3842
3935
 
3843
- return keys.length;
3936
+ let i = ids.indexOf(priv._find_current);
3937
+ if(i < 0) {
3938
+ i = back? ids.length - 1 : 0;
3939
+ } else {
3940
+ i = (i + (back? ids.length - 1 : 1)) % ids.length;
3941
+ }
3942
+ priv._find_current = ids[i];
3943
+ try {
3944
+ yui_graph_center_on(graph, ids[i]);
3945
+ } catch(e) {
3946
+ log_error(`${gobj_short_name(gobj)}: find step failed: ${e}`);
3947
+ }
3948
+ publish_find_result(gobj);
3844
3949
  }
3845
3950
 
3846
3951
  async function graph_zoom_in(gobj)
@@ -9648,6 +9753,7 @@ async function reconcile_fold_now(gobj, opts)
9648
9753
  history_resume(gobj);
9649
9754
  }
9650
9755
  publish_legend_state(gobj, visible);
9756
+ refresh_find(gobj, visible);
9651
9757
  return changed;
9652
9758
  }
9653
9759
 
@@ -10223,11 +10329,6 @@ function ac_load_data(gobj, event, kw, src)
10223
10329
  priv._pending_focus_all = false;
10224
10330
  graph_focus_topic(gobj, ft, all);
10225
10331
  }
10226
- if(priv._pending_find !== null) {
10227
- let term = priv._pending_find;
10228
- priv._pending_find = null;
10229
- publish_find_result(gobj, term, graph_find_nodes(gobj, term));
10230
- }
10231
10332
  });
10232
10333
  }
10233
10334
 
@@ -11054,16 +11155,30 @@ function ac_node_dblclick(gobj, event, kw, src)
11054
11155
  }
11055
11156
 
11056
11157
  /************************************************************
11057
- * Tell whoever asked how many nodes the term matched. The count is
11058
- * the answer to the question the box asks; without it "nothing moved"
11059
- * and "nothing matched" look the same.
11158
+ * Tell whoever asked how many cards the term lit. The count is the
11159
+ * answer to the question the box asks; without it "nothing lit" and
11160
+ * "nothing matched" look the same.
11161
+ *
11162
+ * matches lit cards (on screen)
11163
+ * folded_matches matches folded away, not on screen
11164
+ * hidden_matches matches in hidden topics
11165
+ * current 1-based place of the card Enter centred on
11166
+ * among the lit ones (reading order), 0 = none
11060
11167
  ************************************************************/
11061
- function publish_find_result(gobj, term, matches)
11168
+ function publish_find_result(gobj)
11062
11169
  {
11170
+ let priv = gobj.priv;
11171
+ let found = priv._find_found || {keys: [], folded: 0, hidden: 0};
11172
+ let current = 0;
11173
+ if(priv._find_term && priv._find_current) {
11174
+ current = find_order(gobj).indexOf(priv._find_current) + 1;
11175
+ }
11063
11176
  gobj_publish_event(gobj, "EV_FIND_RESULT", {
11064
- term: term,
11065
- matches: matches,
11066
- hidden_matches: gobj.priv._find_hidden_matches || 0,
11177
+ term: priv._find_term,
11178
+ matches: priv._find_term? (priv._focus_ids || []).length : 0,
11179
+ folded_matches: found.folded,
11180
+ hidden_matches: found.hidden,
11181
+ current: current,
11067
11182
  });
11068
11183
  }
11069
11184
 
@@ -11072,9 +11187,16 @@ function publish_find_result(gobj, term, matches)
11072
11187
  ************************************************************/
11073
11188
  function ac_find_nodes(gobj, event, kw, src)
11074
11189
  {
11075
- let term = (kw && kw.text) || "";
11076
- let matches = graph_find_nodes(gobj, term);
11077
- publish_find_result(gobj, term, matches);
11190
+ graph_find_nodes(gobj, (kw && kw.text) || "");
11191
+ return 0;
11192
+ }
11193
+
11194
+ /************************************************************
11195
+ * Enter in the find box: {back: true} for Shift+Enter.
11196
+ ************************************************************/
11197
+ function ac_find_next(gobj, event, kw, src)
11198
+ {
11199
+ graph_find_step(gobj, !!(kw && kw.back));
11078
11200
  return 0;
11079
11201
  }
11080
11202
 
@@ -11636,6 +11758,7 @@ function create_gclass(gclass_name)
11636
11758
  ["EV_ZOOM_SELECTION", ac_zoom_selection, null],
11637
11759
  ["EV_FOCUS_TOPIC", ac_focus_topic, null],
11638
11760
  ["EV_FIND_NODES", ac_find_nodes, null],
11761
+ ["EV_FIND_NEXT", ac_find_next, null],
11639
11762
  ["EV_TOGGLE_FOLD", ac_toggle_fold, null],
11640
11763
  ["EV_SHOW_MORE", ac_show_more, null],
11641
11764
  ["EV_SET_FOLD_LEVEL", ac_set_fold_level, null],
@@ -11699,6 +11822,7 @@ function create_gclass(gclass_name)
11699
11822
  ["EV_ZOOM_SELECTION", 0],
11700
11823
  ["EV_FOCUS_TOPIC", 0],
11701
11824
  ["EV_FIND_NODES", 0],
11825
+ ["EV_FIND_NEXT", 0],
11702
11826
  ["EV_FIND_RESULT", event_flag_t.EVF_OUTPUT_EVENT],
11703
11827
  ["EV_LAYOUT_AUTOSET", event_flag_t.EVF_OUTPUT_EVENT],
11704
11828
  /* Optional: a host that does not persist the viewport is not
package/src/c_yui_form.js CHANGED
@@ -61,7 +61,8 @@ import {
61
61
  yui_file_control
62
62
  } from "./yui_file_field.js";
63
63
  import {
64
- yui_asset_id
64
+ yui_asset_id,
65
+ yui_asset_release
65
66
  } from "./yui_asset.js";
66
67
 
67
68
  import "./c_yui_form.css";
@@ -192,6 +193,9 @@ function mt_create(gobj)
192
193
  ************************************************************/
193
194
  function mt_destroy(gobj)
194
195
  {
196
+ /* A file preview holds blob urls, which keep their bytes alive
197
+ * until revoked -- and removing the DOM revokes nothing. */
198
+ yui_asset_release(gobj_read_attr(gobj, "$container"));
195
199
  destroy_ui(gobj);
196
200
  }
197
201
 
@@ -3155,6 +3159,51 @@ function ac_clear_record(gobj, event, kw, src)
3155
3159
  return 0;
3156
3160
  }
3157
3161
 
3162
+ /************************************************************
3163
+ * From the host: what it fetched for the asset a `file` column
3164
+ * names -- the `get-asset` answer, or why there is none.
3165
+ * {
3166
+ * name: the column
3167
+ * id: the asset id the answer is for
3168
+ * answer: the get-asset answer, or null
3169
+ * error: why there is no answer, when there is none
3170
+ * }
3171
+ *
3172
+ * Kept on the control and drawn only while the column still names
3173
+ * THAT id: an answer landing after the person picked another file
3174
+ * or cleared the column must not bring the old picture back.
3175
+ ************************************************************/
3176
+ function ac_set_file_preview(gobj, event, kw, src)
3177
+ {
3178
+ let $container = gobj_read_attr(gobj, "$container");
3179
+ let $form = $container? $container.querySelector('form'): null;
3180
+ if(!$form) {
3181
+ log_error(`${gobj_short_name(gobj)}: no form to show the file preview in`);
3182
+ return -1;
3183
+ }
3184
+
3185
+ let $file = null;
3186
+ $form.querySelectorAll('.FILE_FIELD').forEach(($el) => {
3187
+ if($el.dataset.treedb_name === kw.name) {
3188
+ $file = $el;
3189
+ }
3190
+ });
3191
+ if(!$file) {
3192
+ log_error(`${gobj_short_name(gobj)}: no file column '${kw.name}' in the form`);
3193
+ return -1;
3194
+ }
3195
+
3196
+ $file.yui_file_stored = {
3197
+ id: kw.id || "",
3198
+ answer: kw.answer || null,
3199
+ error: kw.error || ""
3200
+ };
3201
+ if($file.yui_file_render) {
3202
+ $file.yui_file_render();
3203
+ }
3204
+ return 0;
3205
+ }
3206
+
3158
3207
  /************************************************************
3159
3208
  * From internal form button for new rows
3160
3209
  * or from load_tabulator_data() to load data from backend
@@ -3390,6 +3439,7 @@ function create_gclass(gclass_name)
3390
3439
  ["EV_RECORD_CHANGED", ac_record_changed, null],
3391
3440
  ["EV_COPY_RECORD", ac_copy_record, null],
3392
3441
  ["EV_PASTE_RECORD", ac_paste_record, null],
3442
+ ["EV_SET_FILE_PREVIEW", ac_set_file_preview, null],
3393
3443
 
3394
3444
  ["EV_WINDOW_MOVED", null, null],
3395
3445
  ["EV_WINDOW_RESIZED", null, null],
@@ -3413,6 +3463,7 @@ function create_gclass(gclass_name)
3413
3463
  ["EV_RECORD_CHANGED", 0],
3414
3464
  ["EV_COPY_RECORD", 0],
3415
3465
  ["EV_PASTE_RECORD", 0],
3466
+ ["EV_SET_FILE_PREVIEW", 0],
3416
3467
 
3417
3468
  ["EV_WINDOW_MOVED", 0],
3418
3469
  ["EV_WINDOW_RESIZED", 0],