<commit_before>// @flow
import {objectIncludes, DIRECTION, type Selection} from '../core/utils.js';
import ActionDetails from '../core/action-details.js';
import MaskedDate from '../masked/date.js';
import createMask, {maskedClass} from '../masked/factory.js';
import type Masked from '../masked/base.js';
import {type Mask} from '../masked/base.js';
import MaskElement from './mask-element.js';
import HTMLMaskElement from './html-mask-element.js';


/** Listens to element events and controls changes between element and {@link Masked} */
export default
class InputMask {
  /**
    View element
    @readonly
  */
  el: MaskElement;

  /**
    Internal {@link Masked} model
    @readonly
  */
  masked: Masked<*>;
  alignCursor: () => void;
  alignCursorFriendly: () => void;

  _listeners: {[string]: Array<Function>};
  _value: string;
  _changingCursorPos: number;
  _unmaskedValue: string;
  _saveSelection: (?Event) => void;
  _selection: Selection;
  _onInput: () => void;
  _onChange: () => void;
  _onDrop: (Event) => void;
  _cursorChanging: TimeoutID;

  /**
    @param {MaskElement|HTMLInputElement|HTMLTextAreaElement} el
    @param {Object} opts
  */
  constructor (el: MaskElement | HTMLTextAreaElement | HTMLInputElement, opts: {[string]: any}) {
    this.el = (el instanceof MaskElement) ?
      el :
      new HTMLMaskElement(el);
    this.masked = createMask(opts);

    this._listeners = {};
    this._value = '';
    this._unmaskedValue = '';

    this._saveSelection = this._saveSelection.bind(this);
    this._onInput = this._onInput.bind(this);
    this._onChange = this._onChange.bind(this);
    this._onDrop = this._onDrop.bind(this);
    this.alignCursor = this.alignCursor.bind(this);
    this.alignCursorFriendly = this.alignCursorFriendly.bind(this);

    this._bindEvents();

    // refresh
    this.updateValue();
    this._onChange();
  }

  /** Read or update mask */
  get mask (): Mask {
    return this.masked.mask;
  }

  set mask (mask: Mask) {
    if (mask == null ||
      mask === this.masked.mask ||
      mask === Date && this.masked instanceof MaskedDate) return;

    if (this.masked.constructor === maskedClass(mask)) {
      this.masked.updateOptions({mask});
      return;
    }

    const masked = createMask({mask});
    masked.unmaskedValue = this.masked.unmaskedValue;
    this.masked = masked;
  }

  /** Raw value */
  get value (): string {
    return this._value;
  }

  set value (str: string) {
    this.masked.value = str;
    this.updateControl();
    this.alignCursor();
  }

  /** Unmasked value */
  get unmaskedValue (): string {
    return this._unmaskedValue;
  }

  set unmaskedValue (str: string) {
    this.masked.unmaskedValue = str;
    this.updateControl();
    this.alignCursor();
  }

  /** Typed unmasked value */
  get typedValue (): any {
    return this.masked.typedValue;
  }

  set typedValue (val: any) {
    this.masked.typedValue = val;
    this.updateControl();
    this.alignCursor();
  }

  /**
    Starts listening to element events
    @protected
  */
  _bindEvents () {
    this.el.bindEvents({
      selectionChange: this._saveSelection,
      input: this._onInput,
      drop: this._onDrop,
      click: this.alignCursorFriendly,
      focus: this.alignCursorFriendly,
      commit: this._onChange,
    });
  }

  /**
    Stops listening to element events
    @protected
   */
  _unbindEvents () {
    this.el.unbindEvents();
  }

  /**
    Fires custom event
    @protected
   */
  _fireEvent (ev: string) {
    const listeners = this._listeners[ev];
    if (!listeners) return;

    listeners.forEach(l => l());
  }

  /**
    Current selection start
    @readonly
  */
  get selectionStart (): number {
    return this._cursorChanging ?
      this._changingCursorPos :

      this.el.selectionStart;
  }

  /** Current cursor position */
  get cursorPos (): number {
    return this._cursorChanging ?
      this._changingCursorPos :

      this.el.selectionEnd;
  }
  set cursorPos (pos: number) {
    if (!this.el.isActive) return;

    this.el.select(pos, pos);
    this._saveSelection();
  }

  /**
    Stores current selection
    @protected
  */
  _saveSelection (/* ev */) {
    if (this.value !== this.el.value) {
      console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console
    }
    this._selection = {
      start: this.selectionStart,
      end: this.cursorPos
    };
  }

  /** Syncronizes model value from view */
  updateValue () {
    this.masked.value = this.el.value;
  }

  /** Syncronizes view from model value, fires change events */
  updateControl () {
    const newUnmaskedValue = this.masked.unmaskedValue;
    const newValue = this.masked.value;
    const isChanged = (this.unmaskedValue !== newUnmaskedValue ||
      this.value !== newValue);

    this._unmaskedValue = newUnmaskedValue;
    this._value = newValue;

    if (this.el.value !== newValue) this.el.value = newValue;
    if (isChanged) this._fireChangeEvents();
  }

  /** Updates options with deep equal check, recreates @{link Masked} model if mask type changes */
  updateOptions (opts: {[string]: any}) {
    if (objectIncludes(this.masked, opts)) return;

    const { mask, ...restOpts } = opts;

    this.mask = mask;
    this.masked.updateOptions(restOpts);

    this.updateControl();
  }

