@humandialog/forms.svelte 0.5.11 → 0.5.13

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.
@@ -3,6 +3,7 @@ export declare class rCombo_item {
3
3
  Name: string | undefined;
4
4
  Avatar: string | undefined;
5
5
  Color: string | undefined;
6
+ Icon: any | undefined;
6
7
  }
7
8
  export declare class rCombo_definition {
8
9
  source: rCombo_item[];
@@ -13,5 +14,6 @@ export declare class rCombo_definition {
13
14
  element_key: string | undefined;
14
15
  element_name: string | undefined;
15
16
  element_avatar: string | undefined;
17
+ element_icon: string | undefined;
16
18
  }
17
19
  export declare const cached_sources: Map<string, Promise<object>>;
@@ -4,11 +4,13 @@ export let key = void 0;
4
4
  export let name = void 0;
5
5
  export let avatar = void 0;
6
6
  export let color = void 0;
7
+ export let icon = void 0;
7
8
  let definition = getContext("rCombo-definition");
8
9
  let item = new rCombo_item();
9
10
  item.Name = name;
10
11
  item.Key = key;
11
12
  item.Avatar = avatar;
12
13
  item.Color = color;
14
+ item.Icon = icon;
13
15
  definition.source.push(item);
14
16
  </script>
@@ -5,6 +5,7 @@ declare const __propDef: {
5
5
  name?: string | undefined;
6
6
  avatar?: string | undefined;
7
7
  color?: string | undefined;
8
+ icon?: any | undefined;
8
9
  };
9
10
  events: {
10
11
  [evt: string]: CustomEvent<any>;
@@ -3,6 +3,7 @@ export class rCombo_item {
3
3
  Name;
4
4
  Avatar; //url to avatar
5
5
  Color;
6
+ Icon = undefined;
6
7
  }
7
8
  export class rCombo_definition {
8
9
  source = [];
@@ -13,5 +14,6 @@ export class rCombo_definition {
13
14
  element_key;
14
15
  element_name;
15
16
  element_avatar;
17
+ element_icon;
16
18
  }
17
19
  export const cached_sources = new Map();
@@ -6,6 +6,7 @@ export let onCollect = void 0;
6
6
  export let key = "";
7
7
  export let name = "";
8
8
  export let avatar = "";
9
+ export let icon = "";
9
10
  let definition = getContext("rCombo-definition");
10
11
  definition.collection_expr = association;
11
12
  definition.collection_path = path;
@@ -14,4 +15,5 @@ definition.onCollect = onCollect;
14
15
  definition.element_key = key;
15
16
  definition.element_name = name;
16
17
  definition.element_avatar = avatar;
18
+ definition.element_icon = icon;
17
19
  </script>
@@ -8,6 +8,7 @@ declare const __propDef: {
8
8
  key?: string | undefined;
9
9
  name?: string | undefined;
10
10
  avatar?: string | undefined;
11
+ icon?: string | undefined;
11
12
  };
12
13
  events: {
13
14
  [evt: string]: CustomEvent<any>;
@@ -84,6 +84,14 @@ function setup(...args) {
84
84
  item = self ?? $contextItemsStore[ctx];
85
85
  if (!typename)
86
86
  typename = $contextTypesStore[ctx];
87
+ if (!typename) {
88
+ if (item.$type)
89
+ typename = item.$type;
90
+ else if (item.$ref) {
91
+ let s2 = item.$ref.split("/");
92
+ typename = s2[1];
93
+ }
94
+ }
87
95
  if (!label)
88
96
  label = a;
89
97
  tick_request_internal = tick_request_internal + 1;
@@ -236,6 +244,7 @@ function selected_item(itm, a2) {
236
244
  let found = definition.source.find((e) => e.Key == choosed_value);
237
245
  if (!found)
238
246
  found = definition.source.find((e) => e.Name == choosed_value);
247
+ console.log("found: ", found);
239
248
  if (found)
240
249
  return found;
241
250
  else
@@ -493,6 +502,8 @@ function source_fetched(source) {
493
502
  if (icon) {
494
503
  if (definition.element_avatar)
495
504
  el.Avatar = e[definition.element_avatar];
505
+ else if (definition.element_icon)
506
+ el.Icon = e[definition.element_icon];
496
507
  else
497
508
  el.Avatar = e.$icon;
498
509
  }
@@ -541,6 +552,10 @@ function on_focus_out(e) {
541
552
  {#if icon && sel_item}
542
553
  {#if sel_item.Color}
543
554
  <Icon size={5} circle={true} color={sel_item.Color}/>
555
+ {:else if sel_item.Icon}
556
+ <Icon size={4} component={sel_item.Icon}/>
557
+ {:else if sel_item.Icon == null}
558
+ <div class="w-4 h-4"></div>
544
559
  {:else}
545
560
  <Icon size={5} circle={true} symbol={sel_item.Avatar} label={sel_item.Name}/>
546
561
  {/if}
@@ -591,6 +606,10 @@ function on_focus_out(e) {
591
606
  <Icon size={5} circle={true} color={item.Color}/>
592
607
  {:else if item.Avatar}
593
608
  <Icon size={5} circle={true} symbol={item.Avatar}/>
609
+ {:else if item.Icon}
610
+ <Icon size={4} component={item.Icon}/>
611
+ {:else if item.Icon == null}
612
+ <div class="w-4 h-4"></div>
594
613
  {:else if item.Name}
595
614
  <Icon size={5} circle={true} label={item.Name}/>
596
615
  {:else}
@@ -12,7 +12,10 @@ $:
12
12
  </script>
13
13
 
14
14
  <!-- svelte-ignore a11y-click-events-have-key-events -->
15
- <div id={id} class="font-medium m-0 p-0 text-sm w-full text-left flex flew-row cursor-context-menu {active_class} {cl}" on:click on:mousemove>
15
+ <div id={id} class="font-medium m-0 p-0 text-sm w-full text-left flex flew-row cursor-context-menu {active_class} {cl}"
16
+ on:click
17
+ on:mousemove
18
+ on:mousedown>
16
19
  <div class="flex items-center justify-center" style:width={`${icon_placeholder_size*0.25}rem`}>
17
20
  {#if cmd.icon}
18
21
  <Icon size={icon_size} component={cmd.icon}/>
@@ -11,6 +11,7 @@ declare const __propDef: {
11
11
  events: {
12
12
  click: MouseEvent;
13
13
  mousemove: MouseEvent;
14
+ mousedown: MouseEvent;
14
15
  } & {
15
16
  [evt: string]: CustomEvent<any>;
16
17
  };
@@ -2,6 +2,8 @@
2
2
  <script>import { tick } from "svelte";
3
3
  import Pallete_row from "./palette.row.svelte";
4
4
  import { createEventDispatcher } from "svelte";
5
+ import Icon from "../../icon.svelte";
6
+ import { isDeviceSmallerThan } from "../../../utils.js";
5
7
  export let commands;
6
8
  export let width_px = 400;
7
9
  export let max_height_px = 500;
@@ -9,8 +11,22 @@ let visible = false;
9
11
  let css_style = "";
10
12
  let filtered_commands = [...commands];
11
13
  let current_command = filtered_commands.length ? filtered_commands[0] : null;
14
+ let isToolbox = false;
12
15
  const dispatch = createEventDispatcher();
16
+ let toolboxX;
17
+ let toolboxY;
18
+ export function showAsToolbox(rect) {
19
+ isToolbox = true;
20
+ visible = true;
21
+ const margin = 15;
22
+ const windowWidth = window.innerWidth - 2 * margin;
23
+ toolboxX = margin;
24
+ toolboxY = rect.bottom + margin;
25
+ css_style = `position: absolute; left:${toolboxX}px; top:${toolboxY}px;`;
26
+ dispatch("palette_shown");
27
+ }
13
28
  export function show(x, y, up = false) {
29
+ isToolbox = false;
14
30
  css_style = `width: ${width_px}px; max-height:${max_height_px}px; position: fixed; left:${x}px; top:${y}px;`;
15
31
  if (up)
16
32
  css_style += " transform: translate(0, -100%);";
@@ -18,6 +34,7 @@ export function show(x, y, up = false) {
18
34
  dispatch("palette_shown");
19
35
  }
20
36
  export function show_fullscreen(_width_px, _height_px) {
37
+ isToolbox = false;
21
38
  width_px = _width_px;
22
39
  max_height_px = _height_px;
23
40
  css_style = `position: fixed; left: 0px; top: 0px; width: ${_width_px}px; height: ${_height_px}px; z-index: 1055;`;
@@ -26,6 +43,7 @@ export function show_fullscreen(_width_px, _height_px) {
26
43
  }
27
44
  export function hide() {
28
45
  visible = false;
46
+ isToolbox = false;
29
47
  dispatch("palette_hidden");
30
48
  }
31
49
  export function execute_selected_command() {
@@ -84,7 +102,6 @@ async function execute_mouse_click(on_choice) {
84
102
  if (!visible)
85
103
  return;
86
104
  dispatch("palette_mouse_click");
87
- await tick();
88
105
  hide();
89
106
  on_choice();
90
107
  }
@@ -100,24 +117,87 @@ function update_current_command(cmd) {
100
117
  rows.find((r) => r.cmd == cmd).is_highlighted = true;
101
118
  current_command = cmd;
102
119
  }
120
+ function buttonMousedown(e) {
121
+ e.preventDefault();
122
+ }
123
+ let isMoving = false;
124
+ let beforeTrackingClient = null;
125
+ let beforeTrackingPos = null;
126
+ function mousedown(e) {
127
+ if (e.touches.length != 1)
128
+ return;
129
+ const touch = e.touches.item(0);
130
+ beforeTrackingClient = new DOMPoint(touch.clientX, touch.clientY);
131
+ beforeTrackingPos = new DOMPoint(toolboxX, toolboxY);
132
+ isMoving = true;
133
+ }
134
+ function mousemove(e) {
135
+ if (isMoving && beforeTrackingClient && beforeTrackingPos) {
136
+ if (e.touches.length != 1)
137
+ return;
138
+ const touch = e.touches.item(0);
139
+ const trackDelta = new DOMPoint(touch.clientX - beforeTrackingClient.x, touch.clientY - beforeTrackingClient.y);
140
+ toolboxX = beforeTrackingPos.x + trackDelta.x;
141
+ css_style = `position: fixed; left:${toolboxX}px; top:${toolboxY}px;`;
142
+ }
143
+ }
144
+ function mouseup(e) {
145
+ isMoving = false;
146
+ beforeTrackingClient = null;
147
+ beforeTrackingPos = null;
148
+ }
103
149
  </script>
104
150
 
105
- <div class="not-prose bg-white dark:bg-stone-800 text-stone-500 dark:text-stone-400 rounded-lg border border-stone-200 dark:border-stone-700 shadow-md z-30"
106
- hidden={!visible}
107
- style={css_style}>
108
- {#if filtered_commands && filtered_commands.length}
109
- {#each filtered_commands as cmd, idx (cmd.caption)}
110
- {@const id = "cpi_" + idx}
111
- <Pallete_row {id}
112
- cmd={cmd}
113
- is_highlighted={cmd == current_command}
114
- on:click={ () => { execute_mouse_click(cmd.on_choice); }}
115
- on:mousemove={ () => { on_mouse_over(cmd); }}
116
- bind:this={rows[idx]}
117
- />
118
- {/each}
119
- {:else}
120
- <p class="text-sm text-stone-500">No results</p>
121
- {/if}
151
+ {#if isToolbox}
152
+ <menu class=" not-prose bg-white dark:bg-stone-800 text-stone-500 dark:text-stone-400 rounded-lg border border-stone-200 dark:border-stone-700 shadow-md
153
+ z-30
154
+ flex flex-row flex-nowrap"
155
+ style={css_style}
156
+ hidden={!visible}
157
+ on:touchstart={mousedown}
158
+ on:touchmove={mousemove}
159
+ on:touchend={mouseup}>
160
+ {#if filtered_commands && filtered_commands.length}
161
+ {#each filtered_commands as cmd, idx (cmd.caption)}
162
+ {@const id = "cpi_" + idx}
163
+ {@const mobile = isDeviceSmallerThan("sm")}
164
+ {@const icon_placeholder_size = mobile ? 12 : 10}
165
+ <button class="font-medium m-0 py-2 pr-4 text-lg sm:text-sm w-full text-left flex flex-row cursor-context-menu focus:outline-none"
166
+ {id}
167
+ bind:this={rows[idx]}
168
+ on:click={ () => { execute_mouse_click(cmd.on_choice); }}
169
+ on:mousedown={buttonMousedown}>
170
+
171
+ <div class="flex items-center justify-center mt-1 sm:mt-0.5" style:width={`${icon_placeholder_size*0.25}rem`}>
172
+ {#if cmd.icon}
173
+ {@const cc = mobile ? 7 : 6}
174
+ {@const icon_size = icon_placeholder_size - cc}
175
+ <Icon size={icon_size} component={cmd.icon}/>
176
+ {/if}
177
+ </div>
178
+ </button>
179
+ {/each}
180
+ {/if}
181
+ </menu>
182
+ {:else}
183
+ <div class="not-prose bg-white dark:bg-stone-800 text-stone-500 dark:text-stone-400 rounded-lg border border-stone-200 dark:border-stone-700 shadow-md z-30"
184
+ hidden={!visible}
185
+ style={css_style}>
186
+ {#if filtered_commands && filtered_commands.length}
187
+ {#each filtered_commands as cmd, idx (cmd.caption)}
188
+ {@const id = "cpi_" + idx}
189
+ <Pallete_row {id}
190
+ cmd={cmd}
191
+ is_highlighted={cmd == current_command}
192
+ on:click={ () => { execute_mouse_click(cmd.on_choice); }}
193
+ on:mousemove={ () => { on_mouse_over(cmd); }}
194
+ on:mousedown={buttonMousedown}
195
+ bind:this={rows[idx]}
196
+ />
197
+ {/each}
198
+ {:else}
199
+ <p class="text-sm text-stone-500">No results</p>
200
+ {/if}
122
201
 
123
- </div>
202
+ </div>
203
+ {/if}
@@ -5,6 +5,7 @@ declare const __propDef: {
5
5
  commands: Document_command[];
6
6
  width_px?: number | undefined;
7
7
  max_height_px?: number | undefined;
8
+ showAsToolbox?: ((rect: DOMRect) => void) | undefined;
8
9
  show?: ((x: number, y: number, up?: boolean) => void) | undefined;
9
10
  show_fullscreen?: ((_width_px: number, _height_px: number) => void) | undefined;
10
11
  hide?: (() => void) | undefined;
@@ -20,12 +21,13 @@ declare const __propDef: {
20
21
  } & {
21
22
  [evt: string]: CustomEvent<any>;
22
23
  };
23
- slots: {}; /**accessor*/
24
+ slots: {};
24
25
  };
25
26
  export type PaletteProps = typeof __propDef.props;
26
27
  export type PaletteEvents = typeof __propDef.events;
27
28
  export type PaletteSlots = typeof __propDef.slots;
28
29
  export default class Palette extends SvelteComponentTyped<PaletteProps, PaletteEvents, PaletteSlots> {
30
+ get showAsToolbox(): (rect: DOMRect) => void;
29
31
  get show(): (x: number, y: number, up?: boolean) => void;
30
32
  get show_fullscreen(): (_width_px: number, _height_px: number) => void;
31
33
  get hide(): () => void;
@@ -63,5 +65,8 @@ export default class Palette extends SvelteComponentTyped<PaletteProps, PaletteE
63
65
  get undefined(): any;
64
66
  /**accessor*/
65
67
  set undefined(_: any);
68
+ get undefined(): any;
69
+ /**accessor*/
70
+ set undefined(_: any);
66
71
  }
67
72
  export {};
@@ -1,9 +1,9 @@
1
1
  <script>import { Selection_helper } from "./internal/Selection_helper";
2
- import { getContext, onDestroy, onMount } from "svelte";
2
+ import { afterUpdate, getContext, onDestroy, onMount } from "svelte";
3
3
  import { Selection_range, Selection_edge } from "./internal/Selection_range";
4
4
  import { data_tick_store, contextItemsStore, contextTypesStore } from "../../stores.js";
5
5
  import { informModification, pushChanges } from "../../updates.js";
6
- import { parseWidthDirective } from "../../utils.js";
6
+ import { isDeviceSmallerThan, parseWidthDirective } from "../../utils.js";
7
7
  import Palette from "./internal/palette.svelte";
8
8
  import FaFont from "svelte-icons/fa/FaFont.svelte";
9
9
  import FaRemoveFormat from "svelte-icons/fa/FaRemoveFormat.svelte";
@@ -13,6 +13,7 @@ import FaComments from "svelte-icons/fa/FaComment.svelte";
13
13
  import FaQuote from "svelte-icons/fa/FaQuoteRight.svelte";
14
14
  import FaWarn from "svelte-icons/fa/FaExclamationTriangle.svelte";
15
15
  import FaInfo from "svelte-icons/fa/FaInfo.svelte";
16
+ import { showMenu } from "../menu";
16
17
  export let value = "";
17
18
  export let placeholder = "";
18
19
  export let self = null;
@@ -62,8 +63,12 @@ else
62
63
  px-2.5`;
63
64
  let last_tick = -1;
64
65
  $: {
65
- if (last_tick < $data_tick_store)
66
- setup_source();
66
+ if (last_tick < $data_tick_store) {
67
+ if (has_changed_value)
68
+ saveData();
69
+ else
70
+ setup_source();
71
+ }
67
72
  }
68
73
  function setup_source() {
69
74
  last_tick = $data_tick_store;
@@ -80,6 +85,8 @@ function setup_source() {
80
85
  if (!value)
81
86
  value = "<p>\u200B</p>";
82
87
  has_changed_value = false;
88
+ if (stored_selection)
89
+ set_selection(stored_selection);
83
90
  }
84
91
  onMount(() => {
85
92
  if (!editable_div)
@@ -95,6 +102,9 @@ onMount(() => {
95
102
  subtree: true
96
103
  });
97
104
  }
105
+ return () => {
106
+ on_blur();
107
+ };
98
108
  });
99
109
  onDestroy(() => {
100
110
  if (!editable_div)
@@ -166,9 +176,12 @@ const on_keydown = (event) => {
166
176
  } else {
167
177
  event.cancelBubble = true;
168
178
  event.preventDefault();
169
- if (is_multiline())
170
- insert_new_line();
171
- else
179
+ if (is_multiline()) {
180
+ if (event.shiftKey)
181
+ insert_character_at_caret_position("\n");
182
+ else
183
+ insert_new_line();
184
+ } else
172
185
  move_cursor_to_next_editable_element();
173
186
  }
174
187
  break;
@@ -176,6 +189,11 @@ const on_keydown = (event) => {
176
189
  store_node_text_and_position();
177
190
  show_command_palette();
178
191
  break;
192
+ case "Tab":
193
+ event.cancelBubble = true;
194
+ event.preventDefault();
195
+ insert_character_at_caret_position(" ");
196
+ break;
179
197
  case "Backspace":
180
198
  if (is_range_selected()) {
181
199
  if (is_multi_range_selection()) {
@@ -715,7 +733,7 @@ function set_tag_and_class_for_paragraph(node, tag, css_class) {
715
733
  while (node && node.nodeType !== Node.ELEMENT_NODE)
716
734
  node = node.parentNode;
717
735
  if (!node)
718
- return;
736
+ return false;
719
737
  let element = node;
720
738
  if (element.tagName == tag.toUpperCase()) {
721
739
  element.className = css_class;
@@ -737,7 +755,7 @@ function set_tag_and_class_for_paragraph(node, tag, css_class) {
737
755
  function do_format(tag, css_class) {
738
756
  const elem = editable_div;
739
757
  const editableElem = editable_div;
740
- let stored_selection = Selection_helper.get_selection(elem);
758
+ let stored_selection2 = Selection_helper.get_selection(elem);
741
759
  let sel = window.getSelection();
742
760
  let should_restore_selection = false;
743
761
  if (sel.isCollapsed || sel.focusNode === sel.anchorNode) {
@@ -766,7 +784,28 @@ function do_format(tag, css_class) {
766
784
  } while (node);
767
785
  }
768
786
  if (should_restore_selection) {
769
- const range = Selection_helper.create_range(editableElem, stored_selection.begin.absolute_index, stored_selection.end.absolute_index);
787
+ const range = Selection_helper.create_range(editableElem, stored_selection2.begin.absolute_index, stored_selection2.end.absolute_index);
788
+ set_selection(range);
789
+ }
790
+ }
791
+ function insert_character_at_caret_position(what_to_insert) {
792
+ if (is_range_selected()) {
793
+ console.log("Unsupported");
794
+ return false;
795
+ } else {
796
+ let sel;
797
+ sel = window.getSelection();
798
+ const focusNode = sel.focusNode;
799
+ const wholeText = focusNode.textContent;
800
+ const carretPos = sel.focusOffset;
801
+ let leftPart = wholeText.substring(0, carretPos);
802
+ let rightPart = wholeText.substring(carretPos);
803
+ focusNode.textContent = leftPart + what_to_insert + rightPart;
804
+ let pos = carretPos + 1;
805
+ let range = new Range();
806
+ range.collapse(true);
807
+ range.setStart(focusNode, pos);
808
+ range.setEnd(focusNode, pos);
770
809
  set_selection(range);
771
810
  }
772
811
  }
@@ -879,7 +918,10 @@ function show_command_palette() {
879
918
  y = rect.y - (preferred_palette_height - bottom_space);
880
919
  } else
881
920
  show_fullscreen = true;
882
- if (show_fullscreen)
921
+ const isSmallDevice = isDeviceSmallerThan("sm");
922
+ if (isSmallDevice) {
923
+ palette.showAsToolbox(rect);
924
+ } else if (show_fullscreen)
883
925
  palette.show_fullscreen(client_rect.width, client_rect.height);
884
926
  else
885
927
  palette.show(x, y, show_above);
@@ -899,25 +941,18 @@ function navigate_command_palette(key) {
899
941
  else if (key == "ArrowUp")
900
942
  palette.navigate_up();
901
943
  }
944
+ let stored_selection = void 0;
902
945
  function on_selection_changed() {
946
+ let active_range = Selection_helper.get_selection(editable_div);
947
+ stored_selection = window.getSelection()?.getRangeAt(0);
903
948
  }
904
949
  let intervalId = 0;
905
950
  function on_focus() {
906
- if (pushChangesImmediately) {
907
- intervalId = setInterval(
908
- () => {
909
- saveData();
910
- },
911
- 2e3
912
- );
913
- }
914
951
  }
915
952
  function on_blur() {
916
953
  let active_range = Selection_helper.get_selection(editable_div);
917
- if (intervalId) {
918
- clearInterval(intervalId);
919
- intervalId = 0;
920
- }
954
+ console.log("rich.edit: on_blur", active_range?.begin?.absolute_index);
955
+ stored_selection = void 0;
921
956
  if (onBlur) {
922
957
  onBlur();
923
958
  onBlur = void 0;
@@ -927,6 +962,12 @@ function on_blur() {
927
962
  $data_tick_store = last_tick;
928
963
  }
929
964
  }
965
+ export function save() {
966
+ if (saveData()) {
967
+ last_tick = $data_tick_store + 1;
968
+ $data_tick_store = last_tick;
969
+ }
970
+ }
930
971
  function saveData() {
931
972
  if (item && a && has_changed_value) {
932
973
  item[a] = changed_value;
@@ -948,7 +989,10 @@ let commands = [
948
989
  // { caption: 'Heading 1', description: 'Big section heading', tags: 'h1', icon: FaHead, icon_size: 6, on_choice: () => { do_format('h2', '') } } ,
949
990
  // { caption: 'Heading 2', description: 'Medium section heading', tags: 'h2', icon: FaHead, icon_size: 5, on_choice: () => { do_format('h3', '') } } ,
950
991
  // { caption: 'Heading 3', description: 'Small section heading', tags: 'h3', icon: FaHead, icon_size: 4, on_choice: () => { do_format('h4', '') } } ,
951
- { caption: "Heading", description: "Description heading", tags: "h2", icon: FaHead, icon_size: 6, on_choice: () => {
992
+ { caption: "Heading 1", description: "Description heading", tags: "h1", icon: FaHead, icon_size: 6, on_choice: () => {
993
+ do_format("h1", "");
994
+ } },
995
+ { caption: "Heading 2", description: "Description heading", tags: "h2", icon: FaHead, icon_size: 6, on_choice: () => {
952
996
  do_format("h2", "");
953
997
  } },
954
998
  // { caption: 'Heading 2', description: 'Medium description heading', tags: 'h3', icon: FaHead, icon_size: 5, on_choice: () => { do_format('h3', '') } } ,
@@ -977,7 +1021,7 @@ let commands = [
977
1021
  on:keydown={on_keydown}
978
1022
  on:mouseup={on_mouseup}
979
1023
  class="{cs} {appearance_class}
980
- prose prose-base sm:prose-base dark:prose-invert {additional_class} overflow-y-auto"
1024
+ prose prose-base sm:prose-base dark:prose-invert {additional_class} overflow-y-auto whitespace-pre-wrap"
981
1025
  on:blur={on_blur}
982
1026
  on:focus={on_focus}
983
1027
  on:focus
@@ -13,6 +13,7 @@ declare const __propDef: {
13
13
  pushChangesImmediately?: boolean | undefined;
14
14
  run?: ((onStop?: undefined) => void) | undefined;
15
15
  getFormattingOperations?: ((withCaptions?: boolean) => any[]) | undefined;
16
+ save?: (() => void) | undefined;
16
17
  };
17
18
  events: {
18
19
  focus: FocusEvent;
@@ -28,5 +29,6 @@ export type RichSlots = typeof __propDef.slots;
28
29
  export default class Rich extends SvelteComponentTyped<RichProps, RichEvents, RichSlots> {
29
30
  get run(): (onStop?: undefined) => void;
30
31
  get getFormattingOperations(): (withCaptions?: boolean) => any[];
32
+ get save(): () => void;
31
33
  }
32
34
  export {};
@@ -16,6 +16,8 @@ export declare class rKanban_definition {
16
16
  titleAttrib: string;
17
17
  titleOnChange: Function | undefined;
18
18
  titleReadOnly: boolean;
19
+ titleHref: string | undefined;
20
+ titleHrefFunc: Function | undefined;
19
21
  summaryAttrib: string;
20
22
  summaryOnChange: Function | undefined;
21
23
  summaryReadOnly: boolean;
@@ -4,7 +4,7 @@ export const KanbanCardBottom = 3;
4
4
  export class rKanban_column {
5
5
  id;
6
6
  title = '';
7
- width = 'w-[240px]';
7
+ width = '';
8
8
  state = '';
9
9
  finishing = false;
10
10
  operations = undefined;
@@ -15,6 +15,8 @@ export class rKanban_definition {
15
15
  titleAttrib = '';
16
16
  titleOnChange = undefined;
17
17
  titleReadOnly = false;
18
+ titleHref = undefined;
19
+ titleHrefFunc = undefined;
18
20
  summaryAttrib = '';
19
21
  summaryOnChange = undefined;
20
22
  summaryReadOnly = false;
@@ -1,4 +1,5 @@
1
1
  <script>import { getContext, tick } from "svelte";
2
+ import { push } from "svelte-spa-router";
2
3
  import {
3
4
  contextItemsStore,
4
5
  isActive,
@@ -8,7 +9,8 @@ import {
8
9
  editable,
9
10
  showFloatingToolbar,
10
11
  informModification,
11
- pushChanges
12
+ pushChanges,
13
+ startEditing
12
14
  } from "../../..";
13
15
  import { FaArrowsAlt, FaTrash, FaPlus, FaExternalLinkAlt } from "svelte-icons/fa";
14
16
  import MoveOperations from "./kanban.move.menu.svelte";
@@ -24,6 +26,8 @@ $:
24
26
  selectedClass = isCardSelected ? "!border-blue-300" : "";
25
27
  $:
26
28
  focusedClass = isCardActive ? "bg-stone-100 dark:bg-stone-700" : "";
29
+ $:
30
+ isLinkLike = isCardActive && (!!definition.titleHref || !!definition.titleHrefFunc);
27
31
  function calculate_active(...args) {
28
32
  return isActive("props", item);
29
33
  }
@@ -96,9 +100,13 @@ let summaryElement;
96
100
  let summaryPlaceholder = false;
97
101
  export async function editProperty(field) {
98
102
  if (field == "Title") {
99
- titleElement.focus();
100
- await tick();
101
- setSelectionAtEnd(titleElement);
103
+ if (isLinkLike) {
104
+ startEditing(titleElement);
105
+ } else {
106
+ titleElement.focus();
107
+ await tick();
108
+ setSelectionAtEnd(titleElement);
109
+ }
102
110
  } else if (field == "Summary") {
103
111
  if (!!summaryElement) {
104
112
  summaryElement.focus();
@@ -127,6 +135,19 @@ export async function editProperty(field) {
127
135
  }
128
136
  }
129
137
  }
138
+ function followDefinedHRef() {
139
+ let link = getHRef();
140
+ if (link)
141
+ push(link);
142
+ }
143
+ function getHRef() {
144
+ if (definition.titleHref)
145
+ return definition.titleHref;
146
+ else if (definition.titleHrefFunc)
147
+ return definition.titleHrefFunc(item);
148
+ else
149
+ return "";
150
+ }
130
151
  </script>
131
152
 
132
153
  <!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -162,25 +183,43 @@ export async function editProperty(field) {
162
183
 
163
184
 
164
185
  {#if isCardActive}
165
- <h3 class=" text-lg font-semibold min-h-[1.75rem]
166
- sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
167
- whitespace-nowrap overflow-clip truncate w-full sm:flex-none
168
- relative"
169
- use:editable={{
170
- action: (text) => onTitleChanged(text),
171
- active: true,
172
- readonly: definition.titleReadOnly,
173
- onFinish: (d) => {titleElement.blur()}}}
174
- bind:this={titleElement}>
175
- {item[definition.titleAttrib]}
186
+ {#if isLinkLike}
187
+ <h3 class=" text-lg font-semibold min-h-[1.75rem]
188
+ sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
189
+ whitespace-nowrap overflow-clip truncate w-full sm:flex-none
190
+ relative
191
+ sm:hover:cursor-pointer underline"
192
+ on:click|stopPropagation={followDefinedHRef}
193
+ use:editable={{
194
+ action: (text) => onTitleChanged(text),
195
+ active: false,
196
+ readonly: definition.titleReadOnly,
197
+ onFinish: (d) => {titleElement.blur()}}}
198
+ bind:this={titleElement}>
199
+ {item[definition.titleAttrib]}
200
+ </h3>
201
+ {:else}
202
+ <h3 class=" text-lg font-semibold min-h-[1.75rem]
203
+ sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
204
+ whitespace-nowrap overflow-clip truncate w-full sm:flex-none
205
+ relative"
206
+ use:editable={{
207
+ action: (text) => onTitleChanged(text),
208
+ active: true,
209
+ readonly: definition.titleReadOnly,
210
+ onFinish: (d) => {titleElement.blur()}}}
211
+ bind:this={titleElement}>
212
+ {item[definition.titleAttrib]}
176
213
 
177
- {#if definition.onOpen}
178
- <button class="absolute top-1 right-0 w-5 h-5 sm:w-3 sm:h-3"
179
- on:click={(e) => definition.onOpen(item)}>
180
- <FaExternalLinkAlt/>
181
- </button>
214
+ {#if definition.onOpen}
215
+ <button class="absolute top-1 right-0 w-5 h-5 sm:w-3 sm:h-3"
216
+ on:click={(e) => definition.onOpen(item)}>
217
+ <FaExternalLinkAlt/>
218
+ </button>
219
+ {/if}
220
+ </h3>
182
221
  {/if}
183
- </h3>
222
+
184
223
  {:else}
185
224
  <h3 class=" text-lg font-semibold min-h-[1.75rem]
186
225
  sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
@@ -1,6 +1,6 @@
1
- <script>import { getContext, afterUpdate, tick } from "svelte";
1
+ <script>import { getContext, afterUpdate, tick, onMount } from "svelte";
2
2
  import { data_tick_store, contextItemsStore, contextTypesStore } from "../../../stores";
3
- import { getActive, activateItem, editable, isActive, isSelected, selectable, startEditing } from "../../../utils.js";
3
+ import { getActive, activateItem, editable, isActive, isSelected, selectable, startEditing, isDeviceSmallerThan } from "../../../utils.js";
4
4
  import Card from "./kanban.card.svelte";
5
5
  import { KanbanColumnTop, KanbanColumnBottom } from "../Kanban";
6
6
  import Inserter from "./kanban.inserter.svelte";
@@ -38,7 +38,6 @@ export function setBorder(what_to_do) {
38
38
  let definition = getContext("rKanban-definition");
39
39
  let columnDef = definition.columns[currentColumnIdx];
40
40
  let column_items = void 0;
41
- let width_class = columnDef.width ? `w-11/12 sm:${columnDef.width}` : "w-11/12 sm:w-[240px]";
42
41
  $:
43
42
  setup_data();
44
43
  $:
@@ -165,6 +164,25 @@ export function activate(e) {
165
164
  }
166
165
  );
167
166
  }
167
+ onMount(() => {
168
+ window.addEventListener("resize", onResizeWindow);
169
+ return () => {
170
+ window.removeEventListener("resize", onResizeWindow);
171
+ };
172
+ });
173
+ let styleWidth = getWidthStyle();
174
+ function onResizeWindow() {
175
+ styleWidth = getWidthStyle();
176
+ }
177
+ function getWidthStyle() {
178
+ const assumed_space = 800;
179
+ const default_column_width = Math.floor(assumed_space / definition.columns.length);
180
+ const column_width = columnDef.width ? columnDef.width : default_column_width;
181
+ if (window.innerWidth >= 640)
182
+ return `width: ${column_width}px; min-width: 180px; max-width: ${column_width}px;`;
183
+ else
184
+ return "width: 92%;";
185
+ }
168
186
  </script>
169
187
 
170
188
  <!-- svelte-ignore a11y-click-events-have-key-events -->
@@ -172,11 +190,11 @@ export function activate(e) {
172
190
  <section class=" snap-center
173
191
  sm:snap-align-none
174
192
  flex-none sm:flex-1
175
- sm:min-w-[180px] sm:max-w-[240px]
176
193
  sm:min-h-[calc(100vh-8rem)]
177
194
  min-h-[calc(100vh-5rem)]
178
195
  rounded-md border border-transparent
179
- {width_class} {selected_class} {focused_class}"
196
+ {selected_class} {focused_class}"
197
+ style={styleWidth}
180
198
  use:selectable={columnDef}
181
199
  on:click={activate}>
182
200
  <header class:cursor-pointer={!is_row_active && columnDef.operations} bind:this={headerElement}>
@@ -237,4 +255,5 @@ export function activate(e) {
237
255
  {/if}
238
256
 
239
257
  </ul>
240
- </section>
258
+ </section>
259
+
@@ -1,7 +1,7 @@
1
1
  <script>import { getContext } from "svelte";
2
2
  import { rKanban_column } from "./Kanban";
3
3
  export let title = "";
4
- export let width = "w-[240px]";
4
+ export let width = "";
5
5
  export let state = "";
6
6
  export let finishing = false;
7
7
  export let operations = void 0;
@@ -2,9 +2,13 @@
2
2
  export let a;
3
3
  export let onChange = void 0;
4
4
  export let readOnly = false;
5
+ export let href = void 0;
6
+ export let hrefFunc = void 0;
5
7
  let definition = getContext("rKanban-definition");
6
8
  definition.titleAttrib = a;
7
9
  definition.titleOnChange = onChange;
8
10
  definition.titleReadOnly = readOnly;
11
+ definition.titleHref = href;
12
+ definition.titleHrefFunc = hrefFunc;
9
13
  </script>
10
14
 
@@ -4,6 +4,8 @@ declare const __propDef: {
4
4
  a: string;
5
5
  onChange?: Function | undefined;
6
6
  readOnly?: boolean | undefined;
7
+ href?: string | undefined;
8
+ hrefFunc?: Function | undefined;
7
9
  };
8
10
  events: {
9
11
  [evt: string]: CustomEvent<any>;
@@ -36,7 +36,7 @@ $:
36
36
  $:
37
37
  focused_class = is_row_active ? "bg-stone-200 dark:bg-stone-700" : "";
38
38
  $:
39
- is_link_like = is_row_selected && (!!definition.title_href || !!definition.title_href_func);
39
+ is_link_like = is_row_active && (!!definition.title_href || !!definition.title_href_func);
40
40
  if (!typename) {
41
41
  if (item.$type)
42
42
  typename = item.$type;
@@ -117,18 +117,24 @@ function on_active_row_clicked(e, part) {
117
117
  }
118
118
  } else if (click_on_empty_space) {
119
119
  if (definition.title_href || definition.title_href_func) {
120
- let link = "";
121
- if (definition.title_href)
122
- link = definition.title_href;
123
- else if (definition.title_href_func)
124
- link = definition.title_href_func(item);
125
- if (link)
126
- push(link);
127
120
  } else {
128
121
  }
129
122
  } else {
130
123
  }
131
124
  }
125
+ function followDefinedHRef() {
126
+ let link = getHRef();
127
+ if (link)
128
+ push(link);
129
+ }
130
+ function getHRef() {
131
+ if (definition.title_href)
132
+ return definition.title_href;
133
+ else if (definition.title_href_func)
134
+ return definition.title_href_func(item);
135
+ else
136
+ return "";
137
+ }
132
138
  function activate_row(e, item2) {
133
139
  activateItem("props", item2, toolbarOperations(item2));
134
140
  if (e)
@@ -174,8 +180,19 @@ async function force_editing(field) {
174
180
  if (!element_node.classList.contains("editable")) {
175
181
  return;
176
182
  }
177
- element_node.focus();
178
- setSelectionAtEnd(element_node);
183
+ if (field == "Title") {
184
+ if (is_link_like) {
185
+ startEditing(element_node, () => {
186
+ placeholder = "";
187
+ });
188
+ } else {
189
+ element_node.focus();
190
+ setSelectionAtEnd(element_node);
191
+ }
192
+ } else {
193
+ element_node.focus();
194
+ setSelectionAtEnd(element_node);
195
+ }
179
196
  }
180
197
  function setSelectionAtEnd(element) {
181
198
  const textNode = element.childNodes[0];
@@ -214,43 +231,58 @@ export function scrollToView() {
214
231
  <slot name="left" element={item}/>
215
232
 
216
233
  <div class="ml-3 w-full py-1"
217
- class:sm:hover:cursor-pointer={is_link_like}
218
234
  use:selectable={item}
219
235
  on:click={(e) => {activate_row(e, item)}}
220
236
  role="row"
221
237
  tabindex="0">
222
238
  <div class="block sm:flex sm:flex-row" on:click={(e) => on_active_row_clicked(e, 'top')}>
223
239
 
224
- {#if is_row_active}
225
- {#key item[title]} <!-- Wymusza pełne wyrenderowanie zwłasza po zmiane z pustego na tekst -->
226
- <p class=" text-lg font-semibold min-h-[1.75rem]
227
- sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
228
- whitespace-nowrap overflow-clip w-full sm:flex-none sm:w-2/3"
229
- id="__hd_list_ctrl_{item[item_key]}_Title"
230
- use:editable={{
231
- action: (text) => {change_name(text)},
232
- active: true,
233
- readonly: definition.title_readonly,
234
- }}>
240
+ {#if is_row_active}
241
+ {#key item[title]} <!-- Wymusza pełne wyrenderowanie zwłasza po zmiane z pustego na tekst -->
242
+ {#if is_link_like}
243
+ <p class=" text-lg font-semibold min-h-[1.75rem]
244
+ sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
245
+ whitespace-nowrap overflow-clip w-full sm:flex-none sm:w-2/3
246
+ sm:hover:cursor-pointer underline"
247
+ id="__hd_list_ctrl_{item[item_key]}_Title"
248
+ on:click|stopPropagation={followDefinedHRef}
249
+ use:editable={{
250
+ action: (text) => {change_name(text)},
251
+ active: false,
252
+ readonly: definition.title_readonly,
253
+ }}
254
+ >
235
255
  {element_title}
256
+ </p>
257
+ {:else}
258
+ <p class=" text-lg font-semibold min-h-[1.75rem]
259
+ sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
260
+ whitespace-nowrap overflow-clip w-full sm:flex-none sm:w-2/3"
261
+ id="__hd_list_ctrl_{item[item_key]}_Title"
262
+ use:editable={{
263
+ action: (text) => {change_name(text)},
264
+ active: true,
265
+ readonly: definition.title_readonly,
266
+ }}>
267
+ {element_title}
236
268
 
237
- {#if definition.onOpen}
238
- <button class="ml-3 w-5 h-5 sm:w-3 sm:h-3"
239
- on:click={(e) => definition.onOpen(item)}>
240
- <FaExternalLinkAlt/>
241
- </button>
242
- {/if}
243
- </p>
269
+ {#if definition.onOpen}
270
+ <button class="ml-3 w-5 h-5 sm:w-3 sm:h-3"
271
+ on:click={(e) => definition.onOpen(item)}>
272
+ <FaExternalLinkAlt/>
273
+ </button>
274
+ {/if}
275
+ </p>
276
+ {/if}
244
277
  {/key}
245
- {:else}
278
+ {:else}
246
279
  <p class=" text-lg font-semibold min-h-[1.75rem]
247
280
  sm:text-sm sm:font-semibold sm:min-h-[1.25rem]
248
281
  whitespace-nowrap overflow-clip w-full sm:flex-none sm:w-2/3"
249
282
  id="__hd_list_ctrl_{item[item_key]}_Title">
250
283
  {element_title}
251
284
  </p>
252
- {/if}
253
-
285
+ {/if}
254
286
 
255
287
 
256
288
  <section class="hidden sm:block w-full sm:flex-none sm:w-1/3">
package/index.d.ts CHANGED
@@ -51,8 +51,8 @@ export { default as KanbanComboProperty } from './components/kanban/kanban.combo
51
51
  export { default as KanbanTagsProperty } from './components/kanban/kanban.tags.svelte';
52
52
  export { default as KanbanCallbacks } from './components/kanban/kanban.callbacks.svelte';
53
53
  export { KanbanColumnTop, KanbanColumnBottom } from './components/kanban/Kanban';
54
- export { selectItem, activateItem, clearActiveItem, isActive, isSelected, getActive, editable, startEditing, selectable, handleSelect, isDeviceSmallerThan } from './utils';
55
- export { mainViewReloader, reloadMainView } from './stores.js';
54
+ export { selectItem, activateItem, clearActiveItem, isActive, isSelected, getActive, editable, startEditing, saveCurrentEditable, selectable, handleSelect, isDeviceSmallerThan } from './utils';
55
+ export { mainContentPageReloader, reloadMainContentPage } from './stores.js';
56
56
  export { data_tick_store, // tmp
57
57
  hasSelectedItem, hasDataItem, setNavigatorTitle } from "./stores";
58
58
  export { contextToolbarOperations, pageToolbarOperations, contextItemsStore, contextTypesStore } from './stores';
package/index.js CHANGED
@@ -57,8 +57,8 @@ export { default as KanbanComboProperty } from './components/kanban/kanban.combo
57
57
  export { default as KanbanTagsProperty } from './components/kanban/kanban.tags.svelte';
58
58
  export { default as KanbanCallbacks } from './components/kanban/kanban.callbacks.svelte';
59
59
  export { KanbanColumnTop, KanbanColumnBottom } from './components/kanban/Kanban';
60
- export { selectItem, activateItem, clearActiveItem, isActive, isSelected, getActive, editable, startEditing, selectable, handleSelect, isDeviceSmallerThan } from './utils';
61
- export { mainViewReloader, reloadMainView } from './stores.js';
60
+ export { selectItem, activateItem, clearActiveItem, isActive, isSelected, getActive, editable, startEditing, saveCurrentEditable, selectable, handleSelect, isDeviceSmallerThan } from './utils';
61
+ export { mainContentPageReloader, reloadMainContentPage } from './stores.js';
62
62
  export { data_tick_store, // tmp
63
63
  hasSelectedItem, hasDataItem, setNavigatorTitle } from "./stores";
64
64
  export { contextToolbarOperations, pageToolbarOperations, contextItemsStore, contextTypesStore } from './stores'; // tmp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humandialog/forms.svelte",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Basic Svelte UI components for Object Reef applications",
5
5
  "devDependencies": {
6
6
  "@playwright/test": "^1.28.1",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "type": "module",
28
28
  "dependencies": {
29
- "@humandialog/auth.svelte": "^1.5.0",
29
+ "@humandialog/auth.svelte": "^1.5.3",
30
30
  "flowbite-svelte": "^0.29.13",
31
31
  "svelte-icons": "^2.1.0",
32
32
  "svelte-spa-router": "^3.3.0"
package/stores.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export function setNavigatorTitle(key: any, title: any): void;
2
2
  export function hasSelectedItem(): boolean;
3
3
  export function hasDataItem(): boolean;
4
- export function reloadMainView(): void;
4
+ export function reloadMainContentPage(): void;
5
5
  export function set_dark_mode_default(value: any): void;
6
6
  export function set_default_tools_visible(value: any): void;
7
7
  export function restore_defults(): void;
@@ -28,7 +28,7 @@ export const contextToolbarOperations: import("svelte/store").Writable<never[]>;
28
28
  export const pageToolbarOperations: import("svelte/store").Writable<never[]>;
29
29
  export const page_title: import("svelte/store").Writable<string>;
30
30
  export const nav_titles: import("svelte/store").Writable<{}>;
31
- export const mainViewReloader: import("svelte/store").Writable<number>;
31
+ export const mainContentPageReloader: import("svelte/store").Writable<number>;
32
32
  export const dark_mode_store: import("svelte/store").Writable<any>;
33
33
  export const main_sidebar_visible_store: import("svelte/store").Writable<any>;
34
34
  export let previously_visible_sidebar: string;
package/stores.js CHANGED
@@ -10,7 +10,7 @@ export const contextToolbarOperations = writable([]);
10
10
  export const pageToolbarOperations = writable([]);
11
11
  export const page_title = writable('');
12
12
  export const nav_titles = writable({});
13
- export const mainViewReloader = writable(1);
13
+ export const mainContentPageReloader = writable(1);
14
14
 
15
15
  export function setNavigatorTitle(key, title)
16
16
  {
@@ -31,11 +31,11 @@ export function hasDataItem()
31
31
  return itm !== null && itm !== undefined;
32
32
  }
33
33
 
34
- export function reloadMainView()
34
+ export function reloadMainContentPage()
35
35
  {
36
- let val = get(mainViewReloader);
36
+ let val = get(mainContentPageReloader);
37
37
  val += 1;
38
- mainViewReloader.set(val);
38
+ mainContentPageReloader.set(val);
39
39
  }
40
40
 
41
41
 
package/utils.d.ts CHANGED
@@ -9,6 +9,7 @@ export function editable(node: any, params: any): {
9
9
  destroy(): void;
10
10
  } | undefined;
11
11
  export function startEditing(element: any, finish_callback: any): void;
12
+ export function saveCurrentEditable(): void;
12
13
  export function selectable(node: any, itm: any): {
13
14
  destroy(): void;
14
15
  };
@@ -33,3 +34,4 @@ export namespace SCREEN_SIZES {
33
34
  const lg: number;
34
35
  const xl: number;
35
36
  }
37
+ export let currentEditable: null;
package/utils.js CHANGED
@@ -97,6 +97,8 @@ export function getActive(context_level)
97
97
  return null;
98
98
  }
99
99
 
100
+ export let currentEditable = null;
101
+
100
102
  export function editable(node, params)
101
103
  {
102
104
  let action;
@@ -122,6 +124,9 @@ export function editable(node, params)
122
124
  const org_text = node.textContent;
123
125
  const blur_listener = async (e) =>
124
126
  {
127
+ if(currentEditable == node)
128
+ currentEditable = null;
129
+
125
130
  let cancel = !node.textContent
126
131
  if(observer)
127
132
  observer.disconnect();
@@ -170,6 +175,7 @@ export function editable(node, params)
170
175
  {
171
176
  node.removeEventListener("blur", blur_listener);
172
177
  node.removeEventListener("keydown", key_listener);
178
+ node.removeEventListener("save", save_listener);
173
179
  node.contentEditable = "false"
174
180
 
175
181
  let sel = window.getSelection();
@@ -206,11 +212,20 @@ export function editable(node, params)
206
212
  node.removeEventListener("finish", (e) => {});
207
213
  }
208
214
 
215
+ const save_listener = async (e) =>
216
+ {
217
+ if(has_changed)
218
+ await action(node.textContent)
219
+ }
220
+
209
221
  const edit_listener = async (e) =>
210
222
  {
211
223
  node.contentEditable = "true"
212
224
  node.addEventListener("blur", blur_listener);
213
225
  node.addEventListener("keydown", key_listener);
226
+
227
+ currentEditable = node;
228
+ node.addEventListener("save", save_listener)
214
229
 
215
230
  node.focus();
216
231
 
@@ -235,6 +250,8 @@ export function editable(node, params)
235
250
  node.addEventListener("blur", blur_listener);
236
251
  node.addEventListener("keydown", key_listener);
237
252
  has_changed = false;
253
+ currentEditable = node;
254
+ node.addEventListener("save", save_listener)
238
255
 
239
256
  observer = new MutationObserver(() => { has_changed = true; });
240
257
  observer.observe( node, {
@@ -298,6 +315,12 @@ export function startEditing(element, finish_callback)
298
315
  }
299
316
  }
300
317
 
318
+ export function saveCurrentEditable()
319
+ {
320
+ if(currentEditable)
321
+ currentEditable.dispatchEvent(new Event("save"))
322
+ }
323
+
301
324
  export function selectable(node, itm)
302
325
  {
303
326
  const select_listener = (e) =>