zent
Version:
一套前端设计语言和基于React的实现
151 lines (125 loc) • 4.69 kB
text/typescript
/* Modified from https://github.com/component/textarea-caret-position/blob/master/index.js */
import createElement from './createElement';
import isBrowser from '../isBrowser';
import isFirefox from '../isFirefox';
// We'll copy the properties below into the mirror div.
// Note that some browsers, such as Firefox, do not concatenate properties
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
// so we have to list every single property explicitly.
const properties = [
'direction', // RTL support
'boxSizing',
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
'height',
'overflowX',
'overflowY', // copy the scrollbar for IE
'borderTopWidth',
'borderRightWidth',
'borderBottomWidth',
'borderLeftWidth',
'borderStyle',
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
'fontStyle',
'fontVariant',
'fontWeight',
'fontStretch',
'fontSize',
'fontSizeAdjust',
'lineHeight',
'fontFamily',
'textAlign',
'textTransform',
'textIndent',
'textDecoration', // might not make a difference, but better be safe
'letterSpacing',
'wordSpacing',
'tabSize',
'MozTabSize',
];
const MIRROR_DIV_ID = 'zent-input-textarea-caret-coordinates-mirror-div';
export interface IGetCaretCoordinatesOption {
debug?: boolean;
}
function getCaretCoordinates(
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
{ debug }: IGetCaretCoordinatesOption
) {
if (!isBrowser) {
throw new Error('getCaretCoordinates should only be called in a browser');
}
if (debug) {
const el = document.getElementById(MIRROR_DIV_ID);
if (el) el.parentNode.removeChild(el);
}
// The mirror div will replicate the textarea's style
const div = createElement('div');
div.id = MIRROR_DIV_ID;
document.body.appendChild(div);
const style = div.style;
const computed = getComputedStyle(element);
const isInput = element.nodeName === 'INPUT';
// Default textarea styles
if (!isInput) {
style.whiteSpace = 'pre-wrap';
style.wordWrap = 'break-word'; // only for textarea-s
} else {
style.whiteSpace = 'nowrap';
}
// Position off-screen
style.position = 'absolute'; // required to return coordinates properly
if (!debug) style.visibility = 'hidden'; // not 'display: none' because we want rendering
// Transfer the element's properties to the div
properties.forEach(prop => {
if (isInput && prop === 'lineHeight') {
// Special case for <input>s because text is rendered centered and line height may be != height
style.lineHeight = computed.height;
} else {
style[prop] = computed[prop];
}
});
if (isFirefox) {
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
if (element.scrollHeight > parseInt(computed.height, 10)) {
style.overflowY = 'scroll';
}
} else {
style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
}
div.textContent = element.value.substring(0, position);
// The second special handling for input type="text" vs textarea:
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
if (isInput) div.textContent = div.textContent.replace(/\s/g, '\u00a0');
const span = createElement('span');
// Wrapping must be replicated *exactly*, including when a long word gets
// onto the next line, with whitespace at the end of the line before (#7).
// The *only* reliable way to do that is to copy the *entire* rest of the
// textarea's content into the <span> created at the caret position.
// For inputs, just '.' would be enough, but no need to bother.
span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
div.appendChild(span);
const rawOffsetLeft =
span.offsetLeft + parseInt(computed.borderLeftWidth, 10);
const lineHeight = parseInt(computed.lineHeight, 10);
const coordinates = {
top: span.offsetTop + parseInt(computed.borderTopWidth, 10),
left: rawOffsetLeft,
// Chrome returns `normal` if you set line-height to `normal`
// In this case, we use font-size as a fallback
// The ratio is just a guess
height: Number.isNaN(lineHeight)
? parseInt(computed.fontSize, 10) * 1.5
: lineHeight,
};
if (debug) {
span.style.backgroundColor = '#aaa';
} else {
document.body.removeChild(div);
}
return coordinates;
}
export default getCaretCoordinates;