Compare commits
	
		
			5 Commits
		
	
	
		
			65d391e050
			...
			da9cbbe8a5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| da9cbbe8a5 | |||
| ccbe62c0d9 | |||
| b1916fe55d | |||
| eb7fdeddac | |||
| 865fdce8f5 | 
							
								
								
									
										16
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.163.1/containers/javascript-node/.devcontainer/base.Dockerfile | ||||
| 
 | ||||
| # [Choice] Node.js version: 14, 12, 10 | ||||
| ARG VARIANT="14-buster" | ||||
| FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} | ||||
| 
 | ||||
| # [Optional] Uncomment this section to install additional OS packages. | ||||
| # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ | ||||
| #     && apt-get -y install --no-install-recommends <your-package-list-here> | ||||
| 
 | ||||
| # [Optional] Uncomment if you want to install an additional version of node using nvm | ||||
| # ARG EXTRA_NODE_VERSION=10 | ||||
| # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" | ||||
| 
 | ||||
| # [Optional] Uncomment if you want to install more global node modules | ||||
| # RUN su node -c "npm install -g <your-package-list-here>" | ||||
							
								
								
									
										29
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: | ||||
| // https://github.com/microsoft/vscode-dev-containers/tree/v0.163.1/containers/javascript-node | ||||
| { | ||||
| 	"name": "Node.js", | ||||
| 	"build": { | ||||
| 		"dockerfile": "Dockerfile", | ||||
| 		// Update 'VARIANT' to pick a Node version: 10, 12, 14 | ||||
| 		"args": { "VARIANT": "14" } | ||||
| 	}, | ||||
| 
 | ||||
| 	// Set *default* container specific settings.json values on container create. | ||||
| 	"settings": {  | ||||
| 		"terminal.integrated.shell.linux": "/bin/bash" | ||||
| 	}, | ||||
| 
 | ||||
| 	// Add the IDs of extensions you want installed when the container is created. | ||||
| 	"extensions": [ | ||||
| 		"dbaeumer.vscode-eslint" | ||||
| 	], | ||||
| 
 | ||||
| 	// Use 'forwardPorts' to make a list of ports inside the container available locally. | ||||
| 	// "forwardPorts": [], | ||||
| 
 | ||||
| 	// Use 'postCreateCommand' to run commands after the container is created. | ||||
| 	// "postCreateCommand": "yarn install", | ||||
| 
 | ||||
| 	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. | ||||
| 	"remoteUser": "node" | ||||
| } | ||||
							
								
								
									
										16
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| { | ||||
|     // Use IntelliSense to learn about possible attributes. | ||||
|     // Hover to view descriptions of existing attributes. | ||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
|             "type": "pwa-chrome", | ||||
|             "request": "launch", | ||||
|             "name": "Launch Chrome against localhost", | ||||
|             "url": "http://localhost:8080/index.html", | ||||
|             "webRoot": "${workspaceFolder}", | ||||
|             "trace": true | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										14
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| { | ||||
|     "files.eol": "\n", | ||||
|       "editor.tabSize": 2, | ||||
|       "editor.formatOnPaste": false, | ||||
|       "editor.formatOnSave": true, | ||||
|       "editor.formatOnType": true, | ||||
|       "[javascript]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|       }, | ||||
|       "[typescript]": { | ||||
|         "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|       }, | ||||
|       "files.trimTrailingWhitespace": true | ||||
| } | ||||
							
								
								
									
										31
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| { | ||||
|   "version": "2.0.0", | ||||
|   "tasks": [ | ||||
|     { | ||||
|       "label": "npm: build", | ||||
|       "type": "npm", | ||||
|       "script": "build", | ||||
|       "problemMatcher": [] | ||||
|     }, | ||||
|     { | ||||
|       "label": "npm: watch", | ||||
|       "type": "npm", | ||||
|       "script": "watch", | ||||
|       "problemMatcher": [], | ||||
|       "presentation": { | ||||
|         "panel": "shared", | ||||
|         "group": "test" | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "npm: serve", | ||||
|       "type": "npm", | ||||
|       "script": "serve", | ||||
|       "problemMatcher": [], | ||||
|       "presentation": { | ||||
|         "panel": "shared", | ||||
|         "group": "test" | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										447
									
								
								DragDropTouch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								DragDropTouch.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,447 @@ | ||||
| var DragDropTouch; | ||||
| (function (DragDropTouch_1) { | ||||
|     'use strict'; | ||||
|     /** | ||||
|      * Object used to hold the data that is being dragged during drag and drop operations. | ||||
|      * | ||||
|      * It may hold one or more data items of different types. For more information about | ||||
|      * drag and drop operations and data transfer objects, see | ||||
|      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer">HTML Drag and Drop API</a>. | ||||
|      * | ||||
|      * This object is created automatically by the @see:DragDropTouch singleton and is | ||||
|      * accessible through the @see:dataTransfer property of all drag events. | ||||
|      */ | ||||
|     var DataTransfer = (function () { | ||||
|         function DataTransfer() { | ||||
|             this._dropEffect = 'move'; | ||||
|             this._effectAllowed = 'all'; | ||||
|             this._data = {}; | ||||
|         } | ||||
|         Object.defineProperty(DataTransfer.prototype, "dropEffect", { | ||||
|             /** | ||||
|              * Gets or sets the type of drag-and-drop operation currently selected. | ||||
|              * The value must be 'none',  'copy',  'link', or 'move'. | ||||
|              */ | ||||
|             get: function () { | ||||
|                 return this._dropEffect; | ||||
|             }, | ||||
|             set: function (value) { | ||||
|                 this._dropEffect = value; | ||||
|             }, | ||||
|             enumerable: true, | ||||
|             configurable: true | ||||
|         }); | ||||
|         Object.defineProperty(DataTransfer.prototype, "effectAllowed", { | ||||
|             /** | ||||
|              * Gets or sets the types of operations that are possible. | ||||
|              * Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link', | ||||
|              * 'linkMove', 'move', 'all' or 'uninitialized'. | ||||
|              */ | ||||
|             get: function () { | ||||
|                 return this._effectAllowed; | ||||
|             }, | ||||
|             set: function (value) { | ||||
|                 this._effectAllowed = value; | ||||
|             }, | ||||
|             enumerable: true, | ||||
|             configurable: true | ||||
|         }); | ||||
|         Object.defineProperty(DataTransfer.prototype, "types", { | ||||
|             /** | ||||
|              * Gets an array of strings giving the formats that were set in the @see:dragstart event. | ||||
|              */ | ||||
|             get: function () { | ||||
|                 return Object.keys(this._data); | ||||
|             }, | ||||
|             enumerable: true, | ||||
|             configurable: true | ||||
|         }); | ||||
|         /** | ||||
|          * Removes the data associated with a given type. | ||||
|          * | ||||
|          * The type argument is optional. If the type is empty or not specified, the data | ||||
|          * associated with all types is removed. If data for the specified type does not exist, | ||||
|          * or the data transfer contains no data, this method will have no effect. | ||||
|          * | ||||
|          * @param type Type of data to remove. | ||||
|          */ | ||||
|         DataTransfer.prototype.clearData = function (type) { | ||||
|             if (type != null) { | ||||
|                 delete this._data[type]; | ||||
|             } | ||||
|             else { | ||||
|                 this._data = null; | ||||
|             } | ||||
|         }; | ||||
|         /** | ||||
|          * Retrieves the data for a given type, or an empty string if data for that type does | ||||
|          * not exist or the data transfer contains no data. | ||||
|          * | ||||
|          * @param type Type of data to retrieve. | ||||
|          */ | ||||
|         DataTransfer.prototype.getData = function (type) { | ||||
|             return this._data[type] || ''; | ||||
|         }; | ||||
|         /** | ||||
|          * Set the data for a given type. | ||||
|          * | ||||
|          * For a list of recommended drag types, please see | ||||
|          * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types.
 | ||||
|          * | ||||
|          * @param type Type of data to add. | ||||
|          * @param value Data to add. | ||||
|          */ | ||||
|         DataTransfer.prototype.setData = function (type, value) { | ||||
|             this._data[type] = value; | ||||
|         }; | ||||
|         /** | ||||
|          * Set the image to be used for dragging if a custom one is desired. | ||||
|          * | ||||
|          * @param img An image element to use as the drag feedback image. | ||||
|          * @param offsetX The horizontal offset within the image. | ||||
|          * @param offsetY The vertical offset within the image. | ||||
|          */ | ||||
|         DataTransfer.prototype.setDragImage = function (img, offsetX, offsetY) { | ||||
|             var ddt = DragDropTouch._instance; | ||||
|             ddt._imgCustom = img; | ||||
|             ddt._imgOffset = { x: offsetX, y: offsetY }; | ||||
|         }; | ||||
|         return DataTransfer; | ||||
|     }()); | ||||
|     DragDropTouch_1.DataTransfer = DataTransfer; | ||||
|     /** | ||||
|      * Defines a class that adds support for touch-based HTML5 drag/drop operations. | ||||
|      * | ||||
|      * The @see:DragDropTouch class listens to touch events and raises the | ||||
|      * appropriate HTML5 drag/drop events as if the events had been caused | ||||
|      * by mouse actions. | ||||
|      * | ||||
|      * The purpose of this class is to enable using existing, standard HTML5 | ||||
|      * drag/drop code on mobile devices running IOS or Android. | ||||
|      * | ||||
|      * To use, include the DragDropTouch.js file on the page. The class will | ||||
|      * automatically start monitoring touch events and will raise the HTML5 | ||||
|      * drag drop events (dragstart, dragenter, dragleave, drop, dragend) which | ||||
|      * should be handled by the application. | ||||
|      * | ||||
|      * For details and examples on HTML drag and drop, see | ||||
|      * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations.
 | ||||
|      */ | ||||
|     var DragDropTouch = (function () { | ||||
|         /** | ||||
|          * Initializes the single instance of the @see:DragDropTouch class. | ||||
|          */ | ||||
|         function DragDropTouch() { | ||||
|             this._lastClick = 0; | ||||
|             // enforce singleton pattern
 | ||||
|             if (DragDropTouch._instance) { | ||||
|                 throw 'DragDropTouch instance already created.'; | ||||
|             } | ||||
|             // detect passive event support
 | ||||
|             // https://github.com/Modernizr/Modernizr/issues/1894
 | ||||
|             var supportsPassive = false; | ||||
|             document.addEventListener('test', function () { }, { | ||||
|                 get passive() { | ||||
|                     supportsPassive = true; | ||||
|                     return true; | ||||
|                 } | ||||
|             }); | ||||
|             // listen to touch events
 | ||||
|             if ('ontouchstart' in document) { | ||||
|                 var d = document, ts = this._touchstart.bind(this), tm = this._touchmove.bind(this), te = this._touchend.bind(this), opt = supportsPassive ? { passive: false, capture: false } : false; | ||||
|                 d.addEventListener('touchstart', ts, opt); | ||||
|                 d.addEventListener('touchmove', tm, opt); | ||||
|                 d.addEventListener('touchend', te); | ||||
|                 d.addEventListener('touchcancel', te); | ||||
|             } | ||||
|         } | ||||
|         /** | ||||
|          * Gets a reference to the @see:DragDropTouch singleton. | ||||
|          */ | ||||
|         DragDropTouch.getInstance = function () { | ||||
|             return DragDropTouch._instance; | ||||
|         }; | ||||
|         // ** event handlers
 | ||||
|         DragDropTouch.prototype._touchstart = function (e) { | ||||
|             var _this = this; | ||||
|             if (this._shouldHandle(e)) { | ||||
|                 // raise double-click and prevent zooming
 | ||||
|                 if (Date.now() - this._lastClick < DragDropTouch._DBLCLICK) { | ||||
|                     if (this._dispatchEvent(e, 'dblclick', e.target)) { | ||||
|                         e.preventDefault(); | ||||
|                         this._reset(); | ||||
|                         return; | ||||
|                     } | ||||
|                 } | ||||
|                 // clear all variables
 | ||||
|                 this._reset(); | ||||
|                 // get nearest draggable element
 | ||||
|                 var src = this._closestDraggable(e.target); | ||||
|                 if (src) { | ||||
|                     // give caller a chance to handle the hover/move events
 | ||||
|                     if (!this._dispatchEvent(e, 'mousemove', e.target) && | ||||
|                         !this._dispatchEvent(e, 'mousedown', e.target)) { | ||||
|                         // get ready to start dragging
 | ||||
|                         this._dragSource = src; | ||||
|                         this._ptDown = this._getPoint(e); | ||||
|                         this._lastTouch = e; | ||||
|                         e.preventDefault(); | ||||
|                         // show context menu if the user hasn't started dragging after a while
 | ||||
|                         setTimeout(function () { | ||||
|                             if (_this._dragSource == src && _this._img == null) { | ||||
|                                 if (_this._dispatchEvent(e, 'contextmenu', src)) { | ||||
|                                     _this._reset(); | ||||
|                                 } | ||||
|                             } | ||||
|                         }, DragDropTouch._CTXMENU); | ||||
|                         if (DragDropTouch._ISPRESSHOLDMODE) { | ||||
|                             this._pressHoldInterval = setTimeout(function () { | ||||
|                                 _this._isDragEnabled = true; | ||||
|                                 _this._touchmove(e); | ||||
|                             }, DragDropTouch._PRESSHOLDAWAIT); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         DragDropTouch.prototype._touchmove = function (e) { | ||||
|             if (this._shouldCancelPressHoldMove(e)) { | ||||
|               this._reset(); | ||||
|               return; | ||||
|             } | ||||
|             if (this._shouldHandleMove(e) || this._shouldHandlePressHoldMove(e)) { | ||||
|                 // see if target wants to handle move
 | ||||
|                 var target = this._getTarget(e); | ||||
|                 if (this._dispatchEvent(e, 'mousemove', target)) { | ||||
|                     this._lastTouch = e; | ||||
|                     e.preventDefault(); | ||||
|                     return; | ||||
|                 } | ||||
|                 // start dragging
 | ||||
|                 if (this._dragSource && !this._img && this._shouldStartDragging(e)) { | ||||
|                     this._dispatchEvent(e, 'dragstart', this._dragSource); | ||||
|                     this._createImage(e); | ||||
|                     this._dispatchEvent(e, 'dragenter', target); | ||||
|                 } | ||||
|                 // continue dragging
 | ||||
|                 if (this._img) { | ||||
|                     this._lastTouch = e; | ||||
|                     e.preventDefault(); // prevent scrolling
 | ||||
|                     if (target != this._lastTarget) { | ||||
|                         this._dispatchEvent(this._lastTouch, 'dragleave', this._lastTarget); | ||||
|                         this._dispatchEvent(e, 'dragenter', target); | ||||
|                         this._lastTarget = target; | ||||
|                     } | ||||
|                     this._moveImage(e); | ||||
|                     this._isDropZone = this._dispatchEvent(e, 'dragover', target); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         DragDropTouch.prototype._touchend = function (e) { | ||||
|             if (this._shouldHandle(e)) { | ||||
|                 // see if target wants to handle up
 | ||||
|                 if (this._dispatchEvent(this._lastTouch, 'mouseup', e.target)) { | ||||
|                     e.preventDefault(); | ||||
|                     return; | ||||
|                 } | ||||
|                 // user clicked the element but didn't drag, so clear the source and simulate a click
 | ||||
|                 if (!this._img) { | ||||
|                     this._dragSource = null; | ||||
|                     this._dispatchEvent(this._lastTouch, 'click', e.target); | ||||
|                     this._lastClick = Date.now(); | ||||
|                 } | ||||
|                 // finish dragging
 | ||||
|                 this._destroyImage(); | ||||
|                 if (this._dragSource) { | ||||
|                     if (e.type.indexOf('cancel') < 0 && this._isDropZone) { | ||||
|                         this._dispatchEvent(this._lastTouch, 'drop', this._lastTarget); | ||||
|                     } | ||||
|                     this._dispatchEvent(this._lastTouch, 'dragend', this._dragSource); | ||||
|                     this._reset(); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         // ** utilities
 | ||||
|         // ignore events that have been handled or that involve more than one touch
 | ||||
|         DragDropTouch.prototype._shouldHandle = function (e) { | ||||
|             return e && | ||||
|                 !e.defaultPrevented && | ||||
|                 e.touches && e.touches.length < 2; | ||||
|         }; | ||||
| 
 | ||||
|         // use regular condition outside of press & hold mode
 | ||||
|         DragDropTouch.prototype._shouldHandleMove = function (e) { | ||||
|           return !DragDropTouch._ISPRESSHOLDMODE && this._shouldHandle(e); | ||||
|         }; | ||||
| 
 | ||||
|         // allow to handle moves that involve many touches for press & hold
 | ||||
|         DragDropTouch.prototype._shouldHandlePressHoldMove = function (e) { | ||||
|           return DragDropTouch._ISPRESSHOLDMODE && | ||||
|               this._isDragEnabled && e && e.touches && e.touches.length; | ||||
|         }; | ||||
| 
 | ||||
|         // reset data if user drags without pressing & holding
 | ||||
|         DragDropTouch.prototype._shouldCancelPressHoldMove = function (e) { | ||||
|           return DragDropTouch._ISPRESSHOLDMODE && !this._isDragEnabled && | ||||
|               this._getDelta(e) > DragDropTouch._PRESSHOLDMARGIN; | ||||
|         }; | ||||
| 
 | ||||
|         // start dragging when specified delta is detected
 | ||||
|         DragDropTouch.prototype._shouldStartDragging = function (e) { | ||||
|             var delta = this._getDelta(e); | ||||
|             return delta > DragDropTouch._THRESHOLD || | ||||
|                 (DragDropTouch._ISPRESSHOLDMODE && delta >= DragDropTouch._PRESSHOLDTHRESHOLD); | ||||
|         } | ||||
| 
 | ||||
|         // clear all members
 | ||||
|         DragDropTouch.prototype._reset = function () { | ||||
|             this._destroyImage(); | ||||
|             this._dragSource = null; | ||||
|             this._lastTouch = null; | ||||
|             this._lastTarget = null; | ||||
|             this._ptDown = null; | ||||
|             this._isDragEnabled = false; | ||||
|             this._isDropZone = false; | ||||
|             this._dataTransfer = new DataTransfer(); | ||||
|             clearInterval(this._pressHoldInterval); | ||||
|         }; | ||||
|         // get point for a touch event
 | ||||
|         DragDropTouch.prototype._getPoint = function (e, page) { | ||||
|             if (e && e.touches) { | ||||
|                 e = e.touches[0]; | ||||
|             } | ||||
|             return { x: page ? e.pageX : e.clientX, y: page ? e.pageY : e.clientY }; | ||||
|         }; | ||||
|         // get distance between the current touch event and the first one
 | ||||
|         DragDropTouch.prototype._getDelta = function (e) { | ||||
|             if (DragDropTouch._ISPRESSHOLDMODE && !this._ptDown) { return 0; } | ||||
|             var p = this._getPoint(e); | ||||
|             return Math.abs(p.x - this._ptDown.x) + Math.abs(p.y - this._ptDown.y); | ||||
|         }; | ||||
|         // get the element at a given touch event
 | ||||
|         DragDropTouch.prototype._getTarget = function (e) { | ||||
|             var pt = this._getPoint(e), el = document.elementFromPoint(pt.x, pt.y); | ||||
|             while (el && getComputedStyle(el).pointerEvents == 'none') { | ||||
|                 el = el.parentElement; | ||||
|             } | ||||
|             return el; | ||||
|         }; | ||||
|         // create drag image from source element
 | ||||
|         DragDropTouch.prototype._createImage = function (e) { | ||||
|             // just in case...
 | ||||
|             if (this._img) { | ||||
|                 this._destroyImage(); | ||||
|             } | ||||
|             // create drag image from custom element or drag source
 | ||||
|             var src = this._imgCustom || this._dragSource; | ||||
|             this._img = src.cloneNode(true); | ||||
|             this._copyStyle(src, this._img); | ||||
|             this._img.style.top = this._img.style.left = '-9999px'; | ||||
|             // if creating from drag source, apply offset and opacity
 | ||||
|             if (!this._imgCustom) { | ||||
|                 var rc = src.getBoundingClientRect(), pt = this._getPoint(e); | ||||
|                 this._imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top }; | ||||
|                 this._img.style.opacity = DragDropTouch._OPACITY.toString(); | ||||
|             } | ||||
|             // add image to document
 | ||||
|             this._moveImage(e); | ||||
|             document.body.appendChild(this._img); | ||||
|         }; | ||||
|         // dispose of drag image element
 | ||||
|         DragDropTouch.prototype._destroyImage = function () { | ||||
|             if (this._img && this._img.parentElement) { | ||||
|                 this._img.parentElement.removeChild(this._img); | ||||
|             } | ||||
|             this._img = null; | ||||
|             this._imgCustom = null; | ||||
|         }; | ||||
|         // move the drag image element
 | ||||
|         DragDropTouch.prototype._moveImage = function (e) { | ||||
|             var _this = this; | ||||
|             requestAnimationFrame(function () { | ||||
|                 if (_this._img) { | ||||
|                     var pt = _this._getPoint(e, true), s = _this._img.style; | ||||
|                     s.position = 'absolute'; | ||||
|                     s.pointerEvents = 'none'; | ||||
|                     s.zIndex = '999999'; | ||||
|                     s.left = Math.round(pt.x - _this._imgOffset.x) + 'px'; | ||||
|                     s.top = Math.round(pt.y - _this._imgOffset.y) + 'px'; | ||||
|                 } | ||||
|             }); | ||||
|         }; | ||||
|         // copy properties from an object to another
 | ||||
|         DragDropTouch.prototype._copyProps = function (dst, src, props) { | ||||
|             for (var i = 0; i < props.length; i++) { | ||||
|                 var p = props[i]; | ||||
|                 dst[p] = src[p]; | ||||
|             } | ||||
|         }; | ||||
|         DragDropTouch.prototype._copyStyle = function (src, dst) { | ||||
|             // remove potentially troublesome attributes
 | ||||
|             DragDropTouch._rmvAtts.forEach(function (att) { | ||||
|                 dst.removeAttribute(att); | ||||
|             }); | ||||
|             // copy canvas content
 | ||||
|             if (src instanceof HTMLCanvasElement) { | ||||
|                 var cSrc = src, cDst = dst; | ||||
|                 cDst.width = cSrc.width; | ||||
|                 cDst.height = cSrc.height; | ||||
|                 cDst.getContext('2d').drawImage(cSrc, 0, 0); | ||||
|             } | ||||
|             // copy style (without transitions)
 | ||||
|             var cs = getComputedStyle(src); | ||||
|             for (var i = 0; i < cs.length; i++) { | ||||
|                 var key = cs[i]; | ||||
|                 if (key.indexOf('transition') < 0) { | ||||
|                     dst.style[key] = cs[key]; | ||||
|                 } | ||||
|             } | ||||
|             dst.style.pointerEvents = 'none'; | ||||
|             // and repeat for all children
 | ||||
|             for (var i = 0; i < src.children.length; i++) { | ||||
|                 this._copyStyle(src.children[i], dst.children[i]); | ||||
|             } | ||||
|         }; | ||||
|         DragDropTouch.prototype._dispatchEvent = function (e, type, target) { | ||||
|             if (e && target) { | ||||
|                 var evt = document.createEvent('Event'), t = e.touches ? e.touches[0] : e; | ||||
|                 evt.initEvent(type, true, true); | ||||
|                 evt.button = 0; | ||||
|                 evt.which = evt.buttons = 1; | ||||
|                 this._copyProps(evt, e, DragDropTouch._kbdProps); | ||||
|                 this._copyProps(evt, t, DragDropTouch._ptProps); | ||||
|                 evt.dataTransfer = this._dataTransfer; | ||||
|                 target.dispatchEvent(evt); | ||||
|                 return evt.defaultPrevented; | ||||
|             } | ||||
|             return false; | ||||
|         }; | ||||
|         // gets an element's closest draggable ancestor
 | ||||
|         DragDropTouch.prototype._closestDraggable = function (e) { | ||||
|             for (; e; e = e.parentElement) { | ||||
|                 if (e.hasAttribute('draggable') && e.draggable) { | ||||
|                     return e; | ||||
|                 } | ||||
|             } | ||||
|             return null; | ||||
|         }; | ||||
|         return DragDropTouch; | ||||
|     }()); | ||||
|     /*private*/ DragDropTouch._instance = new DragDropTouch(); // singleton
 | ||||
|     // constants
 | ||||
|     DragDropTouch._THRESHOLD = 5; // pixels to move before drag starts
 | ||||
|     DragDropTouch._OPACITY = 0.5; // drag image opacity
 | ||||
|     DragDropTouch._DBLCLICK = 500; // max ms between clicks in a double click
 | ||||
|     DragDropTouch._CTXMENU = 900; // ms to hold before raising 'contextmenu' event
 | ||||
|     DragDropTouch._ISPRESSHOLDMODE = false; // decides of press & hold mode presence
 | ||||
|     DragDropTouch._PRESSHOLDAWAIT = 400; // ms to wait before press & hold is detected
 | ||||
|     DragDropTouch._PRESSHOLDMARGIN = 25; // pixels that finger might shiver while pressing
 | ||||
|     DragDropTouch._PRESSHOLDTHRESHOLD = 0; // pixels to move before drag starts
 | ||||
|     // copy styles/attributes from drag source to drag image element
 | ||||
|     DragDropTouch._rmvAtts = 'id,class,style,draggable'.split(','); | ||||
|     // synthesize and dispatch an event
 | ||||
|     // returns true if the event has been handled (e.preventDefault == true)
 | ||||
|     DragDropTouch._kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); | ||||
|     DragDropTouch._ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY,offsetX,offsetY'.split(','); | ||||
|     DragDropTouch_1.DragDropTouch = DragDropTouch; | ||||
| })(DragDropTouch || (DragDropTouch = {})); | ||||
| @ -2,8 +2,9 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Script graph demo</title> | ||||
|     <script src="DragDropTouch.js"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.14.0/js-yaml.min.js"></script> | ||||
|     <script src="index.js" type="module"></script> | ||||
|     <script src="index.js" type="module" defer></script> | ||||
|   </head> | ||||
|   <body style="margin: 24px"> | ||||
|   <style> | ||||
| @ -22,7 +23,7 @@ | ||||
|       white-space: pre-wrap; | ||||
|     } | ||||
|     #graph { | ||||
|       max-width: 400px; | ||||
|       max-width: 600px; | ||||
|       overflow: auto; | ||||
|     } | ||||
|     .code { | ||||
| @ -35,11 +36,6 @@ | ||||
|     <div> | ||||
|       <div class="card" id="graph"> | ||||
|       </div> | ||||
|       <div class="card" id="graph2"> | ||||
|         <script-graph3> | ||||
|           <script-graph-node></script-graph-node> | ||||
|         </script-graph3> | ||||
|       </div> | ||||
|       <div class="card code"> | ||||
|         <h1> Selected node code </h1> | ||||
|         <wc-codemirror mode="json" id="snippet"> | ||||
							
								
								
									
										240
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										240
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -70,24 +70,94 @@ | ||||
|       "resolved": "https://registry.npmjs.org/@vanillawc/wc-codemirror/-/wc-codemirror-1.8.10.tgz", | ||||
|       "integrity": "sha512-UKMD/UOpF1uRl29nlwvwQqSMBqsl+uDWYlGx82wIYbIBJkeqPrSo1Ez1rGi9jc1CL7/XwUr7u+l/kTwZAEkrEg==" | ||||
|     }, | ||||
|     "async": { | ||||
|       "version": "2.6.3", | ||||
|       "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", | ||||
|       "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "lodash": "^4.17.14" | ||||
|       } | ||||
|     }, | ||||
|     "basic-auth": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz", | ||||
|       "integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "builtin-modules": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", | ||||
|       "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "call-bind": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", | ||||
|       "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "function-bind": "^1.1.1", | ||||
|         "get-intrinsic": "^1.0.2" | ||||
|       } | ||||
|     }, | ||||
|     "colors": { | ||||
|       "version": "1.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", | ||||
|       "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "corser": { | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", | ||||
|       "integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "debug": { | ||||
|       "version": "3.2.7", | ||||
|       "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", | ||||
|       "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "ms": "^2.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "deepmerge": { | ||||
|       "version": "4.2.2", | ||||
|       "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", | ||||
|       "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "ecstatic": { | ||||
|       "version": "3.3.2", | ||||
|       "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz", | ||||
|       "integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "he": "^1.1.1", | ||||
|         "mime": "^1.6.0", | ||||
|         "minimist": "^1.1.0", | ||||
|         "url-join": "^2.0.5" | ||||
|       } | ||||
|     }, | ||||
|     "estree-walker": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", | ||||
|       "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "eventemitter3": { | ||||
|       "version": "4.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", | ||||
|       "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "follow-redirects": { | ||||
|       "version": "1.13.3", | ||||
|       "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", | ||||
|       "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "fsevents": { | ||||
|       "version": "2.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", | ||||
| @ -95,6 +165,73 @@ | ||||
|       "dev": true, | ||||
|       "optional": true | ||||
|     }, | ||||
|     "function-bind": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", | ||||
|       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "get-intrinsic": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", | ||||
|       "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "function-bind": "^1.1.1", | ||||
|         "has": "^1.0.3", | ||||
|         "has-symbols": "^1.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "has": { | ||||
|       "version": "1.0.3", | ||||
|       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", | ||||
|       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "function-bind": "^1.1.1" | ||||
|       } | ||||
|     }, | ||||
|     "has-symbols": { | ||||
|       "version": "1.0.2", | ||||
|       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", | ||||
|       "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "he": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", | ||||
|       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "http-proxy": { | ||||
|       "version": "1.18.1", | ||||
|       "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", | ||||
|       "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "eventemitter3": "^4.0.0", | ||||
|         "follow-redirects": "^1.0.0", | ||||
|         "requires-port": "^1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "http-server": { | ||||
|       "version": "0.12.3", | ||||
|       "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.3.tgz", | ||||
|       "integrity": "sha512-be0dKG6pni92bRjq0kvExtj/NrrAd28/8fCXkaI/4piTwQMSDSLMhWyW0NI1V+DBI3aa1HMlQu46/HjVLfmugA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "basic-auth": "^1.0.3", | ||||
|         "colors": "^1.4.0", | ||||
|         "corser": "^2.0.1", | ||||
|         "ecstatic": "^3.3.2", | ||||
|         "http-proxy": "^1.18.0", | ||||
|         "minimist": "^1.2.5", | ||||
|         "opener": "^1.5.1", | ||||
|         "portfinder": "^1.0.25", | ||||
|         "secure-compare": "3.0.1", | ||||
|         "union": "~0.5.0" | ||||
|       } | ||||
|     }, | ||||
|     "is-module": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", | ||||
| @ -114,6 +251,51 @@ | ||||
|       "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.2.1.tgz", | ||||
|       "integrity": "sha512-GSJHHXMGLZDzTRq59IUfL9FCdAlGfqNp/dEa7k7aBaaWD+JKaCjsAk9KYm2V12ItonVaYx2dprN66Zdm1AuBTQ==" | ||||
|     }, | ||||
|     "lodash": { | ||||
|       "version": "4.17.21", | ||||
|       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", | ||||
|       "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "mime": { | ||||
|       "version": "1.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", | ||||
|       "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "minimist": { | ||||
|       "version": "1.2.5", | ||||
|       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", | ||||
|       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "mkdirp": { | ||||
|       "version": "0.5.5", | ||||
|       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", | ||||
|       "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "minimist": "^1.2.5" | ||||
|       } | ||||
|     }, | ||||
|     "ms": { | ||||
|       "version": "2.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | ||||
|       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "object-inspect": { | ||||
|       "version": "1.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", | ||||
|       "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "opener": { | ||||
|       "version": "1.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", | ||||
|       "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "path-parse": { | ||||
|       "version": "1.0.6", | ||||
|       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", | ||||
| @ -126,6 +308,32 @@ | ||||
|       "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "portfinder": { | ||||
|       "version": "1.0.28", | ||||
|       "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", | ||||
|       "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "async": "^2.6.2", | ||||
|         "debug": "^3.1.1", | ||||
|         "mkdirp": "^0.5.5" | ||||
|       } | ||||
|     }, | ||||
|     "qs": { | ||||
|       "version": "6.10.1", | ||||
|       "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", | ||||
|       "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "side-channel": "^1.0.4" | ||||
|       } | ||||
|     }, | ||||
|     "requires-port": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", | ||||
|       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "resolve": { | ||||
|       "version": "1.17.0", | ||||
|       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", | ||||
| @ -144,6 +352,23 @@ | ||||
|         "fsevents": "~2.1.2" | ||||
|       } | ||||
|     }, | ||||
|     "secure-compare": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", | ||||
|       "integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "side-channel": { | ||||
|       "version": "1.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", | ||||
|       "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "call-bind": "^1.0.0", | ||||
|         "get-intrinsic": "^1.0.2", | ||||
|         "object-inspect": "^1.9.0" | ||||
|       } | ||||
|     }, | ||||
|     "tslib": { | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", | ||||
| @ -155,6 +380,21 @@ | ||||
|       "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", | ||||
|       "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "union": { | ||||
|       "version": "0.5.0", | ||||
|       "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", | ||||
|       "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "qs": "^6.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "url-join": { | ||||
|       "version": "2.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz", | ||||
|       "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", | ||||
|       "dev": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -6,13 +6,15 @@ | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "build": "rollup -c", | ||||
|     "watch": "rollup -c -w" | ||||
|     "watch": "rollup -c --watch", | ||||
|     "serve": "http-server ." | ||||
|   }, | ||||
|   "author": "", | ||||
|   "license": "ISC", | ||||
|   "devDependencies": { | ||||
|     "@rollup/plugin-node-resolve": "^9.0.0", | ||||
|     "@rollup/plugin-typescript": "^5.0.2", | ||||
|     "http-server": "^0.12.3", | ||||
|     "rollup": "^2.26.4", | ||||
|     "tslib": "^2.0.1", | ||||
|     "typescript": "^4.0.2" | ||||
|  | ||||
| @ -1,13 +1,11 @@ | ||||
| import resolve from "@rollup/plugin-node-resolve"; | ||||
| import nodeResolve from "@rollup/plugin-node-resolve"; | ||||
| import typescript from "@rollup/plugin-typescript"; | ||||
| 
 | ||||
| export default { | ||||
|   input: "./src/main.js", | ||||
|   output: [ | ||||
|     { | ||||
|       file: "index.js", | ||||
|       format: "cjs" | ||||
|     } | ||||
|   ], | ||||
|   plugins: [resolve(), typescript()] | ||||
|   input: "src/main.js", | ||||
|   output: { | ||||
|     file: "index.js", | ||||
|     format: "es", | ||||
|   }, | ||||
|   plugins: [nodeResolve(), typescript()], | ||||
| }; | ||||
|  | ||||
							
								
								
									
										104
									
								
								src/hat-graph/hat-graph-node.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/hat-graph/hat-graph-node.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| import { css, LitElement, property, svg } from "lit-element"; | ||||
| 
 | ||||
| const NODE_SIZE = 24; | ||||
| 
 | ||||
| export class HatGraphNode extends LitElement { | ||||
|   @property() iconPath?; | ||||
|   dragtarget = undefined; | ||||
|   config = undefined; | ||||
| 
 | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0"); | ||||
| 
 | ||||
|     this.addEventListener("dragstart", () => { | ||||
|       this.classList.add("dragging"); | ||||
|       (window as any)._dragElement = this.dragtarget ?? this; | ||||
|       this.updateNode(""); | ||||
|     }); | ||||
|     this.addEventListener("dragend", () => { | ||||
|       this.classList.remove("dragging"); | ||||
|       (window as any)._dragElement = undefined; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   updateNode(config) { | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent("update-node", { | ||||
|         detail: { config }, | ||||
|         bubbles: true, | ||||
|         composed: true, | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   deleteNode() { | ||||
|     this.dispatchEvent(new CustomEvent("delete-node", { bubbles: true })); | ||||
|   } | ||||
| 
 | ||||
|   placeNode(config) { | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent("place-node", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   get width() { | ||||
|     return NODE_SIZE + 5; | ||||
|   } | ||||
|   get height() { | ||||
|     return NODE_SIZE + 5; | ||||
|   } | ||||
|   render() { | ||||
|     return svg` | ||||
|     <svg | ||||
|       width="${this.width}" | ||||
|       height="${this.height}" | ||||
|       viewBox="${-this.width / 2} 0 ${this.width} ${this.height}" | ||||
|     > | ||||
|       <circle | ||||
|         cx="0" | ||||
|         cy="${this.width / 2}" | ||||
|         r="${NODE_SIZE / 2}" | ||||
|       /> | ||||
|       <g | ||||
|         style="pointer-events: none" | ||||
|         transform="translate(${-12} ${this.width / 2 - 12})" | ||||
|       > | ||||
|         ${this.iconPath ? svg`<path d="${this.iconPath}"/>` : ""} | ||||
|       </g> | ||||
|       </svg> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: flex; | ||||
|         --stroke-clr: var(--stroke-color, rgb(3, 169, 244)); | ||||
|         --hover-clr: var(--hover-color, rgb(255, 152, 0)); | ||||
|       } | ||||
|       :host(:hover) { | ||||
|         --stroke-clr: var(--hover-clr); | ||||
|       } | ||||
|       :host(:focus) { | ||||
|         --stroke-clr: green; | ||||
|         outline: none; | ||||
|       } | ||||
|       :host(.dragging) { | ||||
|         --stroke-clr: gray; | ||||
|         color: gray; | ||||
|       } | ||||
|       :host(.dragging) path { | ||||
|         stroke: gray; | ||||
|         fill: gray; | ||||
|       } | ||||
|       circle { | ||||
|         stroke: var(--stroke-clr); | ||||
|         stroke-width: 2; | ||||
|         fill: white; | ||||
|       } | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| customElements.define("hat-graph-node", HatGraphNode); | ||||
							
								
								
									
										183
									
								
								src/hat-graph/hat-graph.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/hat-graph/hat-graph.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,183 @@ | ||||
| import { css, html, LitElement, property, svg } from "lit-element"; | ||||
| 
 | ||||
| import "./hat-graph-node"; | ||||
| 
 | ||||
| const BRANCH_HEIGHT = 30; | ||||
| const BRANCH_CURVATURE = 25; | ||||
| const VERTICAL_SPACING = 10; | ||||
| const NODE_SIZE = 24; | ||||
| 
 | ||||
| export class HatGraph extends LitElement { | ||||
|   @property() _num_items = 0; | ||||
| 
 | ||||
|   @property({ reflect: true }) branching?; | ||||
|   config?; | ||||
| 
 | ||||
|   updateNode(config) { | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent("update-node", { | ||||
|         detail: { config }, | ||||
|         bubbles: true, | ||||
|         composed: true, | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   deleteNode() { | ||||
|     this.dispatchEvent(new CustomEvent("delete-node", { bubbles: true })); | ||||
|   } | ||||
| 
 | ||||
|   placeNode(config) { | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent("place-node", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   get width() { | ||||
|     let w = 0; | ||||
|     if (this.branching !== undefined) { | ||||
|       for (const c of this.children) { | ||||
|         if (c.slot === "head") continue; | ||||
|         w += (c as any).width; | ||||
|       } | ||||
|     } else { | ||||
|       for (const c of this.children) { | ||||
|         w = Math.max(w, (c as any).width); | ||||
|       } | ||||
|     } | ||||
|     return w; | ||||
|   } | ||||
|   get height() { | ||||
|     let h = 0; | ||||
|     if (this.branching !== undefined) { | ||||
|       for (const c of this.children) { | ||||
|         if (c.slot === "head") continue; | ||||
|         h = Math.max(h, (c as any).height); | ||||
|       } | ||||
|       h += 2 * BRANCH_HEIGHT; | ||||
|     } else { | ||||
|       for (const c of this.children) { | ||||
|         h += (c as any).height + VERTICAL_SPACING; | ||||
|       } | ||||
|       h; | ||||
|     } | ||||
|     return h; | ||||
|   } | ||||
| 
 | ||||
|   async updateChildren() { | ||||
|     this._num_items = this.children.length; | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     let branch_x = []; | ||||
|     let total = 0; | ||||
|     if (this.branching !== undefined) { | ||||
|       for (const c of Array.from(this.children)) { | ||||
|         if (c.slot === "head") continue; | ||||
|         const rect = c.getBoundingClientRect(); | ||||
|         branch_x.push(rect.width / 2 + total); | ||||
|         total += rect.width; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const line_end = this.height - BRANCH_HEIGHT; | ||||
| 
 | ||||
|     return html` | ||||
|       <svg width="${this.width}" height="${this.height}"> | ||||
|         <rect | ||||
|           x="0" | ||||
|           y="0" | ||||
|           width="${this.width}" | ||||
|           height="${this.height}" | ||||
|           fill="white" | ||||
|         /> | ||||
|         ${this.branching !== undefined | ||||
|           ? branch_x.map((x) => { | ||||
|               return svg` | ||||
|                 <path | ||||
|                   class="line" | ||||
|                   d=" | ||||
|                     M ${this.width / 2} 0 | ||||
|                     C ${this.width / 2} ${BRANCH_CURVATURE} | ||||
|                       ${x} ${BRANCH_HEIGHT - BRANCH_CURVATURE} | ||||
|                       ${x} ${BRANCH_HEIGHT} | ||||
|                     L ${x} ${line_end} | ||||
|                     C ${x} ${line_end + BRANCH_CURVATURE} | ||||
|                       ${this.width / 2} ${this.height - BRANCH_CURVATURE} | ||||
|                       ${this.width / 2} ${this.height} | ||||
|                   " | ||||
|                 /> | ||||
|               `;
 | ||||
|             }) | ||||
|           : svg` | ||||
|               <path | ||||
|               class="line" | ||||
|               d=" | ||||
|                 M ${this.width / 2} 0 | ||||
|                 L ${this.width / 2} ${this.height} | ||||
|               " | ||||
|               /> | ||||
|             `}
 | ||||
|       </svg> | ||||
|       <slot name="head"> </slot> | ||||
|       <div | ||||
|         id="branches" | ||||
|         class="${this.querySelector(":scope > [slot='head']") ? "" : "no-head"}" | ||||
|       > | ||||
|         <slot @slotchange=${this.updateChildren}></slot> | ||||
|       </div> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         position: relative; | ||||
|         display: flex; | ||||
|         --stroke-clr: var(--stroke-color, rgb(3, 169, 244)); | ||||
|         --hover-clr: var(--hover-color, rgb(255, 152, 0)); | ||||
|       } | ||||
|       #branches { | ||||
|         position: absolute; | ||||
|         left: 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|       } | ||||
|       :host([branching]) #branches { | ||||
|         top: ${BRANCH_HEIGHT}px; | ||||
|         flex-direction: row; | ||||
|         align-items: start; | ||||
|       } | ||||
|       :host(:not([branching])) #branches { | ||||
|         top: ${VERTICAL_SPACING + NODE_SIZE}px; /* SHould be something else*/ | ||||
|         flex-direction: column; | ||||
|       } | ||||
|       :host(:not([branching])) #branches.no-head { | ||||
|         top: 0; | ||||
|       } | ||||
|       path.line { | ||||
|         stroke: var(--stroke-clr); | ||||
|         stroke-width: 2; | ||||
|         fill: none; | ||||
|       } | ||||
|       :host(:not([branching])) ::slotted(*) { | ||||
|         margin-bottom: 10px; | ||||
|       } | ||||
|       ::slotted(:last-child) { | ||||
|         margin-bottom: 0; | ||||
|       } | ||||
|       ::slotted([slot="head"]) { | ||||
|         position: absolute; | ||||
|         top: ${BRANCH_HEIGHT / 2}px; | ||||
|         left: 50%; | ||||
|         transform: translate(-50%, -50%); | ||||
|       } | ||||
|       :host(:focus-within) ::slotted([slot="head"]) { | ||||
|         --stroke-color: green; | ||||
|       } | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| customElements.define("hat-graph", HatGraph); | ||||
							
								
								
									
										259
									
								
								src/hat-graph/make-graph.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/hat-graph/make-graph.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,259 @@ | ||||
| import { HatGraphNode } from "./hat-graph-node"; | ||||
| 
 | ||||
| import { | ||||
|   mdiCallSplit, | ||||
|   mdiAbTesting, | ||||
|   mdiCheck, | ||||
|   mdiClose, | ||||
|   mdiChevronRight, | ||||
|   mdiExclamation, | ||||
|   mdiTimerOutline, | ||||
|   mdiTrafficLight, | ||||
|   mdiRefresh, | ||||
|   mdiArrowUp, | ||||
|   mdiCodeJson, | ||||
|   mdiCheckBoxOutline, | ||||
|   mdiCheckboxBlankOutline, | ||||
|   mdiAsterisk, | ||||
| } from "@mdi/js"; | ||||
| import { HatGraph } from "./hat-graph"; | ||||
| import { html } from "lit-element"; | ||||
| import { render } from "lit-html"; | ||||
| 
 | ||||
| const OPTIONS = [ | ||||
|   "condition", | ||||
|   "delay", | ||||
|   "device_id", | ||||
|   "event", | ||||
|   "scene", | ||||
|   "service", | ||||
|   "wait_template", | ||||
|   "repeat", | ||||
|   "choose", | ||||
| ]; | ||||
| 
 | ||||
| const ICONS = { | ||||
|   new: mdiAsterisk, | ||||
|   service: mdiChevronRight, | ||||
|   condition: mdiAbTesting, | ||||
|   TRUE: mdiCheck, | ||||
|   FALSE: mdiClose, | ||||
|   delay: mdiTimerOutline, | ||||
|   wait_template: mdiTrafficLight, | ||||
|   event: mdiExclamation, | ||||
|   repeat: mdiRefresh, | ||||
|   repeatReturn: mdiArrowUp, | ||||
|   choose: mdiCallSplit, | ||||
|   chooseChoice: mdiCheckBoxOutline, | ||||
|   chooseDefault: mdiCheckboxBlankOutline, | ||||
|   YAML: mdiCodeJson, | ||||
| }; | ||||
| 
 | ||||
| const SPECIAL_NODES = { | ||||
|   condition: makeConditionNode, | ||||
|   choose: makeChooseNode, | ||||
|   repeat: makeRepeatNode, | ||||
| }; | ||||
| 
 | ||||
| function makeConditionNode(config) { | ||||
|   const graph = document.createElement("hat-graph") as HatGraph; | ||||
|   graph.branching = true; | ||||
|   const focused = () => | ||||
|     graph.dispatchEvent( | ||||
|       new CustomEvent("node-selected", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
| 
 | ||||
|   render( | ||||
|     html` | ||||
|       <hat-graph-node slot="head" .iconPath=${mdiAbTesting} @focus=${focused}> | ||||
|       </hat-graph-node> | ||||
|       <hat-graph-node .iconPath=${mdiCheck} @focus=${focused}></hat-graph-node> | ||||
|       <hat-graph-node .iconPath=${mdiClose} @focus=${focused}></hat-graph-node> | ||||
|     `,
 | ||||
|     graph | ||||
|   ); | ||||
|   return graph; | ||||
| } | ||||
| 
 | ||||
| function makeChooseNode(config) { | ||||
|   const graph = document.createElement("hat-graph") as HatGraph; | ||||
|   graph.config = config; | ||||
|   graph.branching = true; | ||||
| 
 | ||||
|   const focused = () => | ||||
|     graph.dispatchEvent( | ||||
|       new CustomEvent("node-selected", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
| 
 | ||||
|   render( | ||||
|     html` | ||||
|       <hat-graph-node | ||||
|         slot="head" | ||||
|         .iconPath="${mdiCallSplit}" | ||||
|         @focus=${focused} | ||||
|         draggable="true" | ||||
|         .dragtarget=${graph} | ||||
|       > | ||||
|       </hat-graph-node> | ||||
|       ${config.choose?.map((branch) => { | ||||
|         const head = document.createElement("hat-graph-node") as HatGraphNode; | ||||
|         head.iconPath = mdiCheckBoxOutline; | ||||
|         head.addEventListener("focus", focused); | ||||
| 
 | ||||
|         return makeGraph(branch.sequence, head); | ||||
|       })} | ||||
|       ${(() => { | ||||
|         const head = document.createElement("hat-graph-node") as HatGraphNode; | ||||
|         head.iconPath = mdiCheckboxBlankOutline; | ||||
|         head.addEventListener("focus", focused); | ||||
|         return makeGraph(config.default, head); | ||||
|       })()} | ||||
|     `,
 | ||||
|     graph | ||||
|   ); | ||||
|   return graph; | ||||
| } | ||||
| 
 | ||||
| function makeRepeatNode(config) { | ||||
|   const graph = document.createElement("hat-graph") as HatGraph; | ||||
|   graph.branching = true; | ||||
| 
 | ||||
|   const focused = () => | ||||
|     graph.dispatchEvent( | ||||
|       new CustomEvent("node-selected", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
| 
 | ||||
|   render( | ||||
|     html` | ||||
|       <hat-graph-node | ||||
|         .iconPath=${mdiArrowUp} | ||||
|         @focus=${focused} | ||||
|       ></hat-graph-node> | ||||
|       ${makeGraph(config.repeat.sequence)} | ||||
|     `,
 | ||||
|     graph | ||||
|   ); | ||||
|   return graph; | ||||
| } | ||||
| 
 | ||||
| function makeNode(config) { | ||||
|   if (typeof config === "string") return undefined; | ||||
|   const type = OPTIONS.find((key) => key in config) || "yaml"; | ||||
| 
 | ||||
|   if (type in SPECIAL_NODES) { | ||||
|     return SPECIAL_NODES[type](config); | ||||
|   } | ||||
| 
 | ||||
|   const node = document.createElement("hat-graph-node") as HatGraphNode; | ||||
| 
 | ||||
|   node.iconPath = ICONS[type]; | ||||
|   node.draggable = true; | ||||
| 
 | ||||
|   node.config = config; | ||||
| 
 | ||||
|   node.addEventListener("focus", (ev) => { | ||||
|     node.dispatchEvent( | ||||
|       new CustomEvent("node-selected", { detail: { config }, bubbles: true }) | ||||
|     ); | ||||
|   }); | ||||
| 
 | ||||
|   return node; | ||||
| } | ||||
| 
 | ||||
| export function makeGraph(nodes, head = undefined) { | ||||
|   const graph = document.createElement("hat-graph") as HatGraph; | ||||
|   graph.addEventListener("dragenter", (ev) => { | ||||
|     ev.stopPropagation(); | ||||
|     ev.preventDefault(); | ||||
|     try { | ||||
|       graph.appendChild((window as any)._dragElement); | ||||
|     } catch (e) { | ||||
|       if (!(e instanceof DOMException)) throw e; | ||||
|     } | ||||
|   }); | ||||
|   (graph as any).test = "Hello!"; | ||||
| 
 | ||||
|   if (head) { | ||||
|     head.slot = "head"; | ||||
|     graph.appendChild(head); | ||||
|   } | ||||
| 
 | ||||
|   for (const [i, nodeConfig] of nodes.entries()) { | ||||
|     const node = makeNode(nodeConfig); | ||||
|     if (!node) { | ||||
|       window.setTimeout(() => { | ||||
|         const config = [...nodes]; | ||||
|         config.splice(i, 1); | ||||
| 
 | ||||
|         graph.dispatchEvent( | ||||
|           new CustomEvent("update-node", { detail: { config }, bubbles: true }) | ||||
|         ); | ||||
|       }, 100); | ||||
|       continue; | ||||
|     } | ||||
| 
 | ||||
|     node.addEventListener("dragover", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
|       ev.preventDefault(); | ||||
|     }); | ||||
|     node.addEventListener("dragenter", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
|       ev.preventDefault(); | ||||
|       if (node === (window as any)._dragElement) return; | ||||
|       try { | ||||
|         graph.insertBefore((window as any)._dragElement, node); | ||||
|         (window as any)._dragTarget = node; | ||||
|       } catch (e) { | ||||
|         if (!(e instanceof DOMException)) throw e; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     node.addEventListener("drop", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
|       ev.preventDefault(); | ||||
|       if ((window as any)._dragTarget) { | ||||
|         console.log("Drop onto ", (window as any)._dragTarget); | ||||
|         const config = { ...(window as any)._dragElement.config }; | ||||
|         (window as any)._dragTarget.placeNode(config); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     node.addEventListener("update-node", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
| 
 | ||||
|       const config = [...nodes]; | ||||
|       config[i] = ev.detail.config; | ||||
| 
 | ||||
|       graph.dispatchEvent( | ||||
|         new CustomEvent("update-node", { detail: { config }, bubbles: true }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     node.addEventListener("delete-node", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
| 
 | ||||
|       const config = [...nodes]; | ||||
|       config.splice(i, 1); | ||||
| 
 | ||||
|       graph.dispatchEvent( | ||||
|         new CustomEvent("update-node", { detail: { config }, bubbles: true }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     node.addEventListener("place-node", (ev) => { | ||||
|       ev.stopPropagation(); | ||||
| 
 | ||||
|       const config = [...nodes]; | ||||
|       config.splice(i, 0, ev.detail.config); | ||||
|       console.log(config); | ||||
| 
 | ||||
|       graph.dispatchEvent( | ||||
|         new CustomEvent("update-node", { detail: { config }, bubbles: true }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     graph.appendChild(node); | ||||
|   } | ||||
| 
 | ||||
|   return graph; | ||||
| } | ||||
							
								
								
									
										80
									
								
								src/main.js
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								src/main.js
									
									
									
									
									
								
							| @ -1,8 +1,11 @@ | ||||
| import {demoConfig} from "./demo-config"; | ||||
| import { demoConfig2 } from "./demo-config"; | ||||
| import "@vanillawc/wc-codemirror"; | ||||
| 
 | ||||
| import "./script-graph"; | ||||
| import "./script-graph3"; | ||||
| // import "./script-graph3";
 | ||||
| import "./hat-graph/hat-graph-node"; | ||||
| import "./hat-graph/hat-graph"; | ||||
| import { makeGraph } from "./hat-graph/make-graph"; | ||||
| 
 | ||||
| import { ActionHandler } from "./script-to-graph"; | ||||
| 
 | ||||
| @ -12,18 +15,18 @@ let index_counter = 0; | ||||
| let nodes = []; | ||||
| 
 | ||||
| window.onload = () => { | ||||
| 
 | ||||
|   const graph2 = document.createElement("script-graph3"); | ||||
|   document.querySelector("#graph2").appendChild(graph2); | ||||
|   return; | ||||
|   //const graph2 = document.createElement("script-graph3");
 | ||||
|   //document.querySelector("#graph2").appendChild(graph2);
 | ||||
| 
 | ||||
|   let src = demoConfig; | ||||
| 
 | ||||
|   const fullcode = document.querySelector("#fullcode"); | ||||
|   fullcode.mode = "yaml"; | ||||
|   window.setTimeout(()=> fullcode.value = jsyaml.safeDump(src), 100); | ||||
|   window.setTimeout(() => (fullcode.value = jsyaml.safeDump(src)), 100); | ||||
| 
 | ||||
|   const updateButton = document.querySelector("#updateButton"); | ||||
|   updateButton.addEventListener('click', () => { | ||||
|   updateButton.addEventListener("click", () => { | ||||
|     src = jsyaml.safeLoad(fullcode.value); | ||||
|     index_counter = 0; | ||||
|     nodes = []; | ||||
| @ -43,11 +46,68 @@ window.onload = () => { | ||||
|     graph.tree = tr.graph; | ||||
|     const code = document.querySelector("#snippet"); | ||||
|     code.value = jsyaml.safeDump(action); | ||||
|     document.querySelector("#saveSnippet").onclick = () => update(jsyaml.safeLoad(code.value)); | ||||
|     document.querySelector("#deleteSnippet").onclick = () => update(jsyaml.safeLoad(null)); | ||||
|   } | ||||
|     document.querySelector("#saveSnippet").onclick = () => | ||||
|       update(jsyaml.safeLoad(code.value)); | ||||
|     document.querySelector("#deleteSnippet").onclick = () => | ||||
|       update(jsyaml.safeLoad(null)); | ||||
|   }; | ||||
| 
 | ||||
|   graph.tree = tr.graph; | ||||
|   document.querySelector("#graph").appendChild(graph); | ||||
| }; | ||||
| 
 | ||||
| function nodeSelected(ev) { | ||||
|   const code = document.querySelector("#snippet"); | ||||
|   code.value = jsyaml.safeDump(ev.detail.config); | ||||
|   code.currentNode = ev.target; | ||||
| } | ||||
| 
 | ||||
| function rebuildGraph(config) { | ||||
|   const graphCard = document.querySelector("#graph"); | ||||
| 
 | ||||
|   while (graphCard.firstChild) graphCard.removeChild(graphCard.firstChild); | ||||
| 
 | ||||
|   const graph = makeGraph(config); | ||||
|   graph.addEventListener("node-selected", nodeSelected); | ||||
|   graph.addEventListener("update-node", (ev) => { | ||||
|     const code = document.querySelector("#fullcode"); | ||||
|     code.value = jsyaml.safeDump(ev.detail.config); | ||||
|     window.setTimeout(() => rebuildGraph(ev.detail.config), 100); | ||||
|   }); | ||||
| 
 | ||||
|   graphCard.appendChild(graph); | ||||
| } | ||||
| 
 | ||||
| function setup_snippet_editor() { | ||||
|   document.querySelector("#saveSnippet").onclick = () => { | ||||
|     const code = document.querySelector("#snippet"); | ||||
|     if (code.currentNode) { | ||||
|       code.currentNode.updateNode(jsyaml.safeLoad(code.value)); | ||||
|     } | ||||
|   }; | ||||
|   document.querySelector("#deleteSnippet").onclick = () => { | ||||
|     const code = document.querySelector("#snippet"); | ||||
|     if (code.currentNode) { | ||||
|       code.currentNode.deleteNode(); | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function setup() { | ||||
|   setup_snippet_editor(); | ||||
| 
 | ||||
|   let src = demoConfig2; | ||||
|   const fullcode = document.querySelector("#fullcode"); | ||||
|   fullcode.mode = "yaml"; | ||||
|   window.setTimeout(() => (fullcode.value = jsyaml.safeDump(src)), 100); | ||||
| 
 | ||||
|   rebuildGraph(src); | ||||
| 
 | ||||
|   const updateButton = document.querySelector("#updateButton"); | ||||
|   updateButton.addEventListener("click", () => { | ||||
|     src = jsyaml.safeLoad(fullcode.value); | ||||
|     rebuildGraph(src); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| setup(); | ||||
|  | ||||
| @ -4,107 +4,295 @@ import { | ||||
|   css, | ||||
|   svg, | ||||
|   property, | ||||
|   TemplateResult | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| 
 | ||||
| import { mdiAsterisk } from "@mdi/js"; | ||||
| import { | ||||
|   mdiCallSplit, | ||||
|   mdiAbTesting, | ||||
|   mdiCheck, | ||||
|   mdiClose, | ||||
|   mdiChevronRight, | ||||
|   mdiExclamation, | ||||
|   mdiTimerOutline, | ||||
|   mdiTrafficLight, | ||||
|   mdiRefresh, | ||||
|   mdiArrowUp, | ||||
|   mdiCodeJson, | ||||
|   mdiCheckBoxOutline, | ||||
|   mdiCheckboxBlankOutline, | ||||
|   mdiAsterisk, | ||||
|   mdiCircleOutline, | ||||
| } from "@mdi/js"; | ||||
| 
 | ||||
| const SIZE = 35; | ||||
| const DIST = 20; | ||||
| const ICONS = { | ||||
|   "call-split": mdiCallSplit, | ||||
|   "ab-testing": mdiAbTesting, | ||||
|   check: mdiCheck, | ||||
|   close: mdiClose, | ||||
|   "chevron-right": mdiChevronRight, | ||||
|   exclamation: mdiExclamation, | ||||
|   asterisk: mdiAsterisk, | ||||
| }; | ||||
| 
 | ||||
| interface GraphNode extends LitElement{ | ||||
| const SIZE = 24; | ||||
| 
 | ||||
| interface GraphNode extends LitElement { | ||||
|   render_svg(): TemplateResult; | ||||
|   width: number; | ||||
|   height: number; | ||||
| } | ||||
| 
 | ||||
| class ScriptGraphNode extends LitElement { | ||||
| 
 | ||||
|   @property() icon = mdiAsterisk; | ||||
| 
 | ||||
|   get width() { | ||||
|     return SIZE; | ||||
|   } | ||||
|   get height() { | ||||
|     return SIZE + DIST; | ||||
|   } | ||||
| 
 | ||||
|   render_svg() { | ||||
|     return svg` | ||||
|       <circle | ||||
|         cx="0" | ||||
|         cy="${this.width/2}" | ||||
|         r="${SIZE/2}" | ||||
|       /> | ||||
|       <g style="pointer-events: none" transform="translate(-12, ${this.width/2-12})"> | ||||
|       </g> | ||||
|     ` | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| class ScriptGraph3 extends LitElement { | ||||
| 
 | ||||
|   @property() content = []; | ||||
|   @property() _width = 0; | ||||
|   @property() _height = 0; | ||||
|   @property() icon = "chevron-right"; | ||||
| 
 | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     console.log(this.querySelectorAll('*')) | ||||
|     if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0"); | ||||
|   } | ||||
| 
 | ||||
|   get width() { | ||||
|     return SIZE + 5; | ||||
|   } | ||||
|   get height() { | ||||
|     return SIZE + 5; | ||||
|   } | ||||
|   render() { | ||||
|     return svg` | ||||
|     <svg | ||||
|       width="${this.width}" | ||||
|       height="${this.height}" | ||||
|       viewBox="${-this.width / 2} 0 ${this.width} ${this.height}" | ||||
|     > | ||||
|       <circle | ||||
|         cx="0" | ||||
|         cy="${this.width / 2}" | ||||
|         r="${SIZE / 2}" | ||||
|       /> | ||||
|       <g | ||||
|         style="pointer-events: none" | ||||
|         transform="translate(${-12} ${this.width / 2 - 12})" | ||||
|       > | ||||
|         ${ | ||||
|           ICONS[this.icon] | ||||
|             ? svg` | ||||
|           <path d="${ICONS[this.icon]}"/> | ||||
|           ` | ||||
|             : "" | ||||
|         } | ||||
|       </g> | ||||
|       </svg> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: flex; | ||||
|         --stroke-clr: var(--stroke-color, rgb(3, 169, 244)); | ||||
|         --hover-clr: var(--hover-color, rgb(255, 152, 0)); | ||||
|       } | ||||
|       circle { | ||||
|         stroke: var(--stroke-clr); | ||||
|         stroke-width: 2; | ||||
|         fill: white; | ||||
|       } | ||||
|       :host(:hover) { | ||||
|         --stroke-clr: var(--hover-clr); | ||||
|       } | ||||
|       :host(:focus) { | ||||
|         --stroke-clr: green; | ||||
|         outline: none; | ||||
|       } | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ScriptGraphBranch extends LitElement { | ||||
|   @property() _num_items = 0; | ||||
|   @property() _branch_height = 30; | ||||
|   @property() _branch_curve = 25; | ||||
| 
 | ||||
|   get width() { | ||||
|     let w = 0; | ||||
|     for (const c of this.children) { | ||||
|       w += (c as any).width ?? 0; | ||||
|     } | ||||
|     return w; | ||||
|   } | ||||
|   get height() { | ||||
|     let h = 0; | ||||
|     for (const c of this.children) { | ||||
|       h = Math.max(h, (c as any).height ?? 0); | ||||
|     } | ||||
|     return h + 2 * this._branch_height; | ||||
|   } | ||||
| 
 | ||||
|   async updateChildren() { | ||||
|     this.content = []; | ||||
|     let y = 0; | ||||
|     let w = 0; | ||||
|     for (const e of this.querySelectorAll('*') as NodeListOf<GraphNode>) { | ||||
|       this.content.push({ | ||||
|         svg: e.render_svg(), | ||||
|         offset_y: y, | ||||
|       }); | ||||
|       y += e.height; | ||||
|       w = Math.max(w, e.width); | ||||
|     this._num_items = this.children.length; | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     let branch_x = []; | ||||
|     let total = 0; | ||||
|     for (const c of Array.from(this.children)) { | ||||
|       const rect = c.getBoundingClientRect(); | ||||
|       branch_x.push(rect.width / 2 + total); | ||||
|       total += rect.width; | ||||
|     } | ||||
|     this._width = w; | ||||
|     this._height = y; | ||||
|     this.requestUpdate(); | ||||
| 
 | ||||
|     const line_end = this.height - this._branch_height; | ||||
| 
 | ||||
|     return html` | ||||
|       <svg width="${this.width}" height="${this.height}"> | ||||
|         <rect | ||||
|           x="0" | ||||
|           y="0" | ||||
|           width="${this.width}" | ||||
|           height="${this.height}" | ||||
|           fill="white" | ||||
|         /> | ||||
|         ${branch_x.map((x) => { | ||||
|           return svg` | ||||
|               <path | ||||
|                 class="line" | ||||
|                 d=" | ||||
|                   M ${this.width / 2} 0 | ||||
|                   C ${this.width / 2} ${this._branch_curve} | ||||
|                     ${x} ${this._branch_height - this._branch_curve} | ||||
|                     ${x} ${this._branch_height} | ||||
|                   L ${x} ${line_end} | ||||
|                   C ${x} ${line_end + this._branch_curve} | ||||
|                     ${this.width / 2} ${this.height - this._branch_curve} | ||||
|                     ${this.width / 2} ${this.height} | ||||
|                 " | ||||
|               /> | ||||
|             `;
 | ||||
|         })} | ||||
|       </svg> | ||||
|       <script-graph-node id="head" .icon=${"call-split"}> </script-graph-node> | ||||
|       <div id="branches" style="top: ${this._branch_height}px;"> | ||||
|         <slot @slotchange=${this.updateChildren}></slot> | ||||
|       </div> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         position: relative; | ||||
|         display: flex; | ||||
|         --stroke-clr: var(--stroke-color, rgb(3, 169, 244)); | ||||
|         --hover-clr: var(--hover-color, rgb(255, 152, 0)); | ||||
|       } | ||||
|       #branches { | ||||
|         position: absolute; | ||||
|         top: 20px; | ||||
|         left: 0; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|       } | ||||
|       path.line { | ||||
|         stroke: var(--stroke-clr); | ||||
|         stroke-width: 2; | ||||
|         fill: white; | ||||
|       } | ||||
|       #head { | ||||
|         position: Absolute; | ||||
|         top: 5px; | ||||
|         left: 50%; | ||||
|         transform: translate(-50%, -50%); | ||||
|       } | ||||
|       :host(:focus-within) #head { | ||||
|         --stroke-color: green; | ||||
|       } | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class ScriptGraph3 extends LitElement { | ||||
|   @property() content = []; | ||||
|   @property() _width = 0; | ||||
|   @property() _height = 0; | ||||
|   @property() _distance = 20; | ||||
| 
 | ||||
|   async updateChildren() { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   childrenChangedCallback() { | ||||
|     console.log("Children changed"); | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
| 
 | ||||
| 
 | ||||
|     let y = 0; | ||||
|     let nodes = []; | ||||
|     for (const e of this.content) { | ||||
| 
 | ||||
|   get height() { | ||||
|     let h = 0; | ||||
|     for (const c of this.children) { | ||||
|       h += (c as any).height ?? 0; | ||||
|       h += this._distance; | ||||
|     } | ||||
| 
 | ||||
|     return html` | ||||
|     <svg width=500 height=500> | ||||
|       ${this.content.map(e => | ||||
|         svg` | ||||
|           <g transform="translate(${this._width/2} ${e.offset_y})"> | ||||
|           ${e.svg} | ||||
|           </g> | ||||
|         `)}
 | ||||
|     </svg> | ||||
|     <slot | ||||
|     @slotchange=${this.updateChildren} | ||||
|     ></slot> | ||||
|     <style> | ||||
|       slot { | ||||
|         display: none; | ||||
|       } | ||||
|       </style> | ||||
|     ` | ||||
|     return h + this._distance; | ||||
|   } | ||||
| 
 | ||||
|   get width() { | ||||
|     let w = 0; | ||||
|     for (const c of this.children) { | ||||
|       w = Math.max(w, (c as any).width ?? 0); | ||||
|     } | ||||
|     return w; | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     return html` | ||||
|       <svg width="${this.width}" height="${this.height}"> | ||||
|         <rect | ||||
|           x="0" | ||||
|           y="0" | ||||
|           width="${this.width}" | ||||
|           height="${this.height}" | ||||
|           fill="white" | ||||
|         /> | ||||
|         <path | ||||
|           class="line" | ||||
|           d=" | ||||
|             M ${this.width / 2} 0 | ||||
|             L ${this.width / 2} ${this.height} | ||||
|           " | ||||
|         /> | ||||
|       </svg> | ||||
|       <div id="nodes" style="--distance: ${this._distance}px;"> | ||||
|         <slot @slotchange=${this.updateChildren}></slot> | ||||
|       </div> | ||||
|     `;
 | ||||
|   } | ||||
| 
 | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         position: relative; | ||||
|         display: flex; | ||||
|         --stroke-clr: var(--stroke-color, rgb(3, 169, 244)); | ||||
|         --hover-clr: var(--hover-color, rgb(255, 152, 0)); | ||||
|       } | ||||
|       #nodes { | ||||
|         position: absolute; | ||||
|         top: var(--distance, 10px); | ||||
|         left: 0; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|       } | ||||
|       ::slotted(*) { | ||||
|         padding-bottom: var(--distance, 10px); | ||||
|       } | ||||
|       path.line { | ||||
|         stroke: var(--stroke-clr); | ||||
|         stroke-width: 2; | ||||
|         fill: white; | ||||
|       } | ||||
|     `;
 | ||||
|   } | ||||
| } | ||||
| customElements.define("script-graph3", ScriptGraph3); | ||||
| customElements.define("script-graph-node", ScriptGraphNode); | ||||
| customElements.define("script-graph-branch", ScriptGraphBranch); | ||||
|  | ||||
| @ -1,18 +1,9 @@ | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "es6", | ||||
|     "module": "ESNext", | ||||
|     "target": "es2017", | ||||
|     "moduleResolution": "node", | ||||
|     "sourceMap": true, | ||||
|     "emitDecoratorMetadata": true, | ||||
|     "experimentalDecorators": true, | ||||
|     "removeComments": false, | ||||
|     "noImplicitAny": false | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src/**/*.ts" | ||||
|   ], | ||||
|   "exclude": [ | ||||
|     "node_modules" | ||||
|   ] | ||||
|     "resolveJsonModule": true, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "experimentalDecorators": true | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user