@swiftbrowser/web 0.21.0 → 0.23.0
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 +1 -1
- package/src/editorSelection.ts +38 -3
- package/src/plainEditor.ts +24 -3
- package/src/renderer.ts +14 -4
package/package.json
CHANGED
package/src/editorSelection.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type { TextSelectionRange } from './protocol';
|
|
|
11
11
|
|
|
12
12
|
/** The selection each bound editor last reported, or was last moved to. */
|
|
13
13
|
const reported = new WeakMap<HTMLElement, string>();
|
|
14
|
+
/** The text each editor last reported (what the viewer typed): a `text` coming back equal to it is an echo, any other came from outside. */
|
|
15
|
+
const reportedText = new WeakMap<HTMLElement, string>();
|
|
14
16
|
/** A selection the app set while the editor was not focused: where its caret goes when the app focuses it. */
|
|
15
17
|
const pending = new WeakMap<HTMLElement, TextSelectionRange>();
|
|
16
18
|
|
|
@@ -41,14 +43,47 @@ export function wantsDeleteBackward(el: HTMLElement): boolean {
|
|
|
41
43
|
return el.dataset['sbDeleteBackward'] !== undefined;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** The editor reported `text` (the viewer typed it). */
|
|
47
|
+
export function noteReportedText(el: HTMLElement, text: string): void {
|
|
48
|
+
reportedText.set(el, text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where a `text` the app sends came from: the echo of what the editor
|
|
53
|
+
* reported, or outside (another person's edit, the app's own change). The
|
|
54
|
+
* caret survives an echo on its own; an outside change moves what is under
|
|
55
|
+
* it, so the caret is moved too (`shiftOffset`), or the app's selection
|
|
56
|
+
* re-applied even when its numbers did not change.
|
|
57
|
+
*/
|
|
58
|
+
export function textArrived(el: HTMLElement, text: string): 'echo' | 'remote' {
|
|
59
|
+
return reportedText.get(el) === text ? 'echo' : 'remote';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where an offset into `before` is in `after`: the same offset before the
|
|
64
|
+
* change (the common prefix), moved by the change's length after it (the
|
|
65
|
+
* common suffix), and at the change's end inside it.
|
|
66
|
+
*/
|
|
67
|
+
export function shiftOffset(before: string, after: string, offset: number): number {
|
|
68
|
+
const shortest = Math.min(before.length, after.length);
|
|
69
|
+
let prefix = 0;
|
|
70
|
+
while (prefix < shortest && before.charCodeAt(prefix) === after.charCodeAt(prefix)) prefix += 1;
|
|
71
|
+
let suffix = 0;
|
|
72
|
+
while (suffix < shortest - prefix && before.charCodeAt(before.length - 1 - suffix) === after.charCodeAt(after.length - 1 - suffix)) suffix += 1;
|
|
73
|
+
if (offset <= prefix) return offset;
|
|
74
|
+
if (offset >= before.length - suffix) return offset + (after.length - before.length);
|
|
75
|
+
return after.length - suffix;
|
|
76
|
+
}
|
|
77
|
+
|
|
44
78
|
/**
|
|
45
79
|
* The app sent `range` (or no selection): the range the editor moves to now,
|
|
46
80
|
* when it is focused; otherwise null, the range kept for `takePendingSelection`
|
|
47
81
|
* when the app focuses the editor. Nothing for the selection the editor
|
|
48
|
-
* itself reported, coming back
|
|
82
|
+
* itself reported, coming back, unless `force` (the text changed from
|
|
83
|
+
* outside under it, so the same numbers mean a move).
|
|
49
84
|
*/
|
|
50
|
-
export function selectionToApply(el: HTMLElement, range: TextSelectionRange | undefined): TextSelectionRange | null {
|
|
51
|
-
if (!range || reported.get(el) === key(range)) return null;
|
|
85
|
+
export function selectionToApply(el: HTMLElement, range: TextSelectionRange | undefined, force = false): TextSelectionRange | null {
|
|
86
|
+
if (!range || (!force && reported.get(el) === key(range))) return null;
|
|
52
87
|
reported.set(el, key(range));
|
|
53
88
|
if (el.ownerDocument.activeElement === el) return range;
|
|
54
89
|
pending.set(el, range);
|
package/src/plainEditor.ts
CHANGED
|
@@ -9,15 +9,36 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { TextEditorProps, TextSelectionRange } from './protocol';
|
|
12
|
-
import { editorBlurred, isSelectionBound, markSelectionBound, noteViewerSelection, selectionToApply, takePendingSelection, viewerFocused } from './editorSelection';
|
|
12
|
+
import { editorBlurred, isSelectionBound, markSelectionBound, noteViewerSelection, selectionToApply, shiftOffset, takePendingSelection, viewerFocused } from './editorSelection';
|
|
13
|
+
|
|
14
|
+
/** A `text` that came from outside, with the editor's value and caret before it was applied (src/editorSelection.ts, `textArrived`). */
|
|
15
|
+
export interface OutsideChange {
|
|
16
|
+
before: string;
|
|
17
|
+
caret: TextSelectionRange | null;
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
export function isPlainEditor(el: unknown): el is HTMLTextAreaElement {
|
|
15
21
|
return el instanceof HTMLTextAreaElement && el.classList.contains('sb-texteditor');
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
24
|
+
/**
|
|
25
|
+
* After the text: a selection the app set (not the viewer's coming back),
|
|
26
|
+
* moved to now, or when the app focuses the editor. After a text from
|
|
27
|
+
* outside (`outside`): the app's selection even when its numbers did not
|
|
28
|
+
* change, else the viewer's caret moved past the change (setting a
|
|
29
|
+
* textarea's value puts it at the end otherwise).
|
|
30
|
+
*/
|
|
31
|
+
export function applyPlainEditorSelection(area: HTMLTextAreaElement, props: TextEditorProps, outside: OutsideChange | null = null): void {
|
|
20
32
|
markSelectionBound(area, props.selection !== undefined);
|
|
33
|
+
if (outside) {
|
|
34
|
+
if (props.selection !== undefined) {
|
|
35
|
+
const range = selectionToApply(area, props.selection, true);
|
|
36
|
+
if (range) area.setSelectionRange(range.start, range.end);
|
|
37
|
+
} else if (outside.caret) {
|
|
38
|
+
area.setSelectionRange(shiftOffset(outside.before, area.value, outside.caret.start), shiftOffset(outside.before, area.value, outside.caret.end));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
21
42
|
const range = selectionToApply(area, props.selection);
|
|
22
43
|
if (range) area.setSelectionRange(range.start, range.end);
|
|
23
44
|
}
|
package/src/renderer.ts
CHANGED
|
@@ -62,7 +62,7 @@ import { LayoutReporter } from './layoutEvents';
|
|
|
62
62
|
import { createGaugeRing, isCircularGauge, updateGaugeRing } from './gaugeRing';
|
|
63
63
|
import { bitmapStore, showBitmap } from './bitmaps';
|
|
64
64
|
import { clearRichText, runTemplates } from './richText';
|
|
65
|
-
import { markDeleteBackward, wantsDeleteBackward } from './editorSelection';
|
|
65
|
+
import { markDeleteBackward, noteReportedText, textArrived, wantsDeleteBackward } from './editorSelection';
|
|
66
66
|
import { applyPlainEditorSelection, focusPlainEditor, isPlainEditor, listenToPlainEditors } from './plainEditor';
|
|
67
67
|
import { applyRichEditor, createRichEditor, focusRichEditor, isRichEditor, listenToRichEditors, richEditorText } from './richEditor';
|
|
68
68
|
import { applyLayout, setTabHidden, type BoxChange } from './layoutDom';
|
|
@@ -899,6 +899,7 @@ export class Renderer {
|
|
|
899
899
|
const id = Number(owner?.dataset[ID_ATTR]);
|
|
900
900
|
if (!Number.isInteger(id)) return;
|
|
901
901
|
const value = isRichEditor(input) ? richEditorText(input) : input.value;
|
|
902
|
+
noteReportedText(input, value);
|
|
902
903
|
this.onEvent({ type: 'text', id, value });
|
|
903
904
|
};
|
|
904
905
|
|
|
@@ -3925,16 +3926,25 @@ function setDatePickerValue(el: HTMLElement, seconds: number): void {
|
|
|
3925
3926
|
}
|
|
3926
3927
|
}
|
|
3927
3928
|
|
|
3928
|
-
/**
|
|
3929
|
+
/**
|
|
3930
|
+
* Like `applyTextField`: the caret survives an echo of the text the editor
|
|
3931
|
+
* shows. A text from outside (not what the editor reported) would put the
|
|
3932
|
+
* caret at the end: the editor's value and caret from before are kept for
|
|
3933
|
+
* `applyPlainEditorSelection` to move it past the change.
|
|
3934
|
+
*/
|
|
3929
3935
|
function applyTextEditor(area: HTMLTextAreaElement, props: TextEditorProps): void {
|
|
3930
3936
|
const text = props.text ?? '';
|
|
3931
|
-
|
|
3937
|
+
const before = area.value;
|
|
3938
|
+
const outside = before !== text && textArrived(area, text) === 'remote';
|
|
3939
|
+
const focused = area.ownerDocument.activeElement === area;
|
|
3940
|
+
const caret = outside && focused ? { start: area.selectionStart, end: area.selectionEnd } : null;
|
|
3941
|
+
if (before !== text) area.value = text;
|
|
3932
3942
|
// Bound focus, as for a `textfield` (moved in `syncFocus` after the commit).
|
|
3933
3943
|
if (typeof props.focused === 'boolean') area.dataset['sbFocus'] = String(props.focused);
|
|
3934
3944
|
else delete area.dataset['sbFocus'];
|
|
3935
3945
|
markDeleteBackward(area, props.deleteBackward === true);
|
|
3936
3946
|
// A bound selection, under the rich editor's rules (src/plainEditor.ts).
|
|
3937
|
-
applyPlainEditorSelection(area, props);
|
|
3947
|
+
applyPlainEditorSelection(area, props, outside ? { before, caret } : null);
|
|
3938
3948
|
}
|
|
3939
3949
|
|
|
3940
3950
|
// ---------------------------------------------------------------------------
|