Browse Source

add input and composition events to input helper.

pull/9028/head
Dan Walmsley 4 years ago
parent
commit
ddc9b33e1a
  1. 10
      src/Web/Avalonia.Web/Interop/InputHelper.cs
  2. 4
      src/Web/Avalonia.Web/webapp/modules/avalonia.ts
  3. 149
      src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts
  4. 75
      src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts

10
src/Web/Avalonia.Web/Interop/InputHelper.cs

@ -12,7 +12,15 @@ internal static partial class InputHelper
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyDown,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String, JSType.Number, JSType.Boolean>>]
Func<string, string, int, bool> keyUp);
Func<string, string, int, bool> keyUp,
[JSMarshalAs<JSType.Object, JSType.Boolean>]
Func<JSObject, bool> onInput,
[JSMarshalAs<JSType.Object, JSType.Boolean>]
Func<JSObject, bool> onCompositionStart,
[JSMarshalAs<JSType.Object, JSType.Boolean>]
Func<JSObject, bool> onCompositionUpdate,
[JSMarshalAs<JSType.Object, JSType.Boolean>]
Func<JSObject, bool> onCompositionEnd)
[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")]
public static partial void SubscribePointerEvents(

4
src/Web/Avalonia.Web/webapp/modules/avalonia.ts

@ -4,6 +4,7 @@ import { DpiWatcher } from "./avalonia/canvas";
import { Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom";
import { CaretHelper } from "./avalonia/CaretHelper"
export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia.ts", {
@ -11,6 +12,7 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise<void> {
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM
AvaloniaDOM,
CaretHelper
});
}

149
src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts

@ -0,0 +1,149 @@
// Based on https://github.com/component/textarea-caret-position/blob/master/index.js
export class CaretHelper {
public static getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
options?: { debug: boolean }
) {
if (!isBrowser) {
throw new Error(
"textarea-caret-position#getCaretCoordinates should only be called in a browser"
);
}
const debug = (options && options.debug) || false;
if (debug) {
const el = document.querySelector(
"#input-textarea-caret-position-mirror-div"
);
if (el) el.parentNode?.removeChild(el);
}
// The mirror div will replicate the textarea's style
const div = document.createElement("div");
div.id = "input-textarea-caret-position-mirror-div";
document.body.appendChild(div);
const style = div.style;
const computed = window.getComputedStyle
? window.getComputedStyle(element)
: ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9
const isInput = element.nodeName === "INPUT";
// Default textarea styles
style.whiteSpace = "pre-wrap";
if (!isInput) style.wordWrap = "break-word"; // only for textarea-s
// Position off-screen
style.position = "absolute"; // required to return coordinates properly
if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach((prop: string) => {
if (isInput && prop === "lineHeight") {
// Special case for <input>s because text is rendered centered and line height may be != height
if (computed.boxSizing === "border-box") {
const height = parseInt(computed.height);
const outerHeight =
parseInt(computed.paddingTop) +
parseInt(computed.paddingBottom) +
parseInt(computed.borderTopWidth) +
parseInt(computed.borderBottomWidth);
const targetHeight = outerHeight + parseInt(computed.lineHeight);
if (height > targetHeight) {
style.lineHeight = height - outerHeight + "px";
} else if (height === targetHeight) {
style.lineHeight = computed.lineHeight;
} else {
style.lineHeight = "0";
}
} else {
style.lineHeight = computed.height;
}
} else {
(style as any)[prop] = (computed as any)[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height))
style.overflowY = "scroll";
} else {
style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0");
const span = document.createElement("span");
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
const coordinates = {
top: span.offsetTop + parseInt(computed["borderTopWidth"]),
left: span.offsetLeft + parseInt(computed["borderLeftWidth"]),
height: parseInt(computed["lineHeight"]),
};
if (debug) {
span.style.backgroundColor = "#aaa";
} else {
document.body.removeChild(div);
}
return coordinates;
}
}
var properties = [
"direction", // RTL support
"boxSizing",
"width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
"height",
"overflowX",
"overflowY", // copy the scrollbar for IE
"borderTopWidth",
"borderRightWidth",
"borderBottomWidth",
"borderLeftWidth",
"borderStyle",
"paddingTop",
"paddingRight",
"paddingBottom",
"paddingLeft",
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
"fontStyle",
"fontVariant",
"fontWeight",
"fontStretch",
"fontSize",
"fontSizeAdjust",
"lineHeight",
"fontFamily",
"textAlign",
"textTransform",
"textIndent",
"textDecoration", // might not make a difference, but better be safe
"letterSpacing",
"wordSpacing",
"tabSize",
"MozTabSize",
];
const isBrowser = typeof window !== "undefined";
const isFirefox = isBrowser && (window as any).mozInnerScreenX != null;

75
src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts

@ -1,3 +1,5 @@
import { CaretHelper } from "./CaretHelper"
enum RawInputModifiers
{
None = 0,
@ -23,7 +25,11 @@ export class InputHelper {
element: HTMLInputElement,
keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean,
) {
inputCallback: (args: InputEvent) => boolean,
compositionStartCallback: (args: CompositionEvent) => boolean,
compositionUpdateCallback: (args: CompositionEvent) => boolean,
compositionEndCallback: (args: CompositionEvent) => boolean)
{
const keyDownHandler = (args: KeyboardEvent) => {
if (keyDownCallback(args.code, args.key, this.getModifiers(args))) {
args.preventDefault();
@ -38,10 +44,48 @@ export class InputHelper {
};
element.addEventListener("keyup", keyUpHandler);
const inputHandler = (args: Event) => {
var inputEvent = args as InputEvent;
// todo check cast
if (inputCallback(inputEvent)) {
args.preventDefault();
}
};
element.addEventListener("input", inputHandler);
const compositionStartHandler = (args: CompositionEvent) => {
if (compositionStartCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionstart", compositionStartHandler);
const compositionUpdateHandler = (args: CompositionEvent) => {
if (compositionUpdateCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionupdate", compositionUpdateHandler);
const compositionEndHandler = (args: CompositionEvent) => {
if (compositionEndCallback(args)) {
args.preventDefault();
}
};
element.addEventListener("compositionend", compositionEndHandler);
return () => {
element.removeEventListener("keydown", keyDownHandler);
element.removeEventListener("keyup", keyUpHandler);
element.removeEventListener("compositionstart", compositionStartHandler);
element.removeEventListener("compositionupdate", compositionUpdateHandler);
element.removeEventListener("compositionend", compositionEndHandler);
};
}
public static subscribePointerEvents(
@ -114,22 +158,24 @@ export class InputHelper {
inputElement.value = "";
}
public static isInputElement(element: HTMLInputElement | HTMLElement): element is HTMLInputElement {
return (element as HTMLInputElement).setSelectionRange !== undefined;
}
public static focusElement(inputElement: HTMLElement) {
inputElement.focus();
if (this.isInputElement(inputElement)) {
(inputElement as HTMLInputElement).setSelectionRange(0, 0);
}
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) {
inputElement.style.left = (x).toFixed(0) + "px";
inputElement.style.top = (y).toFixed(0) + "px";
let { height, left, top } = CaretHelper.getCaretCoordinates(inputElement, caret);
inputElement.style.left = (x - left).toFixed(0) + "px";
inputElement.style.top = (y - top).toFixed(0) + "px";
}
public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none';
}
@ -138,6 +184,17 @@ export class InputHelper {
inputElement.style.display = 'block';
}
public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) {
if (!inputElement) {
return;
}
inputElement.value = text;
inputElement.setSelectionRange(start, end);
inputElement.style.width = "20px";
inputElement.style.width = inputElement.scrollWidth + "px";
}
private static getModifiers(args: KeyboardEvent): RawInputModifiers {
var modifiers = RawInputModifiers.None;

Loading…
Cancel
Save