diff --git a/src/scaffolding/context-menu.js b/src/scaffolding/context-menu.js new file mode 100644 index 0000000..f6c9201 --- /dev/null +++ b/src/scaffolding/context-menu.js @@ -0,0 +1,45 @@ +import styles from './style.css'; + +class ContextMenu { + constructor (parent) { + this.parent = parent; + + this.root = document.createElement('div'); + this.root.className = styles.contextMenu; + } + + _onmousedown (e) { + if (!this.root.contains(e.target)) { + this.destroy(); + } + } + + add (option) { + const item = document.createElement('button'); + item.className = styles.contextMenuItem; + item.textContent = option.text; + item.addEventListener('click', () => { + this.destroy(); + option.callback(); + }); + item.addEventListener('contextmenu', (e) => { + e.preventDefault(); + }); + this.root.appendChild(item); + } + + show (mouseEvent) { + document.addEventListener('mousedown', this._onmousedown.bind(this)); + const layersRect = this.parent.layersRect; + const x = mouseEvent.clientX - layersRect.left + 1; + const y = mouseEvent.clientY - layersRect.top + 1; + this.root.style.transform = `translate(${x}px, ${y}px)`; + this.parent._addLayer(this.root); + } + + destroy () { + this.root.remove(); + } +} + +export default ContextMenu; diff --git a/src/scaffolding/monitor.js b/src/scaffolding/monitor.js index fc4863f..ffcd059 100644 --- a/src/scaffolding/monitor.js +++ b/src/scaffolding/monitor.js @@ -1,3 +1,4 @@ +import ContextMenu from './context-menu'; import styles from './style.css'; class Monitor { @@ -80,6 +81,18 @@ class Monitor { return label; } + getTarget () { + if (this.targetId) { + return this.parent.vm.runtime.getTargetById(this.targetId); + } + return this.parent.vm.runtime.getTargetForStage(); + } + + getVmVariable () { + const target = this.getTarget(); + return target.variables[this.id]; + } + update (monitor) { // TODO: don't touch if not changed this.x = monitor.get('x'); @@ -144,13 +157,7 @@ class VariableMonitor extends Monitor { } setVariableValue (value) { - let target; - if (this.targetId) { - target = this.parent.vm.runtime.getTargetById(this.targetId); - } else { - target = this.parent.vm.runtime.getTargetForStage(); - } - const variable = target.variables[this.id]; + const variable = this.getVmVariable(); variable.value = value; this._value = value; this.valueElement.textContent = value; @@ -254,6 +261,10 @@ class ListMonitor extends Monitor { this.root.appendChild(this.label); this.root.appendChild(this.rowsOuter); this.root.appendChild(this.footer); + + this.handleImport = this.handleImport.bind(this); + this.handleExport = this.handleExport.bind(this); + this.root.addEventListener('contextmenu', this._oncontextmenu.bind(this)); } _onscroll (e) { @@ -270,6 +281,68 @@ class ListMonitor extends Monitor { this.updateValue(this.value); } + + _oncontextmenu (e) { + e.preventDefault(); + const menu = new ContextMenu(this.parent); + menu.add({ + text: 'import', + callback: this.handleImport + }); + menu.add({ + text: 'export', + callback: this.handleExport + }); + menu.show(e); + } + + handleImport () { + const fileSelector = document.createElement('input'); + fileSelector.type = 'file'; + fileSelector.accept = '.txt,.csv,.tsv'; + fileSelector.style.display = 'none'; + document.body.appendChild(fileSelector); + fileSelector.addEventListener('change', (e) => { + const files = e.target.files; + if (files.length === 0) return; + const file = files[0]; + const reader = new FileReader(); + reader.onload = () => { + const text = reader.result; + // TODO: this doesn't exactly match Scratch behavior + const lines = text.split(/\r?\n/); + this.setValue(lines); + }; + reader.readAsText(file); + }); + fileSelector.click(); + } + + handleExport () { + const value = this.getValue(); + const exported = value.join('\n'); + const blob = new Blob([exported], { + type: 'text/plain' + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.download = `${this.getLabel()}.txt`; + link.href = url; + document.body.appendChild(link); + link.click(); + link.remove(); + } + + getValue () { + return this.getVmVariable().value; + } + + setValue (value) { + const variable = this.getVmVariable(); + variable.value = value; + this.updateValue(value); + } + getRow () { if (this.rows.length) { return this.rows.pop(); diff --git a/src/scaffolding/style.css b/src/scaffolding/style.css index c315069..2910de1 100644 --- a/src/scaffolding/style.css +++ b/src/scaffolding/style.css @@ -211,3 +211,36 @@ align-items: center; flex-wrap: nowrap; } + +.context-menu { + position: absolute; + background-color: white; + color: black; + display: flex; + flex-direction: column; + border-radius: 4px; + overflow: hidden; + box-shadow: 0px 0px 5px 1px hsla(0, 0%, 0%, 0.15); + min-width: 130px; +} +.context-menu-item { + background: none; + border: none; + border-radius: 0; + cursor: pointer; + text-align: left; + font-family: inherit; + font-size: 0.85em; + padding: 8px 12px; + transition: .1s background-color, .1s color; +} +.context-menu-item:focus { + background-color: #ff4c4c55; + border: none; + outline: none; +} +.context-menu-item:hover, +.context-menu-item:active { + color: white; + background-color: #ff4c4c; +}