soiz1's picture
Upload 225 files
7aec436 verified
import ContextMenu from './context-menu';
import DropArea from './drop-area';
import styles from './style.css';
import {readAsText} from '../common/readers';
import downloadBlob from './download';
class Monitor {
constructor (parent, monitor) {
this.parent = parent;
this.id = monitor.get('id');
this.spriteName = monitor.get('spriteName');
this.targetId = monitor.get('targetId');
this.opcode = monitor.get('opcode');
this.params = monitor.get('params');
this.root = document.createElement('div');
this.root.className = styles.monitorRoot;
this.root.dataset.id = this.id;
this.root.dataset.opcode = this.opcode;
this.parent._monitorOverlay.appendChild(this.root);
}
getLabel () {
let label;
if (this.opcode === 'data_variable') {
label = this.params.VARIABLE;
} else if (this.opcode === 'data_listcontents') {
label = this.params.LIST;
} else if (this.opcode === 'motion_xposition') {
label = this.parent.getMessage('var-x');
} else if (this.opcode === 'motion_yposition') {
label = this.parent.getMessage('var-y');
} else if (this.opcode === 'motion_direction') {
label = this.parent.getMessage('var-direction');
} else if (this.opcode === 'sensing_username') {
label = this.parent.getMessage('var-username');
} else if (this.opcode === 'looks_costumenumbername') {
if (this.params.NUMBER_NAME === 'number') {
label = this.parent.getMessage('var-costume-number');
} else {
label = this.parent.getMessage('var-costume-name');
}
} else if (this.opcode === 'looks_backdropnumbername') {
if (this.params.NUMBER_NAME === 'number') {
label = this.parent.getMessage('var-backdrop-number');
} else {
label = this.parent.getMessage('var-backdrop-name');
}
} else if (this.opcode === 'looks_size') {
label = this.parent.getMessage('var-size');
} else if (this.opcode === 'looks_stretchGetX') {
label = this.parent.getMessage('var-stretch-x');
} else if (this.opcode === 'looks_stretchGetY') {
label = this.parent.getMessage('var-stretch-y');
} else if (this.opcode === 'looks_sayWidth') {
label = this.parent.getMessage('var-say-width');
} else if (this.opcode === 'looks_sayHeight') {
label = this.parent.getMessage('var-say-height');
} else if (this.opcode === 'looks_getEffectValue') {
const effect = this.params.EFFECT.toLowerCase();
label = this.parent.getMessage(`var-${effect}-effect`) || this.parent.getMessage("var-effect");
} else if (this.opcode === 'looks_tintColor') {
label = this.parent.getMessage('var-tint-color');
} else if (this.opcode === 'looks_getSpriteVisible') {
label = this.parent.getMessage('var-visible');
} else if (this.opcode === 'looks_layersGetLayer') {
label = this.parent.getMessage('var-layer');
} else if (this.opcode === 'looks_size') {
label = this.parent.getMessage('var-size');
} else if (this.opcode === 'sound_getEffectValue') {
const effect = this.params.EFFECT.toLowerCase();
label = this.parent.getMessage(`var-${effect}-sound-effect`) || this.parent.getMessage("var-sound-effect");
} else if (this.opcode === 'control_get_counter') {
label = this.parent.getMessage('var-get_counter');
} else if (this.opcode === 'sensing_answer') {
label = this.parent.getMessage('var-answer');
} else if (this.opcode === 'sensing_mousedown') {
label = this.parent.getMessage('var-mousedown');
} else if (this.opcode === 'sensing_mouseclicked') {
label = this.parent.getMessage('var-mouseclicked');
} else if (this.opcode === 'sensing_mousex') {
label = this.parent.getMessage('var-mousex');
} else if (this.opcode === 'sensing_mousey') {
label = this.parent.getMessage('var-mousey');
} else if (this.opcode === 'sensing_loudness') {
label = this.parent.getMessage('var-loudness');
} else if (this.opcode === 'sensing_timer') {
label = this.parent.getMessage('var-timer');
} else if (this.opcode === 'sensing_dayssince2000') {
label = this.parent.getMessage('var-dayssince2000');
} else if (this.opcode === 'sensing_getclipboard') {
label = this.parent.getMessage('var-getclipboard');
} else if (this.opcode === 'sensing_getdragmode') {
label = this.parent.getMessage('var-getdragmode');
} else if (this.opcode === 'sensing_loud') {
label = this.parent.getMessage('var-loud');
} else if (this.opcode === 'sensing_loggedin') {
label = this.parent.getMessage('var-loggedin');
} else if (this.opcode === 'sound_volume') {
label = this.parent.getMessage('var-volume');
} else if (this.opcode === 'sensing_current') {
const menu = this.params.CURRENTMENU.toLowerCase();
if (menu === 'year') {
label = this.parent.getMessage('var-year');
} else if (menu === 'month') {
label = this.parent.getMessage('var-month');
} else if (menu === 'date') {
label = this.parent.getMessage('var-date');
} else if (menu === 'dayofweek') {
label = this.parent.getMessage('var-day-of-week');
} else if (menu === 'hour') {
label = this.parent.getMessage('var-hour');
} else if (menu === 'minute') {
label = this.parent.getMessage('var-minute');
} else if (menu === 'second') {
label = this.parent.getMessage('var-second');
}
} else {
const vmLabel = this.parent.vm.runtime.getLabelForOpcode(this.opcode);
if (vmLabel) {
label = vmLabel.label;
} else {
label = this.opcode;
}
}
if (this.spriteName) {
return `${this.spriteName}: ${label}`;
}
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) {
this.x = monitor.get('x');
this.y = monitor.get('y');
this.visible = monitor.get('visible');
this.root.style.transform = `translate(${Math.round(this.x)}px, ${Math.round(this.y)}px)`;
this.root.style.display = this.visible ? '' : 'none';
}
}
class VariableMonitor extends Monitor {
constructor (parent, monitor) {
super(parent, monitor);
this.mode = monitor.get('mode');
if (this.mode === 'large') {
this.valueElement = document.createElement('div');
this.valueElement.className = styles.monitorLargeValue + ' ' + styles.monitorValueColor;
this.root.appendChild(this.valueElement);
} else {
this.inner = document.createElement('div');
this.inner.className = styles.monitorInner;
this.valueRow = document.createElement('div');
this.valueRow.className = styles.monitorRow;
this.label = document.createElement('div');
this.label.className = styles.monitorLabel;
this.label.textContent = this.getLabel();
this.valueElement = document.createElement('div');
this.valueElement.className = styles.monitorValue + ' ' + styles.monitorValueColor;
this.valueRow.appendChild(this.label);
this.valueRow.appendChild(this.valueElement);
this.inner.appendChild(this.valueRow);
if (this.mode === 'slider') {
this.sliderRow = document.createElement('div');
this.sliderRow.className = styles.monitorRow;
this.slider = document.createElement('input');
this.slider.className = styles.monitorSlider;
this.slider.type = 'range';
this.slider.min = monitor.get('sliderMin');
this.slider.max = monitor.get('sliderMax');
this.slider.step = monitor.get('isDiscrete') ? 1 : 0.01;
this.slider.addEventListener('input', this.onsliderchange.bind(this));
this.sliderRow.appendChild(this.slider);
this.inner.appendChild(this.sliderRow);
}
this.root.appendChild(this.inner);
}
this.parent._monitorOverlay.appendChild(this.root);
this._value = '';
}
setVariableValue (value) {
const variable = this.getVmVariable();
variable.value = value;
if (variable.isCloud) {
const runtime = this.parent.vm.runtime;
runtime.ioDevices.cloud.requestUpdateVariable(variable.name, variable.value);
}
this._value = value;
this.valueElement.textContent = value;
}
onsliderchange (e) {
this.setVariableValue(+e.target.value);
}
update (monitor) {
super.update(monitor);
if (!this.visible) {
return;
}
let value = monitor.get('value');
if (typeof value === 'number') {
value = Number(value.toFixed(6));
}
if (this._value !== value) {
this._value = value;
this.valueElement.textContent = value;
if (this.slider) {
this.slider.value = value;
}
}
}
}
const ROW_HEIGHT = 24;
class Row {
constructor (monitor) {
this.monitor = monitor;
this.index = -1;
this.value = '';
this.locked = false;
this.root = document.createElement('label');
this.root.className = styles.monitorRowRoot;
this.indexEl = document.createElement('div');
this.indexEl.className = styles.monitorRowIndex;
this.valueOuter = document.createElement('div');
this.valueOuter.className = styles.monitorRowValueOuter;
this.editable = this.monitor.editable;
if (this.editable) {
this.valueInner = document.createElement('input');
this.valueInner.tabIndex = -1;
this.valueInner.className = styles.monitorRowValueInner;
this.valueInner.readOnly = true;
this.valueInner.addEventListener('click', this._onclickinput.bind(this));
this.valueInner.addEventListener('blur', this._onblurinput.bind(this));
this.valueInner.addEventListener('keypress', this._onkeypressinput.bind(this));
this.valueInner.addEventListener('keydown', this._onkeypressdown.bind(this));
this.valueInner.addEventListener('contextmenu', this._oncontextmenu.bind(this));
this.valueInner.addEventListener('input', this._oninput.bind(this));
this.valueOuter.appendChild(this.valueInner);
this.deleteButton = document.createElement('button');
this.deleteButton.className = styles.monitorRowDelete;
this.deleteButton.textContent = '×';
this.deleteButton.addEventListener('mousedown', this._onclickdelete.bind(this));
this.valueOuter.appendChild(this.deleteButton);
} else {
this.valueInner = document.createElement('div');
this.valueInner.className = styles.monitorRowValueInner;
this.valueOuter.appendChild(this.valueInner);
this.valueInner.addEventListener('contextmenu', this._oncontextmenuuneditable.bind(this));
}
this.root.appendChild(this.indexEl);
this.root.appendChild(this.valueOuter);
}
_onclickinput () {
this.valueInner.focus();
if (this.locked) {
return;
}
this.valueInner.select();
this.valueInner.readOnly = false;
this.locked = true;
this.root.classList.add(styles.monitorRowValueEditing);
this.addNewValue = false;
this.deleteValue = false;
this.valueWasChanged = false;
}
_onblurinput () {
if (!this.locked) {
return;
}
this.unfocus();
if (this.deleteValue) {
const value = [...this.monitor.value];
value.splice(this.index, 1);
this.monitor.setValue(value);
this.monitor.tryToFocusRow(Math.min(value.length - 1, this.index))
} else if (this.valueWasChanged || this.addNewValue) {
const value = [...this.monitor.value];
value[this.index] = this.valueInner.value;
if (this.addNewValue) {
value.splice(this.index + 1, 0, '');
}
this.monitor.setValue(value);
if (this.addNewValue) {
this.monitor.tryToFocusRow(this.index + 1);
}
}
}
_oninput () {
this.valueWasChanged = true;
}
_onkeypressinput (e) {
if (e.key === 'Enter') {
this.addNewValue = true;
this.valueInner.blur();
}
}
_onkeypressdown (e) {
if (e.key === 'Escape') {
this.valueInner.blur();
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Tab') {
e.preventDefault();
let index = this.index;
if (e.key === 'ArrowUp' || (e.key === 'Tab' && e.shiftKey)) {
index--;
if (index < 0) index = this.monitor.value.length - 1;
} else {
index++;
if (index >= this.monitor.value.length) index = 0;
}
this.monitor.tryToFocusRow(index);
}
}
_onclickdelete (e) {
e.preventDefault();
this.deleteValue = true;
this.valueInner.blur();
}
_oncontextmenu (e) {
if (this.locked) {
// Open native context menu instead of custom list one when editing
e.stopPropagation();
} else {
// Right clicking should not focus and highlight input
e.preventDefault();
}
}
_oncontextmenuuneditable (e) {
// When row has been highlighted, eg. by triple click, open native context menu instead of custom
const selection = getSelection();
if (this.valueInner.contains(selection.anchorNode) && !selection.isCollapsed) {
e.stopPropagation();
}
}
setIndex (index) {
if (this.index !== index) {
this.index = index;
this.root.dataset.index = index;
this.root.style.transform = `translateY(${index * ROW_HEIGHT}px)`;
this.indexEl.textContent = index + 1;
}
}
setValue (value) {
if (this.value !== value && !this.locked) {
this.value = value;
if (this.editable) {
this.valueInner.value = value;
} else {
this.valueInner.textContent = value;
}
}
}
focus () {
this.valueInner.click();
if (document.activeElement !== this.valueInner) {
setTimeout(() => this.valueInner.click());
}
}
unfocus () {
if (this.locked) {
this.locked = false;
this.valueInner.readOnly = true;
this.root.classList.remove(styles.monitorRowValueEditing);
}
}
}
class ListMonitor extends Monitor {
constructor (parent, monitor) {
super(parent, monitor);
this.editable = parent.editableLists;
this.rows = new Map();
this.cachedRows = [];
this.scrollTop = 0;
this.oldLength = -1;
this.label = document.createElement('div');
this.label.className = styles.monitorListLabel;
this.label.textContent = this.getLabel();
this.footer = document.createElement('div');
this.footer.className = styles.monitorListFooter;
this.footerText = document.createElement('div');
this.footerText.className = styles.monitorListFooterText;
this.rowsOuter = document.createElement('div');
this.rowsOuter.className = styles.monitorRowsOuter;
this.rowsInner = document.createElement('div');
this.rowsInner.className = styles.monitorRowsInner;
this.rowsInner.addEventListener('scroll', this._onscroll.bind(this), {passive: true});
this.endPoint = document.createElement('div');
this.endPoint.className = styles.monitorRowsEndpoint;
this.emptyLabel = document.createElement('div');
this.emptyLabel.textContent = parent.getMessage('list-empty');
this.emptyLabel.className = styles.monitorEmpty;
if (this.editable) {
this.addButton = document.createElement('button');
this.addButton.className = styles.monitorListAdd;
this.addButton.textContent = '+';
this.addButton.addEventListener('click', this._onclickaddbutton.bind(this));
this.footer.appendChild(this.addButton);
}
this.rowsInner.appendChild(this.endPoint);
this.rowsInner.appendChild(this.emptyLabel);
this.rowsOuter.appendChild(this.rowsInner);
this.footer.appendChild(this.footerText);
this.root.appendChild(this.label);
this.root.appendChild(this.rowsOuter);
this.root.appendChild(this.footer);
this.dropper = new DropArea(this.rowsOuter, this.dropperCallback.bind(this));
this.handleImport = this.handleImport.bind(this);
this.handleExport = this.handleExport.bind(this);
this.root.addEventListener('contextmenu', this._oncontextmenu.bind(this));
}
_onclickaddbutton (e) {
this.setValue([...this.value, '']);
this.tryToFocusRow(this.value.length - 1);
}
unfocusAllRows () {
for (const row of this.rows.values()) {
row.unfocus();
}
}
tryToFocusRow (index) {
if (index >= 0 && index < this.value.length) {
this.unfocusAllRows();
let row = this.rows.get(index);
if (!row) {
row = this.createRow(index);
}
row.focus();
}
}
_onscroll (e) {
this.scrollTop = e.target.scrollTop;
this.updateValue(this.value);
}
_oncontextmenu (e) {
e.preventDefault();
const menu = new ContextMenu(this.parent);
menu.add({
text: this.parent.getMessage('list-import'),
callback: this.handleImport
});
menu.add({
text: this.parent.getMessage('list-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];
readAsText(file).then((text) => this.import(text));
});
fileSelector.click();
}
import (text) {
// TODO: Scratch uses a CSV parser
const lines = text.split(/\r?\n/);
this.setValue(lines);
}
handleExport () {
const value = this.getValue();
const exported = value.join('\n');
const blob = new Blob([exported], {
type: 'text/plain'
});
downloadBlob(`${this.getLabel()}.txt`, blob);
}
dropperCallback (texts) {
this.import(texts.join('\n'));
}
getValue () {
return this.getVmVariable().value;
}
setValue (value) {
const variable = this.getVmVariable();
variable.value = value;
this.updateValue(value);
}
update (monitor) {
super.update(monitor);
if (!this.visible) {
return;
}
this.width = monitor.get('width') || 100;
this.height = monitor.get('height') || 200;
this.root.style.width = `${this.width}px`;
this.root.style.height = `${this.height}px`;
this.updateValue(monitor.get('value'));
}
createRow (index) {
const row = this.cachedRows.pop() || new Row(this);
row.setIndex(index);
row.setValue(this.value[index]);
this.rows.set(index, row);
let foundPlaceInDOM = false;
for (const root of this.rowsInner.children) {
const otherIndexString = root.dataset.index;
if (!otherIndexString) {
continue;
}
const otherIndexNumber = +otherIndexString;
if (otherIndexNumber > index) {
this.rowsInner.insertBefore(row.root, root);
foundPlaceInDOM = true;
break;
}
}
if (!foundPlaceInDOM) {
this.rowsInner.appendChild(row.root);
}
return row;
}
updateValue (value) {
this.value = value;
if (value.length !== this.oldLength) {
this.oldLength = value.length;
this.footerText.textContent = this.parent.getMessage('list-length').replace('{n}', value.length);
this.endPoint.style.transform = `translateY(${value.length * ROW_HEIGHT}px)`;
this.emptyLabel.style.display = value.length ? 'none' : '';
}
let startIndex = Math.floor(this.scrollTop / ROW_HEIGHT) - 5;
if (startIndex < 0) startIndex = 0;
let endIndex = Math.ceil((this.scrollTop + this.height) / ROW_HEIGHT) + 3;
if (endIndex > value.length - 1) endIndex = value.length - 1;
for (const index of this.rows.keys()) {
if (index < startIndex || index > endIndex) {
const row = this.rows.get(index);
if (!row.locked || index >= value.length) {
row.unfocus();
row.root.remove();
this.rows.delete(index);
if (this.cachedRows.length < 10) {
this.cachedRows.push(row);
}
}
}
}
for (let i = startIndex; i <= endIndex; i++) {
const row = this.rows.get(i);
if (row) {
row.setValue(value[i]);
} else {
this.createRow(i);
}
}
}
}
export {
VariableMonitor,
ListMonitor
};