Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,35 @@ <h4 id="skipInvalid" class="section-h"><a href="#skipInvalid">Skip Invalid Mode
</div>
</div>

<h4 id="ignoreCompositionState" class="section-h"><a href="#ignoreCompositionState">Ignore Composition State <kbd>since 7.6.1</kbd></a></h4>
<p>When enabled, ignores IME composition state and processes input events anyway. Useful for Android keyboards that incorrectly set <code>isComposing=true</code> for Latin characters, causing data loss on blur.</p>
<div class="form-item">
<label>Try typing with problematic Android keyboard (Xiaomi, Yandex Keyboard)</label>
<div class="input-checkbox">
<input id="ignoreCompositionState-mask" type="text" value="">
<label>
<input type="checkbox" name="ignoreCompositionState" id="toggle-ignoreCompositionState-mask">
Ignore&nbsp;composition&nbsp;state
</label>
</div>

<div class="toggle-panel">
<label for="ignoreCompositionState-mask-source">source</label>
<input id="ignoreCompositionState-mask-source" type="checkbox">
<div class="toggle-panel-content">
{% highlight javascript %}
IMask(
document.getElementById('ignoreCompositionState-mask'),
{
mask: '000-000-0000',
ignoreCompositionState: <CHECK>,
}
)
{% endhighlight %}
</div>
</div>
</div>

<h3 id="masked-number" class="section-h"><a href="#masked-number">Number Mask</a></h3>
<p>Number mask restricts input to integer or decimal numbers in many ways:</p>
<div class="toggle-panel">
Expand Down Expand Up @@ -1683,5 +1712,20 @@ <h3 id="treeshaking" class="section-h"><a href="#treeshaking">Treeshaking <kbd>s
skipInvalid: e.target.checked,
});
});

var ignoreCompositionStateMask = IMask(
document.getElementById('ignoreCompositionState-mask'),
{
mask: '000-000-0000',
ignoreCompositionState: false,
}
);

