首页 > 解决方案 > Chrome/Android vs Safari/iOS 上的 textarea 行为

问题描述

我正在开发一个 Angular 8 webapp 项目,供多个平台(iOS、Android、Windows 等)使用。我注意到使用 Safari 和 Chrome 在外观上存在一些差异。例如,当ngIf在 a<div>中放置一个错误条件时,当它变为真时它不会显示在 Safari 的视图中,但会显示在 Chrome 中(将 放置ngIf<ng-container>包装器中可以解决此问题)。

我当前的问题是关于使用<textarea>. 在 Safari 中使用以下内容textarea时,会不断出现退格问题。我能够编写第一行文本,并且在回车之前,退格按预期工作(立即删除光标左侧的单个字符)。回车后,如果使用了退格,则光标之前的所有字符都将被删除,而光标之后的所有字符(如果有)则保留。当光标位于第一行以外的任何行上的任何位置时,就会发生这种行为,我仍然可以按预期使用退格键。下面的第一个屏幕截图显示了在我将光标放在第二行末尾之前每行包含的内容。第二个屏幕截图显示了按下退格键后会发生什么。

退格键前的文本区域

退格后的文本区域

使用 Chrome、Firefox 或 Edge 时未观察到此行为,而是按预期运行。我在 Macbook Pro Mojave 上运行,并在发布时使用了最新的浏览器版本。对于开发人员和用户(不仅限于我的设置),这种行为在本地和生产中都会发生。

关于导致这种行为的原因以及如何减轻它的任何想法?

这是正在使用的代码(由于专有问题,我只共享引用的元素/函数,而不是整个文件):

从子 HTML

<textarea class="textbox form-control hammerContainer" id="{{id}}" name="textArea" placeholder="{{placeholder}}" rows="{{rowNumbers}}"
  #textArea value="{{value}}" [attr.minLength]="minlength" [attr.maxLength]="maxlength" (keyup)="writeValue($event.target.value)"
  (blur)="onTouched()" (paste)="onPaste($event)" (click)="getCursorPos(textArea)" (keydown)="getCursorPos(textArea)" (contextmenu)="getCursorPos(textArea)"
  (select)="getCursorPos(textArea)" (press)="getCursorPos(textArea)" (tap)="getCursorPos(textArea)">
</textarea>

来自子 .ts

import { Component, OnInit, Output [...more imports...], EventEmitter, ChangeDetectorRef } from '@angular/core';
@Output() valueChange = new EventEmitter<string>();
private caretPosStart: number = 0;
private caretPosEnd: number = 0;
public onChange: Function = (val: string) => { };
public onTouched: Function = () => { };
constructor(private changeDetectorRef: ChangeDetectorRef) { }
public writeValue(value: any): void {
  if (value !== null && value !== undefined) {
    this.value = value.substring(0,this.maxlength);
    this.onChange(this.value);
    this.valueChange.emit(this.value);
  }
}
public getCursorPos(element: any): void {
  if (element.selectionEnd || element.selectionEnd === 0) {
    this.caretPosStart = element.selectionStart;
    this.caretPosEnd = element.selectionEnd;
  }
}
onPaste(event: ClipboardEvent): void {
  try {
    event.preventDefault(); // Prevent default action in order to not duplicate pasted value
    const clipboardData = event ? event.clipboardData : undefined;
    let pastedText = clipboardData ? clipboardData.getData('text') : '';
    if (pastedText) {
      const selectedText = window.getSelection().toString(); // Get selected text, if any
      // If selectedText has data, then this.value should exist with data as well, hence why there is no additional checks for
      // this.value before setting currentTextArr
      if (selectedText) {
        // Split selectedText and this.value into arrays in order to compare the string values.
        // If any string values match, and based on caret position in case of multiples of same word(s), filter/remove them
        const selectedTextArr = selectedText.split(' ');
        const currentTextArr = this.value.split(' ');
        let firstMatchIndex;
        let currentStrCount = 0;
        for (let x = 0; x < currentTextArr.length; x++) {
          currentStrCount += (currentTextArr[x].length + 1);
          for (let i = 0; i < selectedTextArr.length; i++) {
            if (currentTextArr[x] === selectedTextArr[i] && ((this.caretPosStart < currentStrCount) && ((currentStrCount - 2) <= this.caretPosEnd))) {
              if (!firstMatchIndex) {
                firstMatchIndex = x; // setting index based on the first word match to know where to insert pasted text
              }
              currentTextArr.splice(x, 1);
            }
          }
        }
        // If there was a match, insert the pasted text based on the index of the first matched word, otherwise the pasted text will be placed at the end
        // of the current data. Then format the array back into a string and write the value.
        let finalText;
        if (firstMatchIndex) {
          currentTextArr.splice(firstMatchIndex, 0, pastedText);
          finalText = currentTextArr.join(' ');
        } else {
          finalText = currentTextArr.join(' ') + ' ' + pastedText;
        }
        this.writeValue(finalText);
        this.changeDetectorRef.detectChanges();
        // Update caret position after paste
        this.caretPosStart = finalText.length;
        this.caretPosEnd = finalText.length;
      } else {
        // Check to see if there is existing data
        if (this.value) {
          // If the carotPos is less than the current strings length, add the
          // pasted text where the cursor is
          if (this.caretPosStart < this.value.length) {
            pastedText = this.value.slice(0, this.caretPosStart) + pastedText + this.value.slice(this.caretPosStart);
          } else { // Otherwise add pasted text after current data
            pastedText = this.value + ' ' + pastedText;
          }
        }
        this.writeValue(pastedText);
        this.changeDetectorRef.detectChanges();
        // Update caret position after paste
        this.caretPosStart = pastedText.length;
        this.caretPosEnd = pastedText.length;
      }
    }
  } catch (e) {
    // Do nothing if error occurs. This just prevents the app from breaking if there is an issue handling the pasting of data.
    // However, will still be able to enter additional narrative text manually.
  }
}

从父 HTML

<app-textarea (valueChange)='setSpecialInstructions($event)'>
</app-textarea>

来自父 .ts

setSpecialInstructions(value: string) {
  this.specialInstructions = value;
  this.someService.setSpecialInstructions(this.specialInstructions);
}

标签: htmlangulartypescriptgoogle-chromesafari

解决方案


我忘记添加的一件事是包含<textarea>标签的子组件的 CSS 文件。锤子容器类如下:

.hammerContainer {
  user-select: all !important;
}

user-select是我以前从未遇到过的 CSS 样式(这段代码是别人写的)。经过一番研究,我发现在 Safari 中控制该设置时使用的适当样式-webkit-user-select如下:

.hammer-container {
  user-select: all !important;
  -webkit-user-select: text !important;
}

user-select使用该all设置时,Safari 不支持该设置,因此需要text-webkit-user-select. 确保覆盖的!important默认值none


推荐阅读