  /** Updates cursor */
  updateCursor (cursorPos: number) {<commit_msg>fix #141: set control `_value` on `updateValue()`<commit_after>// @flow
import {objectIncludes, DIRECTION, type Selection} from '../core/utils.js';
import ActionDetails from '../core/action-details.js';
import MaskedDate from '../masked/date.js';
import createMask, {maskedClass} from '../masked/factory.js';
import type Masked from '../masked/base.js';
import {type Mask} from '../masked/base.js';
import MaskElement from './mask-element.js';
import HTMLMaskElement from './html-mask-element.js';


/** Listens to element events and controls changes between element and {@link Masked} */
export default
class InputMask {
  /**
    View element
    @readonly
  */
  el: MaskElement;

  /**
    Internal {@link Masked} model
    @readonly
  */
  masked: Masked<*>;
  alignCursor: () => void;
  alignCursorFriendly: () => void;

  _listeners: {[string]: Array<Function>};
  _value: string;
  _changingCursorPos: number;
  _unmaskedValue: string;
  _saveSelection: (?Event) => void;
  _selection: Selection;
  _onInput: () => void;
  _onChange: () => void;
  _onDrop: (Event) => void;
  _cursorChanging: TimeoutID;

  /**
    @param {MaskElement|HTMLInputElement|HTMLTextAreaElement} el
    @param {Object} opts
  */
  constructor (el: MaskElement | HTMLTextAreaElement | HTMLInputElement, opts: {[string]: any}) {
    this.el = (el instanceof MaskElement) ?
      el :
      new HTMLMaskElement(el);
    this.masked = createMask(opts);

    this._listeners = {};
    this._value = '';
    this._unmaskedValue = '';

    this._saveSelection = this._saveSelection.bind(this);
    this._onInput = this._onInput.bind(this);
    this._onChange = this._onChange.bind(this);
    this._onDrop = this._onDrop.bind(this);
    this.alignCursor = this.alignCursor.bind(this);
    this.alignCursorFriendly = this.alignCursorFriendly.bind(this);

    this._bindEvents();

    // refresh
    this.updateValue();
    this._onChange();
  }

  /** Read or update mask */
  get mask (): Mask {
    return this.masked.mask;
  }

  set mask (mask: Mask) {
    if (mask == null ||
      mask === this.masked.mask ||
      mask === Date && this.masked instanceof MaskedDate) return;

    if (this.masked.constructor === maskedClass(mask)) {
      this.masked.updateOptions({mask});
      return;
    }

    const masked = createMask({mask});
    masked.unmaskedValue = this.masked.unmaskedValue;
    this.masked = masked;
  }

  /** Raw value */
  get value (): string {
    return this._value;
  }

  set value (str: string) {
    this.masked.value = str;
    this.updateControl();
    this.alignCursor();
  }

  /** Unmasked value */
  get unmaskedValue (): string {
    return this._unmaskedValue;
  }

  set unmaskedValue (str: string) {
    this.masked.unmaskedValue = str;
    this.updateControl();
    this.alignCursor();
  }

  /** Typed unmasked value */
  get typedValue (): any {
    return this.masked.typedValue;
  }

  set typedValue (val: any) {
    this.masked.typedValue = val;
    this.updateControl();
    this.alignCursor();
  }

  /**
    Starts listening to element events
    @protected
  */
  _bindEvents () {
    this.el.bindEvents({
      selectionChange: this._saveSelection,
      input: this._onInput,
      drop: this._onDrop,
      click: this.alignCursorFriendly,
      focus: this.alignCursorFriendly,
      commit: this._onChange,
    });
  }

  /**
    Stops listening to element events
    @protected
   */
  _unbindEvents () {
    this.el.unbindEvents();
  }

  /**
    Fires custom event
    @protected
   */
  _fireEvent (ev: string) {
    const listeners = this._listeners[ev];
    if (!listeners) return;

    listeners.forEach(l => l());
  }

  /**
    Current selection start
    @readonly
  */
  get selectionStart (): number {
    return this._cursorChanging ?
      this._changingCursorPos :

      this.el.selectionStart;
  }

  /** Current cursor position */
  get cursorPos (): number {
    return this._cursorChanging ?
      this._changingCursorPos :

      this.el.selectionEnd;
  }
  set cursorPos (pos: number) {
    if (!this.el.isActive) return;

    this.el.select(pos, pos);
    this._saveSelection();
  }

  /**
    Stores current selection
    @protected
  */
  _saveSelection (/* ev */) {
    if (this.value !== this.el.value) {
      console.warn('Element value was changed outside of mask. Syncronize mask using `mask.updateValue()` to work properly.'); // eslint-disable-line no-console
    }
    this._selection = {
      start: this.selectionStart,
      end: this.cursorPos
    };
  }

  /** Syncronizes model value from view */
  updateValue () {
    this.masked.value = this.el.value;
    this._value = this.masked.value;
  }

  /** Syncronizes view from model value, fires change events */
  updateControl () {
    const newUnmaskedValue = this.masked.unmaskedValue;
    const newValue = this.masked.value;
    const isChanged = (this.unmaskedValue !== newUnmaskedValue ||
      this.value !== newValue);

    this._unmaskedValue = newUnmaskedValue;
    this._value = newValue;

    if (this.el.value !== newValue) this.el.value = newValue;
    if (isChanged) this._fireChangeEvents();
  }

  /** Updates options with deep equal check, recreates @{link Masked} model if mask type changes */
  updateOptions (opts: {[string]: any}) {
    if (objectIncludes(this.masked, opts)) return;

    const { mask, ...restOpts } = opts;

    this.mask = mask;
    this.masked.updateOptions(restOpts);

    this.updateControl();
  }

  /** Updates cursor */
  updateCursor (cursorPos: number) {
{'fraction_non_alphanumeric': 0.1374442116768376, 'fraction_numerical': 0.00027324892977502506, 'mean_word_length': 2.928443649373882, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2475070', 'n_tokens_mistral': 3547, 'n_tokens_neox': 3459, 'n_words': 806}