var toggleIgnoreCompositionState = document.getElementById('toggle-ignoreCompositionState-mask');
toggleIgnoreCompositionState.addEventListener('change', function (e) {
ignoreCompositionStateMask.updateOptions({
ignoreCompositionState: e.target.checked,
});
});
});
</script>
6 changes: 3 additions & 3 deletions packages/angular-imask/src/imask.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type InputMask, type InputMaskElement, type FactoryArg, type UpdateOpts } from 'imask';
import { type InputMask, type InputMaskElement, type FactoryArg, type UpdateOpts, type ControlOptions } from 'imask';
import { isPlatformBrowser } from '@angular/common';
import {
Directive, ElementRef, Input, Output, forwardRef, Provider, Renderer2,
Expand Down Expand Up @@ -38,7 +38,7 @@ export const DEFAULT_IMASK_ELEMENT = (elementRef: any) => elementRef.nativeEleme
providers: [MASKEDINPUT_VALUE_ACCESSOR],
})
export class IMaskDirective<
Opts extends FactoryArg,
Opts extends FactoryArg & ControlOptions,
Unmask extends ('typed' | boolean) = false,
V = Value<Opts, Unmask>,
> implements ControlValueAccessor, AfterViewInit, OnDestroy, OnChanges {
Expand Down Expand Up @@ -97,7 +97,7 @@ export class IMaskDirective<
if (!changes['imask'] || !this._viewInitialized) return;

if (this.imask) {
if (this.maskRef) this.maskRef.updateOptions(this.imask as UpdateOpts<Opts>);
if (this.maskRef) this.maskRef.updateOptions(this.imask as any);
else {
this.initMask();
this.onChange(this.maskValue);
Expand Down
14 changes: 11 additions & 3 deletions packages/imask/src/controls/html-mask-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export default
abstract class HTMLMaskElement extends MaskElement {
/** HTMLElement to use mask on */
declare input: HTMLElement;
declare _handlers: EventHandlers;
declare ignoreCompositionState?: boolean;
declare _handlers: EventHandlers;

abstract value: string;

constructor (input: HTMLElement) {
Expand Down Expand Up @@ -58,7 +60,7 @@ abstract class HTMLMaskElement extends MaskElement {
return this._handlers.undo(e);
}

if (!e.isComposing) this._handlers.selectionChange(e);
if (this.ignoreCompositionState || !e.isComposing) this._handlers.selectionChange(e);
}

_onBeforeinput (e: InputEvent) {
Expand All @@ -78,7 +80,13 @@ abstract class HTMLMaskElement extends MaskElement {
}

_onInput (e: InputEvent) {
if (!e.isComposing) this._handlers.input(e);
if (this.ignoreCompositionState || !e.isComposing) this._handlers.input(e);
}

setIgnoreCompositionState (ignoreCompositionState?: boolean) {
if (typeof ignoreCompositionState !== 'undefined') {
this.ignoreCompositionState = Boolean(ignoreCompositionState);
}
}

/** Unbinds HTMLElement events to mask internal events */
Expand Down
10 changes: 8 additions & 2 deletions packages/imask/src/controls/input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DIRECTION, type Selection } from '../core/utils';
import ActionDetails from '../core/action-details';
import createMask, { type UpdateOpts, maskedClass, type FactoryArg, type FactoryReturnMasked } from '../masked/factory';
import createMask, { type UpdateOpts, maskedClass, type FactoryArg, type FactoryReturnMasked, type ControlOptions } from '../masked/factory';
import Masked from '../masked/base';
import MaskElement from './mask-element';
import HTMLInputMaskElement, { type InputElement } from './html-input-mask-element';
Expand Down Expand Up @@ -63,6 +63,7 @@ class InputMask<Opts extends FactoryArg=Record<string, unknown>> {
this.alignCursorFriendly = this.alignCursorFriendly.bind(this);

this._bindEvents();
this._setIgnoreCompositionState((opts as Opts & ControlOptions)?.ignoreCompositionState);

// refresh
this.updateValue();
Expand Down Expand Up @@ -164,6 +165,10 @@ class InputMask<Opts extends FactoryArg=Record<string, unknown>> {
if (this.el) this.el.unbindEvents();
}

_setIgnoreCompositionState (ignoreCompositionState?: boolean) {
this.el.setIgnoreCompositionState?.(ignoreCompositionState);
}

/** Fires custom event */
_fireEvent (ev: string, e?: InputEvent) {
const listeners = this._listeners[ev];
Expand Down Expand Up @@ -244,7 +249,7 @@ class InputMask<Opts extends FactoryArg=Record<string, unknown>> {

/** Updates options with deep equal check, recreates {@link Masked} model if mask type changes */
updateOptions(opts: UpdateOpts<Opts>) {
const { mask, ...restOpts } = opts as any; // TODO types, yes, mask is optional
const { mask, ignoreCompositionState, ...restOpts } = opts as any; // TODO types, yes, mask is optional

const updateMask = !this.maskEquals(mask);
const updateOpts = this.masked.optionsIsChanged(restOpts);
Expand All @@ -253,6 +258,7 @@ class InputMask<Opts extends FactoryArg=Record<string, unknown>> {
if (updateOpts) this.masked.updateOptions(restOpts); // TODO

if (updateMask || updateOpts) this.updateControl();
this._setIgnoreCompositionState(ignoreCompositionState);
}

/** Updates cursor */
Expand Down
5 changes: 4 additions & 1 deletion packages/imask/src/controls/mask-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ abstract class MaskElement {
/** */
abstract bindEvents (handlers: EventHandlers): void;
/** */
abstract unbindEvents (): void
abstract unbindEvents (): void;

/** Sets ignore composition state option */
setIgnoreCompositionState? (ignoreCompositionState?: boolean): void;
}


Expand Down
1 change: 1 addition & 0 deletions packages/imask/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
default as createMask,
normalizeOpts,
type AllFactoryStaticOpts,
type ControlOptions,
type FactoryArg,
type FactoryConstructorOpts,
type FactoryConstructorReturnMasked,
Expand Down
9 changes: 9 additions & 0 deletions packages/imask/src/masked/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ type FactoryStaticOpts =
| MaskedFunctionOptions
;

export
type ControlOptions = {
/**
* When true, ignore IME composition state and process input events anyway.
*/
ignoreCompositionState?: boolean,
};

export
type AllFactoryStaticOpts =
& MaskedDateFactoryOptions
Expand All @@ -43,6 +51,7 @@ type AllFactoryStaticOpts =
& MaskedFunctionOptions
& MaskedEnumOptions
& MaskedRangeOptions
& ControlOptions
;

export
Expand Down
4 changes: 3 additions & 1 deletion packages/react-imask/src/mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export
type Falsy = false | 0 | "" | null | undefined;

export
type ReactMaskOpts = FactoryOpts & { unmask?: 'typed' | boolean };
type ReactMaskOpts = FactoryOpts & { unmask?: 'typed' | boolean; ignoreCompositionState?: boolean };

export
type UnmaskValue<Opts extends ReactMaskOpts> =
Expand All @@ -35,6 +35,7 @@ type ReactMaskProps<
value?: UnmaskValue<ExtractReactMaskOpts<MaskElement, Props>>;
inputRef?: React.Ref<MaskElement>;
ref?: React.Ref<React.ComponentType<Props>>;
ignoreCompositionState?: boolean;
}

const MASK_PROPS: { [key in keyof (AllFactoryStaticOpts & ReactMaskProps<InputMaskElement, AllFactoryStaticOpts>)]: any } = {
Expand Down Expand Up @@ -65,6 +66,7 @@ const MASK_PROPS: { [key in keyof (AllFactoryStaticOpts & ReactMaskProps<InputMa
PropTypes.oneOf(['append', 'remove']),
]),
skipInvalid: PropTypes.bool,
ignoreCompositionState: PropTypes.bool,

// events
onAccept: PropTypes.func,
Expand Down
4 changes: 2 additions & 2 deletions packages/solid-imask/src/input.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { JSX } from 'solid-js/jsx-runtime';
import { createEffect, onCleanup, splitProps } from 'solid-js';
import IMask, { type InputMask, type FactoryArg } from 'imask';
import IMask, { type InputMask, type FactoryArg, type ControlOptions } from 'imask';


// TODO can `directive` be reused here?
// TODO personally hate JSX in libs but seems no way out for solid

const createMaskedInput =
<
Opts extends FactoryArg,
Opts extends FactoryArg & ControlOptions,
Value = {
typedValue: InputMask<Opts>['typedValue'];
value: InputMask<Opts>['value'];
Expand Down
8 changes: 4 additions & 4 deletions packages/svelte-imask/src/action.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import IMask, { type FactoryArg, type InputMask, type UpdateOpts } from 'imask';
import IMask, { type FactoryArg, type InputMask, type UpdateOpts, type ControlOptions } from 'imask';


function fireEvent<Opts extends FactoryArg> (el: HTMLElement, eventName: string, data: InputMask<Opts>) {
function fireEvent<Opts extends FactoryArg & ControlOptions> (el: HTMLElement, eventName: string, data: InputMask<Opts>) {
const e = document.createEvent('CustomEvent');
e.initCustomEvent(eventName, true, true, data);
el.dispatchEvent(e);
}

function initMask<Opts extends FactoryArg> (el: HTMLElement, opts: InputMask<Opts> | Opts): InputMask<Opts> {
function initMask<Opts extends FactoryArg & ControlOptions> (el: HTMLElement, opts: InputMask<Opts> | Opts): InputMask<Opts> {
const maskRef = (opts instanceof IMask.InputMask ? opts : IMask(el, opts as Opts));
return maskRef
.on('accept', () => fireEvent(el, 'accept', maskRef))
Expand All @@ -16,7 +16,7 @@ function initMask<Opts extends FactoryArg> (el: HTMLElement, opts: InputMask<Opt


export default
function IMaskAction<Opts extends FactoryArg> (el: HTMLElement, opts: Opts) {
function IMaskAction<Opts extends FactoryArg & ControlOptions> (el: HTMLElement, opts: Opts) {
let maskRef: InputMask<Opts> | undefined;
let created: boolean | undefined;

Expand Down
3 changes: 2 additions & 1 deletion packages/vue-imask/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type PropType } from 'vue-demi';
import { type FactoryOpts, type MaskedDynamicOptions } from 'imask';
import { type FactoryOpts, type MaskedDynamicOptions, type ControlOptions } from 'imask';

export default {
// common
Expand All @@ -19,6 +19,7 @@ export default {
validator: (value: unknown) => ['append', 'remove'].includes(value as string) || typeof value === 'boolean',
},
skipInvalid: { type: Boolean, required: false, default: undefined },
ignoreCompositionState: { type: Boolean, required: false, default: undefined },

// pattern
placeholderChar: String,
Expand Down