├── .gitignore ├── .babelrc ├── bower.json ├── README.md ├── package.json ├── webpack.config.js ├── src ├── create.js ├── graph.js └── index.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [] 6 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litmus", 3 | "dependencies": { 4 | "alkali": "~0.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Litmus is an explorer for Alkali variables and element updaters. It is recommended that you load litmus with a bookmarklet, found on the pages: 2 | [Litmus](https://kriszyp.github.com/litmus) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litmus", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "build": "webpack", 6 | "watch": "webpack --watch" 7 | }, 8 | "devDependencies": { 9 | "babel-cli": "^6.6.5", 10 | "babel-core": "^6.7.4", 11 | "babel-loader": "^6.2.4", 12 | "babel-preset-es2015": "^6.6.0", 13 | "webpack": "^1.12.14" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src', 3 | output: { 4 | path: 'builds', 5 | filename: 'bundle.js', 6 | }, 7 | module: { 8 | loaders: [ 9 | { 10 | include: /src/, 11 | test: /\.js/, 12 | loader: 'babel-loader' 13 | } 14 | ] 15 | } 16 | }; -------------------------------------------------------------------------------- /src/create.js: -------------------------------------------------------------------------------- 1 | define([], function(){ 2 | // an API designed for creating elements with style. Not something you would normally want to do, 3 | // unless you happen to be building a UI from a bookmarklet 4 | var create = function(parent, tagName, styles){ 5 | var element = parent.appendChild(document.createElement(tagName)) 6 | for(var name in styles){ 7 | element.style[name] = styles[name] 8 | } 9 | return element 10 | } 11 | function makeTriangle(parent, size){ 12 | return create(parent, 'div', { 13 | width: '0', 14 | height: '0', 15 | maringRight: '6px', 16 | borderTop: size + 'px solid transparent', 17 | borderBottom: size + 'px solid transparent', 18 | borderLeft: size + 'px solid #321' 19 | }) 20 | } 21 | create.triangle = makeTriangle 22 | return create 23 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kris Zyp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/graph.js: -------------------------------------------------------------------------------- 1 | define(['./create'], function(create){ 2 | // laying out the graph of connected nodes 3 | var columns = [] 4 | var container 5 | var boundX = window.innerWidth 6 | var boundY = window.innerHeight 7 | var allNodes = [] 8 | function tryPosition(position){ 9 | var column = columns[position.x] 10 | if(!column){ 11 | columns[position.x] = column = [] 12 | } 13 | if(position.x < 0 || position.x > boundX) { 14 | return { 15 | moved: Infinity 16 | } 17 | } 18 | var newPosition = { 19 | x: position.x, 20 | y: position.y, 21 | moved: 0 22 | } 23 | for(var i = 0, l = column.length; i < l; i++){ 24 | var cell = column[i] 25 | if(cell.y < position.y + position.height || cell.y + cell.height > position.y){ 26 | // overlap with this cell 27 | var position 28 | if(cell.y + cell.height / 2 < position.y + position.height / 2){ 29 | // move down 30 | newPosition = { 31 | x: position.x, 32 | y: cell.y + cell.height + 5, 33 | moved: cell.y + cell.height - position.y + 5 34 | } 35 | }else{ 36 | // move up 37 | newPosition = { 38 | x: position.x, 39 | y: cell.y - position.height - 5, 40 | moved: (position.y + position.height) - cell.y + 5 41 | } 42 | } 43 | break 44 | } 45 | } 46 | if(position.height + newPosition.y > boundY){ 47 | newPosition.moved += position.height + newPosition.y - boundY 48 | newPosition.y = boundY - position.height 49 | } 50 | if(newPosition.y < 0){ 51 | newPosition.moved -= newPosition.y 52 | newPosition.y = 0 53 | } 54 | return newPosition 55 | } 56 | 57 | function findPosition(from){ 58 | var best, bestScore = 1 59 | var fromX = from.x 60 | var fromY = from.y 61 | var bestProximity = Infinity 62 | var bestPosition 63 | tryDirections([[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]], 100) 64 | console.log('good enough from first round', bestProximity < 0.000000001) 65 | if (bestProximity > 0.000000001) { 66 | tryDirections([[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]], 50) 67 | tryDirections([[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]], 200) 68 | tryDirections([[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]], 400) 69 | } 70 | function tryDirections(directions, multiplier){ 71 | for(var i = 0; i < directions.length; i++){ 72 | var directionX = directions[i][0] * multiplier 73 | var directionY = directions[i][1] * multiplier 74 | var proposedX = fromX + directionX 75 | var proposedY = fromY + directionY 76 | if (proposedX + from.width > boundX) { 77 | proposedX = boundX - from.width 78 | } 79 | if (proposedY + from.height > boundY) { 80 | proposedY = boundY - from.height 81 | } 82 | if (proposedX < 0) { 83 | proposedX = 0 84 | } 85 | if (proposedY < 0) { 86 | proposedY = 0 87 | } 88 | var proximity = 1 / Math.pow(Math.max(proposedX, 0) + 100, 4) + 89 | 1 / Math.pow(Math.max(boundX - (proposedX + from.width), 0) + 100, 4) + 90 | 1 / Math.pow(Math.max(proposedY, 0) + 100, 4) + 91 | 1 / Math.pow(Math.max(boundY - (proposedY + from.height), 0) + 100, 4) 92 | for(var j = 0; j < allNodes.length; j++){ 93 | var node = allNodes[j] 94 | var nodePosition = getPosition(node) 95 | proximity += 1 / Math.pow( 96 | Math.pow(nodePosition.x + nodePosition.width - proposedX, 2) + 97 | Math.pow(proposedX + from.width - nodePosition.x, 2) - 98 | Math.pow(from.width, 2) / 2 - Math.pow(nodePosition.width, 2) / 2 + 99 | Math.pow(nodePosition.y + nodePosition.height - proposedY, 2) + 100 | Math.pow(proposedY + from.height - nodePosition.y, 2) - 101 | Math.pow(from.height, 2) / 2 - Math.pow(nodePosition.height, 2) / 2, 2) 102 | } 103 | if(proximity < bestProximity){ 104 | bestProximity = proximity 105 | bestPosition = { 106 | x: proposedX, 107 | y: proposedY 108 | } 109 | } 110 | } 111 | } 112 | return bestPosition 113 | } 114 | 115 | function makeConnection(source, target, label){ 116 | var sourceX = source.x 117 | var sourceY = source.y 118 | var targetX = target.x 119 | var targetY = target.y 120 | var angle = Math.atan((targetY - sourceY) / (targetX - sourceX)) 121 | if(targetX < sourceX){ 122 | angle += Math.PI 123 | } 124 | var arrowContainer = create(container, 'div') 125 | var arrow = create(arrowContainer, 'div', { 126 | position: 'absolute', 127 | left: sourceX + 'px', 128 | top: sourceY + 'px', 129 | backgroundColor: '#321', 130 | height: '3px', 131 | width: Math.sqrt(Math.pow(targetX - sourceX, 2) + Math.pow(targetY - sourceY, 2)) + 'px', 132 | zIndex: 200, 133 | transformOrigin: '0 0', 134 | transform: 'rotate(' + angle + 'rad)' 135 | }) 136 | var arrowTriangle = create.triangle(arrow, 9) 137 | arrowTriangle.style.position = 'absolute' 138 | arrowTriangle.style.left = '50%' 139 | arrowTriangle.style.top = '-7px' 140 | midX = (sourceX + targetX) / 2 141 | midY = (sourceY + targetY) / 2 142 | labelNode = create(arrowContainer, 'div', { 143 | position: 'absolute', 144 | left: midX + 'px', 145 | top: midY + 'px', 146 | zIndex: 200, 147 | color: '#000', 148 | fontSize: '14px' 149 | }) 150 | labelNode.textContent = label 151 | return arrowContainer 152 | } 153 | function getAbsolutePosition(node){ 154 | if(node.offsetParent){ 155 | parentPosition = getAbsolutePosition(node.offsetParent) 156 | return { 157 | x: node.offsetLeft + parentPosition.x, 158 | y: node.offsetTop + parentPosition.y 159 | } 160 | } 161 | return { 162 | x: node.offsetLeft, 163 | y: node.offsetTop 164 | } 165 | } 166 | 167 | function getPosition(node){ 168 | var xy = getAbsolutePosition(node) 169 | return { 170 | x: xy.x, 171 | y: xy.y, 172 | height: node.offsetHeight, 173 | width: node.offsetWidth 174 | } 175 | } 176 | var allEdges = [] 177 | function drawEdges(edges){ 178 | edges.forEach(function(edge){ 179 | if(edge.element && edge.element.parentNode){ 180 | edge.element.parentNode.removeChild(edge.element) 181 | } 182 | if(edge.source.offsetParent && edge.target.offsetParent){ 183 | var source = getPosition(edge.source) 184 | var target = getPosition(edge.target) 185 | if(source.x < target.x){ 186 | source.x += source.width 187 | }else{ 188 | target.x += target.width 189 | } 190 | var slope = Math.max(0, Math.min(1, (target.y - source.y) / Math.abs(target.x - source.x) + 0.5)) 191 | source.y += source.height * slope 192 | target.y += target.height * (1 - slope) 193 | edge.element = makeConnection(source, target, edge.label) 194 | } 195 | }) 196 | 197 | } 198 | return { 199 | setContainer: function(containerElement) { 200 | container = containerElement 201 | columns = [] 202 | allNodes = [] 203 | }, 204 | layout: function(nodes, edges) { 205 | nodes.forEach(function(node) { 206 | var to = node.to 207 | if(to){ 208 | var toPosition = getAbsolutePosition(to) 209 | toPosition.width = node.offsetWidth 210 | toPosition.height = node.offsetHeight 211 | var position = findPosition(toPosition) 212 | node.style.left = position.x + 'px' 213 | node.style.top = position.y + 'px' 214 | } 215 | allNodes.push(node) 216 | }) 217 | drawEdges(edges) 218 | allEdges = allEdges.concat(edges) 219 | }, 220 | removeEdge: function(edge) { 221 | allEdges.splice(allEdges.indexOf(edge), 1) 222 | if (edge.element.parentNode) { 223 | edge.element.parentNode.removeChild(edge.element) 224 | } 225 | }, 226 | refresh: function(){ 227 | drawEdges(allEdges) 228 | } 229 | } 230 | }) 231 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | define(['alkali/Updater', 'alkali/Variable', './graph', './create'], function(Updater, Variable, graph, create){ 2 | function getAbsoluteX(element){ 3 | var rect = element.getBoundingClientRect() 4 | return rect.left + window.pageXOffset - document.documentElement.clientLeft 5 | } 6 | function getAbsoluteY(element){ 7 | var rect = element.getBoundingClientRect() 8 | return rect.top + window.pageYOffset - document.documentElement.clientTop 9 | } 10 | function instrumentVariableClass(Class){ 11 | var originalPut = Class.prototype.put 12 | Class.prototype.put = function(value){ 13 | (this.changes || (this.changes = [])).push({ 14 | old: this.valueOf(), 15 | 'new': value, 16 | stack: new Error('Variable change').stack 17 | }) 18 | return originalPut.apply(this, arguments) 19 | } 20 | } 21 | var instrumented = false 22 | function instrumentVariables(){ 23 | if(instrumented){ 24 | return 25 | } 26 | instrumented = true 27 | instrumentVariableClass(Variable) 28 | instrumentVariableClass(Variable.Property) 29 | instrumentVariableClass(Variable.Call) 30 | } 31 | var container; 32 | return window.litmus = function(options){ 33 | var justRefresh = false 34 | if(container){ 35 | container.style.display = 'block' 36 | justRefresh = true 37 | 38 | } else { 39 | container = create(document.body, 'div', { 40 | position: 'absolute', 41 | left: '0', 42 | top: '0', 43 | height: '0', 44 | right: '0', 45 | fontFamily: 'sans-serif, Arial', 46 | fontSize: '10px', 47 | zIndex: '1000000' 48 | }) 49 | graph.setContainer(container) 50 | } 51 | 52 | var boundX = window.innerWidth 53 | var boundY = window.innerHeight 54 | var allElements = document.documentElement.getElementsByTagName('*') 55 | 56 | for(var i = 0, l = allElements.length; i < l; i++){ 57 | var element = allElements[i] 58 | if(element.offsetParent && element.alkaliRenderers){ 59 | var needsRerendering = element.className.indexOf('needs-rerendering') > -1; // sound the alarm 60 | var height = element.offsetHeight 61 | var width = element.offsetWidth 62 | var elementOverlay = create(container, 'div', { 63 | position: 'absolute', 64 | left: getAbsoluteX(element) + 'px', 65 | top: getAbsoluteY(element) + 'px', 66 | width: width + 'px', 67 | height: height + 'px', 68 | backgroundColor: needsRerendering ? '#f00' : '#00f', 69 | opacity: 0.2, 70 | zIndex: 1 71 | }) 72 | elementOverlay.className = 'element-overlay' 73 | elementOverlay.nodeId = 'element-' + i 74 | elementOverlay.targetElement = element 75 | if(needsRerendering){ 76 | alert('An element that was marked as hidden, for deferred rerendering, is visible, and is marked in red. Ensure that Updater.onShowElement is called when any hidden variable-driven element is reshown') 77 | } 78 | } 79 | } 80 | var closeOverlayButton = create(container, 'div', { 81 | position: 'absolute', 82 | left: (boundX - 60) + 'px', 83 | top: '20px', 84 | cursor: 'pointer', 85 | fontSize: '40px', 86 | zIndex: 5000, 87 | }) 88 | closeOverlayButton.textContent = 'X' 89 | closeOverlayButton.addEventListener('click', function(){ 90 | var oldElementOverlays = document.querySelectorAll('.element-overlay') 91 | for (var i = 0; i < oldElementOverlays.length; i++) { 92 | container.removeChild(oldElementOverlays[i]) 93 | } 94 | container.removeChild(closeOverlayButton) 95 | }) 96 | 97 | if (justRefresh) { 98 | return 99 | } 100 | 101 | 102 | var nextId = 1 103 | var nodes = [] 104 | var newNodes = [] 105 | var newEdges = [] 106 | 107 | var processed = {} 108 | function addConnection(source, target, label){ 109 | if (!source) { 110 | return 111 | } 112 | var edge = { 113 | source: source, 114 | target: target, 115 | label: label 116 | } 117 | newEdges.push(edge) 118 | return edge 119 | } 120 | function valueToString(value){ 121 | return fitString('' + value) 122 | } 123 | function fitString(string){ 124 | if(string.length > 20){ 125 | return string.slice(0, 20) + '...' 126 | } 127 | return string 128 | } 129 | function editDialog(element){ 130 | var box = create(container, 'div', { 131 | position: 'absolute', 132 | boxShadow: '2px 2px 3px #888', 133 | backgroundColor: '#eee', 134 | border: '1px solid #888', 135 | borderRadius: '3px', 136 | left: '400px', 137 | top: '300px', 138 | padding: '10px', 139 | zIndex: '10000' 140 | }) 141 | var closeButton = box.appendChild(document.createElement('div')) 142 | closeButton.textContent = 'X' 143 | closeButton.style.float = 'right' 144 | closeButton.style.cursor = 'pointer' 145 | closeButton.addEventListener('click', function(){ 146 | dismiss() 147 | }) 148 | var title = box.appendChild(document.createElement('div')) 149 | title.textContent = 'Edit value' 150 | editArea = box.appendChild(document.createElement('textarea')) 151 | editArea.style.width = '500px' 152 | editArea.style.height = '250px' 153 | var variable = element.variable 154 | var value = variable.valueOf() 155 | try{ 156 | var asSource = typeof value === 'function' ? value.toString() : JSON.stringify(value, null, '\t') 157 | var ok = box.appendChild(document.createElement('button')) 158 | ok.style.display = 'block' 159 | ok.style.margin = '10px' 160 | ok.innerHTML = 'Save' 161 | ok.addEventListener('click', function(){ 162 | try { 163 | variable.put(eval('(' + editArea.value + ')')) 164 | dismiss() 165 | }catch(error){ 166 | alert(error) 167 | } 168 | }) 169 | }catch(error){ 170 | asSource = error 171 | } 172 | editArea.value = asSource 173 | function dismiss(event){ 174 | if(!event || !box.contains(event.target) && container.contains(box)){ 175 | container.removeChild(box) 176 | } 177 | container.removeEventListener(dismiss, false) 178 | } 179 | container.addEventListener('click', dismiss) 180 | var changes = variable.changes 181 | changes && changes.forEach(function(change){ 182 | var changeElement = box.appendChild(document.createElement('div')) 183 | changeElement.textContent = 'Old: ' + valueToString(change.old) + ', new: ' + valueToString(change.new) 184 | changeElement.onclick = function(){ 185 | console.log('old:', change.old) 186 | console.log('new:', change['new']) 187 | console.log('stack:', change.stack) 188 | box.appendChild(document.createElement('div')).textContent = 'check the console for the stack trace of this event' 189 | } 190 | 191 | }) 192 | } 193 | function createVariableBox(text, parent){ 194 | var box = create(parent || container, 'div', { 195 | boxShadow: '2px 2px 3px #888', 196 | backgroundColor: '#eee', 197 | border: '1px solid #888', 198 | borderRadius: '3px', 199 | padding: '4px', 200 | paddingRight: '13px', 201 | zIndex: 100, 202 | maxHeight: boundY + 'px', 203 | overflow: 'auto' 204 | }) 205 | box.isVariable = true 206 | box.textContent = text 207 | 208 | if(!parent){ 209 | box.style.position = 'absolute' 210 | box.draggable = true 211 | var closeButton = create(box, 'div', { 212 | position: 'absolute', 213 | right: '0', 214 | top: '0', 215 | cursor: 'pointer', 216 | fontSize: '10px' 217 | }) 218 | closeButton.textContent = 'X' 219 | closeButton.onclick = function(event) { 220 | event.stopPropagation() 221 | box.style.display = 'none' 222 | graph.refresh() 223 | } 224 | }else{ 225 | box.style.margin = '5px' 226 | } 227 | 228 | return box 229 | } 230 | function processVariable(variable, dependent, parent, key, keepClosed){ 231 | if (!variable || !variable.subscribe){ 232 | return 233 | } 234 | var variableId 235 | if(variable.parent){ 236 | processVariable(variable.parent, dependent, null, null, key) 237 | } 238 | if(variable.getId){ 239 | variableId = 'variable-' + variable.getId() 240 | }else{ 241 | variableId = 'subscriber-' + (variable.id || (variable.id = nextId++)) 242 | } 243 | if(processed[variableId]){ 244 | variableElement = processed[variableId] 245 | variableElement.style.display = 'block' 246 | if(variableElement.expand && !keepClosed){ 247 | variableElement.expand(true) 248 | } 249 | return variableElement 250 | } 251 | var variableElement = createVariableBox('', parent) 252 | variableElement.to = dependent 253 | processed[variableId] = variableElement 254 | if(variable._properties){ 255 | var triangle = create.triangle(variableElement, 6) 256 | triangle.style.display = 'inline-block' 257 | var expanded 258 | variableElement.expand = triangle.onclick = function(expand){ 259 | expanded = typeof expand == 'boolean' ? expand : !expanded 260 | if(expand.stopPropagation){ 261 | expand.stopPropagation() 262 | } 263 | if(expanded){ 264 | childContainer.style.display = 'block' 265 | triangle.style.transform = 'rotate(90deg)' 266 | }else{ 267 | childContainer.style.display = 'none' 268 | triangle.style.transform = 'rotate(0)' 269 | } 270 | graph.refresh() 271 | } 272 | } 273 | var labelNode = create(variableElement, 'span') 274 | labelNode.textContent = 'undefined' 275 | if(key){ 276 | labelNode.textContent = key + ':' 277 | } 278 | new Updater.ElementUpdater({ 279 | element: labelNode, 280 | variable: variable, 281 | renderUpdate: function(newValue){ 282 | var label = '' + newValue 283 | if(key){ 284 | label = key + ': ' + label 285 | } 286 | labelNode.textContent = fitString(label) 287 | } 288 | }) 289 | 290 | variableElement.variable = variable 291 | if(!parent){ 292 | newNodes.push(variableElement) 293 | } 294 | 295 | var childContainer = create(variableElement, 'div') 296 | if(key || keepClosed){ 297 | childContainer.style.display = 'none' 298 | } 299 | for(var childKey in variable._properties){ 300 | processVariable(variable.property(childKey), dependent, childContainer, childKey) 301 | } 302 | variableElement.downstream = true 303 | var args = variable.args 304 | if(args){ 305 | for(var i = 0; i < args.length; i++){ 306 | addConnection(processVariable(args[i], dependent), variableElement, '' + i) 307 | } 308 | } 309 | if(variable.copiedFrom){ 310 | addConnection(processVariable(variable.copiedFrom, dependent), variableElement, 'copied from') 311 | } 312 | if(variable.functionVariable){ 313 | processVariable(variable.functionVariable, dependent, variableElement) 314 | } 315 | if(variable.notifyingValue){ 316 | var previousNotifyingValue; 317 | new Updater({ 318 | variable: variable, 319 | element: document.body, 320 | update: function () { 321 | if (previousNotifyingValue) { 322 | graph.removeEdge(previousNotifyingValue) 323 | } 324 | previousNotifyingValue = addConnection(processVariable(variable.notifyingValue, dependent), variableElement, 'value') 325 | } 326 | }) 327 | 328 | } 329 | return processed[variableId] = variableElement 330 | } 331 | 332 | 333 | var hideEverythingButton = create(container, 'div', { 334 | position: 'absolute', 335 | left: (boundX - 60) + 'px', 336 | top: '20px', 337 | cursor: 'pointer', 338 | fontSize: '40px', 339 | zIndex: 4000, 340 | }) 341 | hideEverythingButton.textContent = 'X' 342 | hideEverythingButton.addEventListener('click', function(){ 343 | container.style.display = 'none' 344 | }) 345 | 346 | var trackButton = create(container, 'button', { 347 | position: 'absolute', 348 | left: (boundX - 200) + 'px', 349 | top: '20px', 350 | zIndex: 5000 351 | }) 352 | trackButton.textContent = 'Track variables' 353 | trackButton.addEventListener('click', function(){ 354 | instrumentVariables() 355 | }) 356 | var draggedVariable, offsetX, offsetY 357 | container.addEventListener('dragstart', function(event){ 358 | draggedVariable = event.target 359 | draggedVariable = draggedVariable.isVariable && draggedVariable 360 | if (draggedVariable) { 361 | offsetX = draggedVariable.offsetLeft - event.clientX 362 | offsetY = draggedVariable.offsetTop - event.clientY 363 | } 364 | }) 365 | document.body.addEventListener('dragover', function(event){ 366 | event.preventDefault() 367 | event.dataTransfer.dropEffect = 'move' 368 | }) 369 | document.body.addEventListener('drop', function(event){ 370 | if(draggedVariable){ 371 | draggedVariable.style.left = (event.clientX + offsetX) + 'px' 372 | draggedVariable.style.top = (event.clientY + offsetY) + 'px' 373 | graph.refresh() 374 | } 375 | }) 376 | 377 | container.addEventListener('click', function(event){ 378 | var litmusElement = event.target 379 | while(litmusElement){ 380 | if(litmusElement == container){ 381 | return 382 | } 383 | if(litmusElement.variable){ 384 | editDialog(litmusElement) 385 | return 386 | } 387 | if(litmusElement.renderer){ 388 | alert(litmusElement.renderer.renderUpdate) 389 | return 390 | } 391 | if(litmusElement.targetElement){ 392 | var element = litmusElement.targetElement 393 | if(element){ 394 | var id = event.target.nodeId 395 | var renderers = element.alkaliRenderers 396 | for(var j = 0; j < renderers.length; j++){ 397 | var renderer = renderers[j] 398 | var rendererId = 'renderer' + renderer.getId() 399 | if (processed[rendererId]) { 400 | processed[rendererId].style.display = 'block' 401 | } else { 402 | var rendererBox = createVariableBox(fitString((renderer.type || 'Renderer') + ' ' + (renderer.name || ''))) 403 | processed[rendererId] = rendererBox; 404 | rendererBox.renderer = renderer 405 | newNodes.push(rendererBox) 406 | rendererBox.to = litmusElement 407 | addConnection(rendererBox, litmusElement, '') 408 | addConnection(processVariable(renderer.variable, rendererBox), rendererBox, '') 409 | } 410 | } 411 | 412 | } 413 | graph.layout(newNodes, newEdges) 414 | newNodes = [] 415 | newEdges = [] 416 | return 417 | } 418 | litmusElement = litmusElement.parentNode 419 | } 420 | // cy.layout(configuration.layout) 421 | }) 422 | } 423 | }); --------------------------------------------------------------------------------