Browse Source

Merge

pull/8963/head
Benedikt Stebner 3 years ago
parent
commit
85b91d3d83
  1. 6
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  2. 6
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor
  3. 28
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  4. 4
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  5. 1
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts
  6. 146
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.js
  7. 149
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts
  8. 21
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts

6
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)
{

6
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;
}
</style>

28
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)

4
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);
}
}
}

1
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"

146
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.js

@ -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 <input>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 <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);
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;
}

149
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 <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;

21
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) {

Loading…
Cancel
Save