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))