mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 16:35:33 +00:00
314 lines
No EOL
9.2 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |