text-field-edit
Advanced tools
Comparing version 4.0.0 to 4.1.0
@@ -0,12 +1,14 @@ | ||
/** Call a function after focusing a field and then restore the previous focus afterwards if necessary */ | ||
declare function withFocus<T>(field: HTMLElement, callback: () => T): T; | ||
/** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */ | ||
export declare function insertTextIntoField(field: HTMLTextAreaElement | HTMLInputElement, text: string): void; | ||
export declare function insertTextIntoField(field: HTMLElement, text: string): void; | ||
/** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */ | ||
export declare function setFieldText(field: HTMLTextAreaElement | HTMLInputElement, text: string): void; | ||
export declare function setFieldText(field: HTMLElement, text: string): void; | ||
/** Get the selected text in a field or an empty string if nothing is selected. */ | ||
export declare function getFieldSelection(field: HTMLTextAreaElement | HTMLInputElement): string; | ||
export declare function getFieldSelection(field: HTMLElement): string; | ||
/** Adds the `wrappingText` before and after field’s selection (or cursor). If `endWrappingText` is provided, it will be used instead of `wrappingText` at on the right. */ | ||
export declare function wrapFieldSelection(field: HTMLTextAreaElement | HTMLInputElement, wrap: string, wrapEnd?: string): void; | ||
type ReplacerCallback = (substring: string, ...args: any[]) => string; | ||
export declare function wrapFieldSelection(field: HTMLElement, wrap: string, wrapEnd?: string): void; | ||
type ReplacerCallback = (substring: string, ...arguments_: any[]) => string; | ||
/** Finds and replaces strings and regex in the field’s value, like `field.value = field.value.replace()` but better */ | ||
export declare function replaceFieldText(field: HTMLTextAreaElement | HTMLInputElement, searchValue: string | RegExp, replacer: string | ReplacerCallback, cursor?: 'select' | 'after-replacement'): void; | ||
export declare function replaceFieldText(field: HTMLInputElement | HTMLTextAreaElement, searchValue: string | RegExp, replacer: string | ReplacerCallback, cursor?: 'select' | 'after-replacement'): void; | ||
/** @deprecated Import `insertTextIntoField` instead */ | ||
@@ -30,1 +32,2 @@ export declare const insert: typeof insertTextIntoField; | ||
export default textFieldEdit; | ||
export { withFocus as _TEST_ONLY_withFocus }; |
135
index.js
@@ -1,8 +0,25 @@ | ||
/** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */ | ||
export function insertTextIntoField(field, text) { | ||
function isNativeField(field) { | ||
return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement; | ||
} | ||
/** Call a function after focusing a field and then restore the previous focus afterwards if necessary */ | ||
function withFocus(field, callback) { | ||
const document = field.ownerDocument; | ||
const initialFocus = document.activeElement; | ||
if (initialFocus !== field) { | ||
if (initialFocus === field) { | ||
return callback(); | ||
} | ||
try { | ||
field.focus(); | ||
return callback(); | ||
} | ||
finally { | ||
field.blur(); // Supports `intialFocus === body` | ||
if (initialFocus instanceof HTMLElement) { | ||
initialFocus.focus(); | ||
} | ||
} | ||
} | ||
// This will insert into the focused field. It shouild always be called inside withFocus. | ||
// Use this one locally if there are multiple `insertTextIntoField` or `document.execCommand` calls | ||
function insertTextWhereverTheFocusIs(document, text) { | ||
if (text === '') { | ||
@@ -15,45 +32,104 @@ // https://github.com/fregante/text-field-edit/issues/16 | ||
} | ||
if (initialFocus === document.body) { | ||
field.blur(); | ||
} | ||
else if (initialFocus instanceof HTMLElement && initialFocus !== field) { | ||
initialFocus.focus(); | ||
} | ||
} | ||
/** Inserts `text` at the cursor’s position, replacing any selection, with **undo** support and by firing the `input` event. */ | ||
export function insertTextIntoField(field, text) { | ||
withFocus(field, () => { | ||
insertTextWhereverTheFocusIs(field.ownerDocument, text); | ||
}); | ||
} | ||
/** Replaces the entire content, equivalent to `field.value = text` but with **undo** support and by firing the `input` event. */ | ||
export function setFieldText(field, text) { | ||
field.select(); | ||
insertTextIntoField(field, text); | ||
if (isNativeField(field)) { | ||
field.select(); | ||
insertTextIntoField(field, text); | ||
} | ||
else { | ||
const document = field.ownerDocument; | ||
withFocus(field, () => { | ||
document.execCommand('selectAll', false, text); | ||
insertTextWhereverTheFocusIs(document, text); | ||
}); | ||
} | ||
} | ||
/** Get the selected text in a field or an empty string if nothing is selected. */ | ||
export function getFieldSelection(field) { | ||
return field.value.slice(field.selectionStart, field.selectionEnd); | ||
if (isNativeField(field)) { | ||
return field.value.slice(field.selectionStart, field.selectionEnd); | ||
} | ||
const selection = field.ownerDocument.getSelection(); | ||
if (selection && field.contains(selection.anchorNode)) { | ||
// The selection is inside the field | ||
return selection.toString(); | ||
} | ||
return ''; | ||
} | ||
/** Adds the `wrappingText` before and after field’s selection (or cursor). If `endWrappingText` is provided, it will be used instead of `wrappingText` at on the right. */ | ||
export function wrapFieldSelection(field, wrap, wrapEnd) { | ||
function wrapFieldSelectionNative(field, wrap, wrapEnd) { | ||
const { selectionStart, selectionEnd } = field; | ||
const selection = getFieldSelection(field); | ||
insertTextIntoField(field, wrap + selection + (wrapEnd ?? wrap)); | ||
insertTextIntoField(field, wrap + selection + wrapEnd); | ||
// Restore the selection around the previously-selected text | ||
field.selectionStart = selectionStart + wrap.length; | ||
field.selectionEnd = selectionEnd + wrap.length; | ||
field.selectionEnd = selectionEnd + wrapEnd.length; | ||
} | ||
function collapseCursor(selection, range, toStart) { | ||
const alteredRange = range.cloneRange(); | ||
alteredRange.collapse(toStart); | ||
selection.removeAllRanges(); | ||
selection.addRange(alteredRange); | ||
} | ||
function wrapFieldSelectionContentEditable(field, before, after) { | ||
const document = field.ownerDocument; | ||
const selection = document.getSelection(); | ||
const selectionRange = selection.getRangeAt(0); | ||
if (after) { | ||
collapseCursor(selection, selectionRange, false); | ||
insertTextIntoField(field, after); | ||
} | ||
if (before) { | ||
collapseCursor(selection, selectionRange, true); | ||
insertTextIntoField(field, before); | ||
// The text added by at the beginning is included in the new selection, while wrapEnd isn't. | ||
// This nudges the selection after the newly-inserted text. | ||
selectionRange.setStart(selectionRange.startContainer, selectionRange.startOffset + before.length); | ||
} | ||
if (after ?? before) { | ||
// Restore selection | ||
selection.removeAllRanges(); | ||
selection.addRange(selectionRange); | ||
} | ||
} | ||
/** Adds the `wrappingText` before and after field’s selection (or cursor). If `endWrappingText` is provided, it will be used instead of `wrappingText` at on the right. */ | ||
export function wrapFieldSelection(field, wrap, | ||
// TODO: Ensure that it works regardless of direction | ||
wrapEnd = wrap) { | ||
if (isNativeField(field)) { | ||
wrapFieldSelectionNative(field, wrap, wrapEnd); | ||
} | ||
else { | ||
wrapFieldSelectionContentEditable(field, wrap, wrapEnd); | ||
} | ||
} | ||
/** Finds and replaces strings and regex in the field’s value, like `field.value = field.value.replace()` but better */ | ||
export function replaceFieldText(field, searchValue, replacer, cursor = 'select') { | ||
if (!isNativeField(field)) { | ||
throw new TypeError('replaceFieldText only supports input and textarea fields'); | ||
} | ||
/** Keeps track of how much each match offset should be adjusted */ | ||
let drift = 0; | ||
field.value.replace(searchValue, (...args) => { | ||
// Select current match to replace it later | ||
const matchStart = drift + args.at(-2); | ||
const matchLength = args[0].length; | ||
field.selectionStart = matchStart; | ||
field.selectionEnd = matchStart + matchLength; | ||
const replacement = typeof replacer === 'string' ? replacer : replacer(...args); | ||
insertTextIntoField(field, replacement); | ||
if (cursor === 'select') { | ||
// Select replacement. Without this, the cursor would be after the replacement | ||
withFocus(field, () => { | ||
field.value.replace(searchValue, (...arguments_) => { | ||
// Select current match to replace it later | ||
const matchStart = drift + arguments_.at(-2); | ||
const matchLength = arguments_[0].length; | ||
field.selectionStart = matchStart; | ||
} | ||
drift += replacement.length - matchLength; | ||
return replacement; | ||
field.selectionEnd = matchStart + matchLength; | ||
const replacement = typeof replacer === 'string' ? replacer : replacer(...arguments_); | ||
insertTextWhereverTheFocusIs(field.ownerDocument, replacement); | ||
if (cursor === 'select') { | ||
// Select replacement. Without this, the cursor would be after the replacement | ||
field.selectionStart = matchStart; | ||
} | ||
drift += replacement.length - matchLength; | ||
return replacement; | ||
}); | ||
}); | ||
@@ -80,1 +156,2 @@ } | ||
export default textFieldEdit; | ||
export { withFocus as _TEST_ONLY_withFocus }; |
{ | ||
"name": "text-field-edit", | ||
"version": "4.0.0", | ||
"version": "4.1.0", | ||
"description": "Insert text in a `<textarea>` and `<input>` (including Undo in most browsers)", | ||
@@ -39,6 +39,6 @@ "keywords": [ | ||
"test": "npm-run-all --silent build --parallel test:*", | ||
"test:blink": "browserify -p esmify test.js | tape-run --browser chrome", | ||
"test:gecko": "browserify -p esmify test.js | tape-run --browser firefox", | ||
"test:blink": "browserify -p esmify index.test.js | tape-run --browser chrome", | ||
"test:gecko": "browserify -p esmify index.test.js | tape-run --browser firefox", | ||
"test:lint": "xo", | ||
"watch": "tsc --watch" | ||
"watch": "tsc --watch --noEmitOnError false" | ||
}, | ||
@@ -48,18 +48,14 @@ "xo": { | ||
"browser" | ||
], | ||
"rules": { | ||
"@typescript-eslint/no-unnecessary-type-assertion": "off", | ||
"@typescript-eslint/prefer-nullish-coalescing": "off", | ||
"@typescript-eslint/prefer-readonly-parameter-types": "off" | ||
} | ||
] | ||
}, | ||
"devDependencies": { | ||
"@sindresorhus/tsconfig": "^4.0.0", | ||
"@sindresorhus/tsconfig": "^5.0.0", | ||
"@types/tape": "^5.6.4", | ||
"browserify": "^17.0.0", | ||
"esmify": "^2.1.1", | ||
"npm-run-all": "^4.1.5", | ||
"tape": "^5.6.6", | ||
"tape-run": "^10.0.0", | ||
"typescript": "^5.2.2", | ||
"xo": "^0.56.0" | ||
"tape": "^5.7.5", | ||
"tape-run": "^11.0.0", | ||
"typescript": "^5.3.3", | ||
"xo": "^0.57.0" | ||
}, | ||
@@ -66,0 +62,0 @@ "engines": { |
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
16771
187
9