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; 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 = 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 = 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, 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); } } } } }