├── .gitignore ├── README.md ├── app.py ├── image.png ├── plugin.py ├── plugins ├── __init__.py └── test.py ├── requirements.txt ├── static ├── css │ ├── litegraph-editor.css │ ├── litegraph.css │ └── style.css └── js │ ├── base.js │ ├── bootbox.js │ ├── index.js │ ├── litegraph-editor.js │ ├── litegraph.js │ └── p.js └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flasklitegraphjs 2 | flask and litegraph js 3 | 4 | Save ans load Json format of litegraph.js 5 | 6 | 7 | ![screnshot](image.png) 8 | 9 | reference 10 | 11 | https://codepen.io/Ni55aN/pen/MOYPEz 12 | 13 | old 14 | https://github.com/retejs/rete 15 | 16 | new 17 | https://github.com/jagenjo/litegraph.js 18 | 19 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import HTMLResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from fastapi.templating import Jinja2Templates 5 | from fastapi.encoders import jsonable_encoder 6 | from fastapi.responses import JSONResponse 7 | import uvicorn 8 | import os 9 | import json 10 | import pprint 11 | 12 | import json 13 | import pprint 14 | import plugin as pg 15 | 16 | pm = pg.pluginManager("plugins") 17 | 18 | data = {} 19 | objid = {} 20 | 21 | 22 | app = FastAPI() 23 | app.mount("/static", StaticFiles(directory="static"), name="static") 24 | templates = Jinja2Templates(directory="templates") 25 | 26 | class NodeGraph: 27 | def __init__(self, node, nobj): 28 | self._node = node 29 | self._nobj = nobj 30 | self._input = [] 31 | self._output = [] 32 | self._computed = False 33 | 34 | def addInput(self, input): 35 | self._input.append(input) 36 | 37 | def addOutput(self, out): 38 | self._output.append(out) 39 | 40 | def getId(self): 41 | return self._node["id"] 42 | 43 | def getType(self): 44 | return self._node["type"] 45 | 46 | def display(self): 47 | if len(self._input)!=0: 48 | for i in self._input: 49 | print("\t %s " % ( i._node["type"] )) 50 | if len(self._output)!=0: 51 | for o in self._output: 52 | print("\t %s " % (o._node["type"])) 53 | 54 | def isComputed(self): 55 | return self._computed 56 | 57 | def run(self): 58 | self._computed = True 59 | print(" RUN %s \n" % ( self._node["type"] )) 60 | param=[] 61 | for i in self._input: 62 | if i.isComputed() == False: 63 | i.run() 64 | param.append(i._nobj.getValue(i._node)) 65 | print(param) 66 | result = self._nobj.execute(param) 67 | 68 | if len(self._output) == 0: 69 | print("RESULT") 70 | print(result) 71 | return result 72 | 73 | for o in self._output: 74 | return o.run() 75 | 76 | class Graph: 77 | def __init__(self): 78 | self._listnode = [] 79 | self._result ="" 80 | 81 | def addNode(self, node, nobj): 82 | self._listnode.append(NodeGraph(node,nobj)) 83 | 84 | def findNode(self, id ): 85 | for ng in self._listnode: 86 | if ng.getId() == id: 87 | return ng 88 | 89 | def findStart(self): 90 | for ng in self._listnode: 91 | if ng.getType() == "basic/start": 92 | return ng 93 | 94 | 95 | def connect(self, idinput, idoutput): 96 | nginput = self.findNode(idinput) 97 | ngoutput = self.findNode(idoutput) 98 | nginput.addOutput(ngoutput) 99 | ngoutput.addInput(nginput) 100 | 101 | def display(self ): 102 | for ng in self._listnode: 103 | ng.display() 104 | 105 | def result(self): 106 | return self.result 107 | 108 | 109 | def findclasse( name ) : 110 | for obj in pm.classes(): 111 | if name == "basic/%s" % type(obj).__name__: 112 | #print(" Found %s " %( type(obj).__name__)) 113 | return obj 114 | return None 115 | 116 | def findStart(data): 117 | for n in data["nodes"]: 118 | if n["type"] == "basic/start": 119 | return n["id"] 120 | 121 | def findNode(id): 122 | for n in data["nodes"]: 123 | if n["id"] == id: 124 | print("found %d " % ( id )) 125 | return n 126 | 127 | def getinput(node): 128 | return node["inputs"] 129 | 130 | def execute(link): 131 | origin = findNode(link[1]) 132 | target = findNode(link[3]) 133 | for i in getinput(target): 134 | n1 = findNode(i["link"]) 135 | n1obj = findclasse(n1["type"]) 136 | print(n1obj.getValue(n1)) 137 | 138 | print(origin["type"]) 139 | objorigin = findclasse(origin["type"]) 140 | objtarget = findclasse(target["type"]) 141 | objid[origin["id"]] = findclasse(origin["type"]) 142 | objid[target["id"]] = findclasse(target["type"]) 143 | print(origin["properties"]) 144 | print(target["type"]) 145 | 146 | def recurcivecross(links, id ): 147 | for n in data["links"]: 148 | if n[1] == id: 149 | execute(n) 150 | recurcivecross(links, n[3]) 151 | 152 | 153 | 154 | @app.get("/listsaved", response_class=HTMLResponse) 155 | async def listfile(request: Request): 156 | arr = os.listdir("datasaved") 157 | result =[] 158 | for a in arr: 159 | result.append({'text':a,'value':a}) 160 | 161 | return json.dumps(result) 162 | 163 | @app.post("/save", response_class=HTMLResponse) 164 | async def save(request: Request): 165 | payload = await request.json() 166 | # TODO check security in string... 167 | with open(os.path.join('datasaved',"%s.json" % (payload["name"])), "w") as file1: 168 | file1.write(json.dumps(payload["data"], indent=4)) 169 | return "OK" 170 | 171 | @app.get("/load/{filejson}", response_class=HTMLResponse) 172 | async def load(filejson: str): 173 | print(filejson) 174 | with open(os.path.join('datasaved',filejson), "r") as file1: 175 | return file1.read() 176 | 177 | @app.post("/run", response_class=JSONResponse) 178 | async def run(request:Request): 179 | payload = await request.json() 180 | g = Graph() 181 | data = payload["data"] 182 | for n in data["nodes"]: 183 | nobj = findclasse(n["type"]) 184 | g.addNode(n,nobj) 185 | for l in data["links"]: 186 | g.connect(l[1],l[3]) 187 | 188 | #g.display() 189 | print(g.findStart().run()) 190 | return JSONResponse( g.findStart().run() ) 191 | 192 | 193 | @app.get("/", response_class=HTMLResponse) 194 | async def home(request: Request): 195 | return templates.TemplateResponse("index.html", {"request": request, "id": id}) 196 | 197 | if __name__ == "__main__": 198 | uvicorn.run("readlitegraph:app", host="127.0.0.1", port=5002, debug=True,log_level="info") 199 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zebulon75018/flasklitegraphjs/b5eacdbc12190b726e611fb33672edf552a6bd07/image.png -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | 4 | class pluginManager: 5 | def __init__(self, path): 6 | self._path = path 7 | self._modules = {} 8 | self._loadmodules() 9 | 10 | def _loadmodules(self): 11 | for filepy in os.listdir(self._path): 12 | if filepy == "__init__.py": 13 | continue 14 | 15 | if filepy[-2:] == "py": 16 | modulename = os.path.join(self._path,filepy[:-3]).replace(os.sep,".") 17 | self._modules[filepy[:-3]] = importlib.import_module(modulename) 18 | 19 | def reloadModules(self): 20 | for key in self._modules: 21 | reload(self._modules[key]) 22 | 23 | @property 24 | def modules(self): 25 | return self._modules 26 | 27 | def classes(self): 28 | for key in self._modules: 29 | for c in dir(self._modules[key]): 30 | if not c.startswith("__"): 31 | my_class = getattr(self._modules[key], c) 32 | yield my_class() 33 | 34 | 35 | if __name__ == "__main__": 36 | from jinja2 import Environment, PackageLoader, select_autoescape 37 | env = Environment( loader=PackageLoader("plugin", package_path='templates'), autoescape=select_autoescape()) 38 | template = env.get_template("plugins.js") 39 | data =[] 40 | pm = pluginManager("plugins") 41 | for obj in pm.classes(): 42 | typenode = {} 43 | for member in obj.__dict__: 44 | strsplitted = member.split("_") 45 | if strsplitted[0] not in typenode: 46 | typenode[strsplitted[0]] = [] 47 | typenode[strsplitted[0]].append({'type':strsplitted[1],'name':strsplitted[2], 'value':obj.__dict__[member]}) 48 | data.append({"name": type(obj).__name__, "type":typenode}) 49 | 50 | print(template.render(data=data)) 51 | 52 | """ 53 | pm.reloadModules() 54 | for obj in pm.classes(): 55 | print(obj) 56 | """ 57 | 58 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zebulon75018/flasklitegraphjs/b5eacdbc12190b726e611fb33672edf552a6bd07/plugins/__init__.py -------------------------------------------------------------------------------- /plugins/test.py: -------------------------------------------------------------------------------- 1 | class start: 2 | def __init__(self): 3 | self.output_number_result = 0 4 | self.property_number_a = "0" 5 | def getValue(self, n): 6 | return n["properties"]["a"] 7 | 8 | def execute(self, params): 9 | pass 10 | 11 | class addition: 12 | def __init__(self): 13 | self.input_number_a = 0 14 | self.input_number_b = 0 15 | self.output_number_result = 0 16 | 17 | def getValue(self, n): 18 | print("getValue addition %d %d " % (int(self.input_number_a), int(self.input_number_b))) 19 | return int(self.input_number_a) + int(self.input_number_b) 20 | 21 | def execute(self, params): 22 | self.input_number_a = params[0] 23 | self.input_number_b = params[1] 24 | print("execute addition %d %d " % (int(self.input_number_a),int(self.input_number_b))) 25 | 26 | class integer: 27 | def __init__(self): 28 | self.output_int_result = 0 29 | self.property_number_a = "1" 30 | 31 | def getValue(self, n): 32 | return n["properties"]["a"] 33 | 34 | def execute(self, params): 35 | pass 36 | 37 | class result: 38 | def __init__(self): 39 | self.input_int_result = 0 40 | def getValue(self, n): 41 | print( self.input_int_result ) 42 | 43 | def execute(self, params): 44 | self.input_int_result = params 45 | return self.input_int_result 46 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.6.0 2 | click==7.1.2 3 | dataclasses==0.8 4 | fastapi==0.61.2 5 | h11==0.11.0 6 | Jinja2==2.11.2 7 | MarkupSafe==1.1.1 8 | ninja==1.10.0.post2 9 | pkg-resources==0.0.0 10 | pydantic==1.7.2 11 | starlette==0.13.6 12 | typing-extensions==3.7.4.3 13 | uvicorn==0.12.3 14 | -------------------------------------------------------------------------------- /static/css/litegraph-editor.css: -------------------------------------------------------------------------------- 1 | .litegraph-editor { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | 7 | background-color: #333; 8 | color: #eee; 9 | font: 14px Tahoma; 10 | 11 | position: relative; 12 | } 13 | 14 | .litegraph-editor h1 { 15 | font-family: "Metro Light", Tahoma; 16 | color: #ddd; 17 | font-size: 28px; 18 | padding-left: 10px; 19 | /*text-shadow: 0 1px 1px #333, 0 -1px 1px #777;*/ 20 | margin: 0; 21 | font-weight: normal; 22 | } 23 | 24 | .litegraph-editor h1 span { 25 | font-family: "Arial"; 26 | font-size: 14px; 27 | font-weight: normal; 28 | color: #aaa; 29 | } 30 | 31 | .litegraph-editor h2 { 32 | font-family: "Metro Light"; 33 | padding: 5px; 34 | margin-left: 10px; 35 | } 36 | 37 | .litegraph-editor * { 38 | box-sizing: border-box; 39 | -moz-box-sizing: border-box; 40 | } 41 | 42 | .litegraph-editor .content { 43 | position: relative; 44 | width: 100%; 45 | height: calc(100% - 80px); 46 | background-color: #1a1a1a; 47 | } 48 | 49 | .litegraph-editor .header, 50 | .litegraph-editor .footer { 51 | position: relative; 52 | height: 40px; 53 | background-color: #333; 54 | /*border-radius: 10px 10px 0 0;*/ 55 | } 56 | 57 | .litegraph-editor .tools, 58 | .litegraph-editor .tools-left, 59 | .litegraph-editor .tools-right { 60 | position: absolute; 61 | top: 2px; 62 | right: 0px; 63 | vertical-align: top; 64 | 65 | margin: 2px 5px 0 0px; 66 | } 67 | 68 | .litegraph-editor .tools-left { 69 | right: auto; 70 | left: 4px; 71 | } 72 | 73 | .litegraph-editor .footer { 74 | height: 40px; 75 | position: relative; 76 | /*border-radius: 0 0 10px 10px;*/ 77 | } 78 | 79 | .litegraph-editor .miniwindow { 80 | background-color: #333; 81 | border: 1px solid #111; 82 | } 83 | 84 | .litegraph-editor .miniwindow .corner-button { 85 | position: absolute; 86 | top: 2px; 87 | right: 2px; 88 | font-family: "Tahoma"; 89 | font-size: 14px; 90 | color: #aaa; 91 | cursor: pointer; 92 | } 93 | 94 | /* BUTTONS **********************/ 95 | 96 | .litegraph-editor .btn { 97 | /*font-family: "Metro Light";*/ 98 | color: #ccc; 99 | font-size: 20px; 100 | min-width: 30px; 101 | /*border-radius: 0.3em;*/ 102 | border: 0 solid #666; 103 | background-color: #3f3f3f; 104 | /*box-shadow: 0 0 3px black;*/ 105 | padding: 4px 10px; 106 | cursor: pointer; 107 | transition: all 1s; 108 | -moz-transition: all 1s; 109 | -webkit-transition: all 0.4s; 110 | } 111 | 112 | .litegraph-editor button:hover { 113 | background-color: #999; 114 | color: #fff; 115 | transition: all 1s; 116 | -moz-transition: all 1s; 117 | -webkit-transition: all 0.4s; 118 | } 119 | 120 | .litegraph-editor button:active { 121 | background-color: white; 122 | } 123 | 124 | .litegraph-editor button.fixed { 125 | position: absolute; 126 | top: 5px; 127 | right: 5px; 128 | font-size: 1.2em; 129 | } 130 | 131 | .litegraph-editor button img { 132 | margin: -4px; 133 | vertical-align: top; 134 | opacity: 0.8; 135 | transition: all 1s; 136 | } 137 | 138 | .litegraph-editor button:hover img { 139 | opacity: 1; 140 | } 141 | 142 | .litegraph-editor .header button { 143 | height: 32px; 144 | vertical-align: top; 145 | } 146 | 147 | .litegraph-editor .footer button { 148 | /*font-size: 16px;*/ 149 | } 150 | 151 | .litegraph-editor .toolbar-widget { 152 | display: inline-block; 153 | } 154 | 155 | .litegraph-editor .editor-area { 156 | width: 100%; 157 | height: 100%; 158 | } 159 | 160 | /* METER *********************/ 161 | 162 | .litegraph-editor .loadmeter { 163 | font-family: "Tahoma"; 164 | color: #aaa; 165 | font-size: 12px; 166 | border-radius: 2px; 167 | width: 130px; 168 | vertical-align: top; 169 | } 170 | 171 | .litegraph-editor .strong { 172 | vertical-align: top; 173 | padding: 3px; 174 | width: 30px; 175 | display: inline-block; 176 | line-height: 8px; 177 | } 178 | 179 | .litegraph-editor .cpuload .bgload, 180 | .litegraph-editor .gpuload .bgload { 181 | display: inline-block; 182 | width: 90px; 183 | height: 15px; 184 | background-image: url("../editor/imgs/load-progress-empty.png"); 185 | } 186 | 187 | .litegraph-editor .cpuload .fgload, 188 | .litegraph-editor .gpuload .fgload { 189 | display: inline-block; 190 | width: 4px; 191 | height: 15px; 192 | max-width: 90px; 193 | background-image: url("../editor/imgs/load-progress-full.png"); 194 | } 195 | 196 | .litegraph-editor textarea.code, .litegraph-editor div.code { 197 | height: 100%; 198 | width: 100%; 199 | background-color: black; 200 | padding: 4px; 201 | font: 16px monospace; 202 | overflow: auto; 203 | resize: none; 204 | outline: none; 205 | } 206 | 207 | .litegraph-editor .codeflask { 208 | background-color: #2a2a2a; 209 | } 210 | 211 | .litegraph-editor .codeflask textarea { 212 | opacity: 0; 213 | } -------------------------------------------------------------------------------- /static/css/litegraph.css: -------------------------------------------------------------------------------- 1 | /* this CSS contains only the basic CSS needed to run the app and use it */ 2 | 3 | .lgraphcanvas { 4 | /*cursor: crosshair;*/ 5 | user-select: none; 6 | -moz-user-select: none; 7 | -webkit-user-select: none; 8 | outline: none; 9 | font-family: Tahoma, sans-serif; 10 | } 11 | 12 | .lgraphcanvas * { 13 | box-sizing: border-box; 14 | } 15 | 16 | .litegraph.litecontextmenu { 17 | font-family: Tahoma, sans-serif; 18 | position: fixed; 19 | top: 100px; 20 | left: 100px; 21 | min-width: 100px; 22 | color: #aaf; 23 | padding: 0; 24 | box-shadow: 0 0 10px black !important; 25 | background-color: #2e2e2e !important; 26 | z-index: 10; 27 | } 28 | 29 | .litegraph.litecontextmenu.dark { 30 | background-color: #000 !important; 31 | } 32 | 33 | .litegraph.litecontextmenu .litemenu-title img { 34 | margin-top: 2px; 35 | margin-left: 2px; 36 | margin-right: 4px; 37 | } 38 | 39 | .litegraph.litecontextmenu .litemenu-entry { 40 | margin: 2px; 41 | padding: 2px; 42 | } 43 | 44 | .litegraph.litecontextmenu .litemenu-entry.submenu { 45 | background-color: #2e2e2e !important; 46 | } 47 | 48 | .litegraph.litecontextmenu.dark .litemenu-entry.submenu { 49 | background-color: #000 !important; 50 | } 51 | 52 | .litegraph .litemenubar ul { 53 | font-family: Tahoma, sans-serif; 54 | margin: 0; 55 | padding: 0; 56 | } 57 | 58 | .litegraph .litemenubar li { 59 | font-size: 14px; 60 | color: #999; 61 | display: inline-block; 62 | min-width: 50px; 63 | padding-left: 10px; 64 | padding-right: 10px; 65 | user-select: none; 66 | -moz-user-select: none; 67 | -webkit-user-select: none; 68 | cursor: pointer; 69 | } 70 | 71 | .litegraph .litemenubar li:hover { 72 | background-color: #777; 73 | color: #eee; 74 | } 75 | 76 | .litegraph .litegraph .litemenubar-panel { 77 | position: absolute; 78 | top: 5px; 79 | left: 5px; 80 | min-width: 100px; 81 | background-color: #444; 82 | box-shadow: 0 0 3px black; 83 | padding: 4px; 84 | border-bottom: 2px solid #aaf; 85 | z-index: 10; 86 | } 87 | 88 | .litegraph .litemenu-entry, 89 | .litemenu-title { 90 | font-size: 12px; 91 | color: #aaa; 92 | padding: 0 0 0 4px; 93 | margin: 2px; 94 | padding-left: 2px; 95 | -moz-user-select: none; 96 | -webkit-user-select: none; 97 | user-select: none; 98 | cursor: pointer; 99 | } 100 | 101 | .litegraph .litemenu-entry .icon { 102 | display: inline-block; 103 | width: 12px; 104 | height: 12px; 105 | margin: 2px; 106 | vertical-align: top; 107 | } 108 | 109 | .litegraph .litemenu-entry.checked .icon { 110 | background-color: #aaf; 111 | } 112 | 113 | .litegraph .litemenu-entry .more { 114 | float: right; 115 | padding-right: 5px; 116 | } 117 | 118 | .litegraph .litemenu-entry.disabled { 119 | opacity: 0.5; 120 | cursor: default; 121 | } 122 | 123 | .litegraph .litemenu-entry.separator { 124 | display: block; 125 | border-top: 1px solid #333; 126 | border-bottom: 1px solid #666; 127 | width: 100%; 128 | height: 0px; 129 | margin: 3px 0 2px 0; 130 | background-color: transparent; 131 | padding: 0 !important; 132 | cursor: default !important; 133 | } 134 | 135 | .litegraph .litemenu-entry.has_submenu { 136 | border-right: 2px solid cyan; 137 | } 138 | 139 | .litegraph .litemenu-title { 140 | color: #dde; 141 | background-color: #111; 142 | margin: 0; 143 | padding: 2px; 144 | cursor: default; 145 | } 146 | 147 | .litegraph .litemenu-entry:hover:not(.disabled):not(.separator) { 148 | background-color: #444 !important; 149 | color: #eee; 150 | transition: all 0.2s; 151 | } 152 | 153 | .litegraph .litemenu-entry .property_name { 154 | display: inline-block; 155 | text-align: left; 156 | min-width: 80px; 157 | min-height: 1.2em; 158 | } 159 | 160 | .litegraph .litemenu-entry .property_value { 161 | display: inline-block; 162 | background-color: rgba(0, 0, 0, 0.5); 163 | text-align: right; 164 | min-width: 80px; 165 | min-height: 1.2em; 166 | vertical-align: middle; 167 | padding-right: 10px; 168 | } 169 | 170 | .litegraph.litesearchbox { 171 | font-family: Tahoma, sans-serif; 172 | position: absolute; 173 | background-color: rgba(0, 0, 0, 0.5); 174 | padding-top: 4px; 175 | } 176 | 177 | .litegraph.litesearchbox input, 178 | .litegraph.litesearchbox select { 179 | margin-top: 3px; 180 | min-width: 60px; 181 | min-height: 1.5em; 182 | background-color: black; 183 | border: 0; 184 | color: white; 185 | padding-left: 10px; 186 | margin-right: 5px; 187 | } 188 | 189 | .litegraph.litesearchbox .name { 190 | display: inline-block; 191 | min-width: 60px; 192 | min-height: 1.5em; 193 | padding-left: 10px; 194 | } 195 | 196 | .litegraph.litesearchbox .helper { 197 | overflow: auto; 198 | max-height: 200px; 199 | margin-top: 2px; 200 | } 201 | 202 | .litegraph.lite-search-item { 203 | font-family: Tahoma, sans-serif; 204 | background-color: rgba(0, 0, 0, 0.5); 205 | color: white; 206 | padding-top: 2px; 207 | } 208 | 209 | .litegraph.lite-search-item:hover, 210 | .litegraph.lite-search-item.selected { 211 | cursor: pointer; 212 | background-color: white; 213 | color: black; 214 | } 215 | 216 | /* DIALOGs ******/ 217 | 218 | .litegraph .dialog { 219 | position: absolute; 220 | top: 50%; 221 | left: 50%; 222 | margin-top: -150px; 223 | margin-left: -200px; 224 | 225 | background-color: #2A2A2A; 226 | 227 | min-width: 400px; 228 | min-height: 200px; 229 | box-shadow: 0 0 4px #111; 230 | border-radius: 6px; 231 | } 232 | 233 | .litegraph .dialog.settings { 234 | left: 10px; 235 | top: 10px; 236 | height: calc( 100% - 20px ); 237 | margin: auto; 238 | } 239 | 240 | .litegraph .dialog .close { 241 | float: right; 242 | margin: 4px; 243 | margin-right: 10px; 244 | cursor: pointer; 245 | font-size: 1.4em; 246 | } 247 | 248 | .litegraph .dialog .close:hover { 249 | color: white; 250 | } 251 | 252 | .litegraph .dialog .dialog-header { 253 | color: #AAA; 254 | border-bottom: 1px solid #161616; 255 | } 256 | 257 | .litegraph .dialog .dialog-header { height: 40px; } 258 | .litegraph .dialog .dialog-footer { height: 50px; padding: 10px; border-top: 1px solid #1a1a1a;} 259 | 260 | .litegraph .dialog .dialog-header .dialog-title { 261 | font: 20px "Arial"; 262 | margin: 4px; 263 | padding: 4px 10px; 264 | display: inline-block; 265 | } 266 | 267 | .litegraph .dialog .dialog-content { 268 | height: calc(100% - 90px); 269 | width: 100%; 270 | min-height: 100px; 271 | display: inline-block; 272 | color: #AAA; 273 | /*background-color: black;*/ 274 | } 275 | 276 | .litegraph .dialog .dialog-content h3 { 277 | margin: 10px; 278 | } 279 | 280 | .litegraph .dialog .dialog-content .connections { 281 | flex-direction: row; 282 | } 283 | 284 | .litegraph .dialog .dialog-content .connections .connections_side { 285 | width: calc(50% - 5px); 286 | min-height: 100px; 287 | background-color: black; 288 | display: flex; 289 | } 290 | 291 | .litegraph .dialog .node_type { 292 | font-size: 1.2em; 293 | display: block; 294 | margin: 10px; 295 | } 296 | 297 | .litegraph .dialog .node_desc { 298 | opacity: 0.5; 299 | display: block; 300 | margin: 10px; 301 | } 302 | 303 | .litegraph .dialog .separator { 304 | display: block; 305 | width: calc( 100% - 4px ); 306 | height: 1px; 307 | border-top: 1px solid #000; 308 | border-bottom: 1px solid #333; 309 | margin: 10px 2px; 310 | padding: 0; 311 | } 312 | 313 | .litegraph .dialog .property { 314 | margin-bottom: 2px; 315 | padding: 4px; 316 | } 317 | 318 | .litegraph .dialog .property_name { 319 | color: #737373; 320 | display: inline-block; 321 | text-align: left; 322 | vertical-align: top; 323 | width: 120px; 324 | padding-left: 4px; 325 | overflow: hidden; 326 | } 327 | 328 | .litegraph .dialog .property_value { 329 | display: inline-block; 330 | text-align: right; 331 | color: #AAA; 332 | background-color: #1A1A1A; 333 | width: calc( 100% - 122px ); 334 | max-height: 300px; 335 | padding: 4px; 336 | padding-right: 12px; 337 | overflow: hidden; 338 | cursor: pointer; 339 | border-radius: 3px; 340 | } 341 | 342 | .litegraph .dialog .property_value:hover { 343 | color: white; 344 | } 345 | 346 | .litegraph .dialog .property.boolean .property_value { 347 | padding-right: 30px; 348 | } 349 | 350 | .litegraph .dialog .btn { 351 | border: 0; 352 | border-radius: 4px; 353 | padding: 4px 20px; 354 | margin-left: 0px; 355 | background-color: #060606; 356 | color: #8e8e8e; 357 | } 358 | 359 | .litegraph .dialog .btn:hover { 360 | background-color: #111; 361 | color: #FFF; 362 | } 363 | 364 | .litegraph .dialog .btn.delete:hover { 365 | background-color: #F33; 366 | color: black; 367 | } 368 | 369 | .litegraph .subgraph_property { 370 | padding: 4px; 371 | } 372 | 373 | .litegraph .subgraph_property:hover { 374 | background-color: #333; 375 | } 376 | 377 | .litegraph .subgraph_property.extra { 378 | margin-top: 8px; 379 | } 380 | 381 | .litegraph .subgraph_property span.name { 382 | font-size: 1.3em; 383 | padding-left: 4px; 384 | } 385 | 386 | .litegraph .subgraph_property span.type { 387 | opacity: 0.5; 388 | margin-right: 20px; 389 | padding-left: 4px; 390 | } 391 | 392 | .litegraph .subgraph_property span.label { 393 | display: inline-block; 394 | width: 60px; 395 | padding: 0px 10px; 396 | } 397 | 398 | .litegraph .subgraph_property input { 399 | width: 140px; 400 | color: #999; 401 | background-color: #1A1A1A; 402 | border-radius: 4px; 403 | border: 0; 404 | margin-right: 10px; 405 | padding: 4px; 406 | padding-left: 10px; 407 | } 408 | 409 | .litegraph .subgraph_property button { 410 | background-color: #1c1c1c; 411 | color: #aaa; 412 | border: 0; 413 | border-radius: 2px; 414 | padding: 4px 10px; 415 | cursor: pointer; 416 | } 417 | 418 | .litegraph .subgraph_property.extra { 419 | color: #ccc; 420 | } 421 | 422 | .litegraph .subgraph_property.extra input { 423 | background-color: #111; 424 | } 425 | 426 | .litegraph .bullet_icon { 427 | margin-left: 10px; 428 | border-radius: 10px; 429 | width: 12px; 430 | height: 12px; 431 | background-color: #666; 432 | display: inline-block; 433 | margin-top: 2px; 434 | margin-right: 4px; 435 | transition: background-color 0.1s ease 0s; 436 | -moz-transition: background-color 0.1s ease 0s; 437 | } 438 | 439 | .litegraph .bullet_icon:hover { 440 | background-color: #698; 441 | cursor: pointer; 442 | } 443 | 444 | /* OLD */ 445 | 446 | .graphcontextmenu { 447 | padding: 4px; 448 | min-width: 100px; 449 | } 450 | 451 | .graphcontextmenu-title { 452 | color: #dde; 453 | background-color: #222; 454 | margin: 0; 455 | padding: 2px; 456 | cursor: default; 457 | } 458 | 459 | .graphmenu-entry { 460 | box-sizing: border-box; 461 | margin: 2px; 462 | padding-left: 20px; 463 | user-select: none; 464 | -moz-user-select: none; 465 | -webkit-user-select: none; 466 | transition: all linear 0.3s; 467 | } 468 | 469 | .graphmenu-entry.event, 470 | .litemenu-entry.event { 471 | border-left: 8px solid orange; 472 | padding-left: 12px; 473 | } 474 | 475 | .graphmenu-entry.disabled { 476 | opacity: 0.3; 477 | } 478 | 479 | .graphmenu-entry.submenu { 480 | border-right: 2px solid #eee; 481 | } 482 | 483 | .graphmenu-entry:hover { 484 | background-color: #555; 485 | } 486 | 487 | .graphmenu-entry.separator { 488 | background-color: #111; 489 | border-bottom: 1px solid #666; 490 | height: 1px; 491 | width: calc(100% - 20px); 492 | -moz-width: calc(100% - 20px); 493 | -webkit-width: calc(100% - 20px); 494 | } 495 | 496 | .graphmenu-entry .property_name { 497 | display: inline-block; 498 | text-align: left; 499 | min-width: 80px; 500 | min-height: 1.2em; 501 | } 502 | 503 | .graphmenu-entry .property_value, 504 | .litemenu-entry .property_value { 505 | display: inline-block; 506 | background-color: rgba(0, 0, 0, 0.5); 507 | text-align: right; 508 | min-width: 80px; 509 | min-height: 1.2em; 510 | vertical-align: middle; 511 | padding-right: 10px; 512 | } 513 | 514 | .graphdialog { 515 | position: absolute; 516 | top: 10px; 517 | left: 10px; 518 | /*min-height: 2em;*/ 519 | background-color: #333; 520 | font-size: 1.2em; 521 | box-shadow: 0 0 10px black !important; 522 | z-index: 10; 523 | } 524 | 525 | .graphdialog.rounded { 526 | border-radius: 12px; 527 | padding-right: 2px; 528 | } 529 | 530 | .graphdialog .name { 531 | display: inline-block; 532 | min-width: 60px; 533 | min-height: 1.5em; 534 | padding-left: 10px; 535 | } 536 | 537 | .graphdialog input, 538 | .graphdialog textarea, 539 | .graphdialog select { 540 | margin: 3px; 541 | min-width: 60px; 542 | min-height: 1.5em; 543 | background-color: black; 544 | border: 0; 545 | color: white; 546 | padding-left: 10px; 547 | outline: none; 548 | } 549 | 550 | .graphdialog textarea { 551 | min-height: 150px; 552 | } 553 | 554 | .graphdialog button { 555 | margin-top: 3px; 556 | vertical-align: top; 557 | background-color: #999; 558 | border: 0; 559 | } 560 | 561 | .graphdialog button.rounded, 562 | .graphdialog input.rounded { 563 | border-radius: 0 12px 12px 0; 564 | } 565 | 566 | .graphdialog .helper { 567 | overflow: auto; 568 | max-height: 200px; 569 | } 570 | 571 | .graphdialog .help-item { 572 | padding-left: 10px; 573 | } 574 | 575 | .graphdialog .help-item:hover, 576 | .graphdialog .help-item.selected { 577 | cursor: pointer; 578 | background-color: white; 579 | color: black; 580 | } 581 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | height: 100%; 4 | } 5 | 6 | .control input, .input-control input { 7 | width: 100%; 8 | border-radius: 30px; 9 | background-color: white; 10 | padding: 2px 6px; 11 | border: 1px solid #999; 12 | font-size: 110%; 13 | width: 170px; 14 | } 15 | 16 | .content { 17 | display: flex; 18 | height: 100%; 19 | } 20 | .content #editor-wrapper { 21 | flex: 2; 22 | } 23 | .content #editor-wrapper #editor .socket.string { 24 | background: #6f377e; 25 | } 26 | .content #editor-wrapper #editor .socket.action { 27 | background: white; 28 | border-color: grey; 29 | border-radius: 3px; 30 | width: 15px; 31 | } 32 | .content #editor-wrapper #editor .node.message-event { 33 | background: #767676; 34 | } 35 | .content #editor-wrapper #editor .input-control input { 36 | width: 140px; 37 | } 38 | .content #telegram { 39 | margin: 15px; 40 | flex: 1; 41 | height: calc(100% - 2 * 15px); 42 | border: 2px solid blue; 43 | background: url("http://78.media.tumblr.com/913fc95846350c30232a5608a322b78e/tumblr_obykzyjxZt1vbllj8o4_1280.png"); 44 | background-size: cover; 45 | } 46 | .content #telegram .messages { 47 | height: calc(100% - 80px); 48 | max-height: calc(100% - 80px); 49 | overflow: auto; 50 | } 51 | .content #telegram .messages .message { 52 | display: flex; 53 | padding: 10px; 54 | } 55 | .content #telegram .messages .message.owner { 56 | flex-direction: row-reverse; 57 | } 58 | .content #telegram .messages .message .avatar { 59 | display: inline-block; 60 | border-radius: 100%; 61 | flex-grow: 0; 62 | flex-shrink: 0; 63 | height: 40px; 64 | margin: 0 12px; 65 | overflow: hidden; 66 | } 67 | .content #telegram .messages .message .avatar img { 68 | height: 40px; 69 | width: 40px; 70 | } 71 | .content #telegram .messages .message .text-wrap { 72 | display: inline-block; 73 | } 74 | .content #telegram .messages .message .text-wrap .text { 75 | padding: 5px; 76 | background: rgba(255, 255, 255, 0.6); 77 | border-radius: 12px; 78 | } 79 | .content #telegram .messages .message .text-wrap .text a { 80 | color: #4f6; 81 | } 82 | .content #telegram .form { 83 | background: #839aed; 84 | height: 80px; 85 | } 86 | -------------------------------------------------------------------------------- /static/js/base.js: -------------------------------------------------------------------------------- 1 | //basic nodes 2 | (function(global) { 3 | var LiteGraph = global.LiteGraph; 4 | 5 | function Start() 6 | { 7 | this.addProperty("url", "text"); 8 | this.widget = this.addWidget("text","url","","url"); 9 | this.addOutput("file", "text"); 10 | } 11 | Start.prototype.onPropertyChanged = function(name, value) { 12 | this.widget.value = value; 13 | } 14 | Start.prototype.setValue = function(v) 15 | { 16 | this.setProperty("file",v); 17 | } 18 | Start.title = "Start"; 19 | Start.desc = "Start Node"; 20 | //Start["@type"] = { type: "enum", values: ["text","arraybuffer","blob","json"] }; 21 | LiteGraph.registerNodeType("basic/start", Start); 22 | 23 | function Contains() 24 | { 25 | this.addProperty("contains", "text"); 26 | this.addInput("text", "text"); 27 | this.widget = this.addWidget("text","contains","","text"); 28 | this.addOutput("true", "boolean"); 29 | this.addOutput("false", "boolean"); 30 | } 31 | Contains.prototype.onPropertyChanged = function(name, value) { 32 | this.widget.value = value; 33 | } 34 | Contains.prototype.setValue = function(v) 35 | { 36 | this.setProperty("contains",v); 37 | } 38 | Contains.title = "Contains"; 39 | Contains.desc = "Input Contains "; 40 | LiteGraph.registerNodeType("if/contains", Contains); 41 | 42 | 43 | function Logs() 44 | { 45 | this.addProperty("message", "text"); 46 | this.addInput("input", "boolean"); 47 | this.widget = this.addWidget("text","","","text"); 48 | } 49 | 50 | Contains.prototype.onPropertyChanged = function(name, value) { 51 | this.widget.value = value; 52 | } 53 | Logs.prototype.setValue = function(v) 54 | { 55 | this.setProperty("message",v); 56 | } 57 | Logs.title = "Log"; 58 | Logs.desc = "Log "; 59 | LiteGraph.registerNodeType("basic/log", Logs); 60 | 61 | 62 | //Subgraph: a node that contains a graph 63 | function Subgraph() { 64 | var that = this; 65 | this.size = [140, 80]; 66 | this.properties = { enabled: true }; 67 | this.enabled = true; 68 | 69 | //create inner graph 70 | this.subgraph = new LiteGraph.LGraph(); 71 | this.subgraph._subgraph_node = this; 72 | this.subgraph._is_subgraph = true; 73 | 74 | this.subgraph.onTrigger = this.onSubgraphTrigger.bind(this); 75 | 76 | //nodes input node added inside 77 | this.subgraph.onInputAdded = this.onSubgraphNewInput.bind(this); 78 | this.subgraph.onInputRenamed = this.onSubgraphRenamedInput.bind(this); 79 | this.subgraph.onInputTypeChanged = this.onSubgraphTypeChangeInput.bind(this); 80 | this.subgraph.onInputRemoved = this.onSubgraphRemovedInput.bind(this); 81 | 82 | this.subgraph.onOutputAdded = this.onSubgraphNewOutput.bind(this); 83 | this.subgraph.onOutputRenamed = this.onSubgraphRenamedOutput.bind(this); 84 | this.subgraph.onOutputTypeChanged = this.onSubgraphTypeChangeOutput.bind(this); 85 | this.subgraph.onOutputRemoved = this.onSubgraphRemovedOutput.bind(this); 86 | } 87 | 88 | Subgraph.title = "Subgraph"; 89 | Subgraph.desc = "Graph inside a node"; 90 | Subgraph.title_color = "#334"; 91 | 92 | Subgraph.prototype.onGetInputs = function() { 93 | return [["enabled", "boolean"]]; 94 | }; 95 | 96 | /* 97 | Subgraph.prototype.onDrawTitle = function(ctx) { 98 | if (this.flags.collapsed) { 99 | return; 100 | } 101 | 102 | ctx.fillStyle = "#555"; 103 | var w = LiteGraph.NODE_TITLE_HEIGHT; 104 | var x = this.size[0] - w; 105 | ctx.fillRect(x, -w, w, w); 106 | ctx.fillStyle = "#333"; 107 | ctx.beginPath(); 108 | ctx.moveTo(x + w * 0.2, -w * 0.6); 109 | ctx.lineTo(x + w * 0.8, -w * 0.6); 110 | ctx.lineTo(x + w * 0.5, -w * 0.3); 111 | ctx.fill(); 112 | }; 113 | */ 114 | 115 | Subgraph.prototype.onDblClick = function(e, pos, graphcanvas) { 116 | var that = this; 117 | setTimeout(function() { 118 | graphcanvas.openSubgraph(that.subgraph); 119 | }, 10); 120 | }; 121 | 122 | /* 123 | Subgraph.prototype.onMouseDown = function(e, pos, graphcanvas) { 124 | if ( 125 | !this.flags.collapsed && 126 | pos[0] > this.size[0] - LiteGraph.NODE_TITLE_HEIGHT && 127 | pos[1] < 0 128 | ) { 129 | var that = this; 130 | setTimeout(function() { 131 | graphcanvas.openSubgraph(that.subgraph); 132 | }, 10); 133 | } 134 | }; 135 | */ 136 | 137 | Subgraph.prototype.onAction = function(action, param) { 138 | this.subgraph.onAction(action, param); 139 | }; 140 | 141 | Subgraph.prototype.onExecute = function() { 142 | this.enabled = this.getInputOrProperty("enabled"); 143 | if (!this.enabled) { 144 | return; 145 | } 146 | 147 | //send inputs to subgraph global inputs 148 | if (this.inputs) { 149 | for (var i = 0; i < this.inputs.length; i++) { 150 | var input = this.inputs[i]; 151 | var value = this.getInputData(i); 152 | this.subgraph.setInputData(input.name, value); 153 | } 154 | } 155 | 156 | //execute 157 | this.subgraph.runStep(); 158 | 159 | //send subgraph global outputs to outputs 160 | if (this.outputs) { 161 | for (var i = 0; i < this.outputs.length; i++) { 162 | var output = this.outputs[i]; 163 | var value = this.subgraph.getOutputData(output.name); 164 | this.setOutputData(i, value); 165 | } 166 | } 167 | }; 168 | 169 | Subgraph.prototype.sendEventToAllNodes = function(eventname, param, mode) { 170 | if (this.enabled) { 171 | this.subgraph.sendEventToAllNodes(eventname, param, mode); 172 | } 173 | }; 174 | 175 | Subgraph.prototype.onDrawBackground = function(ctx, graphcanvas, canvas, pos) 176 | { 177 | if(this.flags.collapsed) 178 | return; 179 | 180 | var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5; 181 | 182 | //button 183 | var over = LiteGraph.isInsideRectangle(pos[0],pos[1],this.pos[0],this.pos[1] + y,this.size[0],LiteGraph.NODE_TITLE_HEIGHT); 184 | ctx.fillStyle = over ? "#555" : "#222"; 185 | ctx.beginPath(); 186 | if (this._shape == LiteGraph.BOX_SHAPE) 187 | ctx.rect(0, y, this.size[0]+1, LiteGraph.NODE_TITLE_HEIGHT); 188 | else 189 | ctx.roundRect( 0, y, this.size[0]+1, LiteGraph.NODE_TITLE_HEIGHT, 0, 8); 190 | ctx.fill(); 191 | 192 | //button 193 | ctx.textAlign = "center"; 194 | ctx.font = "24px Arial"; 195 | ctx.fillStyle = over ? "#DDD" : "#999"; 196 | ctx.fillText( "+", this.size[0] * 0.5, y + 24 ); 197 | } 198 | 199 | Subgraph.prototype.onMouseDown = function(e, localpos, graphcanvas) 200 | { 201 | var y = this.size[1] - LiteGraph.NODE_TITLE_HEIGHT + 0.5; 202 | if(localpos[1] > y) 203 | { 204 | graphcanvas.showSubgraphPropertiesDialog(this); 205 | } 206 | } 207 | 208 | Subgraph.prototype.computeSize = function() 209 | { 210 | var num_inputs = this.inputs ? this.inputs.length : 0; 211 | var num_outputs = this.outputs ? this.outputs.length : 0; 212 | return [ 200, Math.max(num_inputs,num_outputs) * LiteGraph.NODE_SLOT_HEIGHT + LiteGraph.NODE_TITLE_HEIGHT ]; 213 | } 214 | 215 | //**** INPUTS *********************************** 216 | Subgraph.prototype.onSubgraphTrigger = function(event, param) { 217 | var slot = this.findOutputSlot(event); 218 | if (slot != -1) { 219 | this.triggerSlot(slot); 220 | } 221 | }; 222 | 223 | Subgraph.prototype.onSubgraphNewInput = function(name, type) { 224 | var slot = this.findInputSlot(name); 225 | if (slot == -1) { 226 | //add input to the node 227 | this.addInput(name, type); 228 | } 229 | }; 230 | 231 | Subgraph.prototype.onSubgraphRenamedInput = function(oldname, name) { 232 | var slot = this.findInputSlot(oldname); 233 | if (slot == -1) { 234 | return; 235 | } 236 | var info = this.getInputInfo(slot); 237 | info.name = name; 238 | }; 239 | 240 | Subgraph.prototype.onSubgraphTypeChangeInput = function(name, type) { 241 | var slot = this.findInputSlot(name); 242 | if (slot == -1) { 243 | return; 244 | } 245 | var info = this.getInputInfo(slot); 246 | info.type = type; 247 | }; 248 | 249 | Subgraph.prototype.onSubgraphRemovedInput = function(name) { 250 | var slot = this.findInputSlot(name); 251 | if (slot == -1) { 252 | return; 253 | } 254 | this.removeInput(slot); 255 | }; 256 | 257 | //**** OUTPUTS *********************************** 258 | Subgraph.prototype.onSubgraphNewOutput = function(name, type) { 259 | var slot = this.findOutputSlot(name); 260 | if (slot == -1) { 261 | this.addOutput(name, type); 262 | } 263 | }; 264 | 265 | Subgraph.prototype.onSubgraphRenamedOutput = function(oldname, name) { 266 | var slot = this.findOutputSlot(oldname); 267 | if (slot == -1) { 268 | return; 269 | } 270 | var info = this.getOutputInfo(slot); 271 | info.name = name; 272 | }; 273 | 274 | Subgraph.prototype.onSubgraphTypeChangeOutput = function(name, type) { 275 | var slot = this.findOutputSlot(name); 276 | if (slot == -1) { 277 | return; 278 | } 279 | var info = this.getOutputInfo(slot); 280 | info.type = type; 281 | }; 282 | 283 | Subgraph.prototype.onSubgraphRemovedOutput = function(name) { 284 | var slot = this.findInputSlot(name); 285 | if (slot == -1) { 286 | return; 287 | } 288 | this.removeOutput(slot); 289 | }; 290 | // ***************************************************** 291 | 292 | Subgraph.prototype.getExtraMenuOptions = function(graphcanvas) { 293 | var that = this; 294 | return [ 295 | { 296 | content: "Open", 297 | callback: function() { 298 | graphcanvas.openSubgraph(that.subgraph); 299 | } 300 | } 301 | ]; 302 | }; 303 | 304 | Subgraph.prototype.onResize = function(size) { 305 | size[1] += 20; 306 | }; 307 | 308 | Subgraph.prototype.serialize = function() { 309 | var data = LiteGraph.LGraphNode.prototype.serialize.call(this); 310 | data.subgraph = this.subgraph.serialize(); 311 | return data; 312 | }; 313 | //no need to define node.configure, the default method detects node.subgraph and passes the object to node.subgraph.configure() 314 | 315 | Subgraph.prototype.clone = function() { 316 | var node = LiteGraph.createNode(this.type); 317 | var data = this.serialize(); 318 | delete data["id"]; 319 | delete data["inputs"]; 320 | delete data["outputs"]; 321 | node.configure(data); 322 | return node; 323 | }; 324 | 325 | Subgraph.prototype.buildFromNodes = function(nodes) 326 | { 327 | //clear all? 328 | //TODO 329 | 330 | //nodes that connect data between parent graph and subgraph 331 | var subgraph_inputs = []; 332 | var subgraph_outputs = []; 333 | 334 | //mark inner nodes 335 | var ids = {}; 336 | var min_x = 0; 337 | var max_x = 0; 338 | for(var i = 0; i < nodes.length; ++i) 339 | { 340 | var node = nodes[i]; 341 | ids[ node.id ] = node; 342 | min_x = Math.min( node.pos[0], min_x ); 343 | max_x = Math.max( node.pos[0], min_x ); 344 | } 345 | 346 | var last_input_y = 0; 347 | var last_output_y = 0; 348 | 349 | for(var i = 0; i < nodes.length; ++i) 350 | { 351 | var node = nodes[i]; 352 | //check inputs 353 | if( node.inputs ) 354 | for(var j = 0; j < node.inputs.length; ++j) 355 | { 356 | var input = node.inputs[j]; 357 | if( !input || !input.link ) 358 | continue; 359 | var link = node.graph.links[ input.link ]; 360 | if(!link) 361 | continue; 362 | if( ids[ link.origin_id ] ) 363 | continue; 364 | //this.addInput(input.name,link.type); 365 | this.subgraph.addInput(input.name,link.type); 366 | /* 367 | var input_node = LiteGraph.createNode("graph/input"); 368 | this.subgraph.add( input_node ); 369 | input_node.pos = [min_x - 200, last_input_y ]; 370 | last_input_y += 100; 371 | */ 372 | } 373 | 374 | //check outputs 375 | if( node.outputs ) 376 | for(var j = 0; j < node.outputs.length; ++j) 377 | { 378 | var output = node.outputs[j]; 379 | if( !output || !output.links || !output.links.length ) 380 | continue; 381 | var is_external = false; 382 | for(var k = 0; k < output.links.length; ++k) 383 | { 384 | var link = node.graph.links[ output.links[k] ]; 385 | if(!link) 386 | continue; 387 | if( ids[ link.target_id ] ) 388 | continue; 389 | is_external = true; 390 | break; 391 | } 392 | if(!is_external) 393 | continue; 394 | //this.addOutput(output.name,output.type); 395 | /* 396 | var output_node = LiteGraph.createNode("graph/output"); 397 | this.subgraph.add( output_node ); 398 | output_node.pos = [max_x + 50, last_output_y ]; 399 | last_output_y += 100; 400 | */ 401 | } 402 | } 403 | 404 | //detect inputs and outputs 405 | //split every connection in two data_connection nodes 406 | //keep track of internal connections 407 | //connect external connections 408 | 409 | //clone nodes inside subgraph and try to reconnect them 410 | 411 | //connect edge subgraph nodes to extarnal connections nodes 412 | } 413 | 414 | LiteGraph.Subgraph = Subgraph; 415 | LiteGraph.registerNodeType("graph/subgraph", Subgraph); 416 | 417 | //Input for a subgraph 418 | function GraphInput() { 419 | this.addOutput("", "number"); 420 | 421 | this.name_in_graph = ""; 422 | this.properties = { 423 | name: "", 424 | type: "number", 425 | value: 0 426 | }; 427 | 428 | var that = this; 429 | 430 | this.name_widget = this.addWidget( 431 | "text", 432 | "Name", 433 | this.properties.name, 434 | function(v) { 435 | if (!v) { 436 | return; 437 | } 438 | that.setProperty("name",v); 439 | } 440 | ); 441 | this.type_widget = this.addWidget( 442 | "text", 443 | "Type", 444 | this.properties.type, 445 | function(v) { 446 | that.setProperty("type",v); 447 | } 448 | ); 449 | 450 | this.value_widget = this.addWidget( 451 | "number", 452 | "Value", 453 | this.properties.value, 454 | function(v) { 455 | that.setProperty("value",v); 456 | } 457 | ); 458 | 459 | this.widgets_up = true; 460 | this.size = [180, 90]; 461 | } 462 | 463 | GraphInput.title = "Input"; 464 | GraphInput.desc = "Input of the graph"; 465 | 466 | GraphInput.prototype.onConfigure = function() 467 | { 468 | this.updateType(); 469 | } 470 | 471 | //ensures the type in the node output and the type in the associated graph input are the same 472 | GraphInput.prototype.updateType = function() 473 | { 474 | var type = this.properties.type; 475 | this.type_widget.value = type; 476 | 477 | //update output 478 | if(this.outputs[0].type != type) 479 | { 480 | if (!LiteGraph.isValidConnection(this.outputs[0].type,type)) 481 | this.disconnectOutput(0); 482 | this.outputs[0].type = type; 483 | } 484 | 485 | //update widget 486 | if(type == "number") 487 | { 488 | this.value_widget.type = "number"; 489 | this.value_widget.value = 0; 490 | } 491 | else if(type == "boolean") 492 | { 493 | this.value_widget.type = "toggle"; 494 | this.value_widget.value = true; 495 | } 496 | else if(type == "string") 497 | { 498 | this.value_widget.type = "text"; 499 | this.value_widget.value = ""; 500 | } 501 | else 502 | { 503 | this.value_widget.type = null; 504 | this.value_widget.value = null; 505 | } 506 | this.properties.value = this.value_widget.value; 507 | 508 | //update graph 509 | if (this.graph && this.name_in_graph) { 510 | this.graph.changeInputType(this.name_in_graph, type); 511 | } 512 | } 513 | 514 | //this is executed AFTER the property has changed 515 | GraphInput.prototype.onPropertyChanged = function(name,v) 516 | { 517 | if( name == "name" ) 518 | { 519 | if (v == "" || v == this.name_in_graph || v == "enabled") { 520 | return false; 521 | } 522 | if(this.graph) 523 | { 524 | if (this.name_in_graph) { 525 | //already added 526 | this.graph.renameInput( this.name_in_graph, v ); 527 | } else { 528 | this.graph.addInput( v, this.properties.type ); 529 | } 530 | } //what if not?! 531 | this.name_widget.value = v; 532 | this.name_in_graph = v; 533 | } 534 | else if( name == "type" ) 535 | { 536 | this.updateType(); 537 | } 538 | else if( name == "value" ) 539 | { 540 | } 541 | } 542 | 543 | GraphInput.prototype.getTitle = function() { 544 | if (this.flags.collapsed) { 545 | return this.properties.name; 546 | } 547 | return this.title; 548 | }; 549 | 550 | GraphInput.prototype.onAction = function(action, param) { 551 | if (this.properties.type == LiteGraph.EVENT) { 552 | this.triggerSlot(0, param); 553 | } 554 | }; 555 | 556 | GraphInput.prototype.onExecute = function() { 557 | var name = this.properties.name; 558 | //read from global input 559 | var data = this.graph.inputs[name]; 560 | if (!data) { 561 | this.setOutputData(0, this.properties.value ); 562 | return; 563 | } 564 | 565 | this.setOutputData(0, data.value !== undefined ? data.value : this.properties.value ); 566 | }; 567 | 568 | GraphInput.prototype.onRemoved = function() { 569 | if (this.name_in_graph) { 570 | this.graph.removeInput(this.name_in_graph); 571 | } 572 | }; 573 | 574 | LiteGraph.GraphInput = GraphInput; 575 | LiteGraph.registerNodeType("graph/input", GraphInput); 576 | 577 | //Output for a subgraph 578 | function GraphOutput() { 579 | this.addInput("", ""); 580 | 581 | this.name_in_graph = ""; 582 | this.properties = {}; 583 | var that = this; 584 | 585 | Object.defineProperty(this.properties, "name", { 586 | get: function() { 587 | return that.name_in_graph; 588 | }, 589 | set: function(v) { 590 | if (v == "" || v == that.name_in_graph) { 591 | return; 592 | } 593 | if (that.name_in_graph) { 594 | //already added 595 | that.graph.renameOutput(that.name_in_graph, v); 596 | } else { 597 | that.graph.addOutput(v, that.properties.type); 598 | } 599 | that.name_widget.value = v; 600 | that.name_in_graph = v; 601 | }, 602 | enumerable: true 603 | }); 604 | 605 | Object.defineProperty(this.properties, "type", { 606 | get: function() { 607 | return that.inputs[0].type; 608 | }, 609 | set: function(v) { 610 | if (v == "action" || v == "event") { 611 | v = LiteGraph.ACTION; 612 | } 613 | if (!LiteGraph.isValidConnection(that.inputs[0].type,v)) 614 | that.disconnectInput(0); 615 | that.inputs[0].type = v; 616 | if (that.name_in_graph) { 617 | //already added 618 | that.graph.changeOutputType( 619 | that.name_in_graph, 620 | that.inputs[0].type 621 | ); 622 | } 623 | that.type_widget.value = v || ""; 624 | }, 625 | enumerable: true 626 | }); 627 | 628 | this.name_widget = this.addWidget("text","Name",this.properties.name,"name"); 629 | this.type_widget = this.addWidget("text","Type",this.properties.type,"type"); 630 | this.widgets_up = true; 631 | this.size = [180, 60]; 632 | } 633 | 634 | GraphOutput.title = "Output"; 635 | GraphOutput.desc = "Output of the graph"; 636 | 637 | GraphOutput.prototype.onExecute = function() { 638 | this._value = this.getInputData(0); 639 | this.graph.setOutputData(this.properties.name, this._value); 640 | }; 641 | 642 | GraphOutput.prototype.onAction = function(action, param) { 643 | if (this.properties.type == LiteGraph.ACTION) { 644 | this.graph.trigger(this.properties.name, param); 645 | } 646 | }; 647 | 648 | GraphOutput.prototype.onRemoved = function() { 649 | if (this.name_in_graph) { 650 | this.graph.removeOutput(this.name_in_graph); 651 | } 652 | }; 653 | 654 | GraphOutput.prototype.getTitle = function() { 655 | if (this.flags.collapsed) { 656 | return this.properties.name; 657 | } 658 | return this.title; 659 | }; 660 | 661 | LiteGraph.GraphOutput = GraphOutput; 662 | LiteGraph.registerNodeType("graph/output", GraphOutput); 663 | 664 | //Constant 665 | function ConstantNumber() { 666 | this.addOutput("value", "number"); 667 | this.addProperty("value", 1.0); 668 | this.widget = this.addWidget("number","value",1,"value"); 669 | this.widgets_up = true; 670 | this.size = [180, 30]; 671 | } 672 | 673 | ConstantNumber.title = "Const Number"; 674 | ConstantNumber.desc = "Constant number"; 675 | 676 | ConstantNumber.prototype.onExecute = function() { 677 | this.setOutputData(0, parseFloat(this.properties["value"])); 678 | }; 679 | 680 | ConstantNumber.prototype.getTitle = function() { 681 | if (this.flags.collapsed) { 682 | return this.properties.value; 683 | } 684 | return this.title; 685 | }; 686 | 687 | ConstantNumber.prototype.setValue = function(v) 688 | { 689 | this.setProperty("value",v); 690 | } 691 | 692 | ConstantNumber.prototype.onDrawBackground = function(ctx) { 693 | //show the current value 694 | this.outputs[0].label = this.properties["value"].toFixed(3); 695 | }; 696 | 697 | LiteGraph.registerNodeType("basic/const", ConstantNumber); 698 | 699 | function ConstantBoolean() { 700 | this.addOutput("", "boolean"); 701 | this.addProperty("value", true); 702 | this.widget = this.addWidget("toggle","value",true,"value"); 703 | this.widgets_up = true; 704 | this.size = [140, 30]; 705 | } 706 | 707 | ConstantBoolean.title = "Const Boolean"; 708 | ConstantBoolean.desc = "Constant boolean"; 709 | ConstantBoolean.prototype.getTitle = ConstantNumber.prototype.getTitle; 710 | 711 | ConstantBoolean.prototype.onExecute = function() { 712 | this.setOutputData(0, this.properties["value"]); 713 | }; 714 | 715 | ConstantBoolean.prototype.setValue = ConstantNumber.prototype.setValue; 716 | 717 | ConstantBoolean.prototype.onGetInputs = function() { 718 | return [["toggle", LiteGraph.ACTION]]; 719 | }; 720 | 721 | ConstantBoolean.prototype.onAction = function(action) 722 | { 723 | this.setValue( !this.properties.value ); 724 | } 725 | 726 | LiteGraph.registerNodeType("basic/boolean", ConstantBoolean); 727 | 728 | function ConstantString() { 729 | this.addOutput("", "string"); 730 | this.addProperty("value", ""); 731 | this.widget = this.addWidget("text","value","","value"); //link to property value 732 | this.widgets_up = true; 733 | this.size = [180, 30]; 734 | } 735 | 736 | ConstantString.title = "Const String"; 737 | ConstantString.desc = "Constant string"; 738 | 739 | ConstantString.prototype.getTitle = ConstantNumber.prototype.getTitle; 740 | 741 | ConstantString.prototype.onExecute = function() { 742 | this.setOutputData(0, this.properties["value"]); 743 | }; 744 | 745 | ConstantString.prototype.setValue = ConstantNumber.prototype.setValue; 746 | 747 | ConstantString.prototype.onDropFile = function(file) 748 | { 749 | var that = this; 750 | var reader = new FileReader(); 751 | reader.onload = function(e) 752 | { 753 | that.setProperty("value",e.target.result); 754 | } 755 | reader.readAsText(file); 756 | } 757 | 758 | LiteGraph.registerNodeType("basic/string", ConstantString); 759 | 760 | function ConstantObject() { 761 | this.addOutput("obj", "object"); 762 | this.size = [120, 30]; 763 | this._object = {}; 764 | } 765 | 766 | ConstantObject.title = "Const Object"; 767 | ConstantObject.desc = "Constant Object"; 768 | 769 | ConstantObject.prototype.onExecute = function() { 770 | this.setOutputData(0, this._object); 771 | }; 772 | 773 | LiteGraph.registerNodeType( "basic/object", ConstantObject ); 774 | 775 | function ConstantFile() { 776 | this.addInput("url", ""); 777 | this.addOutput("", ""); 778 | this.addProperty("url", ""); 779 | this.addProperty("type", "text"); 780 | this.widget = this.addWidget("text","url","","url"); 781 | this._data = null; 782 | } 783 | 784 | ConstantFile.title = "Const File"; 785 | ConstantFile.desc = "Fetches a file from an url"; 786 | ConstantFile["@type"] = { type: "enum", values: ["text","arraybuffer","blob","json"] }; 787 | 788 | ConstantFile.prototype.onPropertyChanged = function(name, value) { 789 | if (name == "url") 790 | { 791 | if( value == null || value == "") 792 | this._data = null; 793 | else 794 | { 795 | this.fetchFile(value); 796 | } 797 | } 798 | } 799 | 800 | ConstantFile.prototype.onExecute = function() { 801 | var url = this.getInputData(0) || this.properties.url; 802 | if(url && (url != this._url || this._type != this.properties.type)) 803 | this.fetchFile(url); 804 | this.setOutputData(0, this._data ); 805 | }; 806 | 807 | ConstantFile.prototype.setValue = ConstantNumber.prototype.setValue; 808 | 809 | ConstantFile.prototype.fetchFile = function(url) { 810 | var that = this; 811 | if(!url || url.constructor !== String) 812 | { 813 | that._data = null; 814 | that.boxcolor = null; 815 | return; 816 | } 817 | 818 | this._url = url; 819 | this._type = this.properties.type; 820 | if (url.substr(0, 4) == "http" && LiteGraph.proxy) { 821 | url = LiteGraph.proxy + url.substr(url.indexOf(":") + 3); 822 | } 823 | fetch(url) 824 | .then(function(response) { 825 | if(!response.ok) 826 | throw new Error("File not found"); 827 | 828 | if(that.properties.type == "arraybuffer") 829 | return response.arrayBuffer(); 830 | else if(that.properties.type == "text") 831 | return response.text(); 832 | else if(that.properties.type == "json") 833 | return response.json(); 834 | else if(that.properties.type == "blob") 835 | return response.blob(); 836 | }) 837 | .then(function(data) { 838 | that._data = data; 839 | that.boxcolor = "#AEA"; 840 | }) 841 | .catch(function(error) { 842 | that._data = null; 843 | that.boxcolor = "red"; 844 | console.error("error fetching file:",url); 845 | }); 846 | }; 847 | 848 | ConstantFile.prototype.onDropFile = function(file) 849 | { 850 | var that = this; 851 | this._url = file.name; 852 | this._type = this.properties.type; 853 | this.properties.url = file.name; 854 | var reader = new FileReader(); 855 | reader.onload = function(e) 856 | { 857 | that.boxcolor = "#AEA"; 858 | var v = e.target.result; 859 | if( that.properties.type == "json" ) 860 | v = JSON.parse(v); 861 | that._data = v; 862 | } 863 | if(that.properties.type == "arraybuffer") 864 | reader.readAsArrayBuffer(file); 865 | else if(that.properties.type == "text" || that.properties.type == "json") 866 | reader.readAsText(file); 867 | else if(that.properties.type == "blob") 868 | return reader.readAsBinaryString(file); 869 | } 870 | 871 | LiteGraph.registerNodeType("basic/file", ConstantFile); 872 | 873 | //to store json objects 874 | function ConstantData() { 875 | this.addOutput("", ""); 876 | this.addProperty("value", ""); 877 | this.widget = this.addWidget("text","json","","value"); 878 | this.widgets_up = true; 879 | this.size = [140, 30]; 880 | this._value = null; 881 | } 882 | 883 | ConstantData.title = "Const Data"; 884 | ConstantData.desc = "Constant Data"; 885 | 886 | ConstantData.prototype.onPropertyChanged = function(name, value) { 887 | this.widget.value = value; 888 | if (value == null || value == "") { 889 | return; 890 | } 891 | 892 | try { 893 | this._value = JSON.parse(value); 894 | this.boxcolor = "#AEA"; 895 | } catch (err) { 896 | this.boxcolor = "red"; 897 | } 898 | }; 899 | 900 | ConstantData.prototype.onExecute = function() { 901 | this.setOutputData(0, this._value); 902 | }; 903 | 904 | ConstantData.prototype.setValue = ConstantNumber.prototype.setValue; 905 | 906 | LiteGraph.registerNodeType("basic/data", ConstantData); 907 | 908 | //to store json objects 909 | function ConstantArray() { 910 | this._value = []; 911 | this.addInput("", ""); 912 | this.addOutput("", "array"); 913 | this.addOutput("length", "number"); 914 | this.addProperty("value", "[]"); 915 | this.widget = this.addWidget("text","array",this.properties.value,"value"); 916 | this.widgets_up = true; 917 | this.size = [140, 50]; 918 | } 919 | 920 | ConstantArray.title = "Const Array"; 921 | ConstantArray.desc = "Constant Array"; 922 | 923 | ConstantArray.prototype.onPropertyChanged = function(name, value) { 924 | this.widget.value = value; 925 | if (value == null || value == "") { 926 | return; 927 | } 928 | 929 | try { 930 | if(value[0] != "[") 931 | this._value = JSON.parse("[" + value + "]"); 932 | else 933 | this._value = JSON.parse(value); 934 | this.boxcolor = "#AEA"; 935 | } catch (err) { 936 | this.boxcolor = "red"; 937 | } 938 | }; 939 | 940 | ConstantArray.prototype.onExecute = function() { 941 | var v = this.getInputData(0); 942 | if(v && v.length) //clone 943 | { 944 | if(!this._value) 945 | this._value = new Array(); 946 | this._value.length = v.length; 947 | for(var i = 0; i < v.length; ++i) 948 | this._value[i] = v[i]; 949 | } 950 | this.setOutputData(0, this._value ); 951 | this.setOutputData(1, this._value ? ( this._value.length || 0) : 0 ); 952 | }; 953 | 954 | ConstantArray.prototype.setValue = ConstantNumber.prototype.setValue; 955 | 956 | LiteGraph.registerNodeType("basic/array", ConstantArray); 957 | 958 | function SetArray() 959 | { 960 | this.addInput("arr", "array"); 961 | this.addInput("value", ""); 962 | this.addOutput("arr", "array"); 963 | this.properties = { index: 0 }; 964 | this.widget = this.addWidget("number","i",this.properties.index,"index"); 965 | } 966 | 967 | SetArray.title = "Set Array"; 968 | SetArray.desc = "Sets index of array"; 969 | 970 | SetArray.prototype.onExecute = function() { 971 | var arr = this.getInputData(0); 972 | if(!arr) 973 | return; 974 | var v = this.getInputData(1); 975 | if(v === undefined ) 976 | return; 977 | if(this.properties.index) 978 | arr[ Math.floor(this.properties.index) ] = v; 979 | this.setOutputData(0,arr); 980 | }; 981 | 982 | LiteGraph.registerNodeType("basic/set_array", SetArray ); 983 | 984 | function ArrayElement() { 985 | this.addInput("array", "array,table,string"); 986 | this.addInput("index", "number"); 987 | this.addOutput("value", ""); 988 | this.addProperty("index",0); 989 | } 990 | 991 | ArrayElement.title = "Array[i]"; 992 | ArrayElement.desc = "Returns an element from an array"; 993 | 994 | ArrayElement.prototype.onExecute = function() { 995 | var array = this.getInputData(0); 996 | var index = this.getInputData(1); 997 | if(index == null) 998 | index = this.properties.index; 999 | if(array == null || index == null ) 1000 | return; 1001 | this.setOutputData(0, array[Math.floor(Number(index))] ); 1002 | }; 1003 | 1004 | LiteGraph.registerNodeType("basic/array[]", ArrayElement); 1005 | 1006 | function TableElement() { 1007 | this.addInput("table", "table"); 1008 | this.addInput("row", "number"); 1009 | this.addInput("col", "number"); 1010 | this.addOutput("value", ""); 1011 | this.addProperty("row",0); 1012 | this.addProperty("column",0); 1013 | } 1014 | 1015 | TableElement.title = "Table[row][col]"; 1016 | TableElement.desc = "Returns an element from a table"; 1017 | 1018 | TableElement.prototype.onExecute = function() { 1019 | var table = this.getInputData(0); 1020 | var row = this.getInputData(1); 1021 | var col = this.getInputData(2); 1022 | if(row == null) 1023 | row = this.properties.row; 1024 | if(col == null) 1025 | col = this.properties.column; 1026 | if(table == null || row == null || col == null) 1027 | return; 1028 | var row = table[Math.floor(Number(row))]; 1029 | if(row) 1030 | this.setOutputData(0, row[Math.floor(Number(col))] ); 1031 | else 1032 | this.setOutputData(0, null ); 1033 | }; 1034 | 1035 | LiteGraph.registerNodeType("basic/table[][]", TableElement); 1036 | 1037 | function ObjectProperty() { 1038 | this.addInput("obj", ""); 1039 | this.addOutput("", ""); 1040 | this.addProperty("value", ""); 1041 | this.widget = this.addWidget("text","prop.","",this.setValue.bind(this) ); 1042 | this.widgets_up = true; 1043 | this.size = [140, 30]; 1044 | this._value = null; 1045 | } 1046 | 1047 | ObjectProperty.title = "Object property"; 1048 | ObjectProperty.desc = "Outputs the property of an object"; 1049 | 1050 | ObjectProperty.prototype.setValue = function(v) { 1051 | this.properties.value = v; 1052 | this.widget.value = v; 1053 | }; 1054 | 1055 | ObjectProperty.prototype.getTitle = function() { 1056 | if (this.flags.collapsed) { 1057 | return "in." + this.properties.value; 1058 | } 1059 | return this.title; 1060 | }; 1061 | 1062 | ObjectProperty.prototype.onPropertyChanged = function(name, value) { 1063 | this.widget.value = value; 1064 | }; 1065 | 1066 | ObjectProperty.prototype.onExecute = function() { 1067 | var data = this.getInputData(0); 1068 | if (data != null) { 1069 | this.setOutputData(0, data[this.properties.value]); 1070 | } 1071 | }; 1072 | 1073 | LiteGraph.registerNodeType("basic/object_property", ObjectProperty); 1074 | 1075 | function ObjectKeys() { 1076 | this.addInput("obj", ""); 1077 | this.addOutput("keys", "array"); 1078 | this.size = [140, 30]; 1079 | } 1080 | 1081 | ObjectKeys.title = "Object keys"; 1082 | ObjectKeys.desc = "Outputs an array with the keys of an object"; 1083 | 1084 | ObjectKeys.prototype.onExecute = function() { 1085 | var data = this.getInputData(0); 1086 | if (data != null) { 1087 | this.setOutputData(0, Object.keys(data) ); 1088 | } 1089 | }; 1090 | 1091 | LiteGraph.registerNodeType("basic/object_keys", ObjectKeys); 1092 | 1093 | 1094 | function SetObject() 1095 | { 1096 | this.addInput("obj", ""); 1097 | this.addInput("value", ""); 1098 | this.addOutput("obj", ""); 1099 | this.properties = { property: "" }; 1100 | this.name_widget = this.addWidget("text","prop.",this.properties.property,"property"); 1101 | } 1102 | 1103 | SetObject.title = "Set Object"; 1104 | SetObject.desc = "Adds propertiesrty to object"; 1105 | 1106 | SetObject.prototype.onExecute = function() { 1107 | var obj = this.getInputData(0); 1108 | if(!obj) 1109 | return; 1110 | var v = this.getInputData(1); 1111 | if(v === undefined ) 1112 | return; 1113 | if(this.properties.property) 1114 | obj[ this.properties.property ] = v; 1115 | this.setOutputData(0,obj); 1116 | }; 1117 | 1118 | LiteGraph.registerNodeType("basic/set_object", SetObject ); 1119 | 1120 | 1121 | function MergeObjects() { 1122 | this.addInput("A", ""); 1123 | this.addInput("B", ""); 1124 | this.addOutput("", ""); 1125 | this._result = {}; 1126 | var that = this; 1127 | this.addWidget("button","clear","",function(){ 1128 | that._result = {}; 1129 | }); 1130 | this.size = this.computeSize(); 1131 | } 1132 | 1133 | MergeObjects.title = "Merge Objects"; 1134 | MergeObjects.desc = "Creates an object copying properties from others"; 1135 | 1136 | MergeObjects.prototype.onExecute = function() { 1137 | var A = this.getInputData(0); 1138 | var B = this.getInputData(1); 1139 | var C = this._result; 1140 | if(A) 1141 | for(var i in A) 1142 | C[i] = A[i]; 1143 | if(B) 1144 | for(var i in B) 1145 | C[i] = B[i]; 1146 | this.setOutputData(0,C); 1147 | }; 1148 | 1149 | LiteGraph.registerNodeType("basic/merge_objects", MergeObjects ); 1150 | 1151 | //Store as variable 1152 | function Variable() { 1153 | this.size = [60, 30]; 1154 | this.addInput("in"); 1155 | this.addOutput("out"); 1156 | this.properties = { varname: "myname", container: Variable.LITEGRAPH }; 1157 | this.value = null; 1158 | } 1159 | 1160 | Variable.title = "Variable"; 1161 | Variable.desc = "store/read variable value"; 1162 | 1163 | Variable.LITEGRAPH = 0; //between all graphs 1164 | Variable.GRAPH = 1; //only inside this graph 1165 | Variable.GLOBALSCOPE = 2; //attached to Window 1166 | 1167 | Variable["@container"] = { type: "enum", values: {"litegraph":Variable.LITEGRAPH, "graph":Variable.GRAPH,"global": Variable.GLOBALSCOPE} }; 1168 | 1169 | Variable.prototype.onExecute = function() { 1170 | var container = this.getContainer(); 1171 | 1172 | if(this.isInputConnected(0)) 1173 | { 1174 | this.value = this.getInputData(0); 1175 | container[ this.properties.varname ] = this.value; 1176 | this.setOutputData(0, this.value ); 1177 | return; 1178 | } 1179 | 1180 | this.setOutputData( 0, container[ this.properties.varname ] ); 1181 | }; 1182 | 1183 | Variable.prototype.getContainer = function() 1184 | { 1185 | switch(this.properties.container) 1186 | { 1187 | case Variable.GRAPH: 1188 | if(this.graph) 1189 | return this.graph.vars; 1190 | return {}; 1191 | break; 1192 | case Variable.GLOBALSCOPE: 1193 | return global; 1194 | break; 1195 | case Variable.LITEGRAPH: 1196 | default: 1197 | return LiteGraph.Globals; 1198 | break; 1199 | } 1200 | } 1201 | 1202 | Variable.prototype.getTitle = function() { 1203 | return this.properties.varname; 1204 | }; 1205 | 1206 | LiteGraph.registerNodeType("basic/variable", Variable); 1207 | 1208 | function length(v) { 1209 | if(v && v.length != null) 1210 | return Number(v.length); 1211 | return 0; 1212 | } 1213 | 1214 | LiteGraph.wrapFunctionAsNode( 1215 | "basic/length", 1216 | length, 1217 | [""], 1218 | "number" 1219 | ); 1220 | 1221 | function DownloadData() { 1222 | this.size = [60, 30]; 1223 | this.addInput("data", 0 ); 1224 | this.addInput("download", LiteGraph.ACTION ); 1225 | this.properties = { filename: "data.json" }; 1226 | this.value = null; 1227 | var that = this; 1228 | this.addWidget("button","Download","", function(v){ 1229 | if(!that.value) 1230 | return; 1231 | that.downloadAsFile(); 1232 | }); 1233 | } 1234 | 1235 | DownloadData.title = "Download"; 1236 | DownloadData.desc = "Download some data"; 1237 | 1238 | DownloadData.prototype.downloadAsFile = function() 1239 | { 1240 | if(this.value == null) 1241 | return; 1242 | 1243 | var str = null; 1244 | if(this.value.constructor === String) 1245 | str = this.value; 1246 | else 1247 | str = JSON.stringify(this.value); 1248 | 1249 | var file = new Blob([str]); 1250 | var url = URL.createObjectURL( file ); 1251 | var element = document.createElement("a"); 1252 | element.setAttribute('href', url); 1253 | element.setAttribute('download', this.properties.filename ); 1254 | element.style.display = 'none'; 1255 | document.body.appendChild(element); 1256 | element.click(); 1257 | document.body.removeChild(element); 1258 | setTimeout( function(){ URL.revokeObjectURL( url ); }, 1000*60 ); //wait one minute to revoke url 1259 | } 1260 | 1261 | DownloadData.prototype.onAction = function(action, param) { 1262 | var that = this; 1263 | setTimeout( function(){ that.downloadAsFile(); }, 100); //deferred to avoid blocking the renderer with the popup 1264 | } 1265 | 1266 | DownloadData.prototype.onExecute = function() { 1267 | if (this.inputs[0]) { 1268 | this.value = this.getInputData(0); 1269 | } 1270 | }; 1271 | 1272 | DownloadData.prototype.getTitle = function() { 1273 | if (this.flags.collapsed) { 1274 | return this.properties.filename; 1275 | } 1276 | return this.title; 1277 | }; 1278 | 1279 | LiteGraph.registerNodeType("basic/download", DownloadData); 1280 | 1281 | 1282 | 1283 | //Watch a value in the editor 1284 | function Watch() { 1285 | this.size = [60, 30]; 1286 | this.addInput("value", 0, { label: "" }); 1287 | this.value = 0; 1288 | } 1289 | 1290 | Watch.title = "Watch"; 1291 | Watch.desc = "Show value of input"; 1292 | 1293 | Watch.prototype.onExecute = function() { 1294 | if (this.inputs[0]) { 1295 | this.value = this.getInputData(0); 1296 | } 1297 | }; 1298 | 1299 | Watch.prototype.getTitle = function() { 1300 | if (this.flags.collapsed) { 1301 | return this.inputs[0].label; 1302 | } 1303 | return this.title; 1304 | }; 1305 | 1306 | Watch.toString = function(o) { 1307 | if (o == null) { 1308 | return "null"; 1309 | } else if (o.constructor === Number) { 1310 | return o.toFixed(3); 1311 | } else if (o.constructor === Array) { 1312 | var str = "["; 1313 | for (var i = 0; i < o.length; ++i) { 1314 | str += Watch.toString(o[i]) + (i + 1 != o.length ? "," : ""); 1315 | } 1316 | str += "]"; 1317 | return str; 1318 | } else { 1319 | return String(o); 1320 | } 1321 | }; 1322 | 1323 | Watch.prototype.onDrawBackground = function(ctx) { 1324 | //show the current value 1325 | this.inputs[0].label = Watch.toString(this.value); 1326 | }; 1327 | 1328 | LiteGraph.registerNodeType("basic/watch", Watch); 1329 | 1330 | //in case one type doesnt match other type but you want to connect them anyway 1331 | function Cast() { 1332 | this.addInput("in", 0); 1333 | this.addOutput("out", 0); 1334 | this.size = [40, 30]; 1335 | } 1336 | 1337 | Cast.title = "Cast"; 1338 | Cast.desc = "Allows to connect different types"; 1339 | 1340 | Cast.prototype.onExecute = function() { 1341 | this.setOutputData(0, this.getInputData(0)); 1342 | }; 1343 | 1344 | LiteGraph.registerNodeType("basic/cast", Cast); 1345 | 1346 | //Show value inside the debug console 1347 | function Console() { 1348 | this.mode = LiteGraph.ON_EVENT; 1349 | this.size = [80, 30]; 1350 | this.addProperty("msg", ""); 1351 | this.addInput("log", LiteGraph.EVENT); 1352 | this.addInput("msg", 0); 1353 | } 1354 | 1355 | Console.title = "Console"; 1356 | Console.desc = "Show value inside the console"; 1357 | 1358 | Console.prototype.onAction = function(action, param) { 1359 | if (action == "log") { 1360 | console.log(param); 1361 | } else if (action == "warn") { 1362 | console.warn(param); 1363 | } else if (action == "error") { 1364 | console.error(param); 1365 | } 1366 | }; 1367 | 1368 | Console.prototype.onExecute = function() { 1369 | var msg = this.getInputData(1); 1370 | if (msg !== null) { 1371 | this.properties.msg = msg; 1372 | } 1373 | console.log(msg); 1374 | }; 1375 | 1376 | Console.prototype.onGetInputs = function() { 1377 | return [ 1378 | ["log", LiteGraph.ACTION], 1379 | ["warn", LiteGraph.ACTION], 1380 | ["error", LiteGraph.ACTION] 1381 | ]; 1382 | }; 1383 | 1384 | LiteGraph.registerNodeType("basic/console", Console); 1385 | 1386 | //Show value inside the debug console 1387 | function Alert() { 1388 | this.mode = LiteGraph.ON_EVENT; 1389 | this.addProperty("msg", ""); 1390 | this.addInput("", LiteGraph.EVENT); 1391 | var that = this; 1392 | this.widget = this.addWidget("text", "Text", "", "msg"); 1393 | this.widgets_up = true; 1394 | this.size = [200, 30]; 1395 | } 1396 | 1397 | Alert.title = "Alert"; 1398 | Alert.desc = "Show an alert window"; 1399 | Alert.color = "#510"; 1400 | 1401 | Alert.prototype.onConfigure = function(o) { 1402 | this.widget.value = o.properties.msg; 1403 | }; 1404 | 1405 | Alert.prototype.onAction = function(action, param) { 1406 | var msg = this.properties.msg; 1407 | setTimeout(function() { 1408 | alert(msg); 1409 | }, 10); 1410 | }; 1411 | 1412 | LiteGraph.registerNodeType("basic/alert", Alert); 1413 | 1414 | //Execites simple code 1415 | function NodeScript() { 1416 | this.size = [60, 30]; 1417 | this.addProperty("onExecute", "return A;"); 1418 | this.addInput("A", ""); 1419 | this.addInput("B", ""); 1420 | this.addOutput("out", ""); 1421 | 1422 | this._func = null; 1423 | this.data = {}; 1424 | } 1425 | 1426 | NodeScript.prototype.onConfigure = function(o) { 1427 | if (o.properties.onExecute && LiteGraph.allow_scripts) 1428 | this.compileCode(o.properties.onExecute); 1429 | else 1430 | console.warn("Script not compiled, LiteGraph.allow_scripts is false"); 1431 | }; 1432 | 1433 | NodeScript.title = "Script"; 1434 | NodeScript.desc = "executes a code (max 100 characters)"; 1435 | 1436 | NodeScript.widgets_info = { 1437 | onExecute: { type: "code" } 1438 | }; 1439 | 1440 | NodeScript.prototype.onPropertyChanged = function(name, value) { 1441 | if (name == "onExecute" && LiteGraph.allow_scripts) 1442 | this.compileCode(value); 1443 | else 1444 | console.warn("Script not compiled, LiteGraph.allow_scripts is false"); 1445 | }; 1446 | 1447 | NodeScript.prototype.compileCode = function(code) { 1448 | this._func = null; 1449 | if (code.length > 256) { 1450 | console.warn("Script too long, max 256 chars"); 1451 | } else { 1452 | var code_low = code.toLowerCase(); 1453 | var forbidden_words = [ 1454 | "script", 1455 | "body", 1456 | "document", 1457 | "eval", 1458 | "nodescript", 1459 | "function" 1460 | ]; //bad security solution 1461 | for (var i = 0; i < forbidden_words.length; ++i) { 1462 | if (code_low.indexOf(forbidden_words[i]) != -1) { 1463 | console.warn("invalid script"); 1464 | return; 1465 | } 1466 | } 1467 | try { 1468 | this._func = new Function("A", "B", "C", "DATA", "node", code); 1469 | } catch (err) { 1470 | console.error("Error parsing script"); 1471 | console.error(err); 1472 | } 1473 | } 1474 | }; 1475 | 1476 | NodeScript.prototype.onExecute = function() { 1477 | if (!this._func) { 1478 | return; 1479 | } 1480 | 1481 | try { 1482 | var A = this.getInputData(0); 1483 | var B = this.getInputData(1); 1484 | var C = this.getInputData(2); 1485 | this.setOutputData(0, this._func(A, B, C, this.data, this)); 1486 | } catch (err) { 1487 | console.error("Error in script"); 1488 | console.error(err); 1489 | } 1490 | }; 1491 | 1492 | NodeScript.prototype.onGetOutputs = function() { 1493 | return [["C", ""]]; 1494 | }; 1495 | 1496 | LiteGraph.registerNodeType("basic/script", NodeScript); 1497 | })(this); 1498 | -------------------------------------------------------------------------------- /static/js/bootbox.js: -------------------------------------------------------------------------------- 1 | /*! @preserve 2 | * bootbox.js 3 | * version: 5.5.2 4 | * author: Nick Payne 5 | * license: MIT 6 | * http://bootboxjs.com/ 7 | */ 8 | (function (root, factory) { 9 | 'use strict'; 10 | if (typeof define === 'function' && define.amd) { 11 | // AMD 12 | define(['jquery'], factory); 13 | } else if (typeof exports === 'object') { 14 | // Node, CommonJS-like 15 | module.exports = factory(require('jquery')); 16 | } else { 17 | // Browser globals (root is window) 18 | root.bootbox = factory(root.jQuery); 19 | } 20 | }(this, function init($, undefined) { 21 | 'use strict'; 22 | 23 | // Polyfills Object.keys, if necessary. 24 | // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys 25 | if (!Object.keys) { 26 | Object.keys = (function () { 27 | var hasOwnProperty = Object.prototype.hasOwnProperty, 28 | hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), 29 | dontEnums = [ 30 | 'toString', 31 | 'toLocaleString', 32 | 'valueOf', 33 | 'hasOwnProperty', 34 | 'isPrototypeOf', 35 | 'propertyIsEnumerable', 36 | 'constructor' 37 | ], 38 | dontEnumsLength = dontEnums.length; 39 | 40 | return function (obj) { 41 | if (typeof obj !== 'function' && (typeof obj !== 'object' || obj === null)) { 42 | throw new TypeError('Object.keys called on non-object'); 43 | } 44 | 45 | var result = [], prop, i; 46 | 47 | for (prop in obj) { 48 | if (hasOwnProperty.call(obj, prop)) { 49 | result.push(prop); 50 | } 51 | } 52 | 53 | if (hasDontEnumBug) { 54 | for (i = 0; i < dontEnumsLength; i++) { 55 | if (hasOwnProperty.call(obj, dontEnums[i])) { 56 | result.push(dontEnums[i]); 57 | } 58 | } 59 | } 60 | 61 | return result; 62 | }; 63 | }()); 64 | } 65 | 66 | var exports = {}; 67 | 68 | var VERSION = '5.5.2'; 69 | exports.VERSION = VERSION; 70 | 71 | var locales = { 72 | en : { 73 | OK : 'OK', 74 | CANCEL : 'Cancel', 75 | CONFIRM : 'OK' 76 | } 77 | }; 78 | 79 | var templates = { 80 | dialog: 81 | '', 88 | header: 89 | '', 92 | footer: 93 | '', 94 | closeButton: 95 | '', 96 | form: 97 | '
', 98 | button: 99 | '', 100 | option: 101 | '', 102 | promptMessage: 103 | '
', 104 | inputs: { 105 | text: 106 | '', 107 | textarea: 108 | '', 109 | email: 110 | '', 111 | select: 112 | '', 113 | checkbox: 114 | '
', 115 | radio: 116 | '
', 117 | date: 118 | '', 119 | time: 120 | '', 121 | number: 122 | '', 123 | password: 124 | '', 125 | range: 126 | '' 127 | } 128 | }; 129 | 130 | 131 | var defaults = { 132 | // default language 133 | locale: 'en', 134 | // show backdrop or not. Default to static so user has to interact with dialog 135 | backdrop: 'static', 136 | // animate the modal in/out 137 | animate: true, 138 | // additional class string applied to the top level dialog 139 | className: null, 140 | // whether or not to include a close button 141 | closeButton: true, 142 | // show the dialog immediately by default 143 | show: true, 144 | // dialog container 145 | container: 'body', 146 | // default value (used by the prompt helper) 147 | value: '', 148 | // default input type (used by the prompt helper) 149 | inputType: 'text', 150 | // switch button order from cancel/confirm (default) to confirm/cancel 151 | swapButtonOrder: false, 152 | // center modal vertically in page 153 | centerVertical: false, 154 | // Append "multiple" property to the select when using the "prompt" helper 155 | multiple: false, 156 | // Automatically scroll modal content when height exceeds viewport height 157 | scrollable: false, 158 | // whether or not to destroy the modal on hide 159 | reusable: false 160 | }; 161 | 162 | 163 | // PUBLIC FUNCTIONS 164 | // ************************************************************************************************************* 165 | 166 | // Return all currently registered locales, or a specific locale if "name" is defined 167 | exports.locales = function (name) { 168 | return name ? locales[name] : locales; 169 | }; 170 | 171 | 172 | // Register localized strings for the OK, CONFIRM, and CANCEL buttons 173 | exports.addLocale = function (name, values) { 174 | $.each(['OK', 'CANCEL', 'CONFIRM'], function (_, v) { 175 | if (!values[v]) { 176 | throw new Error('Please supply a translation for "' + v + '"'); 177 | } 178 | }); 179 | 180 | locales[name] = { 181 | OK: values.OK, 182 | CANCEL: values.CANCEL, 183 | CONFIRM: values.CONFIRM 184 | }; 185 | 186 | return exports; 187 | }; 188 | 189 | 190 | // Remove a previously-registered locale 191 | exports.removeLocale = function (name) { 192 | if (name !== 'en') { 193 | delete locales[name]; 194 | } 195 | else { 196 | throw new Error('"en" is used as the default and fallback locale and cannot be removed.'); 197 | } 198 | 199 | return exports; 200 | }; 201 | 202 | 203 | // Set the default locale 204 | exports.setLocale = function (name) { 205 | return exports.setDefaults('locale', name); 206 | }; 207 | 208 | 209 | // Override default value(s) of Bootbox. 210 | exports.setDefaults = function () { 211 | var values = {}; 212 | 213 | if (arguments.length === 2) { 214 | // allow passing of single key/value... 215 | values[arguments[0]] = arguments[1]; 216 | } else { 217 | // ... and as an object too 218 | values = arguments[0]; 219 | } 220 | 221 | $.extend(defaults, values); 222 | 223 | return exports; 224 | }; 225 | 226 | 227 | // Hides all currently active Bootbox modals 228 | exports.hideAll = function () { 229 | $('.bootbox').modal('hide'); 230 | 231 | return exports; 232 | }; 233 | 234 | 235 | // Allows the base init() function to be overridden 236 | exports.init = function (_$) { 237 | return init(_$ || $); 238 | }; 239 | 240 | 241 | // CORE HELPER FUNCTIONS 242 | // ************************************************************************************************************* 243 | 244 | // Core dialog function 245 | exports.dialog = function (options) { 246 | if ($.fn.modal === undefined) { 247 | throw new Error( 248 | '"$.fn.modal" is not defined; please double check you have included ' + 249 | 'the Bootstrap JavaScript library. See https://getbootstrap.com/docs/4.4/getting-started/javascript/ ' + 250 | 'for more details.' 251 | ); 252 | } 253 | 254 | options = sanitize(options); 255 | 256 | if ($.fn.modal.Constructor.VERSION) { 257 | options.fullBootstrapVersion = $.fn.modal.Constructor.VERSION; 258 | var i = options.fullBootstrapVersion.indexOf('.'); 259 | options.bootstrap = options.fullBootstrapVersion.substring(0, i); 260 | } 261 | else { 262 | // Assuming version 2.3.2, as that was the last "supported" 2.x version 263 | options.bootstrap = '2'; 264 | options.fullBootstrapVersion = '2.3.2'; 265 | console.warn('Bootbox will *mostly* work with Bootstrap 2, but we do not officially support it. Please upgrade, if possible.'); 266 | } 267 | 268 | var dialog = $(templates.dialog); 269 | var innerDialog = dialog.find('.modal-dialog'); 270 | var body = dialog.find('.modal-body'); 271 | var header = $(templates.header); 272 | var footer = $(templates.footer); 273 | var buttons = options.buttons; 274 | 275 | var callbacks = { 276 | onEscape: options.onEscape 277 | }; 278 | 279 | body.find('.bootbox-body').html(options.message); 280 | 281 | // Only attempt to create buttons if at least one has 282 | // been defined in the options object 283 | if (getKeyLength(options.buttons) > 0) { 284 | each(buttons, function (key, b) { 285 | var button = $(templates.button); 286 | button.data('bb-handler', key); 287 | button.addClass(b.className); 288 | 289 | switch (key) { 290 | case 'ok': 291 | case 'confirm': 292 | button.addClass('bootbox-accept'); 293 | break; 294 | 295 | case 'cancel': 296 | button.addClass('bootbox-cancel'); 297 | break; 298 | } 299 | 300 | button.html(b.label); 301 | footer.append(button); 302 | 303 | callbacks[key] = b.callback; 304 | }); 305 | 306 | body.after(footer); 307 | } 308 | 309 | if (options.animate === true) { 310 | dialog.addClass('fade'); 311 | } 312 | 313 | if (options.className) { 314 | dialog.addClass(options.className); 315 | } 316 | 317 | if (options.size) { 318 | // Requires Bootstrap 3.1.0 or higher 319 | if (options.fullBootstrapVersion.substring(0, 3) < '3.1') { 320 | console.warn('"size" requires Bootstrap 3.1.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); 321 | } 322 | 323 | switch (options.size) { 324 | case 'small': 325 | case 'sm': 326 | innerDialog.addClass('modal-sm'); 327 | break; 328 | 329 | case 'large': 330 | case 'lg': 331 | innerDialog.addClass('modal-lg'); 332 | break; 333 | 334 | case 'extra-large': 335 | case 'xl': 336 | innerDialog.addClass('modal-xl'); 337 | 338 | // Requires Bootstrap 4.2.0 or higher 339 | if (options.fullBootstrapVersion.substring(0, 3) < '4.2') { 340 | console.warn('Using size "xl"/"extra-large" requires Bootstrap 4.2.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); 341 | } 342 | break; 343 | } 344 | } 345 | 346 | if (options.scrollable) { 347 | innerDialog.addClass('modal-dialog-scrollable'); 348 | 349 | // Requires Bootstrap 4.3.0 or higher 350 | if (options.fullBootstrapVersion.substring(0, 3) < '4.3') { 351 | console.warn('Using "scrollable" requires Bootstrap 4.3.0 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); 352 | } 353 | } 354 | 355 | if (options.title) { 356 | body.before(header); 357 | dialog.find('.modal-title').html(options.title); 358 | } 359 | 360 | if (options.closeButton) { 361 | var closeButton = $(templates.closeButton); 362 | 363 | if (options.title) { 364 | if (options.bootstrap > 3) { 365 | dialog.find('.modal-header').append(closeButton); 366 | } 367 | else { 368 | dialog.find('.modal-header').prepend(closeButton); 369 | } 370 | } else { 371 | closeButton.prependTo(body); 372 | } 373 | } 374 | 375 | if (options.centerVertical) { 376 | innerDialog.addClass('modal-dialog-centered'); 377 | 378 | // Requires Bootstrap 4.0.0-beta.3 or higher 379 | if (options.fullBootstrapVersion < '4.0.0') { 380 | console.warn('"centerVertical" requires Bootstrap 4.0.0-beta.3 or higher. You appear to be using ' + options.fullBootstrapVersion + '. Please upgrade to use this option.'); 381 | } 382 | } 383 | 384 | // Bootstrap event listeners; these handle extra 385 | // setup & teardown required after the underlying 386 | // modal has performed certain actions. 387 | 388 | if(!options.reusable) { 389 | // make sure we unbind any listeners once the dialog has definitively been dismissed 390 | dialog.one('hide.bs.modal', { dialog: dialog }, unbindModal); 391 | } 392 | 393 | if (options.onHide) { 394 | if ($.isFunction(options.onHide)) { 395 | dialog.on('hide.bs.modal', options.onHide); 396 | } 397 | else { 398 | throw new Error('Argument supplied to "onHide" must be a function'); 399 | } 400 | } 401 | 402 | if(!options.reusable) { 403 | dialog.one('hidden.bs.modal', { dialog: dialog }, destroyModal); 404 | } 405 | 406 | if (options.onHidden) { 407 | if ($.isFunction(options.onHidden)) { 408 | dialog.on('hidden.bs.modal', options.onHidden); 409 | } 410 | else { 411 | throw new Error('Argument supplied to "onHidden" must be a function'); 412 | } 413 | } 414 | 415 | if (options.onShow) { 416 | if ($.isFunction(options.onShow)) { 417 | dialog.on('show.bs.modal', options.onShow); 418 | } 419 | else { 420 | throw new Error('Argument supplied to "onShow" must be a function'); 421 | } 422 | } 423 | 424 | dialog.one('shown.bs.modal', { dialog: dialog }, focusPrimaryButton); 425 | 426 | if (options.onShown) { 427 | if ($.isFunction(options.onShown)) { 428 | dialog.on('shown.bs.modal', options.onShown); 429 | } 430 | else { 431 | throw new Error('Argument supplied to "onShown" must be a function'); 432 | } 433 | } 434 | 435 | // Bootbox event listeners; used to decouple some 436 | // behaviours from their respective triggers 437 | 438 | if (options.backdrop === true) { 439 | // A boolean true/false according to the Bootstrap docs 440 | // should show a dialog the user can dismiss by clicking on 441 | // the background. 442 | // We always only ever pass static/false to the actual 443 | // $.modal function because with "true" we can't trap 444 | // this event (the .modal-backdrop swallows it) 445 | // However, we still want to sort-of respect true 446 | // and invoke the escape mechanism instead 447 | dialog.on('click.dismiss.bs.modal', function (e) { 448 | // @NOTE: the target varies in >= 3.3.x releases since the modal backdrop 449 | // moved *inside* the outer dialog rather than *alongside* it 450 | if (dialog.children('.modal-backdrop').length) { 451 | e.currentTarget = dialog.children('.modal-backdrop').get(0); 452 | } 453 | 454 | if (e.target !== e.currentTarget) { 455 | return; 456 | } 457 | 458 | dialog.trigger('escape.close.bb'); 459 | }); 460 | } 461 | 462 | dialog.on('escape.close.bb', function (e) { 463 | // the if statement looks redundant but it isn't; without it 464 | // if we *didn't* have an onEscape handler then processCallback 465 | // would automatically dismiss the dialog 466 | if (callbacks.onEscape) { 467 | processCallback(e, dialog, callbacks.onEscape); 468 | } 469 | }); 470 | 471 | 472 | dialog.on('click', '.modal-footer button:not(.disabled)', function (e) { 473 | var callbackKey = $(this).data('bb-handler'); 474 | 475 | if (callbackKey !== undefined) { 476 | // Only process callbacks for buttons we recognize: 477 | processCallback(e, dialog, callbacks[callbackKey]); 478 | } 479 | }); 480 | 481 | dialog.on('click', '.bootbox-close-button', function (e) { 482 | // onEscape might be falsy but that's fine; the fact is 483 | // if the user has managed to click the close button we 484 | // have to close the dialog, callback or not 485 | processCallback(e, dialog, callbacks.onEscape); 486 | }); 487 | 488 | dialog.on('keyup', function (e) { 489 | if (e.which === 27) { 490 | dialog.trigger('escape.close.bb'); 491 | } 492 | }); 493 | 494 | // the remainder of this method simply deals with adding our 495 | // dialog element to the DOM, augmenting it with Bootstrap's modal 496 | // functionality and then giving the resulting object back 497 | // to our caller 498 | 499 | $(options.container).append(dialog); 500 | 501 | dialog.modal({ 502 | backdrop: options.backdrop, 503 | keyboard: false, 504 | show: false 505 | }); 506 | 507 | if (options.show) { 508 | dialog.modal('show'); 509 | } 510 | 511 | return dialog; 512 | }; 513 | 514 | 515 | // Helper function to simulate the native alert() behavior. **NOTE**: This is non-blocking, so any 516 | // code that must happen after the alert is dismissed should be placed within the callback function 517 | // for this alert. 518 | exports.alert = function () { 519 | var options; 520 | 521 | options = mergeDialogOptions('alert', ['ok'], ['message', 'callback'], arguments); 522 | 523 | // @TODO: can this move inside exports.dialog when we're iterating over each 524 | // button and checking its button.callback value instead? 525 | if (options.callback && !$.isFunction(options.callback)) { 526 | throw new Error('alert requires the "callback" property to be a function when provided'); 527 | } 528 | 529 | // override the ok and escape callback to make sure they just invoke 530 | // the single user-supplied one (if provided) 531 | options.buttons.ok.callback = options.onEscape = function () { 532 | if ($.isFunction(options.callback)) { 533 | return options.callback.call(this); 534 | } 535 | 536 | return true; 537 | }; 538 | 539 | return exports.dialog(options); 540 | }; 541 | 542 | 543 | // Helper function to simulate the native confirm() behavior. **NOTE**: This is non-blocking, so any 544 | // code that must happen after the confirm is dismissed should be placed within the callback function 545 | // for this confirm. 546 | exports.confirm = function () { 547 | var options; 548 | 549 | options = mergeDialogOptions('confirm', ['cancel', 'confirm'], ['message', 'callback'], arguments); 550 | 551 | // confirm specific validation; they don't make sense without a callback so make 552 | // sure it's present 553 | if (!$.isFunction(options.callback)) { 554 | throw new Error('confirm requires a callback'); 555 | } 556 | 557 | // overrides; undo anything the user tried to set they shouldn't have 558 | options.buttons.cancel.callback = options.onEscape = function () { 559 | return options.callback.call(this, false); 560 | }; 561 | 562 | options.buttons.confirm.callback = function () { 563 | return options.callback.call(this, true); 564 | }; 565 | 566 | return exports.dialog(options); 567 | }; 568 | 569 | 570 | // Helper function to simulate the native prompt() behavior. **NOTE**: This is non-blocking, so any 571 | // code that must happen after the prompt is dismissed should be placed within the callback function 572 | // for this prompt. 573 | exports.prompt = function () { 574 | var options; 575 | var promptDialog; 576 | var form; 577 | var input; 578 | var shouldShow; 579 | var inputOptions; 580 | 581 | // we have to create our form first otherwise 582 | // its value is undefined when gearing up our options 583 | // @TODO this could be solved by allowing message to 584 | // be a function instead... 585 | form = $(templates.form); 586 | 587 | // prompt defaults are more complex than others in that 588 | // users can override more defaults 589 | options = mergeDialogOptions('prompt', ['cancel', 'confirm'], ['title', 'callback'], arguments); 590 | 591 | if (!options.value) { 592 | options.value = defaults.value; 593 | } 594 | 595 | if (!options.inputType) { 596 | options.inputType = defaults.inputType; 597 | } 598 | 599 | // capture the user's show value; we always set this to false before 600 | // spawning the dialog to give us a chance to attach some handlers to 601 | // it, but we need to make sure we respect a preference not to show it 602 | shouldShow = (options.show === undefined) ? defaults.show : options.show; 603 | 604 | // This is required prior to calling the dialog builder below - we need to 605 | // add an event handler just before the prompt is shown 606 | options.show = false; 607 | 608 | // Handles the 'cancel' action 609 | options.buttons.cancel.callback = options.onEscape = function () { 610 | return options.callback.call(this, null); 611 | }; 612 | 613 | // Prompt submitted - extract the prompt value. This requires a bit of work, 614 | // given the different input types available. 615 | options.buttons.confirm.callback = function () { 616 | var value; 617 | 618 | if (options.inputType === 'checkbox') { 619 | value = input.find('input:checked').map(function () { 620 | return $(this).val(); 621 | }).get(); 622 | } else if (options.inputType === 'radio') { 623 | value = input.find('input:checked').val(); 624 | } 625 | else { 626 | if (input[0].checkValidity && !input[0].checkValidity()) { 627 | // prevents button callback from being called 628 | return false; 629 | } else { 630 | if (options.inputType === 'select' && options.multiple === true) { 631 | value = input.find('option:selected').map(function () { 632 | return $(this).val(); 633 | }).get(); 634 | } 635 | else { 636 | value = input.val(); 637 | } 638 | } 639 | } 640 | 641 | return options.callback.call(this, value); 642 | }; 643 | 644 | // prompt-specific validation 645 | if (!options.title) { 646 | throw new Error('prompt requires a title'); 647 | } 648 | 649 | if (!$.isFunction(options.callback)) { 650 | throw new Error('prompt requires a callback'); 651 | } 652 | 653 | if (!templates.inputs[options.inputType]) { 654 | throw new Error('Invalid prompt type'); 655 | } 656 | 657 | // create the input based on the supplied type 658 | input = $(templates.inputs[options.inputType]); 659 | 660 | switch (options.inputType) { 661 | case 'text': 662 | case 'textarea': 663 | case 'email': 664 | case 'password': 665 | input.val(options.value); 666 | 667 | if (options.placeholder) { 668 | input.attr('placeholder', options.placeholder); 669 | } 670 | 671 | if (options.pattern) { 672 | input.attr('pattern', options.pattern); 673 | } 674 | 675 | if (options.maxlength) { 676 | input.attr('maxlength', options.maxlength); 677 | } 678 | 679 | if (options.required) { 680 | input.prop({ 'required': true }); 681 | } 682 | 683 | if (options.rows && !isNaN(parseInt(options.rows))) { 684 | if (options.inputType === 'textarea') { 685 | input.attr({ 'rows': options.rows }); 686 | } 687 | } 688 | 689 | break; 690 | 691 | 692 | case 'date': 693 | case 'time': 694 | case 'number': 695 | case 'range': 696 | input.val(options.value); 697 | 698 | if (options.placeholder) { 699 | input.attr('placeholder', options.placeholder); 700 | } 701 | 702 | if (options.pattern) { 703 | input.attr('pattern', options.pattern); 704 | } 705 | 706 | if (options.required) { 707 | input.prop({ 'required': true }); 708 | } 709 | 710 | // These input types have extra attributes which affect their input validation. 711 | // Warning: For most browsers, date inputs are buggy in their implementation of 'step', so 712 | // this attribute will have no effect. Therefore, we don't set the attribute for date inputs. 713 | // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Setting_maximum_and_minimum_dates 714 | if (options.inputType !== 'date') { 715 | if (options.step) { 716 | if (options.step === 'any' || (!isNaN(options.step) && parseFloat(options.step) > 0)) { 717 | input.attr('step', options.step); 718 | } 719 | else { 720 | throw new Error('"step" must be a valid positive number or the value "any". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-step for more information.'); 721 | } 722 | } 723 | } 724 | 725 | if (minAndMaxAreValid(options.inputType, options.min, options.max)) { 726 | if (options.min !== undefined) { 727 | input.attr('min', options.min); 728 | } 729 | if (options.max !== undefined) { 730 | input.attr('max', options.max); 731 | } 732 | } 733 | 734 | break; 735 | 736 | 737 | case 'select': 738 | var groups = {}; 739 | inputOptions = options.inputOptions || []; 740 | 741 | if (!$.isArray(inputOptions)) { 742 | throw new Error('Please pass an array of input options'); 743 | } 744 | 745 | if (!inputOptions.length) { 746 | throw new Error('prompt with "inputType" set to "select" requires at least one option'); 747 | } 748 | 749 | // placeholder is not actually a valid attribute for select, 750 | // but we'll allow it, assuming it might be used for a plugin 751 | if (options.placeholder) { 752 | input.attr('placeholder', options.placeholder); 753 | } 754 | 755 | if (options.required) { 756 | input.prop({ 'required': true }); 757 | } 758 | 759 | if (options.multiple) { 760 | input.prop({ 'multiple': true }); 761 | } 762 | 763 | each(inputOptions, function (_, option) { 764 | // assume the element to attach to is the input... 765 | var elem = input; 766 | 767 | if (option.value === undefined || option.text === undefined) { 768 | throw new Error('each option needs a "value" property and a "text" property'); 769 | } 770 | 771 | // ... but override that element if this option sits in a group 772 | 773 | if (option.group) { 774 | // initialise group if necessary 775 | if (!groups[option.group]) { 776 | groups[option.group] = $('').attr('label', option.group); 777 | } 778 | 779 | elem = groups[option.group]; 780 | } 781 | 782 | var o = $(templates.option); 783 | o.attr('value', option.value).text(option.text); 784 | elem.append(o); 785 | }); 786 | 787 | each(groups, function (_, group) { 788 | input.append(group); 789 | }); 790 | 791 | // safe to set a select's value as per a normal input 792 | input.val(options.value); 793 | 794 | break; 795 | 796 | 797 | case 'checkbox': 798 | var checkboxValues = $.isArray(options.value) ? options.value : [options.value]; 799 | inputOptions = options.inputOptions || []; 800 | 801 | if (!inputOptions.length) { 802 | throw new Error('prompt with "inputType" set to "checkbox" requires at least one option'); 803 | } 804 | 805 | // checkboxes have to nest within a containing element, so 806 | // they break the rules a bit and we end up re-assigning 807 | // our 'input' element to this container instead 808 | input = $('
'); 809 | 810 | each(inputOptions, function (_, option) { 811 | if (option.value === undefined || option.text === undefined) { 812 | throw new Error('each option needs a "value" property and a "text" property'); 813 | } 814 | 815 | var checkbox = $(templates.inputs[options.inputType]); 816 | 817 | checkbox.find('input').attr('value', option.value); 818 | checkbox.find('label').append('\n' + option.text); 819 | 820 | // we've ensured values is an array so we can always iterate over it 821 | each(checkboxValues, function (_, value) { 822 | if (value === option.value) { 823 | checkbox.find('input').prop('checked', true); 824 | } 825 | }); 826 | 827 | input.append(checkbox); 828 | }); 829 | break; 830 | 831 | 832 | case 'radio': 833 | // Make sure that value is not an array (only a single radio can ever be checked) 834 | if (options.value !== undefined && $.isArray(options.value)) { 835 | throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"'); 836 | } 837 | 838 | inputOptions = options.inputOptions || []; 839 | 840 | if (!inputOptions.length) { 841 | throw new Error('prompt with "inputType" set to "radio" requires at least one option'); 842 | } 843 | 844 | // Radiobuttons have to nest within a containing element, so 845 | // they break the rules a bit and we end up re-assigning 846 | // our 'input' element to this container instead 847 | input = $('
'); 848 | 849 | // Radiobuttons should always have an initial checked input checked in a "group". 850 | // If value is undefined or doesn't match an input option, select the first radiobutton 851 | var checkFirstRadio = true; 852 | 853 | each(inputOptions, function (_, option) { 854 | if (option.value === undefined || option.text === undefined) { 855 | throw new Error('each option needs a "value" property and a "text" property'); 856 | } 857 | 858 | var radio = $(templates.inputs[options.inputType]); 859 | 860 | radio.find('input').attr('value', option.value); 861 | radio.find('label').append('\n' + option.text); 862 | 863 | if (options.value !== undefined) { 864 | if (option.value === options.value) { 865 | radio.find('input').prop('checked', true); 866 | checkFirstRadio = false; 867 | } 868 | } 869 | 870 | input.append(radio); 871 | }); 872 | 873 | if (checkFirstRadio) { 874 | input.find('input[type="radio"]').first().prop('checked', true); 875 | } 876 | break; 877 | } 878 | 879 | // now place it in our form 880 | form.append(input); 881 | 882 | form.on('submit', function (e) { 883 | e.preventDefault(); 884 | // Fix for SammyJS (or similar JS routing library) hijacking the form post. 885 | e.stopPropagation(); 886 | 887 | // @TODO can we actually click *the* button object instead? 888 | // e.g. buttons.confirm.click() or similar 889 | promptDialog.find('.bootbox-accept').trigger('click'); 890 | }); 891 | 892 | if ($.trim(options.message) !== '') { 893 | // Add the form to whatever content the user may have added. 894 | var message = $(templates.promptMessage).html(options.message); 895 | form.prepend(message); 896 | options.message = form; 897 | } 898 | else { 899 | options.message = form; 900 | } 901 | 902 | // Generate the dialog 903 | promptDialog = exports.dialog(options); 904 | 905 | // clear the existing handler focusing the submit button... 906 | promptDialog.off('shown.bs.modal', focusPrimaryButton); 907 | 908 | // ...and replace it with one focusing our input, if possible 909 | promptDialog.on('shown.bs.modal', function () { 910 | // need the closure here since input isn't 911 | // an object otherwise 912 | input.focus(); 913 | }); 914 | 915 | if (shouldShow === true) { 916 | promptDialog.modal('show'); 917 | } 918 | 919 | return promptDialog; 920 | }; 921 | 922 | 923 | // INTERNAL FUNCTIONS 924 | // ************************************************************************************************************* 925 | 926 | // Map a flexible set of arguments into a single returned object 927 | // If args.length is already one just return it, otherwise 928 | // use the properties argument to map the unnamed args to 929 | // object properties. 930 | // So in the latter case: 931 | // mapArguments(["foo", $.noop], ["message", "callback"]) 932 | // -> { message: "foo", callback: $.noop } 933 | function mapArguments(args, properties) { 934 | var argn = args.length; 935 | var options = {}; 936 | 937 | if (argn < 1 || argn > 2) { 938 | throw new Error('Invalid argument length'); 939 | } 940 | 941 | if (argn === 2 || typeof args[0] === 'string') { 942 | options[properties[0]] = args[0]; 943 | options[properties[1]] = args[1]; 944 | } else { 945 | options = args[0]; 946 | } 947 | 948 | return options; 949 | } 950 | 951 | 952 | // Merge a set of default dialog options with user supplied arguments 953 | function mergeArguments(defaults, args, properties) { 954 | return $.extend( 955 | // deep merge 956 | true, 957 | // ensure the target is an empty, unreferenced object 958 | {}, 959 | // the base options object for this type of dialog (often just buttons) 960 | defaults, 961 | // args could be an object or array; if it's an array properties will 962 | // map it to a proper options object 963 | mapArguments( 964 | args, 965 | properties 966 | ) 967 | ); 968 | } 969 | 970 | 971 | // This entry-level method makes heavy use of composition to take a simple 972 | // range of inputs and return valid options suitable for passing to bootbox.dialog 973 | function mergeDialogOptions(className, labels, properties, args) { 974 | var locale; 975 | if (args && args[0]) { 976 | locale = args[0].locale || defaults.locale; 977 | var swapButtons = args[0].swapButtonOrder || defaults.swapButtonOrder; 978 | 979 | if (swapButtons) { 980 | labels = labels.reverse(); 981 | } 982 | } 983 | 984 | // build up a base set of dialog properties 985 | var baseOptions = { 986 | className: 'bootbox-' + className, 987 | buttons: createLabels(labels, locale) 988 | }; 989 | 990 | // Ensure the buttons properties generated, *after* merging 991 | // with user args are still valid against the supplied labels 992 | return validateButtons( 993 | // merge the generated base properties with user supplied arguments 994 | mergeArguments( 995 | baseOptions, 996 | args, 997 | // if args.length > 1, properties specify how each arg maps to an object key 998 | properties 999 | ), 1000 | labels 1001 | ); 1002 | } 1003 | 1004 | 1005 | // Checks each button object to see if key is valid. 1006 | // This function will only be called by the alert, confirm, and prompt helpers. 1007 | function validateButtons(options, buttons) { 1008 | var allowedButtons = {}; 1009 | each(buttons, function (key, value) { 1010 | allowedButtons[value] = true; 1011 | }); 1012 | 1013 | each(options.buttons, function (key) { 1014 | if (allowedButtons[key] === undefined) { 1015 | throw new Error('button key "' + key + '" is not allowed (options are ' + buttons.join(' ') + ')'); 1016 | } 1017 | }); 1018 | 1019 | return options; 1020 | } 1021 | 1022 | 1023 | 1024 | // From a given list of arguments, return a suitable object of button labels. 1025 | // All this does is normalise the given labels and translate them where possible. 1026 | // e.g. "ok", "confirm" -> { ok: "OK", cancel: "Annuleren" } 1027 | function createLabels(labels, locale) { 1028 | var buttons = {}; 1029 | 1030 | for (var i = 0, j = labels.length; i < j; i++) { 1031 | var argument = labels[i]; 1032 | var key = argument.toLowerCase(); 1033 | var value = argument.toUpperCase(); 1034 | 1035 | buttons[key] = { 1036 | label: getText(value, locale) 1037 | }; 1038 | } 1039 | 1040 | return buttons; 1041 | } 1042 | 1043 | 1044 | 1045 | // Get localized text from a locale. Defaults to 'en' locale if no locale 1046 | // provided or a non-registered locale is requested 1047 | function getText(key, locale) { 1048 | var labels = locales[locale]; 1049 | 1050 | return labels ? labels[key] : locales.en[key]; 1051 | } 1052 | 1053 | 1054 | 1055 | // Filter and tidy up any user supplied parameters to this dialog. 1056 | // Also looks for any shorthands used and ensures that the options 1057 | // which are returned are all normalized properly 1058 | function sanitize(options) { 1059 | var buttons; 1060 | var total; 1061 | 1062 | if (typeof options !== 'object') { 1063 | throw new Error('Please supply an object of options'); 1064 | } 1065 | 1066 | if (!options.message) { 1067 | throw new Error('"message" option must not be null or an empty string.'); 1068 | } 1069 | 1070 | // make sure any supplied options take precedence over defaults 1071 | options = $.extend({}, defaults, options); 1072 | 1073 | //make sure backdrop is either true, false, or 'static' 1074 | if (!options.backdrop) { 1075 | options.backdrop = (options.backdrop === false || options.backdrop === 0) ? false : 'static'; 1076 | } else { 1077 | options.backdrop = typeof options.backdrop === 'string' && options.backdrop.toLowerCase() === 'static' ? 'static' : true; 1078 | } 1079 | 1080 | // no buttons is still a valid dialog but it's cleaner to always have 1081 | // a buttons object to iterate over, even if it's empty 1082 | if (!options.buttons) { 1083 | options.buttons = {}; 1084 | } 1085 | 1086 | buttons = options.buttons; 1087 | 1088 | total = getKeyLength(buttons); 1089 | 1090 | each(buttons, function (key, button, index) { 1091 | if ($.isFunction(button)) { 1092 | // short form, assume value is our callback. Since button 1093 | // isn't an object it isn't a reference either so re-assign it 1094 | button = buttons[key] = { 1095 | callback: button 1096 | }; 1097 | } 1098 | 1099 | // before any further checks make sure by now button is the correct type 1100 | if ($.type(button) !== 'object') { 1101 | throw new Error('button with key "' + key + '" must be an object'); 1102 | } 1103 | 1104 | if (!button.label) { 1105 | // the lack of an explicit label means we'll assume the key is good enough 1106 | button.label = key; 1107 | } 1108 | 1109 | if (!button.className) { 1110 | var isPrimary = false; 1111 | if (options.swapButtonOrder) { 1112 | isPrimary = index === 0; 1113 | } 1114 | else { 1115 | isPrimary = index === total - 1; 1116 | } 1117 | 1118 | if (total <= 2 && isPrimary) { 1119 | // always add a primary to the main option in a one or two-button dialog 1120 | button.className = 'btn-primary'; 1121 | } else { 1122 | // adding both classes allows us to target both BS3 and BS4 without needing to check the version 1123 | button.className = 'btn-secondary btn-default'; 1124 | } 1125 | } 1126 | }); 1127 | 1128 | return options; 1129 | } 1130 | 1131 | 1132 | // Returns a count of the properties defined on the object 1133 | function getKeyLength(obj) { 1134 | return Object.keys(obj).length; 1135 | } 1136 | 1137 | 1138 | // Tiny wrapper function around jQuery.each; just adds index as the third parameter 1139 | function each(collection, iterator) { 1140 | var index = 0; 1141 | $.each(collection, function (key, value) { 1142 | iterator(key, value, index++); 1143 | }); 1144 | } 1145 | 1146 | 1147 | function focusPrimaryButton(e) { 1148 | e.data.dialog.find('.bootbox-accept').first().trigger('focus'); 1149 | } 1150 | 1151 | 1152 | function destroyModal(e) { 1153 | // ensure we don't accidentally intercept hidden events triggered 1154 | // by children of the current dialog. We shouldn't need to handle this anymore, 1155 | // now that Bootstrap namespaces its events, but still worth doing. 1156 | if (e.target === e.data.dialog[0]) { 1157 | e.data.dialog.remove(); 1158 | } 1159 | } 1160 | 1161 | 1162 | function unbindModal(e) { 1163 | if (e.target === e.data.dialog[0]) { 1164 | e.data.dialog.off('escape.close.bb'); 1165 | e.data.dialog.off('click'); 1166 | } 1167 | } 1168 | 1169 | 1170 | // Handle the invoked dialog callback 1171 | function processCallback(e, dialog, callback) { 1172 | e.stopPropagation(); 1173 | e.preventDefault(); 1174 | 1175 | // by default we assume a callback will get rid of the dialog, 1176 | // although it is given the opportunity to override this 1177 | 1178 | // so, if the callback can be invoked and it *explicitly returns false* 1179 | // then we'll set a flag to keep the dialog active... 1180 | var preserveDialog = $.isFunction(callback) && callback.call(dialog, e) === false; 1181 | 1182 | // ... otherwise we'll bin it 1183 | if (!preserveDialog) { 1184 | dialog.modal('hide'); 1185 | } 1186 | } 1187 | 1188 | // Validate `min` and `max` values based on the current `inputType` value 1189 | function minAndMaxAreValid(type, min, max) { 1190 | var result = false; 1191 | var minValid = true; 1192 | var maxValid = true; 1193 | 1194 | if (type === 'date') { 1195 | if (min !== undefined && !(minValid = dateIsValid(min))) { 1196 | console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your min value may not be enforced by this browser.'); 1197 | } 1198 | else if (max !== undefined && !(maxValid = dateIsValid(max))) { 1199 | console.warn('Browsers which natively support the "date" input type expect date values to be of the form "YYYY-MM-DD" (see ISO-8601 https://www.iso.org/iso-8601-date-and-time-format.html). Bootbox does not enforce this rule, but your max value may not be enforced by this browser.'); 1200 | } 1201 | } 1202 | else if (type === 'time') { 1203 | if (min !== undefined && !(minValid = timeIsValid(min))) { 1204 | throw new Error('"min" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.'); 1205 | } 1206 | else if (max !== undefined && !(maxValid = timeIsValid(max))) { 1207 | throw new Error('"max" is not a valid time. See https://www.w3.org/TR/2012/WD-html-markup-20120315/datatypes.html#form.data.time for more information.'); 1208 | } 1209 | } 1210 | else { 1211 | if (min !== undefined && isNaN(min)) { 1212 | minValid = false; 1213 | throw new Error('"min" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-min for more information.'); 1214 | } 1215 | 1216 | if (max !== undefined && isNaN(max)) { 1217 | maxValid = false; 1218 | throw new Error('"max" must be a valid number. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.'); 1219 | } 1220 | } 1221 | 1222 | if (minValid && maxValid) { 1223 | if (max <= min) { 1224 | throw new Error('"max" must be greater than "min". See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-max for more information.'); 1225 | } 1226 | else { 1227 | result = true; 1228 | } 1229 | } 1230 | 1231 | return result; 1232 | } 1233 | 1234 | function timeIsValid(value) { 1235 | return /([01][0-9]|2[0-3]):[0-5][0-9]?:[0-5][0-9]/.test(value); 1236 | } 1237 | 1238 | function dateIsValid(value) { 1239 | return /(\d{4})-(\d{2})-(\d{2})/.test(value); 1240 | } 1241 | 1242 | // The Bootbox object 1243 | return exports; 1244 | })); 1245 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | var botAvatar = 2 | "https://robohash.org/liberovelitdolores.bmp?size=50x50&set=set1"; 3 | var userAvatar = 4 | "http://icons.iconarchive.com/icons/visualpharm/must-have/256/User-icon.png"; 5 | 6 | var telegram = new Vue({ 7 | el: "#telegram", 8 | data: { 9 | botSleep: 1000, 10 | textfield: "", 11 | users: [ 12 | { avatar: botAvatar, username: "Halmstad" }, 13 | { avatar: userAvatar, username: "Wako", owner: true } 14 | ], 15 | messages: [ 16 | { 17 | user: 0, 18 | text: 19 | "Hello, I'm a bot in a Messenger. I can be programmed without writing any code. Just use the node editor on the left" 20 | }, 21 | { 22 | user: 0, 23 | text: 24 | "This example is a demonstration of [Rete.js](https://github.com/retejs/rete) framework. Based on it you can make your powerful editor" 25 | }, 26 | { 27 | user: 0, 28 | text: 29 | "If you like the project, you can [support it](https://github.com/retejs/rete#donate)" 30 | } 31 | ] 32 | }, 33 | methods: { 34 | formatMsg(msg) { 35 | return msg.replace( 36 | /\[(.+?)\]\((.+?)\)/g, 37 | '$1' 38 | ); 39 | }, 40 | onMessage() { 41 | var ms = this.$refs.messages; 42 | setTimeout(() => { 43 | ms.scrollTop = ms.scrollHeight; 44 | }, 100); 45 | }, 46 | sendOwner(message) { 47 | this.messages.push({ user: 1, text: message }); 48 | receiveBot(message); 49 | this.onMessage(); 50 | }, 51 | sendBot(message) { 52 | this.messages.push({ user: 0, text: message }); 53 | this.onMessage(); 54 | } 55 | } 56 | }); 57 | 58 | var onMessageTask = []; 59 | function receiveBot(msg) { 60 | setTimeout(async () => { 61 | await onMessageTask.map(t => t.run(msg)); 62 | }, telegram.botSleep); 63 | } 64 | 65 | function receiveUser(msg) { 66 | telegram.sendBot(msg); 67 | } 68 | 69 | var actSocket = new Rete.Socket("Action"); 70 | var strSocket = new Rete.Socket("String"); 71 | 72 | const JsRenderPlugin = { 73 | install(editor, params = {}) { 74 | editor.on("rendercontrol", ({ el, control }) => { 75 | if (control.render && control.render !== "js") return; 76 | 77 | control.handler(el, editor); 78 | }); 79 | } 80 | }; 81 | 82 | class InputControl extends Rete.Control { 83 | constructor(key) { 84 | super(); 85 | this.render = "js"; 86 | this.key = key; 87 | } 88 | 89 | handler(el, editor) { 90 | var input = document.createElement("input"); 91 | el.appendChild(input); 92 | 93 | var text = this.getData(this.key) || "Some message.."; 94 | 95 | input.value = text; 96 | this.putData(this.key, text); 97 | input.addEventListener("change", () => { 98 | this.putData(this.key, input.value); 99 | }); 100 | } 101 | } 102 | 103 | class MessageEventComponent extends Rete.Component { 104 | constructor() { 105 | super("Message event"); 106 | this.task = { 107 | outputs: { act: "option", text: "output" }, 108 | init(task) { 109 | onMessageTask.push(task); 110 | } 111 | }; 112 | } 113 | 114 | builder(node) { 115 | var out1 = new Rete.Output("act", "Action", actSocket); 116 | var out2 = new Rete.Output("text", "Text", strSocket); 117 | return node.addOutput(out1).addOutput(out2); 118 | } 119 | 120 | worker(node, inputs, msg) { 121 | return { text: msg }; 122 | } 123 | } 124 | 125 | class MessageSendComponent extends Rete.Component { 126 | constructor() { 127 | super("Message send"); 128 | this.task = { 129 | outputs: {} 130 | }; 131 | } 132 | 133 | builder(node) { 134 | var inp1 = new Rete.Input("act", "Action", actSocket, true); 135 | var inp2 = new Rete.Input("text", "Text", strSocket); 136 | 137 | var ctrl = new InputControl("text"); 138 | inp2.addControl(ctrl); 139 | 140 | return node.addInput(inp1).addInput(inp2); 141 | } 142 | 143 | worker(node, inputs) { 144 | var text = inputs["text"] ? inputs["text"][0] : node.data.text; //default text 145 | console.log("msg send"); 146 | receiveUser(text); 147 | } 148 | } 149 | 150 | class MessageMatchComponent extends Rete.Component { 151 | constructor() { 152 | super("Message match"); 153 | this.task = { 154 | outputs: { t: "option", f: "option" } 155 | }; 156 | } 157 | 158 | builder(node) { 159 | var inp1 = new Rete.Input("act", "Action", actSocket); 160 | var inp2 = new Rete.Input("text", "Text", strSocket); 161 | var out1 = new Rete.Output("t", "True", actSocket); 162 | var out2 = new Rete.Output("f", "False", actSocket); 163 | var ctrl = new InputControl("regexp"); 164 | 165 | return node 166 | .addControl(ctrl) 167 | .addInput(inp1) 168 | .addInput(inp2) 169 | .addOutput(out1) 170 | .addOutput(out2); 171 | } 172 | worker(node, inputs) { 173 | var text = inputs["text"] ? inputs["text"][0] : ""; 174 | 175 | if (!text.match(new RegExp(node.data.regexp, "gi"))) this.closed = ["t"]; 176 | else this.closed = ["f"]; 177 | } 178 | } 179 | 180 | class MessageComponent extends Rete.Component { 181 | constructor() { 182 | super("Message"); 183 | this.task = { 184 | outputs: { text: "output" } 185 | }; 186 | } 187 | 188 | builder(node) { 189 | var out = new Rete.Output("text", "Text", strSocket); 190 | var ctrl = new InputControl("text"); 191 | 192 | return node.addControl(ctrl).addOutput(out); 193 | } 194 | 195 | worker(node, inputs) { 196 | return { text: node.data.text }; 197 | } 198 | } 199 | 200 | var components = [ 201 | new MessageEventComponent(), 202 | new MessageSendComponent(), 203 | new MessageMatchComponent(), 204 | new MessageComponent() 205 | ]; 206 | 207 | var container = document.getElementById("editor"); 208 | var editor = new Rete.NodeEditor("demo@0.1.0", container); 209 | editor.use(VueRenderPlugin); 210 | editor.use(ConnectionPlugin); 211 | editor.use(ContextMenuPlugin); 212 | editor.use(JsRenderPlugin); 213 | editor.use(TaskPlugin); 214 | 215 | var engine = new Rete.Engine("demo@0.1.0"); 216 | 217 | components.map(c => { 218 | editor.register(c); 219 | engine.register(c); 220 | }); 221 | 222 | editor 223 | .fromJSON({ 224 | id: "demo@0.1.0", 225 | nodes: { 226 | "1": { 227 | id: 1, 228 | data: {}, 229 | group: null, 230 | inputs: {}, 231 | outputs: { 232 | act: { connections: [{ node: 4, input: "act" }] }, 233 | text: { connections: [{ node: 4, input: "text" }] } 234 | }, 235 | position: [44, 138], 236 | name: "Message event" 237 | }, 238 | "2": { 239 | id: 2, 240 | data: {}, 241 | group: null, 242 | inputs: { 243 | act: { connections: [{ node: 4, output: "f" }] }, 244 | text: { connections: [{ node: 3, output: "text" }] } 245 | }, 246 | outputs: {}, 247 | position: [673.2072854903905, 194.82554933538893], 248 | name: "Message send" 249 | }, 250 | "3": { 251 | id: 3, 252 | data: { text: "ッ" }, 253 | group: null, 254 | inputs: {}, 255 | outputs: { text: { connections: [{ node: 2, input: "text" }] } }, 256 | position: [334.3043696236001, 298.2715347978209], 257 | name: "Message" 258 | }, 259 | "4": { 260 | id: 4, 261 | data: { regexp: ".*hello.*" }, 262 | group: null, 263 | inputs: { 264 | act: { connections: [{ node: 1, output: "act" }] }, 265 | text: { connections: [{ node: 1, output: "text" }] } 266 | }, 267 | outputs: { 268 | t: { connections: [{ node: 5, input: "act" }] }, 269 | f: { connections: [{ node: 2, input: "act" }] } 270 | }, 271 | position: [333.40730287320383, 22.1000138522662], 272 | name: "Message match" 273 | }, 274 | "5": { 275 | id: 5, 276 | data: {}, 277 | group: null, 278 | inputs: { 279 | act: { connections: [{ node: 4, output: "t" }] }, 280 | text: { connections: [{ node: 6, output: "text" }] } 281 | }, 282 | outputs: {}, 283 | position: [670.6284575254812, -103.66713461561366], 284 | name: "Message send" 285 | }, 286 | "6": { 287 | id: 6, 288 | data: { text: "Hello!" }, 289 | group: null, 290 | inputs: {}, 291 | outputs: { text: { connections: [{ node: 5, input: "text" }] } }, 292 | position: [317.85328833563574, -143.3955998177927], 293 | name: "Message" 294 | } 295 | }, 296 | groups: {} 297 | }) 298 | .then(() => { 299 | editor.on("error", err => { 300 | alertify.error(err.message); 301 | }); 302 | 303 | editor.on( 304 | "process connectioncreated connectionremoved nodecreated", 305 | async function() { 306 | if (engine.silent) return; 307 | onMessageTask = []; 308 | console.log("process"); 309 | await engine.abort(); 310 | await engine.process(editor.toJSON()); 311 | } 312 | ); 313 | 314 | editor.trigger("process"); 315 | editor.view.resize(); 316 | AreaPlugin.zoomAt(editor); 317 | }); -------------------------------------------------------------------------------- /static/js/litegraph-editor.js: -------------------------------------------------------------------------------- 1 | //Creates an interface to access extra features from a graph (like play, stop, live, etc) 2 | function Editor(container_id, options) { 3 | options = options || {}; 4 | 5 | //fill container 6 | var html = "
"; 7 | html += "
"; 8 | html += ""; 9 | 10 | var root = document.createElement("div"); 11 | this.root = root; 12 | root.className = "litegraph litegraph-editor"; 13 | root.innerHTML = html; 14 | 15 | this.tools = root.querySelector(".tools"); 16 | this.content = root.querySelector(".content"); 17 | this.footer = root.querySelector(".footer"); 18 | 19 | var canvas = root.querySelector(".graphcanvas"); 20 | 21 | //create graph 22 | var graph = (this.graph = new LGraph()); 23 | var graphcanvas = (this.graphcanvas = new LGraphCanvas(canvas, graph)); 24 | graphcanvas.background_image = "imgs/grid.png"; 25 | graph.onAfterExecute = function() { 26 | graphcanvas.draw(true); 27 | }; 28 | 29 | graphcanvas.onDropItem = this.onDropItem.bind(this); 30 | 31 | //add stuff 32 | //this.addToolsButton("loadsession_button","Load","imgs/icon-load.png", this.onLoadButton.bind(this), ".tools-left" ); 33 | //this.addToolsButton("savesession_button","Save","imgs/icon-save.png", this.onSaveButton.bind(this), ".tools-left" ); 34 | this.addLoadCounter(); 35 | this.addToolsButton( 36 | "playnode_button", 37 | "Play", 38 | "imgs/icon-play.png", 39 | this.onPlayButton.bind(this), 40 | ".tools-right" 41 | ); 42 | this.addToolsButton( 43 | "playstepnode_button", 44 | "Step", 45 | "imgs/icon-playstep.png", 46 | this.onPlayStepButton.bind(this), 47 | ".tools-right" 48 | ); 49 | 50 | if (!options.skip_livemode) { 51 | this.addToolsButton( 52 | "livemode_button", 53 | "Live", 54 | "imgs/icon-record.png", 55 | this.onLiveButton.bind(this), 56 | ".tools-right" 57 | ); 58 | } 59 | if (!options.skip_maximize) { 60 | this.addToolsButton( 61 | "maximize_button", 62 | "", 63 | "imgs/icon-maximize.png", 64 | this.onFullscreenButton.bind(this), 65 | ".tools-right" 66 | ); 67 | } 68 | if (options.miniwindow) { 69 | this.addMiniWindow(300, 200); 70 | } 71 | 72 | //append to DOM 73 | var parent = document.getElementById(container_id); 74 | if (parent) { 75 | parent.appendChild(root); 76 | } 77 | 78 | graphcanvas.resize(); 79 | //graphcanvas.draw(true,true); 80 | } 81 | 82 | Editor.prototype.addLoadCounter = function() { 83 | var meter = document.createElement("div"); 84 | meter.className = "headerpanel loadmeter toolbar-widget"; 85 | 86 | var html = 87 | "
CPU
"; 88 | html += 89 | "
GFX
"; 90 | 91 | meter.innerHTML = html; 92 | this.root.querySelector(".header .tools-left").appendChild(meter); 93 | var self = this; 94 | 95 | setInterval(function() { 96 | meter.querySelector(".cpuload .fgload").style.width = 97 | 2 * self.graph.execution_time * 90 + "px"; 98 | if (self.graph.status == LGraph.STATUS_RUNNING) { 99 | meter.querySelector(".gpuload .fgload").style.width = 100 | self.graphcanvas.render_time * 10 * 90 + "px"; 101 | } else { 102 | meter.querySelector(".gpuload .fgload").style.width = 4 + "px"; 103 | } 104 | }, 200); 105 | }; 106 | 107 | Editor.prototype.addToolsButton = function( id, name, icon_url, callback, container ) { 108 | if (!container) { 109 | container = ".tools"; 110 | } 111 | 112 | var button = this.createButton(name, icon_url, callback); 113 | button.id = id; 114 | this.root.querySelector(container).appendChild(button); 115 | }; 116 | 117 | Editor.prototype.createButton = function(name, icon_url, callback) { 118 | var button = document.createElement("button"); 119 | if (icon_url) { 120 | button.innerHTML = " "; 121 | } 122 | button.classList.add("btn"); 123 | button.innerHTML += name; 124 | if(callback) 125 | button.addEventListener("click", callback ); 126 | return button; 127 | }; 128 | 129 | Editor.prototype.onLoadButton = function() { 130 | var panel = this.graphcanvas.createPanel("Load session",{closable:true}); 131 | //TO DO 132 | 133 | this.root.appendChild(panel); 134 | }; 135 | 136 | Editor.prototype.onSaveButton = function() {}; 137 | 138 | Editor.prototype.onPlayButton = function() { 139 | var graph = this.graph; 140 | var button = this.root.querySelector("#playnode_button"); 141 | 142 | if (graph.status == LGraph.STATUS_STOPPED) { 143 | button.innerHTML = " Stop"; 144 | graph.start(); 145 | } else { 146 | button.innerHTML = " Play"; 147 | graph.stop(); 148 | } 149 | }; 150 | 151 | Editor.prototype.onPlayStepButton = function() { 152 | var graph = this.graph; 153 | graph.runStep(1); 154 | this.graphcanvas.draw(true, true); 155 | }; 156 | 157 | Editor.prototype.onLiveButton = function() { 158 | var is_live_mode = !this.graphcanvas.live_mode; 159 | this.graphcanvas.switchLiveMode(true); 160 | this.graphcanvas.draw(); 161 | var url = this.graphcanvas.live_mode 162 | ? "imgs/gauss_bg_medium.jpg" 163 | : "imgs/gauss_bg.jpg"; 164 | var button = this.root.querySelector("#livemode_button"); 165 | button.innerHTML = !is_live_mode 166 | ? " Live" 167 | : " Edit"; 168 | }; 169 | 170 | Editor.prototype.onDropItem = function(e) 171 | { 172 | var that = this; 173 | for(var i = 0; i < e.dataTransfer.files.length; ++i) 174 | { 175 | var file = e.dataTransfer.files[i]; 176 | var ext = LGraphCanvas.getFileExtension(file.name); 177 | var reader = new FileReader(); 178 | if(ext == "json") 179 | { 180 | reader.onload = function(event) { 181 | var data = JSON.parse( event.target.result ); 182 | that.graph.configure(data); 183 | }; 184 | reader.readAsText(file); 185 | } 186 | } 187 | } 188 | 189 | Editor.prototype.goFullscreen = function() { 190 | if (this.root.requestFullscreen) { 191 | this.root.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT); 192 | } else if (this.root.mozRequestFullscreen) { 193 | this.root.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT); 194 | } else if (this.root.webkitRequestFullscreen) { 195 | this.root.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); 196 | } else { 197 | throw "Fullscreen not supported"; 198 | } 199 | 200 | var self = this; 201 | setTimeout(function() { 202 | self.graphcanvas.resize(); 203 | }, 100); 204 | }; 205 | 206 | Editor.prototype.onFullscreenButton = function() { 207 | this.goFullscreen(); 208 | }; 209 | 210 | Editor.prototype.addMiniWindow = function(w, h) { 211 | var miniwindow = document.createElement("div"); 212 | miniwindow.className = "litegraph miniwindow"; 213 | miniwindow.innerHTML = 214 | ""; 219 | var canvas = miniwindow.querySelector("canvas"); 220 | var that = this; 221 | 222 | var graphcanvas = new LGraphCanvas( canvas, this.graph ); 223 | graphcanvas.show_info = false; 224 | graphcanvas.background_image = "imgs/grid.png"; 225 | graphcanvas.scale = 0.25; 226 | graphcanvas.allow_dragnodes = false; 227 | graphcanvas.allow_interaction = false; 228 | graphcanvas.render_shadows = false; 229 | graphcanvas.max_zoom = 0.25; 230 | this.miniwindow_graphcanvas = graphcanvas; 231 | graphcanvas.onClear = function() { 232 | graphcanvas.scale = 0.25; 233 | graphcanvas.allow_dragnodes = false; 234 | graphcanvas.allow_interaction = false; 235 | }; 236 | graphcanvas.onRenderBackground = function(canvas, ctx) { 237 | ctx.strokeStyle = "#567"; 238 | var tl = that.graphcanvas.convertOffsetToCanvas([0, 0]); 239 | var br = that.graphcanvas.convertOffsetToCanvas([ 240 | that.graphcanvas.canvas.width, 241 | that.graphcanvas.canvas.height 242 | ]); 243 | tl = this.convertCanvasToOffset(tl); 244 | br = this.convertCanvasToOffset(br); 245 | ctx.lineWidth = 1; 246 | ctx.strokeRect( 247 | Math.floor(tl[0]) + 0.5, 248 | Math.floor(tl[1]) + 0.5, 249 | Math.floor(br[0] - tl[0]), 250 | Math.floor(br[1] - tl[1]) 251 | ); 252 | }; 253 | 254 | miniwindow.style.position = "absolute"; 255 | miniwindow.style.top = "4px"; 256 | miniwindow.style.right = "4px"; 257 | 258 | var close_button = document.createElement("div"); 259 | close_button.className = "corner-button"; 260 | close_button.innerHTML = "❌"; 261 | close_button.addEventListener("click", function(e) { 262 | graphcanvas.setGraph(null); 263 | miniwindow.parentNode.removeChild(miniwindow); 264 | }); 265 | miniwindow.appendChild(close_button); 266 | 267 | this.root.querySelector(".content").appendChild(miniwindow); 268 | }; 269 | 270 | LiteGraph.Editor = Editor; 271 | -------------------------------------------------------------------------------- /static/js/p.js: -------------------------------------------------------------------------------- 1 | //basic nodes 2 | (function(global) { 3 | var LiteGraph = global.LiteGraph; 4 | 5 | 6 | 7 | function addition() { 8 | this.addInput("a", "number"); 9 | this.addInput("b", "number"); 10 | 11 | this.addOutput("result", "number"); 12 | 13 | 14 | //this.widget = this.addWidget("number","value",1,"value"); 15 | //this.widgets_up = true; 16 | //this.size = [180, 30]; 17 | } 18 | 19 | addition.title = "addition"; 20 | addition.desc = "addition"; 21 | 22 | LiteGraph.registerNodeType("basic/addition", addition); 23 | 24 | 25 | 26 | function integer() { 27 | 28 | this.addOutput("result", "number"); 29 | 30 | this.addProperty("a", "+"); 31 | 32 | //this.widget = this.addWidget("number","value",1,"value"); 33 | //this.widgets_up = true; 34 | //this.size = [180, 30]; 35 | } 36 | 37 | integer.title = "integer"; 38 | integer.desc = "integer"; 39 | 40 | LiteGraph.registerNodeType("basic/integer", integer); 41 | 42 | 43 | 44 | function result() { 45 | this.addInput("result", "number"); 46 | 47 | 48 | 49 | //this.widget = this.addWidget("number","value",1,"value"); 50 | //this.widgets_up = true; 51 | //this.size = [180, 30]; 52 | } 53 | 54 | result.title = "result"; 55 | result.desc = "result"; 56 | 57 | LiteGraph.registerNodeType("basic/result", result); 58 | 59 | 60 | 61 | function start() { 62 | 63 | this.addOutput("result", "number"); 64 | 65 | this.addProperty("a", "0"); 66 | 67 | //this.widget = this.addWidget("number","value",1,"value"); 68 | //this.widgets_up = true; 69 | //this.size = [180, 30]; 70 | } 71 | 72 | start.title = "start"; 73 | start.desc = "start"; 74 | 75 | LiteGraph.registerNodeType("basic/start", start); 76 | 77 | 78 | 79 | })(this); 80 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 | 110 | 111 | 112 | --------------------------------------------------------------------------------