diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
index d41a397c12..c17d32ec17 100644
--- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
+++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
@@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Media;
+using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@@ -57,7 +58,7 @@ namespace Avalonia.Controls
public TextInputMethodSurroundingText SurroundingText => new()
{
Text = _presenter?.Text ?? "",
- CursorOffset = _presenter?.SelectionEnd ?? 0,
+ CursorOffset = _presenter?.CaretIndex ?? 0,
AnchorOffset = _presenter?.SelectionStart ?? 0
};
@@ -73,7 +74,8 @@ namespace Avalonia.Controls
_parent.SelectionEnd = end;
}
- private void OnCaretBoundsChanged(object? sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
+ private void OnCaretBoundsChanged(object? sender, EventArgs e) =>
+ Dispatcher.UIThread.Post(() => CursorRectangleChanged?.Invoke(this, EventArgs.Empty), DispatcherPriority.Input);
private void OnTextBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
index 293f55be01..180c090c03 100644
--- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
+++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
@@ -48,13 +48,13 @@
}
#inputElement {
- opacity: 0.2;
+ opacity: 0.5;
position: absolute;
- width: 400px;
- height: 75px;
+ height: 20px;
top: 0px;
left: 0px;
z-index: 1000;
+ overflow: hidden;
}
diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
index 7a386d5683..edd9c3e6e3 100644
--- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
+++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
@@ -349,12 +349,12 @@ namespace Avalonia.Web.Blazor
IsComposing = true;
break;
case WebCompositionEventArgs.WebCompositionEventType.Update:
- _client.SetPreeditText(e.Data);
+ _client?.SetPreeditText(e.Data);
break;
case WebCompositionEventArgs.WebCompositionEventType.End:
IsComposing = false;
- _client.SetPreeditText(null);
- _topLevelImpl.RawTextEvent(e.Data);
+ _client?.SetPreeditText(null);
+ _topLevelImpl.RawTextEvent(e.Data);
break;
}
}
@@ -452,11 +452,15 @@ namespace Avalonia.Web.Blazor
_client = client;
- if (IsActive)
+ if (IsActive && _client != null)
{
_inputHelper.Show();
_inputElementFocused = true;
_inputHelper.Focus();
+
+ var surroundingText = _client.SurroundingText;
+
+ _inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
else
{
@@ -467,28 +471,28 @@ namespace Avalonia.Web.Blazor
private void SurroundingTextChanged(object? sender, EventArgs e)
{
- if(_client != null)
+ if(_client != null && IsComposing)
{
var surroundingText = _client.SurroundingText;
-
+
_inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
}
}
public void SetCursorRect(Rect rect)
{
+ _inputHelper?.Focus();
var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height);
- _inputHelper?.SetBounds(bounds);
-
- if (_client != null && _client.SupportsSurroundingText && !IsComposing)
+ if (_client != null)
{
var surroundingText = _client.SurroundingText;
- _inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
+ _inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset,
+ surroundingText.CursorOffset);
+
+ _inputHelper?.SetBounds(bounds, surroundingText.CursorOffset);
}
-
- _inputHelper?.Focus();
}
public void SetOptions(TextInputOptions options)
diff --git a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
index debe23e302..8872339f91 100644
--- a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
+++ b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
@@ -122,9 +122,9 @@ namespace Avalonia.Web.Blazor.Interop
_module.Invoke(SetSurroundingTextSymbol, _inputElement, text, start, end);
}
- public void SetBounds(PixelRect bounds)
+ public void SetBounds(PixelRect bounds, int caret)
{
- _module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height);
+ _module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height, caret);
}
}
}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
index 396126a1a6..369f628a44 100644
--- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
@@ -4,3 +4,4 @@ export { FocusHelper } from "./Avalonia/FocusHelper"
export { NativeControlHost } from "./Avalonia/NativeControlHost"
export { SizeWatcher } from "./Avalonia/SizeWatcher"
export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas"
+export { CaretHelper } from "./Avalonia/CaretHelper"
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.js b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.js
deleted file mode 100644
index ef20585657..0000000000
--- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.js
+++ /dev/null
@@ -1,146 +0,0 @@
-(function () {
-
-// We'll copy the properties below into the mirror div.
-// Note that some browsers, such as Firefox, do not concatenate properties
-// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
-// so we have to list every single property explicitly.
- 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'
-
- ];
-
- var isBrowser = (typeof window !== 'undefined');
- var isFirefox = (isBrowser && window.mozInnerScreenX != null);
-
-
-function getCaretCoordinates(element, position, options) {
- if (!isBrowser) {
- throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
- }
-
- var debug = options && options.debug || false;
- if (debug) {
- var el = document.querySelector('#input-textarea-caret-position-mirror-div');
- if (el) el.parentNode.removeChild(el);
- }
-
- // The mirror div will replicate the textarea's style
- var div = document.createElement('div');
- div.id = 'input-textarea-caret-position-mirror-div';
- document.body.appendChild(div);
-
- var style = div.style;
- var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9
- var 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(function (prop) {
- if (isInput && prop === 'lineHeight') {
- // Special case for s because text is rendered centered and line height may be != height
- if (computed.boxSizing === "border-box") {
- var height = parseInt(computed.height);
- var outerHeight =
- parseInt(computed.paddingTop) +
- parseInt(computed.paddingBottom) +
- parseInt(computed.borderTopWidth) +
- parseInt(computed.borderBottomWidth);
- var 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[prop] = computed[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');
-
- var 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 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);
-
- var 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;
-}
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts
new file mode 100644
index 0000000000..5709854087
--- /dev/null
+++ b/src/Web/Avalonia.Web.Blazor/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 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 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;
diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
index 7886825f32..15170c170a 100644
--- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
+++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
@@ -1,6 +1,8 @@
-export class InputHelper {
+import {CaretHelper} from "./CaretHelper";
+
+export class InputHelper {
static inputCallback?: DotNet.DotNetObject;
- static compositionCallback?: DotNet.DotNetObject
+ static compositionCallback?: DotNet.DotNetObject
public static start(inputElement: HTMLInputElement, compositionCallback: DotNet.DotNetObject, inputCallback: DotNet.DotNetObject)
{
@@ -27,14 +29,17 @@
inputElement.style.cursor = kind;
}
- public static setBounds(inputElement: HTMLInputElement, x: number, y: number, width: number, height: number)
+ public static setBounds(inputElement: HTMLInputElement, x: number, y: number, width: number, height: number, caret: number)
{
- inputElement.style.left = (x - 5).toFixed(0) + "px";
- inputElement.style.top = (y - 5).toFixed(0) + "px";
- inputElement.style.height = "20px";
- inputElement.style.width = "200px";
+ if(inputElement.selectionStart) {
+ inputElement.style.left = (x).toFixed(0) + "px";
+ inputElement.style.top = (y).toFixed(0) + "px";
+
+ let {height, left, top} = CaretHelper.getCaretCoordinates(inputElement, caret);
- getCaretCoordinates(inputElement, inputElement.selectionEnd);
+ inputElement.style.left = (x - left).toFixed(0) + "px";
+ inputElement.style.top = (y - top).toFixed(0) + "px";
+ }
}
public static hide(inputElement: HTMLInputElement) {