343 lines
16 KiB
JavaScript
343 lines
16 KiB
JavaScript
import { __assign, __extends } from "tslib";
|
|
import * as React from 'react';
|
|
import { Async, getDocument, getNativeProps, initializeComponentRef, inputProperties, isIE11, KeyCodes, } from '../../Utilities';
|
|
import { WindowContext } from '@fluentui/react-window-provider';
|
|
var SELECTION_FORWARD = 'forward';
|
|
var SELECTION_BACKWARD = 'backward';
|
|
/**
|
|
* {@docCategory Autofill}
|
|
*/
|
|
var Autofill = /** @class */ (function (_super) {
|
|
__extends(Autofill, _super);
|
|
function Autofill(props) {
|
|
var _this = _super.call(this, props) || this;
|
|
_this._inputElement = React.createRef();
|
|
_this._autoFillEnabled = true;
|
|
// Composition events are used when the character/text requires several keystrokes to be completed.
|
|
// Some examples of this are mobile text input and languages like Japanese or Arabic.
|
|
// Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart
|
|
_this._onCompositionStart = function (ev) {
|
|
_this.setState({ isComposing: true });
|
|
_this._autoFillEnabled = false;
|
|
};
|
|
// Composition events are used when the character/text requires several keystrokes to be completed.
|
|
// Some examples of this are mobile text input and languages like Japanese or Arabic.
|
|
// Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart
|
|
_this._onCompositionUpdate = function () {
|
|
if (isIE11()) {
|
|
_this._updateValue(_this._getCurrentInputValue(), true);
|
|
}
|
|
};
|
|
// Composition events are used when the character/text requires several keystrokes to be completed.
|
|
// Some examples of this are mobile text input and languages like Japanese or Arabic.
|
|
// Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart
|
|
_this._onCompositionEnd = function (ev) {
|
|
var inputValue = _this._getCurrentInputValue();
|
|
_this._tryEnableAutofill(inputValue, _this.value, false, true);
|
|
_this.setState({ isComposing: false });
|
|
// Due to timing, this needs to be async, otherwise no text will be selected.
|
|
_this._async.setTimeout(function () {
|
|
// it's technically possible that the value of isComposing is reset during this timeout,
|
|
// so explicitly trigger this with composing=true here, since it is supposed to be the
|
|
// update for composition end
|
|
_this._updateValue(_this._getCurrentInputValue(), false);
|
|
}, 0);
|
|
};
|
|
_this._onClick = function () {
|
|
if (_this.value && _this.value !== '' && _this._autoFillEnabled) {
|
|
_this._autoFillEnabled = false;
|
|
}
|
|
};
|
|
_this._onKeyDown = function (ev) {
|
|
if (_this.props.onKeyDown) {
|
|
_this.props.onKeyDown(ev);
|
|
}
|
|
// If the event is actively being composed, then don't alert autofill.
|
|
// Right now typing does not have isComposing, once that has been fixed any should be removed.
|
|
if (!ev.nativeEvent.isComposing) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
switch (ev.which) {
|
|
case KeyCodes.backspace:
|
|
_this._autoFillEnabled = false;
|
|
break;
|
|
case KeyCodes.left:
|
|
case KeyCodes.right:
|
|
if (_this._autoFillEnabled) {
|
|
_this.setState(function (prev) { return ({
|
|
inputValue: _this.props.suggestedDisplayValue || prev.inputValue,
|
|
}); });
|
|
_this._autoFillEnabled = false;
|
|
}
|
|
break;
|
|
default:
|
|
if (!_this._autoFillEnabled) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
if (_this.props.enableAutofillOnKeyPress.indexOf(ev.which) !== -1) {
|
|
_this._autoFillEnabled = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
_this._onInputChanged = function (ev) {
|
|
var value = _this._getCurrentInputValue(ev);
|
|
if (!_this.state.isComposing) {
|
|
_this._tryEnableAutofill(value, _this.value, ev.nativeEvent.isComposing);
|
|
}
|
|
// If it is not IE11 and currently composing, update the value
|
|
if (!(isIE11() && _this.state.isComposing)) {
|
|
var nativeEventComposing = ev.nativeEvent.isComposing;
|
|
var isComposing = nativeEventComposing === undefined ? _this.state.isComposing : nativeEventComposing;
|
|
_this._updateValue(value, isComposing);
|
|
}
|
|
};
|
|
_this._onChanged = function () {
|
|
// Swallow this event, we don't care about it
|
|
// We must provide it because React PropTypes marks it as required, but onInput serves the correct purpose
|
|
return;
|
|
};
|
|
/**
|
|
* Updates the current input value as well as getting a new display value.
|
|
* @param newValue - The new value from the input
|
|
*/
|
|
_this._updateValue = function (newValue, composing) {
|
|
// Only proceed if the value is nonempty and is different from the old value
|
|
// This is to work around the fact that, in IE 11, inputs with a placeholder fire an onInput event on focus
|
|
if (!newValue && newValue === _this.value) {
|
|
return;
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
var _a = _this.props, onInputChange = _a.onInputChange, onInputValueChange = _a.onInputValueChange;
|
|
if (onInputChange) {
|
|
newValue = (onInputChange === null || onInputChange === void 0 ? void 0 : onInputChange(newValue, composing)) || '';
|
|
}
|
|
_this.setState({ inputValue: newValue }, function () { return onInputValueChange === null || onInputValueChange === void 0 ? void 0 : onInputValueChange(newValue, composing); });
|
|
};
|
|
initializeComponentRef(_this);
|
|
_this._async = new Async(_this);
|
|
_this.state = {
|
|
inputValue: props.defaultVisibleValue || '',
|
|
isComposing: false,
|
|
};
|
|
return _this;
|
|
}
|
|
Autofill.getDerivedStateFromProps = function (props, state) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
if (props.updateValueInWillReceiveProps) {
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
var updatedInputValue = props.updateValueInWillReceiveProps();
|
|
// Don't update if we have a null value or the value isn't changing
|
|
// the value should still update if an empty string is passed in
|
|
if (updatedInputValue !== null && updatedInputValue !== state.inputValue && !state.isComposing) {
|
|
return __assign(__assign({}, state), { inputValue: updatedInputValue });
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
Object.defineProperty(Autofill.prototype, "cursorLocation", {
|
|
get: function () {
|
|
if (this._inputElement.current) {
|
|
var inputElement = this._inputElement.current;
|
|
if (inputElement.selectionDirection !== SELECTION_FORWARD) {
|
|
return inputElement.selectionEnd;
|
|
}
|
|
else {
|
|
return inputElement.selectionStart;
|
|
}
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(Autofill.prototype, "isValueSelected", {
|
|
get: function () {
|
|
return Boolean(this.inputElement && this.inputElement.selectionStart !== this.inputElement.selectionEnd);
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(Autofill.prototype, "value", {
|
|
get: function () {
|
|
return this._getControlledValue() || this.state.inputValue || '';
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(Autofill.prototype, "selectionStart", {
|
|
get: function () {
|
|
return this._inputElement.current ? this._inputElement.current.selectionStart : -1;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(Autofill.prototype, "selectionEnd", {
|
|
get: function () {
|
|
return this._inputElement.current ? this._inputElement.current.selectionEnd : -1;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Object.defineProperty(Autofill.prototype, "inputElement", {
|
|
get: function () {
|
|
return this._inputElement.current;
|
|
},
|
|
enumerable: false,
|
|
configurable: true
|
|
});
|
|
Autofill.prototype.componentDidUpdate = function (_, _1, cursor) {
|
|
var _a;
|
|
var _b = this.props, suggestedDisplayValue = _b.suggestedDisplayValue, shouldSelectFullInputValueInComponentDidUpdate = _b.shouldSelectFullInputValueInComponentDidUpdate, preventValueSelection = _b.preventValueSelection;
|
|
var differenceIndex = 0;
|
|
if (preventValueSelection) {
|
|
return;
|
|
}
|
|
var document = ((_a = this.context) === null || _a === void 0 ? void 0 : _a.window.document) || getDocument(this._inputElement.current);
|
|
var isFocused = this._inputElement.current && this._inputElement.current === (document === null || document === void 0 ? void 0 : document.activeElement);
|
|
if (isFocused &&
|
|
this._autoFillEnabled &&
|
|
this.value &&
|
|
suggestedDisplayValue &&
|
|
_doesTextStartWith(suggestedDisplayValue, this.value)) {
|
|
var shouldSelectFullRange = false;
|
|
if (shouldSelectFullInputValueInComponentDidUpdate) {
|
|
shouldSelectFullRange = shouldSelectFullInputValueInComponentDidUpdate();
|
|
}
|
|
if (shouldSelectFullRange) {
|
|
this._inputElement.current.setSelectionRange(0, suggestedDisplayValue.length, SELECTION_BACKWARD);
|
|
}
|
|
else {
|
|
while (differenceIndex < this.value.length &&
|
|
this.value[differenceIndex].toLocaleLowerCase() === suggestedDisplayValue[differenceIndex].toLocaleLowerCase()) {
|
|
differenceIndex++;
|
|
}
|
|
if (differenceIndex > 0) {
|
|
this._inputElement.current.setSelectionRange(differenceIndex, suggestedDisplayValue.length, SELECTION_BACKWARD);
|
|
}
|
|
}
|
|
}
|
|
else if (this._inputElement.current) {
|
|
if (cursor !== null && !this._autoFillEnabled && !this.state.isComposing) {
|
|
this._inputElement.current.setSelectionRange(cursor.start, cursor.end, cursor.dir);
|
|
}
|
|
}
|
|
};
|
|
Autofill.prototype.componentWillUnmount = function () {
|
|
this._async.dispose();
|
|
};
|
|
Autofill.prototype.render = function () {
|
|
var nativeProps = getNativeProps(this.props, inputProperties);
|
|
var style = __assign(__assign({}, this.props.style), { fontFamily: 'inherit' });
|
|
return (React.createElement("input", __assign({ autoCapitalize: "off", autoComplete: "off", "aria-autocomplete": 'both' }, nativeProps, { style: style, ref: this._inputElement, value: this._getDisplayValue(), onCompositionStart: this._onCompositionStart, onCompositionUpdate: this._onCompositionUpdate, onCompositionEnd: this._onCompositionEnd,
|
|
// TODO (Fabric 8?) - switch to calling only onChange. See notes in TextField._onInputChange.
|
|
onChange: this._onChanged, onInput: this._onInputChanged, onKeyDown: this._onKeyDown, onClick: this.props.onClick ? this.props.onClick : this._onClick, "data-lpignore": true })));
|
|
};
|
|
Autofill.prototype.focus = function () {
|
|
this._inputElement.current && this._inputElement.current.focus();
|
|
};
|
|
Autofill.prototype.clear = function () {
|
|
this._autoFillEnabled = true;
|
|
this._updateValue('', false);
|
|
this._inputElement.current && this._inputElement.current.setSelectionRange(0, 0);
|
|
};
|
|
Autofill.prototype.getSnapshotBeforeUpdate = function () {
|
|
var _a, _b;
|
|
var inel = this._inputElement.current;
|
|
if (inel && inel.selectionStart !== this.value.length) {
|
|
return {
|
|
start: (_a = inel.selectionStart) !== null && _a !== void 0 ? _a : inel.value.length,
|
|
end: (_b = inel.selectionEnd) !== null && _b !== void 0 ? _b : inel.value.length,
|
|
dir: inel.selectionDirection || 'backward' || 'none',
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
Autofill.prototype._getCurrentInputValue = function (ev) {
|
|
if (ev && ev.target && ev.target.value) {
|
|
return ev.target.value;
|
|
}
|
|
else if (this.inputElement && this.inputElement.value) {
|
|
return this.inputElement.value;
|
|
}
|
|
else {
|
|
return '';
|
|
}
|
|
};
|
|
/**
|
|
* Attempts to enable autofill. Whether or not autofill is enabled depends on the input value,
|
|
* whether or not any text is selected, and only if the new input value is longer than the old input value.
|
|
* Autofill should never be set to true if the value is composing. Once compositionEnd is called, then
|
|
* it should be completed.
|
|
* See https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent for more information on composition.
|
|
* @param newValue - new input value
|
|
* @param oldValue - old input value
|
|
* @param isComposing - if true then the text is actively being composed and it has not completed.
|
|
* @param isComposed - if the text is a composed text value.
|
|
*/
|
|
Autofill.prototype._tryEnableAutofill = function (newValue, oldValue, isComposing, isComposed) {
|
|
if (!isComposing &&
|
|
newValue &&
|
|
this._inputElement.current &&
|
|
this._inputElement.current.selectionStart === newValue.length &&
|
|
!this._autoFillEnabled &&
|
|
(newValue.length > oldValue.length || isComposed)) {
|
|
this._autoFillEnabled = true;
|
|
}
|
|
};
|
|
Autofill.prototype._getDisplayValue = function () {
|
|
if (this._autoFillEnabled) {
|
|
return _getDisplayValue(this.value, this.props.suggestedDisplayValue);
|
|
}
|
|
return this.value;
|
|
};
|
|
Autofill.prototype._getControlledValue = function () {
|
|
var value = this.props.value;
|
|
if (value === undefined || typeof value === 'string') {
|
|
return value;
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.warn("props.value of Autofill should be a string, but it is ".concat(value, " with type of ").concat(typeof value));
|
|
return value.toString();
|
|
};
|
|
Autofill.defaultProps = {
|
|
enableAutofillOnKeyPress: [KeyCodes.down, KeyCodes.up],
|
|
};
|
|
// need to check WindowContext to get the provided document
|
|
Autofill.contextType = WindowContext;
|
|
return Autofill;
|
|
}(React.Component));
|
|
export { Autofill };
|
|
/**
|
|
* Returns a string that should be used as the display value.
|
|
* It evaluates this based on whether or not the suggested value starts with the input value
|
|
* and whether or not autofill is enabled.
|
|
* @param inputValue - the value that the input currently has.
|
|
* @param suggestedDisplayValue - the possible full value
|
|
*/
|
|
function _getDisplayValue(inputValue, suggestedDisplayValue) {
|
|
var displayValue = inputValue;
|
|
if (suggestedDisplayValue && inputValue && _doesTextStartWith(suggestedDisplayValue, displayValue)) {
|
|
displayValue = suggestedDisplayValue;
|
|
}
|
|
return displayValue;
|
|
}
|
|
function _doesTextStartWith(text, startWith) {
|
|
if (!text || !startWith) {
|
|
return false;
|
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
for (var _i = 0, _a = [text, startWith]; _i < _a.length; _i++) {
|
|
var val = _a[_i];
|
|
if (typeof val !== 'string') {
|
|
throw new Error("".concat(Autofill.name
|
|
// eslint-disable-next-line @fluentui/max-len
|
|
, " received non-string value \"").concat(val, "\" of type ").concat(typeof val, " from either input's value or suggestedDisplayValue"));
|
|
}
|
|
}
|
|
}
|
|
return text.toLocaleLowerCase().indexOf(startWith.toLocaleLowerCase()) === 0;
|
|
}
|
|
//# sourceMappingURL=Autofill.js.map
|