@maskito/core
Advanced tools
Comparing version 2.2.0 to 2.3.0
434
index.cjs.js
@@ -5,13 +5,231 @@ 'use strict'; | ||
const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.querySelector('input,textarea') || | ||
e; | ||
function getContentEditableSelection(element) { | ||
const { anchorOffset = 0, focusOffset = 0 } = element.ownerDocument.getSelection() || {}; | ||
const from = Math.min(anchorOffset, focusOffset); | ||
const to = Math.max(anchorOffset, focusOffset); | ||
return [from, to]; | ||
} | ||
const MASKITO_DEFAULT_OPTIONS = { | ||
mask: /^.*$/, | ||
preprocessors: [], | ||
postprocessors: [], | ||
plugins: [], | ||
overwriteMode: 'shift', | ||
function setContentEditableSelection(element, [from, to]) { | ||
var _a, _b; | ||
const document = element.ownerDocument; | ||
const range = document.createRange(); | ||
range.setStart(element.firstChild || element, Math.min(from, ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0)); | ||
range.setEnd(element.lastChild || element, Math.min(to, ((_b = element.textContent) === null || _b === void 0 ? void 0 : _b.length) || 0)); | ||
const selection = document.getSelection(); | ||
if (selection) { | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
} | ||
} | ||
class ContentEditableAdapter { | ||
constructor(element) { | ||
this.element = element; | ||
this.maxLength = Infinity; | ||
} | ||
get value() { | ||
return this.element.innerText.replace(/\n\n$/, '\n'); | ||
} | ||
set value(value) { | ||
// Setting into innerHTML of element with `white-space: pre;` style | ||
this.element.innerHTML = value.replace(/\n$/, '\n\n'); | ||
} | ||
get selectionStart() { | ||
return getContentEditableSelection(this.element)[0]; | ||
} | ||
get selectionEnd() { | ||
return getContentEditableSelection(this.element)[1]; | ||
} | ||
setSelectionRange(from, to) { | ||
setContentEditableSelection(this.element, [from || 0, to || 0]); | ||
} | ||
} | ||
function maskitoAdaptContentEditable(element) { | ||
const adapter = new ContentEditableAdapter(element); | ||
return new Proxy(element, { | ||
get(target, prop) { | ||
if (prop in adapter) { | ||
return adapter[prop]; | ||
} | ||
const nativeProperty = target[prop]; | ||
return typeof nativeProperty === 'function' | ||
? nativeProperty.bind(target) | ||
: nativeProperty; | ||
}, | ||
set(target, prop, val, receiver) { | ||
return Reflect.set(prop in adapter ? adapter : target, prop, val, receiver); | ||
}, | ||
}); | ||
} | ||
class EventListener { | ||
constructor(element) { | ||
this.element = element; | ||
this.listeners = []; | ||
} | ||
listen(eventType, fn, options) { | ||
const untypedFn = fn; | ||
this.element.addEventListener(eventType, untypedFn, options); | ||
this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); | ||
} | ||
destroy() { | ||
this.listeners.forEach(stopListen => stopListen()); | ||
} | ||
} | ||
const HotkeyModifier = { | ||
CTRL: 1 << 0, | ||
ALT: 1 << 1, | ||
SHIFT: 1 << 2, | ||
META: 1 << 3, | ||
}; | ||
// TODO add variants that can be processed correctly | ||
const HotkeyCode = { | ||
Y: 89, | ||
Z: 90, | ||
}; | ||
/** | ||
* Checks if the passed keyboard event match the required hotkey. | ||
* | ||
* @example | ||
* input.addEventListener('keydown', (event) => { | ||
* if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) { | ||
* // redo hotkey pressed | ||
* } | ||
* }) | ||
* | ||
* @return will return `true` only if the {@link HotkeyCode} matches and only the necessary | ||
* {@link HotkeyModifier modifiers} have been pressed | ||
*/ | ||
function isHotkey(event, modifiers, hotkeyCode) { | ||
return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) && | ||
event.altKey === !!(modifiers & HotkeyModifier.ALT) && | ||
event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) && | ||
event.metaKey === !!(modifiers & HotkeyModifier.META) && | ||
/** | ||
* We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more | ||
* "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties. | ||
* @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue} | ||
*/ | ||
// eslint-disable-next-line sonar/deprecation | ||
event.keyCode === hotkeyCode); | ||
} | ||
function isRedo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows | ||
isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
function isUndo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
/** | ||
* Sets value to element, and dispatches input event | ||
* if you passed ELementState, it also sets selection range | ||
* | ||
* @example | ||
* maskitoUpdateElement(input, newValue); | ||
* maskitoUpdateElement(input, elementState); | ||
* | ||
* @see {@link https://github.com/taiga-family/maskito/issues/804 issue} | ||
* | ||
* @return void | ||
*/ | ||
function maskitoUpdateElement(element, valueOrElementState) { | ||
var _a; | ||
const initialValue = element.value; | ||
if (typeof valueOrElementState === 'string') { | ||
element.value = valueOrElementState; | ||
} | ||
else { | ||
const [from, to] = valueOrElementState.selection; | ||
element.value = valueOrElementState.value; | ||
if (element.matches(':focus')) { | ||
(_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to); | ||
} | ||
} | ||
if (element.value !== initialValue) { | ||
element.dispatchEvent(new Event('input', | ||
/** | ||
* React handles this event only on bubbling phase | ||
* | ||
* here is the list of events that are processed in the capture stage, others are processed in the bubbling stage | ||
* https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222 | ||
*/ | ||
{ bubbles: true })); | ||
} | ||
} | ||
function areElementValuesEqual(sampleState, ...states) { | ||
return states.every(({ value }) => value === sampleState.value); | ||
} | ||
function areElementStatesEqual(sampleState, ...states) { | ||
return states.every(({ value, selection }) => value === sampleState.value && | ||
selection[0] === sampleState.selection[0] && | ||
selection[1] === sampleState.selection[1]); | ||
} | ||
function getLineSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const nearestBreak = isForward | ||
? value.slice(from).indexOf('\n') + 1 || value.length | ||
: value.slice(0, to).lastIndexOf('\n') + 1; | ||
const selectFrom = isForward ? from : nearestBreak; | ||
const selectTo = isForward ? nearestBreak : to; | ||
return [selectFrom, selectTo]; | ||
} | ||
function getNotEmptySelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to]; | ||
return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length)); | ||
} | ||
const TRAILING_SPACES_REG = /\s+$/g; | ||
const LEADING_SPACES_REG = /^\s+/g; | ||
const SPACE_REG = /\s/; | ||
function getWordSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
if (isForward) { | ||
const valueAfterSelectionStart = value.slice(from); | ||
const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ | ||
'', | ||
]; | ||
const nearestWordEndIndex = valueAfterSelectionStart | ||
.trimStart() | ||
.search(SPACE_REG); | ||
return [ | ||
from, | ||
nearestWordEndIndex !== -1 | ||
? from + leadingSpaces.length + nearestWordEndIndex | ||
: value.length, | ||
]; | ||
} | ||
const valueBeforeSelectionEnd = value.slice(0, to); | ||
const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; | ||
const selectedWordLength = valueBeforeSelectionEnd | ||
.trimEnd() | ||
.split('') | ||
.reverse() | ||
.findIndex(char => char.match(SPACE_REG)); | ||
return [ | ||
selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, | ||
to, | ||
]; | ||
} | ||
class MaskHistory { | ||
@@ -59,11 +277,2 @@ constructor() { | ||
function areElementValuesEqual(sampleState, ...states) { | ||
return states.every(({ value }) => value === sampleState.value); | ||
} | ||
function areElementStatesEqual(sampleState, ...states) { | ||
return states.every(({ value, selection }) => value === sampleState.value && | ||
selection[0] === sampleState.selection[0] && | ||
selection[1] === sampleState.selection[1]); | ||
} | ||
function applyOverwriteMode({ value, selection }, newCharacters, mode) { | ||
@@ -261,162 +470,2 @@ const [from, to] = selection; | ||
class EventListener { | ||
constructor(element) { | ||
this.element = element; | ||
this.listeners = []; | ||
} | ||
listen(eventType, fn, options) { | ||
const untypedFn = fn; | ||
this.element.addEventListener(eventType, untypedFn, options); | ||
this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); | ||
} | ||
destroy() { | ||
this.listeners.forEach(stopListen => stopListen()); | ||
} | ||
} | ||
const HotkeyModifier = { | ||
CTRL: 1 << 0, | ||
ALT: 1 << 1, | ||
SHIFT: 1 << 2, | ||
META: 1 << 3, | ||
}; | ||
// TODO add variants that can be processed correctly | ||
const HotkeyCode = { | ||
Y: 89, | ||
Z: 90, | ||
}; | ||
/** | ||
* Checks if the passed keyboard event match the required hotkey. | ||
* | ||
* @example | ||
* input.addEventListener('keydown', (event) => { | ||
* if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) { | ||
* // redo hotkey pressed | ||
* } | ||
* }) | ||
* | ||
* @return will return `true` only if the {@link HotkeyCode} matches and only the necessary | ||
* {@link HotkeyModifier modifiers} have been pressed | ||
*/ | ||
function isHotkey(event, modifiers, hotkeyCode) { | ||
return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) && | ||
event.altKey === !!(modifiers & HotkeyModifier.ALT) && | ||
event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) && | ||
event.metaKey === !!(modifiers & HotkeyModifier.META) && | ||
/** | ||
* We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more | ||
* "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties. | ||
* @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue} | ||
*/ | ||
// eslint-disable-next-line sonar/deprecation | ||
event.keyCode === hotkeyCode); | ||
} | ||
function isRedo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows | ||
isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
function isUndo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
/** | ||
* Sets value to element, and dispatches input event | ||
* if you passed ELementState, it also sets selection range | ||
* | ||
* @example | ||
* maskitoUpdateElement(input, newValue); | ||
* maskitoUpdateElement(input, elementState); | ||
* | ||
* @see {@link https://github.com/taiga-family/maskito/issues/804 issue} | ||
* | ||
* @return void | ||
*/ | ||
function maskitoUpdateElement(element, valueOrElementState) { | ||
var _a; | ||
const initialValue = element.value; | ||
if (typeof valueOrElementState === 'string') { | ||
element.value = valueOrElementState; | ||
} | ||
else { | ||
const [from, to] = valueOrElementState.selection; | ||
element.value = valueOrElementState.value; | ||
if (element.matches(':focus')) { | ||
(_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to); | ||
} | ||
} | ||
if (element.value !== initialValue) { | ||
element.dispatchEvent(new Event('input', | ||
/** | ||
* React handles this event only on bubbling phase | ||
* | ||
* here is the list of events that are processed in the capture stage, others are processed in the bubbling stage | ||
* https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222 | ||
*/ | ||
{ bubbles: true })); | ||
} | ||
} | ||
function getLineSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const nearestBreak = isForward | ||
? value.slice(from).indexOf('\n') + 1 || value.length | ||
: value.slice(0, to).lastIndexOf('\n') + 1; | ||
const selectFrom = isForward ? from : nearestBreak; | ||
const selectTo = isForward ? nearestBreak : to; | ||
return [selectFrom, selectTo]; | ||
} | ||
function getNotEmptySelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to]; | ||
return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length)); | ||
} | ||
const TRAILING_SPACES_REG = /\s+$/g; | ||
const LEADING_SPACES_REG = /^\s+/g; | ||
const SPACE_REG = /\s/; | ||
function getWordSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
if (isForward) { | ||
const valueAfterSelectionStart = value.slice(from); | ||
const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ | ||
'', | ||
]; | ||
const nearestWordEndIndex = valueAfterSelectionStart | ||
.trimStart() | ||
.search(SPACE_REG); | ||
return [ | ||
from, | ||
nearestWordEndIndex !== -1 | ||
? from + leadingSpaces.length + nearestWordEndIndex | ||
: value.length, | ||
]; | ||
} | ||
const valueBeforeSelectionEnd = value.slice(0, to); | ||
const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; | ||
const selectedWordLength = valueBeforeSelectionEnd | ||
.trimEnd() | ||
.split('') | ||
.reverse() | ||
.findIndex(char => char.match(SPACE_REG)); | ||
return [ | ||
selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, | ||
to, | ||
]; | ||
} | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
@@ -479,2 +528,15 @@ /** | ||
const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.isContentEditable | ||
? maskitoAdaptContentEditable(e) | ||
: e.querySelector('input,textarea') || | ||
e; | ||
const MASKITO_DEFAULT_OPTIONS = { | ||
mask: /^.*$/, | ||
preprocessors: [], | ||
postprocessors: [], | ||
plugins: [], | ||
overwriteMode: 'shift', | ||
}; | ||
class Maskito extends MaskHistory { | ||
@@ -503,2 +565,3 @@ constructor(element, maskitoOptions) { | ||
this.eventListener.listen('beforeinput', event => { | ||
var _a; | ||
const isForward = event.inputType.includes('Forward'); | ||
@@ -543,2 +606,3 @@ this.updateHistory(this.elementState); | ||
case 'insertLineBreak': | ||
case 'insertParagraph': | ||
return this.handleEnter(event); | ||
@@ -549,3 +613,7 @@ case 'insertFromPaste': | ||
default: | ||
return this.handleInsert(event, event.data || ''); | ||
return this.handleInsert(event, event.data || | ||
( | ||
// `event.data` for `contentEditable` is always `null` for paste/drop events | ||
(_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData('text/plain')) || | ||
''); | ||
} | ||
@@ -629,3 +697,5 @@ }); | ||
initialState.value.slice(initialTo); | ||
if (newPossibleValue === newElementState.value && !force) { | ||
if (newPossibleValue === newElementState.value && | ||
!force && | ||
!this.element.isContentEditable) { | ||
return; | ||
@@ -665,3 +735,4 @@ } | ||
} | ||
if (newPossibleValue !== newElementState.value) { | ||
if (newPossibleValue !== newElementState.value || | ||
this.element.isContentEditable) { | ||
event.preventDefault(); | ||
@@ -676,3 +747,3 @@ this.updateElementState(newElementState, { | ||
handleEnter(event) { | ||
if (this.isTextArea) { | ||
if (this.isTextArea || this.element.isContentEditable) { | ||
this.handleInsert(event, '\n'); | ||
@@ -686,2 +757,3 @@ } | ||
exports.Maskito = Maskito; | ||
exports.maskitoAdaptContentEditable = maskitoAdaptContentEditable; | ||
exports.maskitoInitialCalibrationPlugin = maskitoInitialCalibrationPlugin; | ||
@@ -688,0 +760,0 @@ exports.maskitoPipe = maskitoPipe; |
435
index.esm.js
@@ -1,12 +0,230 @@ | ||
const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.querySelector('input,textarea') || | ||
e; | ||
function getContentEditableSelection(element) { | ||
const { anchorOffset = 0, focusOffset = 0 } = element.ownerDocument.getSelection() || {}; | ||
const from = Math.min(anchorOffset, focusOffset); | ||
const to = Math.max(anchorOffset, focusOffset); | ||
return [from, to]; | ||
} | ||
const MASKITO_DEFAULT_OPTIONS = { | ||
mask: /^.*$/, | ||
preprocessors: [], | ||
postprocessors: [], | ||
plugins: [], | ||
overwriteMode: 'shift', | ||
function setContentEditableSelection(element, [from, to]) { | ||
var _a, _b; | ||
const document = element.ownerDocument; | ||
const range = document.createRange(); | ||
range.setStart(element.firstChild || element, Math.min(from, ((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0)); | ||
range.setEnd(element.lastChild || element, Math.min(to, ((_b = element.textContent) === null || _b === void 0 ? void 0 : _b.length) || 0)); | ||
const selection = document.getSelection(); | ||
if (selection) { | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
} | ||
} | ||
class ContentEditableAdapter { | ||
constructor(element) { | ||
this.element = element; | ||
this.maxLength = Infinity; | ||
} | ||
get value() { | ||
return this.element.innerText.replace(/\n\n$/, '\n'); | ||
} | ||
set value(value) { | ||
// Setting into innerHTML of element with `white-space: pre;` style | ||
this.element.innerHTML = value.replace(/\n$/, '\n\n'); | ||
} | ||
get selectionStart() { | ||
return getContentEditableSelection(this.element)[0]; | ||
} | ||
get selectionEnd() { | ||
return getContentEditableSelection(this.element)[1]; | ||
} | ||
setSelectionRange(from, to) { | ||
setContentEditableSelection(this.element, [from || 0, to || 0]); | ||
} | ||
} | ||
function maskitoAdaptContentEditable(element) { | ||
const adapter = new ContentEditableAdapter(element); | ||
return new Proxy(element, { | ||
get(target, prop) { | ||
if (prop in adapter) { | ||
return adapter[prop]; | ||
} | ||
const nativeProperty = target[prop]; | ||
return typeof nativeProperty === 'function' | ||
? nativeProperty.bind(target) | ||
: nativeProperty; | ||
}, | ||
set(target, prop, val, receiver) { | ||
return Reflect.set(prop in adapter ? adapter : target, prop, val, receiver); | ||
}, | ||
}); | ||
} | ||
class EventListener { | ||
constructor(element) { | ||
this.element = element; | ||
this.listeners = []; | ||
} | ||
listen(eventType, fn, options) { | ||
const untypedFn = fn; | ||
this.element.addEventListener(eventType, untypedFn, options); | ||
this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); | ||
} | ||
destroy() { | ||
this.listeners.forEach(stopListen => stopListen()); | ||
} | ||
} | ||
const HotkeyModifier = { | ||
CTRL: 1 << 0, | ||
ALT: 1 << 1, | ||
SHIFT: 1 << 2, | ||
META: 1 << 3, | ||
}; | ||
// TODO add variants that can be processed correctly | ||
const HotkeyCode = { | ||
Y: 89, | ||
Z: 90, | ||
}; | ||
/** | ||
* Checks if the passed keyboard event match the required hotkey. | ||
* | ||
* @example | ||
* input.addEventListener('keydown', (event) => { | ||
* if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) { | ||
* // redo hotkey pressed | ||
* } | ||
* }) | ||
* | ||
* @return will return `true` only if the {@link HotkeyCode} matches and only the necessary | ||
* {@link HotkeyModifier modifiers} have been pressed | ||
*/ | ||
function isHotkey(event, modifiers, hotkeyCode) { | ||
return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) && | ||
event.altKey === !!(modifiers & HotkeyModifier.ALT) && | ||
event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) && | ||
event.metaKey === !!(modifiers & HotkeyModifier.META) && | ||
/** | ||
* We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more | ||
* "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties. | ||
* @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue} | ||
*/ | ||
// eslint-disable-next-line sonar/deprecation | ||
event.keyCode === hotkeyCode); | ||
} | ||
function isRedo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows | ||
isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
function isUndo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
/** | ||
* Sets value to element, and dispatches input event | ||
* if you passed ELementState, it also sets selection range | ||
* | ||
* @example | ||
* maskitoUpdateElement(input, newValue); | ||
* maskitoUpdateElement(input, elementState); | ||
* | ||
* @see {@link https://github.com/taiga-family/maskito/issues/804 issue} | ||
* | ||
* @return void | ||
*/ | ||
function maskitoUpdateElement(element, valueOrElementState) { | ||
var _a; | ||
const initialValue = element.value; | ||
if (typeof valueOrElementState === 'string') { | ||
element.value = valueOrElementState; | ||
} | ||
else { | ||
const [from, to] = valueOrElementState.selection; | ||
element.value = valueOrElementState.value; | ||
if (element.matches(':focus')) { | ||
(_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to); | ||
} | ||
} | ||
if (element.value !== initialValue) { | ||
element.dispatchEvent(new Event('input', | ||
/** | ||
* React handles this event only on bubbling phase | ||
* | ||
* here is the list of events that are processed in the capture stage, others are processed in the bubbling stage | ||
* https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222 | ||
*/ | ||
{ bubbles: true })); | ||
} | ||
} | ||
function areElementValuesEqual(sampleState, ...states) { | ||
return states.every(({ value }) => value === sampleState.value); | ||
} | ||
function areElementStatesEqual(sampleState, ...states) { | ||
return states.every(({ value, selection }) => value === sampleState.value && | ||
selection[0] === sampleState.selection[0] && | ||
selection[1] === sampleState.selection[1]); | ||
} | ||
function getLineSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const nearestBreak = isForward | ||
? value.slice(from).indexOf('\n') + 1 || value.length | ||
: value.slice(0, to).lastIndexOf('\n') + 1; | ||
const selectFrom = isForward ? from : nearestBreak; | ||
const selectTo = isForward ? nearestBreak : to; | ||
return [selectFrom, selectTo]; | ||
} | ||
function getNotEmptySelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to]; | ||
return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length)); | ||
} | ||
const TRAILING_SPACES_REG = /\s+$/g; | ||
const LEADING_SPACES_REG = /^\s+/g; | ||
const SPACE_REG = /\s/; | ||
function getWordSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
if (isForward) { | ||
const valueAfterSelectionStart = value.slice(from); | ||
const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ | ||
'', | ||
]; | ||
const nearestWordEndIndex = valueAfterSelectionStart | ||
.trimStart() | ||
.search(SPACE_REG); | ||
return [ | ||
from, | ||
nearestWordEndIndex !== -1 | ||
? from + leadingSpaces.length + nearestWordEndIndex | ||
: value.length, | ||
]; | ||
} | ||
const valueBeforeSelectionEnd = value.slice(0, to); | ||
const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; | ||
const selectedWordLength = valueBeforeSelectionEnd | ||
.trimEnd() | ||
.split('') | ||
.reverse() | ||
.findIndex(char => char.match(SPACE_REG)); | ||
return [ | ||
selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, | ||
to, | ||
]; | ||
} | ||
class MaskHistory { | ||
@@ -54,11 +272,2 @@ constructor() { | ||
function areElementValuesEqual(sampleState, ...states) { | ||
return states.every(({ value }) => value === sampleState.value); | ||
} | ||
function areElementStatesEqual(sampleState, ...states) { | ||
return states.every(({ value, selection }) => value === sampleState.value && | ||
selection[0] === sampleState.selection[0] && | ||
selection[1] === sampleState.selection[1]); | ||
} | ||
function applyOverwriteMode({ value, selection }, newCharacters, mode) { | ||
@@ -256,162 +465,2 @@ const [from, to] = selection; | ||
class EventListener { | ||
constructor(element) { | ||
this.element = element; | ||
this.listeners = []; | ||
} | ||
listen(eventType, fn, options) { | ||
const untypedFn = fn; | ||
this.element.addEventListener(eventType, untypedFn, options); | ||
this.listeners.push(() => this.element.removeEventListener(eventType, untypedFn)); | ||
} | ||
destroy() { | ||
this.listeners.forEach(stopListen => stopListen()); | ||
} | ||
} | ||
const HotkeyModifier = { | ||
CTRL: 1 << 0, | ||
ALT: 1 << 1, | ||
SHIFT: 1 << 2, | ||
META: 1 << 3, | ||
}; | ||
// TODO add variants that can be processed correctly | ||
const HotkeyCode = { | ||
Y: 89, | ||
Z: 90, | ||
}; | ||
/** | ||
* Checks if the passed keyboard event match the required hotkey. | ||
* | ||
* @example | ||
* input.addEventListener('keydown', (event) => { | ||
* if (isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z)) { | ||
* // redo hotkey pressed | ||
* } | ||
* }) | ||
* | ||
* @return will return `true` only if the {@link HotkeyCode} matches and only the necessary | ||
* {@link HotkeyModifier modifiers} have been pressed | ||
*/ | ||
function isHotkey(event, modifiers, hotkeyCode) { | ||
return (event.ctrlKey === !!(modifiers & HotkeyModifier.CTRL) && | ||
event.altKey === !!(modifiers & HotkeyModifier.ALT) && | ||
event.shiftKey === !!(modifiers & HotkeyModifier.SHIFT) && | ||
event.metaKey === !!(modifiers & HotkeyModifier.META) && | ||
/** | ||
* We intentionally use legacy {@link KeyboardEvent#keyCode `keyCode`} property. It is more | ||
* "keyboard-layout"-independent than {@link KeyboardEvent#key `key`} or {@link KeyboardEvent#code `code`} properties. | ||
* @see {@link https://github.com/taiga-family/maskito/issues/315 `KeyboardEvent#code` issue} | ||
*/ | ||
// eslint-disable-next-line sonar/deprecation | ||
event.keyCode === hotkeyCode); | ||
} | ||
function isRedo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Y) || // Windows | ||
isHotkey(event, HotkeyModifier.CTRL | HotkeyModifier.SHIFT, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META | HotkeyModifier.SHIFT, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
function isUndo(event) { | ||
return (isHotkey(event, HotkeyModifier.CTRL, HotkeyCode.Z) || // Windows & Android | ||
isHotkey(event, HotkeyModifier.META, HotkeyCode.Z) // macOS & iOS | ||
); | ||
} | ||
/** | ||
* Sets value to element, and dispatches input event | ||
* if you passed ELementState, it also sets selection range | ||
* | ||
* @example | ||
* maskitoUpdateElement(input, newValue); | ||
* maskitoUpdateElement(input, elementState); | ||
* | ||
* @see {@link https://github.com/taiga-family/maskito/issues/804 issue} | ||
* | ||
* @return void | ||
*/ | ||
function maskitoUpdateElement(element, valueOrElementState) { | ||
var _a; | ||
const initialValue = element.value; | ||
if (typeof valueOrElementState === 'string') { | ||
element.value = valueOrElementState; | ||
} | ||
else { | ||
const [from, to] = valueOrElementState.selection; | ||
element.value = valueOrElementState.value; | ||
if (element.matches(':focus')) { | ||
(_a = element.setSelectionRange) === null || _a === void 0 ? void 0 : _a.call(element, from, to); | ||
} | ||
} | ||
if (element.value !== initialValue) { | ||
element.dispatchEvent(new Event('input', | ||
/** | ||
* React handles this event only on bubbling phase | ||
* | ||
* here is the list of events that are processed in the capture stage, others are processed in the bubbling stage | ||
* https://github.com/facebook/react/blob/cb2439624f43c510007f65aea5c50a8bb97917e4/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js#L222 | ||
*/ | ||
{ bubbles: true })); | ||
} | ||
} | ||
function getLineSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const nearestBreak = isForward | ||
? value.slice(from).indexOf('\n') + 1 || value.length | ||
: value.slice(0, to).lastIndexOf('\n') + 1; | ||
const selectFrom = isForward ? from : nearestBreak; | ||
const selectTo = isForward ? nearestBreak : to; | ||
return [selectFrom, selectTo]; | ||
} | ||
function getNotEmptySelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
const notEmptySelection = isForward ? [from, to + 1] : [from - 1, to]; | ||
return notEmptySelection.map(x => Math.min(Math.max(x, 0), value.length)); | ||
} | ||
const TRAILING_SPACES_REG = /\s+$/g; | ||
const LEADING_SPACES_REG = /^\s+/g; | ||
const SPACE_REG = /\s/; | ||
function getWordSelection({ value, selection }, isForward) { | ||
const [from, to] = selection; | ||
if (from !== to) { | ||
return [from, to]; | ||
} | ||
if (isForward) { | ||
const valueAfterSelectionStart = value.slice(from); | ||
const [leadingSpaces] = valueAfterSelectionStart.match(LEADING_SPACES_REG) || [ | ||
'', | ||
]; | ||
const nearestWordEndIndex = valueAfterSelectionStart | ||
.trimStart() | ||
.search(SPACE_REG); | ||
return [ | ||
from, | ||
nearestWordEndIndex !== -1 | ||
? from + leadingSpaces.length + nearestWordEndIndex | ||
: value.length, | ||
]; | ||
} | ||
const valueBeforeSelectionEnd = value.slice(0, to); | ||
const [trailingSpaces] = valueBeforeSelectionEnd.match(TRAILING_SPACES_REG) || ['']; | ||
const selectedWordLength = valueBeforeSelectionEnd | ||
.trimEnd() | ||
.split('') | ||
.reverse() | ||
.findIndex(char => char.match(SPACE_REG)); | ||
return [ | ||
selectedWordLength !== -1 ? to - trailingSpaces.length - selectedWordLength : 0, | ||
to, | ||
]; | ||
} | ||
/* eslint-disable @typescript-eslint/ban-types */ | ||
@@ -474,2 +523,15 @@ /** | ||
const MASKITO_DEFAULT_ELEMENT_PREDICATE = e => e.isContentEditable | ||
? maskitoAdaptContentEditable(e) | ||
: e.querySelector('input,textarea') || | ||
e; | ||
const MASKITO_DEFAULT_OPTIONS = { | ||
mask: /^.*$/, | ||
preprocessors: [], | ||
postprocessors: [], | ||
plugins: [], | ||
overwriteMode: 'shift', | ||
}; | ||
class Maskito extends MaskHistory { | ||
@@ -498,2 +560,3 @@ constructor(element, maskitoOptions) { | ||
this.eventListener.listen('beforeinput', event => { | ||
var _a; | ||
const isForward = event.inputType.includes('Forward'); | ||
@@ -538,2 +601,3 @@ this.updateHistory(this.elementState); | ||
case 'insertLineBreak': | ||
case 'insertParagraph': | ||
return this.handleEnter(event); | ||
@@ -544,3 +608,7 @@ case 'insertFromPaste': | ||
default: | ||
return this.handleInsert(event, event.data || ''); | ||
return this.handleInsert(event, event.data || | ||
( | ||
// `event.data` for `contentEditable` is always `null` for paste/drop events | ||
(_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.getData('text/plain')) || | ||
''); | ||
} | ||
@@ -624,3 +692,5 @@ }); | ||
initialState.value.slice(initialTo); | ||
if (newPossibleValue === newElementState.value && !force) { | ||
if (newPossibleValue === newElementState.value && | ||
!force && | ||
!this.element.isContentEditable) { | ||
return; | ||
@@ -660,3 +730,4 @@ } | ||
} | ||
if (newPossibleValue !== newElementState.value) { | ||
if (newPossibleValue !== newElementState.value || | ||
this.element.isContentEditable) { | ||
event.preventDefault(); | ||
@@ -671,3 +742,3 @@ this.updateElementState(newElementState, { | ||
handleEnter(event) { | ||
if (this.isTextArea) { | ||
if (this.isTextArea || this.element.isContentEditable) { | ||
this.handleInsert(event, '\n'); | ||
@@ -678,2 +749,2 @@ } | ||
export { MASKITO_DEFAULT_ELEMENT_PREDICATE, MASKITO_DEFAULT_OPTIONS, Maskito, maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement }; | ||
export { MASKITO_DEFAULT_ELEMENT_PREDICATE, MASKITO_DEFAULT_OPTIONS, Maskito, maskitoAdaptContentEditable, maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement }; |
{ | ||
"name": "@maskito/core", | ||
"version": "2.2.0", | ||
"version": "2.3.0", | ||
"description": "The main zero-dependency and framework-agnostic Maskito's package to create an input mask", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
export { MASKITO_DEFAULT_ELEMENT_PREDICATE, MASKITO_DEFAULT_OPTIONS, } from './lib/constants'; | ||
export { Maskito } from './lib/mask'; | ||
export { MaskitoElementPredicate, MaskitoMask, MaskitoMaskExpression, MaskitoOptions, MaskitoPlugin, MaskitoPostprocessor, MaskitoPreprocessor, } from './lib/types'; | ||
export { maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement, } from './lib/utils'; | ||
export { MaskitoElement, MaskitoElementPredicate, MaskitoMask, MaskitoMaskExpression, MaskitoOptions, MaskitoPlugin, MaskitoPostprocessor, MaskitoPreprocessor, } from './lib/types'; | ||
export { maskitoAdaptContentEditable, maskitoInitialCalibrationPlugin, maskitoPipe, maskitoStrictCompositionPlugin, maskitoTransform, maskitoUpdateElement, } from './lib/utils'; | ||
//# sourceMappingURL=index.d.ts.map |
import { MaskHistory } from './classes'; | ||
import type { ElementState, MaskitoOptions, TypedInputEvent } from './types'; | ||
import type { ElementState, MaskitoElement, MaskitoOptions, TypedInputEvent } from './types'; | ||
export declare class Maskito extends MaskHistory { | ||
@@ -12,3 +12,3 @@ private readonly element; | ||
private readonly teardowns; | ||
constructor(element: HTMLInputElement | HTMLTextAreaElement, maskitoOptions: MaskitoOptions); | ||
constructor(element: MaskitoElement, maskitoOptions: MaskitoOptions); | ||
private get elementState(); | ||
@@ -15,0 +15,0 @@ private get maxLength(); |
@@ -1,2 +0,3 @@ | ||
export type MaskitoElementPredicate = (element: HTMLElement) => HTMLInputElement | HTMLTextAreaElement | Promise<HTMLInputElement | HTMLTextAreaElement>; | ||
import type { MaskitoElement } from './maskito-element'; | ||
export type MaskitoElementPredicate = (element: HTMLElement) => MaskitoElement | Promise<MaskitoElement>; | ||
//# sourceMappingURL=element-predicate.d.ts.map |
@@ -6,2 +6,3 @@ export * from './element-predicate'; | ||
export * from './mask-processors'; | ||
export * from './maskito-element'; | ||
export * from './plugin'; | ||
@@ -8,0 +9,0 @@ export * from './selection-range'; |
import type { MaskitoOptions } from './mask-options'; | ||
export type MaskitoPlugin = (element: HTMLInputElement | HTMLTextAreaElement, options: Required<MaskitoOptions>) => (() => void) | void; | ||
import type { MaskitoElement } from './maskito-element'; | ||
export type MaskitoPlugin = (element: MaskitoElement, options: Required<MaskitoOptions>) => (() => void) | void; | ||
//# sourceMappingURL=plugin.d.ts.map |
export interface TypedInputEvent extends InputEvent { | ||
inputType: 'deleteByCut' | 'deleteContentBackward' | 'deleteContentForward' | 'deleteHardLineBackward' | 'deleteHardLineForward' | 'deleteSoftLineBackward' | 'deleteSoftLineForward' | 'deleteWordBackward' | 'deleteWordForward' | 'historyRedo' | 'historyUndo' | 'insertCompositionText' | 'insertFromDrop' | 'insertFromPaste' | 'insertLineBreak' | 'insertReplacementText' | 'insertText'; | ||
inputType: 'deleteByCut' | 'deleteContentBackward' | 'deleteContentForward' | 'deleteHardLineBackward' | 'deleteHardLineForward' | 'deleteSoftLineBackward' | 'deleteSoftLineForward' | 'deleteWordBackward' | 'deleteWordForward' | 'historyRedo' | 'historyUndo' | 'insertCompositionText' | 'insertFromDrop' | 'insertFromPaste' | 'insertLineBreak' | 'insertParagraph' | 'insertReplacementText' | 'insertText'; | ||
} | ||
//# sourceMappingURL=typed-input-event.d.ts.map |
@@ -1,2 +0,2 @@ | ||
import type { ElementState } from '../../types'; | ||
import type { ElementState, MaskitoElement } from '../../types'; | ||
/** | ||
@@ -14,3 +14,3 @@ * Sets value to element, and dispatches input event | ||
*/ | ||
export declare function maskitoUpdateElement(element: HTMLInputElement | HTMLTextAreaElement, valueOrElementState: ElementState | string): void; | ||
export declare function maskitoUpdateElement(element: MaskitoElement, valueOrElementState: ElementState | string): void; | ||
//# sourceMappingURL=update-element.d.ts.map |
@@ -0,3 +1,6 @@ | ||
export * from './content-editable'; | ||
export * from './dom/event-listener'; | ||
export * from './dom/get-content-editable-selection'; | ||
export * from './dom/history-events'; | ||
export * from './dom/set-content-editable-selection'; | ||
export * from './dom/update-element'; | ||
@@ -4,0 +7,0 @@ export * from './element-states-equality'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
91006
89
1650