spiegel-keyman/web/source/dom/designIFrame.ts
2019-10-31 10:29:59 +07:00

314 lines
No EOL
9.2 KiB
TypeScript

namespace com.keyman.dom {
class SelectionCaret {
node: Node;
offset: number;
constructor(node, offset) {
this.node = node;
this.offset = offset;
}
}
class SelectionRange {
start: SelectionCaret;
end: SelectionCaret;
constructor(start, end) {
this.start = start;
this.end = end;
}
}
class StyleCommand {
cmd: string;
stateType: number;
cache: string|boolean;
constructor(c: string, s:number) {
this.cmd = c;
this.stateType = s;
}
}
export class DesignIFrame extends text.OutputTarget {
root: HTMLIFrameElement;
doc: Document;
docRoot: HTMLElement;
commandCache: StyleCommand[];
constructor(ele: HTMLIFrameElement) {
super();
this.root = ele;
if(ele.contentWindow && ele.contentWindow.document && ele.contentWindow.document.designMode == 'on') {
this.doc = ele.contentWindow.document;
this.docRoot = ele.contentWindow.document.documentElement;
} else {
throw "Specified IFrame is not in design-mode!";
}
}
getElement(): HTMLIFrameElement {
return this.root;
}
hasSelection(): boolean {
let Lsel = this.doc.getSelection();
let outerSel = document.getSelection();
// If the outer doc's selection matches, we're active.
if(outerSel.anchorNode == Lsel.anchorNode && outerSel.focusNode == Lsel.focusNode) {
return true;
} else {
// Problem: for testing, we can't enforce the ideal (ie: first) condition.
// Technically, the IFrame _will_ always have its own internal selection, though... so... it kinda works?
return true;
}
}
clearSelection(): void {
if(this.hasSelection()) {
let Lsel = this.doc.getSelection();
if(!Lsel.isCollapsed) {
Lsel.deleteFromDocument(); // I2134, I2192
}
} else {
console.warn("Attempted to clear an unowned Selection!");
}
}
invalidateSelection(): void { /* No cache maintenance needed here, partly because
* it's impossible to cache a Selection; it mutates.
*/ }
getCarets(): SelectionRange {
let Lsel = this.doc.getSelection();
let code = Lsel.anchorNode.compareDocumentPosition(Lsel.focusNode);
if(Lsel.isCollapsed) {
let caret = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
return new SelectionRange(caret, caret);
} else {
let anchor = new SelectionCaret(Lsel.anchorNode, Lsel.anchorOffset);
let focus = new SelectionCaret(Lsel.focusNode, Lsel.focusOffset);
if(anchor.node == focus.node) {
code = (focus.offset - anchor.offset > 0) ? 2 : 4;
}
if(code & 2) {
return new SelectionRange(anchor, focus);
} else { // Default
// can test against code & 4 to ensure Focus is before anchor, though.
return new SelectionRange(focus, anchor);
}
}
}
getDeadkeyCaret(): number {
return this.getTextBeforeCaret().kmwLength();
}
getTextBeforeCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().start;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(0, caret.offset);
}
getTextAfterCaret(): string {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
if(caret.node.nodeType != 3) {
return ''; // Must be a text node to provide a context.
}
return caret.node.textContent.substr(caret.offset);
}
getText(): string {
return this.docRoot.innerText;
}
deleteCharsBeforeCaret(dn: number) {
if(!this.hasSelection() || dn <= 0) {
return;
}
let start = this.getCarets().start;
// Bounds-check on the number of chars to delete.
if(dn > start.offset) {
dn = start.offset;
}
if(start.node.nodeType != 3) {
console.warn("Deletion of characters requested without available context!");
return; // No context to delete characters from.
}
let range = this.doc.createRange();
let dnOffset = start.offset - start.node.nodeValue.substr(0, start.offset)._kmwSubstr(-dn).length;
range.setStart(start.node, dnOffset);
range.setEnd(start.node, start.offset);
this.adjustDeadkeys(-dn);
range.deleteContents();
// No need to reposition the caret - the DOM will auto-move the selection accordingly, since
// we didn't use the selection to delete anything.
}
insertTextBeforeCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let start = this.getCarets().start;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
this.adjustDeadkeys(delta);
// While Selection.extend() is really nice for this, IE doesn't support it whatsoever.
// However, IE (11, at least) DOES support setting selections via ranges, so we can still
// manage the caret properly.
let finalCaret = this.root.ownerDocument.createRange();
if(start.node.nodeType == 3) {
let textStart = <Text> start.node;
textStart.insertData(start.offset, s);
finalCaret.setStart(textStart, start.offset + s.length);
} else {
// Create a new text node - empty control
var n = this.doc.createTextNode(s);
let range = this.doc.createRange();
range.setStart(start.node, start.offset);
range.collapse(true);
range.insertNode(n);
}
finalCaret.collapse(true);
Lsel.removeAllRanges();
try {
Lsel.addRange(finalCaret);
} catch(e) {
// Chrome (through 4.0 at least) throws an exception because it has not synchronised its content with the selection.
// scrollIntoView synchronises the content for selection
start.node.parentElement.scrollIntoView();
Lsel.addRange(finalCaret);
}
Lsel.collapseToEnd();
}
protected setTextAfterCaret(s: string) {
if(!this.hasSelection()) {
return;
}
let caret = this.getCarets().end;
let delta = s._kmwLength();
let Lsel = this.doc.getSelection();
if(delta == 0) {
return;
}
// This is designed explicitly for use in direct-setting operations; deadkeys
// will be handled after this method.
if(caret.node.nodeType == 3) {
let textStart = <Text> caret.node;
textStart.replaceData(caret.offset, textStart.length, s);
} else {
// Create a new text node - empty control
var n = caret.node.ownerDocument.createTextNode(s);
let range = this.root.ownerDocument.createRange();
range.setStart(caret.node, caret.offset);
range.collapse(true);
range.insertNode(n);
}
}
/**
* Function saveProperties
* Scope Private
* Description Build and create list of styles that can be applied in iframes
*/
saveProperties() {
// Formerly _CacheCommands.
var _CacheableCommands=[
new StyleCommand('backcolor',1), new StyleCommand('fontname',1), new StyleCommand('fontsize',1),
new StyleCommand('forecolor',1), new StyleCommand('bold',0), new StyleCommand('italic',0),
new StyleCommand('strikethrough',0), new StyleCommand('subscript',0),
new StyleCommand('superscript',0), new StyleCommand('underline',0)
];
if(this.doc.defaultView) {
_CacheableCommands.push(new StyleCommand('hilitecolor',1));
}
for(var n=0; n < _CacheableCommands.length; n++) { // I1511 - array prototype extended
let cmd = _CacheableCommands[n];
//KeymanWeb._Debug('Command:'+_CacheableCommands[n][0]);
if(cmd.stateType == 1) {
cmd.cache = this.doc.queryCommandValue(cmd.cmd);
} else {
cmd.cache = this.doc.queryCommandState(cmd.cmd);
}
}
this.commandCache = _CacheableCommands;
}
/**
* Function restoreProperties
* Scope Private
* Description Restore styles in IFRAMEs (??)
*/
restoreProperties(_func?: () => void): void {
// Formerly _CacheCommandsReset.
if(!this.commandCache) {
console.error("No command cache exists to restore!");
}
for(var n=0; n < this.commandCache.length; n++) { // I1511 - array prototype extended
let cmd = this.commandCache[n];
//KeymanWeb._Debug('ResetCacheCommand:'+_CacheableCommands[n][0]+'='+_CacheableCommands[n][2]);
if(cmd.stateType == 1) {
if(this.doc.queryCommandValue(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
this.doc.execCommand(cmd.cmd, false, <string> cmd.cache);
}
} else if(this.doc.queryCommandState(cmd.cmd) != cmd.cache) {
if(_func) {
_func();
}
//KeymanWeb._Debug('executing command '+_CacheableCommand[n][0]);
this.doc.execCommand(cmd.cmd, false, null);
}
}
}
}
}