From 90664d47bf07c1c159f7e23fe2e30376ed622784 Mon Sep 17 00:00:00 2001 From: Dominik Reh Date: Sun, 26 Feb 2023 17:58:00 +0100 Subject: [PATCH] Fixes for PR #128 Replaced the caret position detection with a more robust version Added option to turn it off --- javascript/_caretPosition.js | 145 +++++++++++++++++++++++++++++ javascript/tagAutocomplete.js | 23 +++-- scripts/tag_autocomplete_helper.py | 1 + 3 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 javascript/_caretPosition.js diff --git a/javascript/_caretPosition.js b/javascript/_caretPosition.js new file mode 100644 index 0000000..43a05d9 --- /dev/null +++ b/javascript/_caretPosition.js @@ -0,0 +1,145 @@ +// From https://github.com/component/textarea-caret-position + +// 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. +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', + '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' + +]; + +var isBrowser = (typeof window !== 'undefined'); +var isFirefox = (isBrowser && window.mozInnerScreenX != null); + +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; + if (debug) { + var el = document.querySelector('#input-textarea-caret-position-mirror-div'); + if (el) el.parentNode.removeChild(el); + } + + // The mirror div will replicate the textarea's style + var div = document.createElement('div'); + div.id = 'input-textarea-caret-position-mirror-div'; + document.body.appendChild(div); + + var style = div.style; + var computed = window.getComputedStyle ? window.getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9 + var isInput = element.nodeName === 'INPUT'; + + // Default textarea styles + style.whiteSpace = 'pre-wrap'; + if (!isInput) + style.wordWrap = 'break-word'; // only for textarea-s + + // 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(function (prop) { + if (isInput && prop === 'lineHeight') { + // Special case for s because text is rendered centered and line height may be != height + 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(computed.lineHeight); + if (height > targetHeight) { + style.lineHeight = height - outerHeight + "px"; + } else if (height === targetHeight) { + style.lineHeight = computed.lineHeight; + } else { + style.lineHeight = 0; + } + } else { + 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)) + 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'); + + 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 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); + + var coordinates = { + top: span.offsetTop + parseInt(computed['borderTopWidth']), + left: span.offsetLeft + parseInt(computed['borderLeftWidth']), + height: parseInt(computed['lineHeight']) + }; + + if (debug) { + span.style.backgroundColor = '#aaa'; + } else { + document.body.removeChild(div); + } + + return coordinates; +} diff --git a/javascript/tagAutocomplete.js b/javascript/tagAutocomplete.js index e80d3bb..904512b 100644 --- a/javascript/tagAutocomplete.js +++ b/javascript/tagAutocomplete.js @@ -137,6 +137,7 @@ async function syncOptions() { modelListMode: opts["tac_activeIn.modelListMode"] }, // Results related settings + slidingPopup: opts["tac_slidingPopup"], maxResults: opts["tac_maxResults"], showAllResults: opts["tac_showAllResults"], resultStepLength: opts["tac_resultStepLength"], @@ -224,22 +225,20 @@ function isVisible(textArea) { let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); return resultsDiv.style.display === "block"; } -function getTextWidth(text, font) { - // re-use canvas object for better performance - var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); - var context = canvas.getContext("2d"); - context.font = font; - var metrics = context.measureText(text); - return metrics.width; -} function showResults(textArea) { let textAreaId = getTextAreaIdentifier(textArea); let resultsDiv = gradioApp().querySelector('.autocompleteResults' + textAreaId); resultsDiv.style.display = "block"; - let txt = textArea.value.slice(0,textArea.selectionEnd); - let width = getTextWidth(txt, document.fonts)*1.25; - let offset = width % textArea.clientWidth; - resultsDiv.style.left = offset+"px"; + + if (CFG.slidingPopup) { + let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left; + let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - resultsDiv.offsetWidth); + + resultsDiv.style.left = `${offset}px`; + } else { + if (resultsDiv.style.left) + resultsDiv.style.removeProperty("left"); + } } function hideResults(textArea) { let textAreaId = getTextAreaIdentifier(textArea); diff --git a/scripts/tag_autocomplete_helper.py b/scripts/tag_autocomplete_helper.py index a04c02b..c5d8e16 100644 --- a/scripts/tag_autocomplete_helper.py +++ b/scripts/tag_autocomplete_helper.py @@ -253,6 +253,7 @@ def on_ui_settings(): shared.opts.add_option("tac_activeIn.modelList", shared.OptionInfo("", "List of model names (with file extension) or their hashes to use as black/whitelist, separated by commas.", section=TAC_SECTION)) shared.opts.add_option("tac_activeIn.modelListMode", shared.OptionInfo("Blacklist", "Mode to use for model list", gr.Dropdown, lambda: {"choices": ["Blacklist","Whitelist"]}, section=TAC_SECTION)) # Results related settings + shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION)) shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION)) shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION)) shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))