UNPKG

textchecker-element

Version:
418 lines 18.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createCaretCoordinatesDiv = createCaretCoordinatesDiv; exports.measureFontZoom = measureFontZoom; exports.styleCaretCoordinatesDiv = styleCaretCoordinatesDiv; exports.resetStyleCaretCoordinatesDiv = resetStyleCaretCoordinatesDiv; exports.updateCaretCoordinates = updateCaretCoordinates; exports.getCoordinates = getCaretCoordinates; /** * MIT License * Copyright (C) 2017-2018 DFKI GmbH * * * based on: * * https://github.com/component/textarea-caret-position * MIT License * Copyright (c) 2015 Jonathan Ong me@jongleberry.com * */ // The properties that we copy into a mirrored div. // Note that some browsers, such as Firefox, // do not concatenate properties, i.e. padding-top, bottom etc. -> padding, // so we have to do every single property specifically. var 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", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "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" ]; //MOD for ensuring faux span uses correct styles var spanProperties = [ { n: "direction", v: "inherit" }, // RTL support { n: "boxSizing", v: "inherit" }, { n: "width", v: "auto" }, { n: "height", v: "auto" }, { n: "height", v: "auto" }, { n: "borderTopWidth", v: "0px" }, { n: "borderRightWidth", v: "0px" }, { n: "borderBottomWidth", v: "0px" }, { n: "borderLeftWidth", v: "0px" }, { n: "borderStyle", v: "none" }, { n: "paddingTop", v: "0px" }, { n: "paddingRight", v: "0px" }, { n: "paddingBottom", v: "0px" }, { n: "paddingLeft", v: "0px" }, { n: "marginTop", v: "0px" }, { n: "maringRight", v: "0px" }, { n: "maringBottom", v: "0px" }, { n: "maringLeft", v: "0px" }, { n: "fontStyle", v: "inherit" }, { n: "fontVariant", v: "inherit" }, { n: "fontWeight", v: "inherit" }, { n: "fontStretch", v: "inherit" }, { n: "fontSize", v: "inherit" }, { n: "fontSizeAdjust", v: "inherit" }, { n: "lineHeight", v: "inherit" }, { n: "fontFamily", v: "inherit" }, { n: "textAlign", v: "inherit" }, { n: "textTransform", v: "inherit" }, { n: "textIndent", v: "inherit" }, { n: "textDecoration", v: "inherit" }, // might not make a difference, but better be safe { n: "letterSpacing", v: "inherit" }, { n: "wordSpacing", v: "inherit" }, { n: "tabSize", v: "inherit" } ]; var isBrowser = typeof window !== "undefined"; var isFirefox = isBrowser && globalThis.mozInnerScreenX != null; var _getComputedStyle = globalThis.getComputedStyle ? globalThis.getComputedStyle : function (element) { // currentStyle for IE < 9 return element.currentStyle; }; var lastStyleTargetType; function createCaretCoordinatesDiv(options) { var id = (options && options.id) || "input-textarea-caret-position-mirror-div"; var debug = (options && options.debug) || false; var reuse = debug || (options && options.reuse) || false; var div; if (reuse) { div = document.getElementById(id); } if (!div) { div = document.createElement("div"); div.id = id; document.body.appendChild(div); lastStyleTargetType = void 0; //since DIV is newly created: "reset" any cached values } return div; } /** * In some environments, a font-zoom my be active, e.g. on Android accessibility settings * may increase the font-size, so that applying the measured font-size will actually be * zoomed (again). * * This function tests, if the applied font-size is equal to the measured/computed one and * returns the factor, i.e. * 1: not zoomed * < 1: decreased font-size (zoom-out) * > 1: increased font-size (zoom-in) * * @returns {number} the zoom-factor for font-size */ function measureFontZoom() { var span = document.createElement("span"); var style = span.style; // position off-screen style.position = "absolute"; // required to return coordinates properly // if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering style.fontSize = "100px"; document.body.appendChild(span); var measured = parseFloat(_getComputedStyle(span).getPropertyValue("font-size")); if (span.parentNode !== null) { //<- just to be save, but the span should have been attached to body span.parentNode.removeChild(span); } return isFinite(measured) ? measured / 100 : 1; } function styleCaretCoordinatesDiv(element, position, div, options) { var debug = (options && options.debug) || false; var reuse = debug || (options && options.reuse) || false; if (reuse) { var forceUpdateStyle = (options && options.forceUpdateStyle) || false; if (!forceUpdateStyle && options && !options.reuse) { //if not option "reuse": do force update styling (since DIV is newly created) forceUpdateStyle = true; } var guessIfUpdateStyle; if (!forceUpdateStyle) { guessIfUpdateStyle = (options && options.guessIfUpdateStyle) || false; } else { guessIfUpdateStyle = false; } //try to guess, if updating styles is necessary (only works, if text-input elements of the same type are styled the same) if (guessIfUpdateStyle) { if ((typeof guessIfUpdateStyle === "function" && !guessIfUpdateStyle(div)) || lastStyleTargetType === element.tagName) { return; ////////////// EARLY EXIT /////////////////////// } } lastStyleTargetType = element.tagName; } else { lastStyleTargetType = void 0; } var style = div.style; var computed = _getComputedStyle(element); var isInput = element.nodeName === "INPUT"; //MODIFICATION: adjust lineHeight for INPUT // default textarea styles style.whiteSpace = "pre-wrap"; if (!isInput) style.wordWrap = "break-word"; // only for textarea-s else if (!options || !options.allowInputWrap) style.wordWrap = "normal"; // explicitly reset wordWrap to avoid interference from inheriting style etc // position off-screen style.position = "absolute"; // required to return coordinates properly if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering if (options && options.fontZoom === true) { options.fontZoom = 1 / measureFontZoom(); } // transfer the element's properties to the div var propList = options && options.additionalStyles ? properties.concat(options.additionalStyles) : properties; propList.forEach(function (prop) { if (isInput && prop === "lineHeight") { //MODIFICATION: for INPUT the text is rendered centered -> set lineHeight equal to computed height, if element is larger than the lineHeight //MODIFICATION: if "normal" lineHeight, force value: var cc = computed; if (computed["lineHeight"] === "normal") { style["lineHeight"] = "1em"; style["height"] = computed["height"]; cc = _getComputedStyle(div); } //console.log('height: '+computed['height']+', lineHeight: '+cc['lineHeight']); if (computed.boxSizing === "border-box") { var height = parseInt(computed.height); var outerHeight = parseInt(computed.paddingTop) + parseInt(computed.paddingBottom) + parseInt(computed.borderTopWidth) + parseInt(computed.borderBottomWidth); var targetHeight = outerHeight + parseInt(cc.lineHeight); if (height > targetHeight) { style.lineHeight = height - outerHeight + "px"; } else if (height === targetHeight) { style.lineHeight = cc.lineHeight; } else { style.lineHeight = 0; } } else { var ch, clh, th, oh = 0; if (isFinite((clh = parseFloat(cc["lineHeight"]))) && isFinite((ch = parseFloat(computed["height"])))) { ["borderTop", "borderBottom", "paddingTop", "paddingBottom", "marginTop", "marginBottom"].forEach(function (n) { //TODO consider boxSizing? th = parseFloat(computed[n]); if (isFinite(th)) { oh += th; } }); // if(ch > clh){ if (oh === 0) style["lineHeight"] = computed["height"]; else { th = Math.max(ch - oh, 0); style["lineHeight"] = (th > 0 ? th : ch) + "px"; } // } else { // style['lineHeight'] = (ch - (clh-ch))+'px'; // } } else { style[prop] = cc[prop]; } } } else if (options && typeof options.fontZoom === "number" && (prop === "fontSize" || prop === "lineHeight")) { //MODIFICATION: option for applying zoom-factor to font-size & line-height var fsize; if (isFinite((fsize = parseFloat(computed[prop])))) { style[prop] = fsize * options.fontZoom + "px"; } else { style[prop] = computed[prop]; } } 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)) style.overflowY = "scroll"; } } // HELPER: reset any cached value (i.e. force re-styling in next invocation) function resetStyleCaretCoordinatesDiv() { lastStyleTargetType = void 0; } // HELPER: remove reused faux DIV if present function resetCaretCoordinatesDiv(options) { var id = (options && options.id) || "input-textarea-caret-position-mirror-div"; var div = document.getElementById(id); if (div) { document.body.removeChild(div); } } function getText(element, options) { if (options) { if (options.text) { if (typeof options.text === "function") { return options.text(element, options); } return options.text; } } return element.value; } const createCachedComputedStyle = () => { let prevValue = ""; let prevComputedStyle = null; /** * @type {HTMLTextAreaElement} */ return (element) => { const newValue = element.value; if (prevValue === newValue) { return prevComputedStyle; } prevValue = newValue; prevComputedStyle = _getComputedStyle(element); return prevComputedStyle; }; }; const cachedComputedStyle = createCachedComputedStyle(); function updateCaretCoordinates(element, position, div, options) { if (element.scrollLeft) div.scrollLeft = element.scrollLeft; if (element.scrollTop) div.scrollTop = element.scrollTop; if (element.dir) div.dir = element.dir; if (options && options.additionalAttributes) { options.additionalAttributes.forEach(function (attr) { div[attr] = element[attr]; }); } var computed = cachedComputedStyle(element); var isInput = element.nodeName === "INPUT"; div.textContent = getText(element, options).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 && (!options || !options.allowInputWrap)) div.textContent = div.textContent.replace(/\s/g, "\u00a0"); var span = document.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 why bother? span.textContent = getText(element, options).substring(position) || "."; // || because a completely empty faux span doesn't render at all //set ID for faux span? if (options.fauxId) { span.id = options.fauxId; } // force span to inherit from shadow DIV if (options.forceClearFauxStyle) { var spanStyle = span.style; spanProperties.forEach(function (prop) { spanStyle[prop.n] = prop.v; }); } div.appendChild(span); var coordinates = { top: span.offsetTop + parseInt(computed["borderTopWidth"]), left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), height: parseInt(computed["lineHeight"]) }; if (options && options.returnHeight) { coordinates.height = span.offsetHeight; } return coordinates; } /** * @param {HTMLInput | HTMLTextArea} element * the HTML text control for which to determine the coordinates * @param {Number} position * the index of the character (within the text of the text-control) where the caret should appear * @param {PlainObject} [options] OPTIONAL * options for calculating the caret coordinates: * options.reuse BOOLEAN: reuse shadow DIV that is used for calculating the caret coordinates (DEFAULT: false) * options.returnDiv BOOLEAN: if reuse was enabled, returns the shadow DIV in the coordinates-object in property <code>_div</code> (DEFAULT: false) * options.returnHeight BOOLEAN: returns the caret height in the returned coordinates-object in property <code>height</code> (DEFAULT: false) * options.id STRING: the id attribute for the shadow DIV (DEFAULT: "input-textarea-caret-position-mirror-div") * options.guessIfUpdateStyle BOOLEAN | FUNCTION: if TRUE, styling of the shadow DIV is not updated, if the current target element has the same type (Tag Name) as the previous one. * If function: a callback for determining, if the shadow DIV's style should be updated (return TRUE, if it should get updated): callback(shadowDiv) : boolean * NOTE this option is only relevant, if "reuse" is TRUE. * (DEFAULT: false) * options.forceUpdateStyle BOOLEAN: force updating the style of the shadow DIV; only relevant, if "reuse" is TRUE (DEFAULT: false) * options.forceClearFauxStyle BOOLEAN: force faux span to use "cleared" style (e.g. in case SPAN is globally styled) (DEFAULT: false) * options.fauxId STRING: use ID for faux span (e.g. for styling faux span) (DEFAULT: undefined) * options.fontZoom NUMBER | BOOLEAN: apply zoom factor to font-size. * If <code>true</code> (boolean) the zoom factor will be calculated using measureFontZoom(), and the option-value * (<code>true</code>) will be replaced with the measured zoom factor. * (DEFAULT: undefined) * * options.allowInputWrap BOOLEAN: if TRUE, allows text-wrapping for INPUT elements (note: the W3C specifically states that text in INPUT will not be wrapped, even if styles would "request" it, like "word-wrap: break-word" or "word-break: break-all | break-word" or similar) * (DEFAULT: false) * options.additionalStyles ARRAY<STRING>: transfers additional styles properties from the target element to the shadow DIV * options.additionalAttributes ARRAY<STRING>: transfers additional (node) attributes from the target element to the shadow DIV * * * options.text STRING | FUNCTION: the text value that should be used for the calculation. * If function: a callback which's return value is used as the text: <code>callback(element, options) : string</code> * */ function getCaretCoordinates(element, position, options) { if (!isBrowser) { throw new Error("textarea-caret-position#getCaretCoordinates should only be called in a browser"); } var debug = (options && options.debug) || false; var reuse = debug || (options && options.reuse) || false; // mirrored div var div = createCaretCoordinatesDiv(options); styleCaretCoordinatesDiv(element, position, div, options); var coordinates = updateCaretCoordinates(element, position, div, options); if (debug) { //TODO support styling for mirror elements div.style.backgroundColor = "#aaa"; coordinates._div = div; } var returnDiv = options && options.returnDiv; if (!reuse && !returnDiv) { resetStyleCaretCoordinatesDiv(); document.body.removeChild(div); } else if (returnDiv) { coordinates._div = div; } return coordinates; } //# sourceMappingURL=text-caret-pos.js.map