From e642a7493f9b6c56f2f5336fe16d45ff36c27ca7 Mon Sep 17 00:00:00 2001 From: HowToSD Date: Tue, 10 Jun 2025 20:53:28 -0700 Subject: [PATCH] Added PandasExcelFileSheetName and PyListToString nodes as well fixing ShowText.js. Added dependency on lxml to address broken html node without lxml --- __init__.py | 6 + docs/reference/node_reference.md | 2 + .../pandas_excel_file_sheet_names.md | 23 ++++ docs/reference/py_list_to_string.md | 17 +++ .../workflows/excel_file_sheet_names.json | 1 + modules/cda/py_list_to_string.py | 39 ++++++ .../pandas_excel_file_sheet_names.py | 51 ++++++++ pyproject.toml | 2 +- requirements.txt | 3 +- tests/cda/py_list_to_string_test.py | 29 +++++ .../pandas_excel_file_sheet_names_test.py | 34 +++++ web/js/ShowDataFrame.js | 66 ---------- web/js/ShowIndex.js | 66 ---------- web/js/ShowText.js | 123 ++++++++++++++---- web/js/ShowTextGeneric.js | 66 ---------- 15 files changed, 305 insertions(+), 223 deletions(-) create mode 100644 docs/reference/pandas_excel_file_sheet_names.md create mode 100644 docs/reference/py_list_to_string.md create mode 100644 examples/workflows/excel_file_sheet_names.json create mode 100644 modules/cda/py_list_to_string.py create mode 100644 modules/pandas_wrapper/pandas_excel_file_sheet_names.py create mode 100644 tests/cda/py_list_to_string_test.py create mode 100644 tests/pandas_wrapper/pandas_excel_file_sheet_names_test.py delete mode 100644 web/js/ShowDataFrame.js delete mode 100644 web/js/ShowIndex.js delete mode 100644 web/js/ShowTextGeneric.js diff --git a/__init__.py b/__init__.py index a9e47a6..a652b2e 100644 --- a/__init__.py +++ b/__init__.py @@ -13,6 +13,7 @@ from .modules.cda.py_int_to_string import PyIntToString from .modules.cda.py_kv_float_create import PyKvFloatCreate from .modules.cda.py_kv_int_create import PyKvIntCreate from .modules.cda.py_kv_string_create import PyKvStringCreate +from .modules.cda.py_list_to_string import PyListToString from .modules.cda.py_string_to_datetime import PyStringToDatetime from .modules.cda.py_string_to_dict import PyStringToDict from .modules.cda.py_string_to_float import PyStringToFloat @@ -71,6 +72,7 @@ from .modules.pandas_wrapper.pandas_dropna import PandasDropNA from .modules.pandas_wrapper.pandas_eq import PandasEq from .modules.pandas_wrapper.pandas_eq_scalar_float import PandasEqScalarFloat from .modules.pandas_wrapper.pandas_eq_scalar_int import PandasEqScalarInt +from .modules.pandas_wrapper.pandas_excel_file_sheet_names import PandasExcelFileSheetNames from .modules.pandas_wrapper.pandas_exp import PandasExp from .modules.pandas_wrapper.pandas_feature_split_to_numpy import PandasFeatureSplitToNumpy from .modules.pandas_wrapper.pandas_feature_split_to_pt import PandasFeatureSplitToPt @@ -253,6 +255,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PandasEq": PandasEq, "PandasEqScalarFloat": PandasEqScalarFloat, "PandasEqScalarInt": PandasEqScalarInt, + "PandasExcelFileSheetNames": PandasExcelFileSheetNames, "PandasExp": PandasExp, "PandasFeatureSplitToNumpy": PandasFeatureSplitToNumpy, "PandasFeatureSplitToPt": PandasFeatureSplitToPt, @@ -361,6 +364,7 @@ NODE_CLASS_MAPPINGS: Dict[str, Type[T]] = { "PyKvFloatCreate": PyKvFloatCreate, "PyKvIntCreate": PyKvIntCreate, "PyKvStringCreate": PyKvStringCreate, + "PyListToString": PyListToString, "PyStringToDatetime": PyStringToDatetime, "PyStringToDict": PyStringToDict, "PyStringToFloat": PyStringToFloat, @@ -446,6 +450,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PandasEq": "Pandas Eq", "PandasEqScalarFloat": "Pandas Eq Scalar Float", "PandasEqScalarInt": "Pandas Eq Scalar Int", + "PandasExcelFileSheetNames": "Pandas Excel File Sheet Names", "PandasExp": "Pandas Exp", "PandasFeatureSplitToNumpy": "Pandas Feature Split To Numpy", "PandasFeatureSplitToPt": "Pandas Feature Split To Pt", @@ -554,6 +559,7 @@ NODE_DISPLAY_NAME_MAPPINGS: Dict[str, str] = { "PyKvFloatCreate": "Py Kv Float Create", "PyKvIntCreate": "Py Kv Int Create", "PyKvStringCreate": "Py Kv String Create", + "PyListToString": "Py List To String", "PyStringToDatetime": "Py String To Datetime", "PyStringToDict": "Py String To Dict", "PyStringToFloat": "Py String To Float", diff --git a/docs/reference/node_reference.md b/docs/reference/node_reference.md index bbb3b20..852d1d7 100644 --- a/docs/reference/node_reference.md +++ b/docs/reference/node_reference.md @@ -118,6 +118,7 @@ | [Pandas Create Series From List](pandas_create_series_from_list.md) | Creates a Pandas Series from a Python list. | | [Pandas Create Series From List Index List](pandas_create_series_from_list_index_list.md) | Creates a Pandas Series from a Python list of cell values and a list for index. | | [Pandas Create With Index](pandas_create_with_index.md) | Creates a pandas DataFrame using values entered in the text field. Input data is assumed to have an index. | +| [Pandas Excel File Sheet Names](pandas_excel_file_sheet_names.md) | Returns the string list of sheet names contained in the Excel file. | | [Pandas Load CSV](pandas_load_csv.md) | Loads CSV files into a pandas DataFrame. | | [Pandas Load CSV With Encoding](pandas_load_csv_with_encoding.md) | Loads CSV files into a pandas DataFrame. | | [Pandas Load CSV With Index](pandas_load_csv_with_index.md) | Loads CSV files into a pandas DataFrame. | @@ -189,6 +190,7 @@ | [Py Kv Float Create](py_kv_float_create.md) | Creates a Python dictionary with a string key and a float value. | | [Py Kv Int Create](py_kv_int_create.md) | Creates a Python dictionary with an string key and an integer value. | | [Py Kv String Create](py_kv_string_create.md) | Creates a Python dictionary with a string key and a string value. | +| [Py List To String](py_list_to_string.md) | Converts a Python list value to a Python string | | [Py String To Datetime](py_string_to_datetime.md) | Converts a Python string to a Python datetime.datetime. | | [Py String To Dict](py_string_to_dict.md) | Creates a Python dictionary using the string values entered in the text field. | | [Py String To Float](py_string_to_float.md) | Converts a Python string to a float. | diff --git a/docs/reference/pandas_excel_file_sheet_names.md b/docs/reference/pandas_excel_file_sheet_names.md new file mode 100644 index 0000000..7302da4 --- /dev/null +++ b/docs/reference/pandas_excel_file_sheet_names.md @@ -0,0 +1,23 @@ +# Pandas Excel File Sheet Names +Returns the string list of sheet names contained in the Excel file. + +This node uses ExcelFile method using the openpyxl package. +The openpyxl package is specified as a dependency in requirements.txt, so you should not need to manually install it. +See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.ExcelFile.html) for details. + +To display the list, connect output to PyListToString node to convert the list to a string, then connect the output to Pandas Show Text node to display the string. + +## Input +| Name | Data type | +|---|---| +| file_path | String | + +## Output +| Data type | +|---| +| Pylist | + +
+Category: IO + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/docs/reference/py_list_to_string.md b/docs/reference/py_list_to_string.md new file mode 100644 index 0000000..c45642d --- /dev/null +++ b/docs/reference/py_list_to_string.md @@ -0,0 +1,17 @@ +# Py List To String +Converts a Python list value to a Python string + +## Input +| Name | Data type | +|---|---| +| value | Pylist | + +## Output +| Data type | +|---| +| String | + +
+Category: Python wrapper + +ComfyUI Data Analysis Node Reference. © 2025 Hide Inada (HowToSD.com). All rights reserved. diff --git a/examples/workflows/excel_file_sheet_names.json b/examples/workflows/excel_file_sheet_names.json new file mode 100644 index 0000000..c371b97 --- /dev/null +++ b/examples/workflows/excel_file_sheet_names.json @@ -0,0 +1 @@ +{"id":"d3e44069-d0c1-4d4b-b205-9aac7b243871","revision":0,"last_node_id":3,"last_link_id":2,"nodes":[{"id":2,"type":"PyListToString","pos":[743.5132446289062,244.38381958007812],"size":[214.20001220703125,26],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"PYLIST","link":1}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[2]}],"properties":{"Node name for S&R":"PyListToString"}},{"id":1,"type":"PandasExcelFileSheetNames","pos":[350.7860107421875,242.56568908691406],"size":[365.4000244140625,58],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file_path","name":"file_path","type":"STRING","widget":{"name":"file_path"},"link":null}],"outputs":[{"localized_name":"PYLIST","name":"PYLIST","type":"PYLIST","links":[1]}],"properties":{"Node name for S&R":"PandasExcelFileSheetNames"},"widgets_values":["custom_nodes/data-analysis/examples/datasets/test_file.xlsx"]},{"id":3,"type":"PandasShowText","pos":[987.1495361328125,242.56565856933594],"size":[210,76],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"text","name":"text","type":"STRING","link":2}],"outputs":[{"localized_name":"STRING","name":"STRING","shape":6,"type":"STRING","links":null}],"properties":{"Node name for S&R":"PandasShowText"},"widgets_values":["['Sheet1', 'FruitPrice']"]}],"links":[[1,1,0,2,0,"PYLIST"],[2,2,0,3,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.1000000000000005,"offset":[75.57754666842986,116.52528218135221]},"frontendVersion":"1.16.9","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4} \ No newline at end of file diff --git a/modules/cda/py_list_to_string.py b/modules/cda/py_list_to_string.py new file mode 100644 index 0000000..d068968 --- /dev/null +++ b/modules/cda/py_list_to_string.py @@ -0,0 +1,39 @@ +from typing import Any, Dict, List + +class PyListToString: + """ + Converts a Python list value to a Python string + + category: Python wrapper + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "value": ("PYLIST", {}) + } + } + + RETURN_TYPES: tuple = ("STRING",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + def f(self, value: List) -> tuple: + """ + Converts a Python list value to a Python string + + Args: + value (list): List value to convert. + + Returns: + tuple: A tuple containing the converted value. + """ + return (str(value),) + \ No newline at end of file diff --git a/modules/pandas_wrapper/pandas_excel_file_sheet_names.py b/modules/pandas_wrapper/pandas_excel_file_sheet_names.py new file mode 100644 index 0000000..20f1251 --- /dev/null +++ b/modules/pandas_wrapper/pandas_excel_file_sheet_names.py @@ -0,0 +1,51 @@ +from typing import Any, Dict +import pandas as pd + + +class PandasExcelFileSheetNames: + """ + Returns the string list of sheet names contained in the Excel file. + + This node uses ExcelFile method using the openpyxl package. + The openpyxl package is specified as a dependency in requirements.txt, so you should not need to manually install it. + See [Pandas documentation](https://pandas.pydata.org/docs/reference/api/pandas.ExcelFile.html) for details. + + To display the list, connect output to PyListToString node to convert the list to a string, then connect the output to Pandas Show Text node to display the string. + + category: IO + """ + + @classmethod + def INPUT_TYPES(cls) -> Dict[str, Any]: + """ + Defines the input types for the function. + + Returns: + Dict[str, Any]: A dictionary specifying required input types. + """ + return { + "required": { + "file_path": ("STRING", {"multiline": False}), + } + } + + RETURN_TYPES: tuple = ("PYLIST",) + FUNCTION: str = "f" + CATEGORY: str = "Data Analysis" + + @classmethod + def IS_CHANGED(cls, **kw): + return float("NaN") + + def f(self, file_path: str) -> tuple: + """ + Returns the string list of sheet names contained in the Excel file. + + Args: + file_path (str): The path to the Excel file. + + Returns: + tuple: A tuple containing a string list of sheet names. + """ + pylist = pd.ExcelFile(file_path, engine="openpyxl").sheet_names + return (pylist,) diff --git a/pyproject.toml b/pyproject.toml index 51b8cc7..dca1e39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "data-analysis" description = "Data analysis custom modules for ComfyUI – Integrates Pandas, Seaborn and Matplotlib into ComfyUI." -version = "3.0.7" +version = "3.1.0" license = {file = "LICENSE"} [project.urls] diff --git a/requirements.txt b/requirements.txt index b504a51..b689164 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ pandas>=2.2.3 seaborn>=0.13.2 scipy>=1.14.1 scikit-learn>=1.6.1 -openpyxl>=3.1.5 \ No newline at end of file +openpyxl>=3.1.5 +lxml>=5.4.0 \ No newline at end of file diff --git a/tests/cda/py_list_to_string_test.py b/tests/cda/py_list_to_string_test.py new file mode 100644 index 0000000..965a59f --- /dev/null +++ b/tests/cda/py_list_to_string_test.py @@ -0,0 +1,29 @@ +import os +import sys +import unittest + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from cda.py_list_to_string import PyListToString + +class TestPyListToString(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PyListToString() + self.test_cases = [ + ([1,2,3], "[1, 2, 3]"), + (["apple", "banana"], "['apple', 'banana']"), + ([], "[]") + ] + + def test_list_to_string(self): + """Test converting a list to string.""" + for value, expected in self.test_cases: + with self.subTest(value=value): + retval = self.node.f(value=value)[0] + self.assertEqual(retval, expected, f"Mismatch for value={value}") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/pandas_wrapper/pandas_excel_file_sheet_names_test.py b/tests/pandas_wrapper/pandas_excel_file_sheet_names_test.py new file mode 100644 index 0000000..234d23c --- /dev/null +++ b/tests/pandas_wrapper/pandas_excel_file_sheet_names_test.py @@ -0,0 +1,34 @@ +import os +import sys +import unittest +import pandas as pd + +PROJECT_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "..")) +MODULE_ROOT = os.path.join(PROJECT_ROOT, "modules") +sys.path.append(MODULE_ROOT) +from pandas_wrapper.pandas_excel_file_sheet_names import PandasExcelFileSheetNames + + +class TestPandasExcelFileSheetNames(unittest.TestCase): + def setUp(self): + """Set up test data.""" + self.node = PandasExcelFileSheetNames() + self.test_file_path = os.path.join( + os.path.dirname(__file__), "..", "..", + "examples", "datasets", "test_file.xlsx") + + self.test_data_list = [ + "Sheet1", + "FruitPrice" + ] + + def test_excel_file_sheet_names(self): + """Reads sheet names from an excel file. + """ + expected = self.test_data_list + actual = self.node.f(self.test_file_path)[0] + self.assertEqual(actual, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/js/ShowDataFrame.js b/web/js/ShowDataFrame.js deleted file mode 100644 index 4fe2492..0000000 --- a/web/js/ShowDataFrame.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -This code is based on -https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js - -See credit/credit.md for the full license. -*/ - -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; - -// Displays input text on a node -app.registerExtension({ - name: "HowToSD.ShowDataFrame", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "PandasShowDataFrame") { // This needs to match NODE_CLASS_MAPPINGS in Python - function populate(text) { - if (this.widgets) { - // Clear all widgets on the node - for (let i = 0; i < this.widgets.length; i++) { - // if onRemove method exists, call it. - this.widgets[i].onRemove?.(); - } - this.widgets.length = 0; - } - - const v = [...text]; // Copy the text list (e.g. text.copy()) - if (!v[0]) { - v.shift(); - } - for (const list of v) { - const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.value = list; - } - - requestAnimationFrame(() => { - const sz = this.computeSize(); - if (sz[0] < this.size[0]) { - sz[0] = this.size[0]; - } - if (sz[1] < this.size[1]) { - sz[1] = this.size[1]; - } - this.onResize?.(sz); - app.graph.setDirtyCanvas(true, false); - }); - } - - // When the node is executed we will be sent the input text, display this in the widget - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - populate.call(this, message.text); - }; - - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - onConfigure?.apply(this, arguments); - if (this.widgets_values?.length) { - populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1)); - } - }; - } - }, -}); \ No newline at end of file diff --git a/web/js/ShowIndex.js b/web/js/ShowIndex.js deleted file mode 100644 index cbf8217..0000000 --- a/web/js/ShowIndex.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -This code is based on -https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js - -See credit/credit.md for the full license. -*/ - -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; - -// Displays input text on a node -app.registerExtension({ - name: "HowToSD.ShowIndex", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (nodeData.name === "PandasShowIndex") { // This needs to match NODE_CLASS_MAPPINGS in Python - function populate(text) { - if (this.widgets) { - // Clear all widgets on the node - for (let i = 0; i < this.widgets.length; i++) { - // if onRemove method exists, call it. - this.widgets[i].onRemove?.(); - } - this.widgets.length = 0; - } - - const v = [...text]; // Copy the text list (e.g. text.copy()) - if (!v[0]) { - v.shift(); - } - for (const list of v) { - const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.value = list; - } - - requestAnimationFrame(() => { - const sz = this.computeSize(); - if (sz[0] < this.size[0]) { - sz[0] = this.size[0]; - } - if (sz[1] < this.size[1]) { - sz[1] = this.size[1]; - } - this.onResize?.(sz); - app.graph.setDirtyCanvas(true, false); - }); - } - - // When the node is executed we will be sent the input text, display this in the widget - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - populate.call(this, message.text); - }; - - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - onConfigure?.apply(this, arguments); - if (this.widgets_values?.length) { - populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1)); - } - }; - } - }, -}); \ No newline at end of file diff --git a/web/js/ShowText.js b/web/js/ShowText.js index 1fa1df7..934d4ff 100644 --- a/web/js/ShowText.js +++ b/web/js/ShowText.js @@ -8,57 +8,134 @@ See credit/credit.md for the full license. import { app } from "../../../scripts/app.js"; import { ComfyWidgets } from "../../../scripts/widgets.js"; -// Displays input text on a node app.registerExtension({ name: "HowToSD.ShowText", async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (["PandasShowText", "CDAShowInt", "CDAShowFloat"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python + + // Each node needs to match NODE_CLASS_MAPPINGS in Python + const targetNodes = [ + "PandasShowText", + "PandasShowIndex", + "PandasShowDataFrame", + "CDAShowInt", + "CDAShowFloat", + "NumpyShow", + "PandasShowSeries", + ] + + const noConvertedFieldNodes = [ + "CDAShowInt", + "CDAShowFloat", + ] + + if (targetNodes.includes(nodeData.name)) { + function populate(text) { + // 'this' is not bound automatically — it's explicitly set via .call() in onExecuted. if (this.widgets) { - for (let i = 1; i < this.widgets.length; i++) { + // On older frontend versions there is a hidden converted-widget + // Legacy version: const isConvertedWidget = +!!this.inputs?.[0].widget; + const hasInputWidget = !!(this.inputs?.[0]?.widget); // Coerce to boolean: true if widget exists + let isConvertedWidget = hasInputWidget ? 1 : 0; // Convert boolean to integer (1 or 0) + + if (noConvertedFieldNodes.includes(nodeData.name)){ // Remove all widgets + isConvertedWidget = 0; + } + + // Remove all dynamically added widgets, preserving the auto-converted input widget if present + for (let i = isConvertedWidget; i < this.widgets.length; i++) { this.widgets[i].onRemove?.(); } - this.widgets.length = 1; + this.widgets.length = isConvertedWidget; } - const v = [...text]; - if (!v[0]) { + const v = [...text]; // Shallow copy of text array into v + // If the first element is empty or undefined, remove it from the array + if (!v[0]) { v.shift(); } - for (const list of v) { - const w = ComfyWidgets["STRING"](this, "text2", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.value = list; + for (let row of v) { // Iterate over v. v can be 1d or 2d. + if (!(row instanceof Array)) row = [row]; + for (const elem of row) { + // Create a new STRING widget + // Use field name like "text_0", "text_1", accounting for existing widgets + const fieldName = "text_" + (this.widgets?.length ?? 0); + const w = ComfyWidgets["STRING"]( + this, + fieldName, + ["STRING", { multiline: true }], + app).widget; + w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.value = elem; + } } + /* + Defer until the next animation frame so all widgets are rendered and layout is stable. + Then compute the node size needed to fit new content. + If computeSize underestimates it, use the current node size as the floor to preserve layout. + */ requestAnimationFrame(() => { - const sz = this.computeSize(); - if (sz[0] < this.size[0]) { - sz[0] = this.size[0]; - } - if (sz[1] < this.size[1]) { - sz[1] = this.size[1]; - } - this.onResize?.(sz); - app.graph.setDirtyCanvas(true, false); + const sz = this.computeSize(); // Compute size based on current widgets + + // Adjust to ensure the node fits its current visual width/height + if (sz[0] < this.size[0]) sz[0] = this.size[0]; // Fit existing node width + if (sz[1] < this.size[1]) sz[1] = this.size[1]; // Fit existing node height + + this.onResize?.(sz); // Apply the size adjustment + app.graph.setDirtyCanvas(true, false); // Trigger a redraw of the canvas }); } + // **onExecute** setup // When the node is executed we will be sent the input text, display this in the widget const onExecuted = nodeType.prototype.onExecuted; nodeType.prototype.onExecuted = function (message) { + // Call the original onExecuted handler (if one exists), forwarding all arguments onExecuted?.apply(this, arguments); + + // Call our populate function, binding 'this' to the current node populate.call(this, message.text); }; + // **configure** setup + const VALUES = Symbol(); // Unique internal key to store widget values + const configure = nodeType.prototype.configure; + + nodeType.prototype.configure = function () { + // Store unmodified widget values as they get removed on configure by new frontend + // This value is applied in onConfigure + this[VALUES] = arguments[0]?.widgets_values; + + // Call the original configure handler, if it exists + return configure?.apply(this, arguments); + }; + + // **onConfigure** setup + // Populate node's widgets using the saved values in Configure const onConfigure = nodeType.prototype.onConfigure; nodeType.prototype.onConfigure = function () { onConfigure?.apply(this, arguments); - if (this.widgets_values?.length) { - populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1)); + + // Read saved widget values + const widgets_values = this[VALUES]; + if (widgets_values?.length) { + // In newer frontend there seems to be a delay in creating the initial widget + requestAnimationFrame(() => { + // Legacy code + // populate.call(this, + // widgets_values.slice(+(widgets_values.length > 1 && this.inputs?.[0].widget))); + + const hasAutoWidget = widgets_values.length > 1 && this.inputs?.[0]?.widget; + let startIndex = hasAutoWidget ? 1 : 0; + if (noConvertedFieldNodes.includes(nodeData.name)){ // Remove all widgets + startIndex = 0; + } + populate.call(this, widgets_values.slice(startIndex)); + }); } }; } }, -}); \ No newline at end of file +}); diff --git a/web/js/ShowTextGeneric.js b/web/js/ShowTextGeneric.js deleted file mode 100644 index e56aa46..0000000 --- a/web/js/ShowTextGeneric.js +++ /dev/null @@ -1,66 +0,0 @@ -/* -This code is based on -https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/main/web/js/showText.js - -See credit/credit.md for the full license. -*/ - -import { app } from "../../../scripts/app.js"; -import { ComfyWidgets } from "../../../scripts/widgets.js"; - -// Displays input text on a node -app.registerExtension({ - name: "HowToSD.ShowTextGeneric", - async beforeRegisterNodeDef(nodeType, nodeData, app) { - if (["NumpyShow", "PandasShowSeries"].includes(nodeData.name)) { // This needs to match NODE_CLASS_MAPPINGS in Python - function populate(text) { - if (this.widgets) { - // Clear all widgets on the node - for (let i = 0; i < this.widgets.length; i++) { - // if onRemove method exists, call it. - this.widgets[i].onRemove?.(); - } - this.widgets.length = 0; - } - - const v = [...text]; // Copy the text list (e.g. text.copy()) - if (!v[0]) { - v.shift(); - } - for (const list of v) { - const w = ComfyWidgets["STRING"](this, "text0", ["STRING", { multiline: true }], app).widget; - w.inputEl.readOnly = true; - w.inputEl.style.opacity = 0.6; - w.value = list; - } - - requestAnimationFrame(() => { - const sz = this.computeSize(); - if (sz[0] < this.size[0]) { - sz[0] = this.size[0]; - } - if (sz[1] < this.size[1]) { - sz[1] = this.size[1]; - } - this.onResize?.(sz); - app.graph.setDirtyCanvas(true, false); - }); - } - - // When the node is executed we will be sent the input text, display this in the widget - const onExecuted = nodeType.prototype.onExecuted; - nodeType.prototype.onExecuted = function (message) { - onExecuted?.apply(this, arguments); - populate.call(this, message.text); - }; - - const onConfigure = nodeType.prototype.onConfigure; - nodeType.prototype.onConfigure = function () { - onConfigure?.apply(this, arguments); - if (this.widgets_values?.length) { - populate.call(this, this.widgets_values.slice(+this.widgets_values.length > 1)); - } - }; - } - }, -}); \ No newline at end of file