@jinntec/fore 2.9.0 → 3.1.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.
Files changed (42) hide show
  1. package/README.md +0 -2
  2. package/dist/fore-dev.js +9505 -6057
  3. package/dist/fore.js +9218 -5996
  4. package/index.js +1 -0
  5. package/package.json +4 -2
  6. package/src/DependentXPathQueries.js +37 -2
  7. package/src/ForeElementMixin.js +64 -38
  8. package/src/actions/fx-delete.js +424 -73
  9. package/src/actions/fx-insert.js +472 -73
  10. package/src/actions/fx-setattribute.js +5 -5
  11. package/src/actions/fx-setvalue.js +11 -9
  12. package/src/authoring-check.js +231 -0
  13. package/src/fore.js +32 -77
  14. package/src/functions/registerFunction.js +65 -20
  15. package/src/fx-bind.js +128 -73
  16. package/src/fx-fore.js +653 -219
  17. package/src/fx-instance.js +145 -143
  18. package/src/fx-model.js +292 -102
  19. package/src/fx-speech.js +268 -0
  20. package/src/fx-submission.js +246 -135
  21. package/src/fx-var.js +28 -4
  22. package/src/json/JSONDomFacade.js +84 -0
  23. package/src/json/JSONLens.js +67 -0
  24. package/src/json/JSONNode.js +248 -0
  25. package/src/json/lensFromNode.js +5 -0
  26. package/src/modelitem.js +16 -2
  27. package/src/tools/fx-action-log.js +1 -1
  28. package/src/ui/UIElement.js +16 -2
  29. package/src/ui/abstract-control.js +14 -7
  30. package/src/ui/fx-case.js +3 -1
  31. package/src/ui/fx-control.js +4 -2
  32. package/src/ui/fx-group.js +1 -1
  33. package/src/ui/fx-items.js +26 -32
  34. package/src/ui/fx-output.js +8 -38
  35. package/src/ui/fx-repeat-attributes.js +1 -1
  36. package/src/ui/fx-repeat.js +683 -247
  37. package/src/ui/fx-repeatitem.js +16 -1
  38. package/src/ui/repeat-base.js +9 -5
  39. package/src/withDraggability.js +0 -1
  40. package/src/xpath-evaluation.js +1763 -740
  41. package/src/xpath-path.js +274 -24
  42. package/src/xpath-util.js +92 -46
