Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
68 lines
4 KiB
JavaScript
68 lines
4 KiB
JavaScript
// Shared editing operations for the desktop actor and Android content bridge.
|
|
export class InputEditor {
|
|
constructor(win) { this.contentWindow=win; this.document=win.document; this.editing=false; }
|
|
focus(element) { this.inputElement=element; }
|
|
state() {
|
|
const e=this.inputElement;
|
|
const text=e.isContentEditable ? e.textContent : e.value;
|
|
const selection=this.contentWindow.getSelection();
|
|
let start=e.selectionStart ?? text.length, end=e.selectionEnd ?? start;
|
|
if(e.isContentEditable && selection.rangeCount && e.contains(selection.anchorNode)) {
|
|
const range=this.document.createRange();range.selectNodeContents(e);
|
|
range.setEnd(selection.anchorNode,selection.anchorOffset);start=range.toString().length;
|
|
range.setEnd(selection.focusNode,selection.focusOffset);end=range.toString().length;
|
|
[start,end]=[Math.min(start,end),Math.max(start,end)];
|
|
}
|
|
return {type:e.isContentEditable?'contenteditable':e.localName,inputType:e.type || (e.classList.contains('recipient')?'tel':'text'),
|
|
inputMode:e.inputMode || (e.classList.contains('recipient')?'tel':''),
|
|
text:e.type==='password'?'':text,selectionStart:start,selectionEnd:end};
|
|
}
|
|
edit(p) {
|
|
const e=this.inputElement,win=this.contentWindow;
|
|
if(!e?.isConnected || this.document.hidden) throw Error('INPUT_CONTEXT_EXPIRED');
|
|
this.editing=true;
|
|
try {e.focus();} finally {this.editing=false;}
|
|
// A touch can focus a new contenteditable while Gecko retains the previous
|
|
// field's selection. execCommand follows that selection, not activeElement.
|
|
if(e.isContentEditable) {
|
|
const selection=win.getSelection();
|
|
if(!e.contains(selection.anchorNode) || !e.contains(selection.focusNode)) {
|
|
const range=this.document.createRange();
|
|
range.selectNodeContents(e);range.collapse(false);
|
|
selection.removeAllRanges();selection.addRange(range);
|
|
}
|
|
}
|
|
if(p.operation==='key') {
|
|
let key=typeof p.key==='object'?p.key.key:p.key;
|
|
if(key===8 || key==='Backspace') this.document.execCommand('delete');
|
|
else if(key===13 || key==='Enter') {
|
|
const options={key:'Enter',code:'Enter',keyCode:13,charCode:13,which:13,bubbles:true,cancelable:true};
|
|
const accepted=e.dispatchEvent(new win.KeyboardEvent('keydown',options)) && e.dispatchEvent(new win.KeyboardEvent('keypress',options));
|
|
if(accepted && (e.isContentEditable || e.localName==='textarea')) this.document.execCommand('insertLineBreak');
|
|
e.dispatchEvent(new win.KeyboardEvent('keyup',options));
|
|
} else {
|
|
const text=typeof key==='string'?key:String.fromCodePoint(p.charCode || key);
|
|
if(text.length>16) throw Error('INVALID_VALUE');
|
|
this.document.execCommand('insertText',false,text);
|
|
}
|
|
} else if(['selection','replace'].includes(p.operation)) {
|
|
const state=this.state();
|
|
const start=p.operation==='selection'?p.start:state.selectionStart+(p.offset||0);
|
|
const length=p.length||0;
|
|
if(!Number.isInteger(start)||!Number.isInteger(length)||start<0||length<0||start+length>(e.value ?? e.textContent).length) throw Error('INVALID_VALUE');
|
|
if(e.isContentEditable) {
|
|
const walker=this.document.createTreeWalker(e,win.NodeFilter.SHOW_TEXT);
|
|
const points=[];let offset=0,node;
|
|
while((node=walker.nextNode())) {points.push({node,start:offset,end:offset+node.length});offset+=node.length;}
|
|
const point=index=>{const part=points.find(p=>index<=p.end);return part?[part.node,index-part.start]:[e,0];};
|
|
const range=this.document.createRange();range.setStart(...point(start));range.setEnd(...point(start+length));
|
|
win.getSelection().removeAllRanges();win.getSelection().addRange(range);
|
|
} else e.setSelectionRange(start,start+length);
|
|
if(p.operation==='replace') {
|
|
if(typeof p.text!=='string'||p.text.length>4096) throw Error('INVALID_VALUE');
|
|
this.document.execCommand('insertText',false,p.text);
|
|
}
|
|
} else throw Error('METHOD_NOT_SUPPORTED');
|
|
return this.state();
|
|
}
|
|
}
|