@@ -0,0 +1,268 @@
1
+ // fx-speech.js — Fore-compatible voice input component with focus alignment, restart, repeat/back commands, and visual listening indicator
2
+ class FxSpeech extends HTMLElement {
3
+ constructor() {
4
+ super();
5
+ this.attachShadow({ mode: 'open' });
6
+ this.mode = this.getAttribute('mode') || 'guided';
7
+ this.currentIndex = 0;
8
+ this.controls = [];
9
+ this.recognition = null;
10
+ this.lastInputCaptured = false;
11
+ this.awaitingInput = false;
12
+ this.waitingToAdvance = false;
13
+ }
14
+
15
+ connectedCallback() {
16
+ this.shadowRoot.innerHTML = `
17
+ <style>
18
+ button { margin: 0.5em; padding: 0.5em 1em; font-size: 1em; }
19
+ #status { display: inline-block; margin-left: 1em; font-weight: bold; color: green; visibility: hidden; }
20
+ #status.listening { visibility: visible; animation: pulse 1s infinite; }
21
+ @keyframes pulse {
22
+ 0% { opacity: 0.3; }
23
+ 50% { opacity: 1; }
24
+ 100% { opacity: 0.3; }
25
+ }
26
+ </style>
27
+ <button id="start">🎤 Start Speech Input</button>
28
+ <button id="retry" style="display:none;">🔁 Continue</button>
29
+ <span id="status">🎧 Listening…</span>
30
+ `;
31
+
32
+ this.controls = Array.from(document.querySelectorAll('fx-control'));
33
+ this.initSpeech();
34
+
35
+ this.shadowRoot.getElementById('start').addEventListener('click', () => {
36
+ this.startInteraction();
37
+ });
38
+
39
+ this.shadowRoot.getElementById('retry').addEventListener('click', () => {
40
+ this.startGuided();
41
+ });
42
+
43
+ document.addEventListener('focusin', e => {
44
+ const targetControl = e.target.closest('fx-control');
45
+ if (targetControl) {
46
+ const index = this.controls.indexOf(targetControl);
47
+ if (index !== -1) {
48
+ this.currentIndex = index;
49
+ }
50
+ }
51
+ });
52
+ }
53
+
54
+ initSpeech() {
55
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
56
+ if (!SpeechRecognition) {
57
+ alert('Web Speech API not supported in this browser.');
58
+ return;
59
+ }
60
+ this.recognition = new SpeechRecognition();
61
+ this.recognition.lang = 'en-US';
62
+ this.recognition.interimResults = false;
63
+ this.recognition.continuous = false;
64
+
65
+ this.recognition.onresult = event => {
66
+ this.lastSpoken = null;
67
+ const spoken = event.results[0][0].transcript.trim();
68
+ console.log('Recognized:', spoken);
69
+ this.lastInputCaptured = true;
70
+ this.awaitingInput = false;
71
+ this.toggleListening(false);
72
+
73
+ if (this.mode === 'guided') {
74
+ this.lastSpoken = spoken.toLowerCase();
75
+ this.applyGuidedInput(this.lastSpoken);
76
+ } else {
77
+ this.handleCommandInput(spoken.toLowerCase());
78
+ }
79
+ };
80
+
81
+ this.recognition.onerror = e => {
82
+ console.warn('Speech error:', e.error);
83
+ this.awaitingInput = false;
84
+ this.toggleListening(false);
85
+ if (this.mode === 'guided' && !this.waitingToAdvance) this.retryGuided();
86
+ };
87
+
88
+ this.recognition.onend = () => {
89
+ this.recognitionActive = false;
90
+ console.log('Recognition ended');
91
+ this.toggleListening(false);
92
+ if (this.mode === 'guided') {
93
+ if (this.lastInputCaptured && !['next', 'back'].includes(this.lastSpoken)) {
94
+ this.advanceToNextField();
95
+ } else if (this.awaitingInput && !this.waitingToAdvance) {
96
+ this.retryGuided();
97
+ }
98
+ }
99
+ };
100
+
101
+ this.recognition.onstart = () => {
102
+ this.recognitionActive = true;
103
+ console.log('Recognition started');
104
+ this.toggleListening(true);
105
+ };
106
+ }
107
+
108
+ toggleListening(state) {
109
+ const status = this.shadowRoot.getElementById('status');
110
+ if (state) {
111
+ status.classList.add('listening');
112
+ } else {
113
+ status.classList.remove('listening');
114
+ }
115
+ }
116
+
117
+ speak(text, callback) {
118
+ const utterance = new SpeechSynthesisUtterance(text);
119
+ utterance.onend = async () => {
120
+ await this.waitForSpeechSynthesisToEnd();
121
+ if (callback) callback();
122
+ };
123
+ speechSynthesis.speak(utterance);
124
+ }
125
+
126
+ async waitForSpeechSynthesisToEnd() {
127
+ while (speechSynthesis.speaking) {
128
+ await new Promise(resolve => setTimeout(resolve, 50));
129
+ }
130
+ }
131
+
132
+ getLabelText(control) {
133
+ return (
134
+ control.getAttribute('aria-label') ||
135
+ control.querySelector('label')?.textContent?.trim() ||
136
+ 'unknown field'
137
+ );
138
+ }
139
+
140
+ getInputElement(control) {
141
+ return control.querySelector('input, textarea, select');
142
+ }
143
+
144
+ startInteraction() {
145
+ this.shadowRoot.getElementById('retry').style.display = 'none';
146
+ if (this.mode === 'guided') {
147
+ this.startGuided();
148
+ } else {
149
+ this.recognition.start();
150
+ }
151
+ }
152
+
153
+ startGuided() {
154
+ this.shadowRoot.getElementById('retry').style.display = 'none';
155
+ if (this.currentIndex >= this.controls.length) {
156
+ this.speak('All fields completed.', () => {
157
+ this.currentIndex = 0;
158
+ this.shadowRoot.getElementById('start').textContent = '🔁 Restart Speech Input';
159
+ this.shadowRoot.getElementById('retry').style.display = 'inline-block';
160
+ });
161
+ return;
162
+ }
163
+ this.lastInputCaptured = false;
164
+ this.awaitingInput = true;
165
+ this.waitingToAdvance = false;
166
+ const control = this.controls[this.currentIndex];
167
+ const label = this.getLabelText(control);
168
+ const input = this.getInputElement(control);
169
+ input?.focus();
170
+
171
+ console.log('Prompting for field:', label);
172
+ this.speak(`Please say value for ${label}`, () => {
173
+ console.log('Starting recognition for:', label);
174
+ if (!this.recognitionActive) this.recognition.start();
175
+ });
176
+ }
177
+
178
+ retryGuided() {
179
+ this.shadowRoot.getElementById('retry').style.display = 'inline-block';
180
+ this.awaitingInput = false;
181
+ this.speak('Please try again or tap continue.');
182
+ }
183
+
184
+ applyGuidedInput(spoken) {
185
+ if (spoken === 'clear') {
186
+ const control = this.controls[this.currentIndex];
187
+ const input = this.getInputElement(control);
188
+ if (input) {
189
+ input.value = '';
190
+ input.dispatchEvent(new Event('input', { bubbles: true }));
191
+ this.speak('Cleared');
192
+ }
193
+ return;
194
+ }
195
+ if (spoken === 'next') {
196
+ this.advanceToNextField();
197
+ return;
198
+ }
199
+ if (spoken === 'repeat') {
200
+ this.startGuided();
201
+ return;
202
+ }
203
+ if (spoken === 'back') {
204
+ this.currentIndex = Math.max(0, this.currentIndex - 1);
205
+ this.startGuided();
206
+ return;
207
+ }
208
+ const control = this.controls[this.currentIndex];
209
+ const input = this.getInputElement(control);
210
+ if (input) {
211
+ input.value = spoken;
212
+ input.dispatchEvent(new Event('input', { bubbles: true }));
213
+ }
214
+ }
215
+
216
+ advanceToNextField() {
217
+ this.waitingToAdvance = true;
218
+ setTimeout(() => {
219
+ this.currentIndex++;
220
+ this.startGuided();
221
+ }, 1000);
222
+ }
223
+
224
+ handleCommandInput(spoken) {
225
+ if (spoken.startsWith('skip to')) {
226
+ const label = spoken.replace('skip to', '').trim();
227
+ const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label);
228
+ if (target) {
229
+ this.currentIndex = this.controls.indexOf(target);
230
+ this.getInputElement(target)?.focus();
231
+ this.speak(`Skipping to ${label}`);
232
+ } else {
233
+ this.speak(`Label "${label}" not found.`);
234
+ }
235
+ return;
236
+ }
237
+
238
+ if (spoken === 'next') {
239
+ this.currentIndex++;
240
+ return;
241
+ }
242
+ if (spoken === 'repeat') {
243
+ this.startGuided();
244
+ return;
245
+ }
246
+ if (spoken === 'back') {
247
+ this.currentIndex = Math.max(0, this.currentIndex - 1);
248
+ this.startGuided();
249
+ return;
250
+ }
251
+
252
+ const [label, ...rest] = spoken.split(' ');
253
+ const value = rest.join(' ');
254
+ const target = this.controls.find(ctrl => this.getLabelText(ctrl).toLowerCase() === label);
255
+ if (target) {
256
+ const input = this.getInputElement(target);
257
+ if (input) {
258
+ input.value = value;
259
+ input.dispatchEvent(new Event('input', { bubbles: true }));
260
+ input.focus();
261
+ }
262
+ } else {
263
+ this.speak(`Label "${label}" not found.`);
264
+ }
265
+ }
266
+ }
267
+
268
+ customElements.define('fx-speech', FxSpeech);