blob: e26e7ef63ad8f1e7cd6c55a68f8ee79c9a4bcc69 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './ace-modes.js';
import './style.css';
import {Editors} from './editors';
import {Elm} from './elm/Main.elm';
import {Evaluator} from './evaluator';
import {LinkSharing} from './share';
import {waitForElement} from './util';
// A message sent from Elm (see elm/Ports.elm).
interface ElmMessage {
command: string;
payload: any;
}
// Gets the user's light/dark preference (e.g. macOS Catalina Dark Mode).
function preferredColorScheme(): string {
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function save(key: string, value: object) {
localStorage.setItem(key, JSON.stringify(value));
}
function load(key: string): object | undefined {
const item = localStorage.getItem(key);
if (item == undefined) {
return undefined;
}
return JSON.parse(item);
}
function main() {
const savedModel = load('model') as any;
const savedEditors = load('editors') as any;
const linkSharing = new LinkSharing();
linkSharing.fixupModel(savedModel);
const app = Elm.Main.init({
node: document.getElementById('Elm'),
flags: {
model: savedModel,
preferredScheme: preferredColorScheme(),
},
});
const editors = Editors.fromJSON(savedEditors);
const evaluator = new Evaluator(editors, () => {
save('editors', editors.toJSON());
});
linkSharing.init(editors, evaluator);
evaluator.onDeloymentUpdated(deployment => {
app.ports.fromJS.send({
command: 'deploymentUpdated',
payload: deployment,
});
});
app.ports.toJS.subscribe(({command, payload}: ElmMessage) => {
switch (command) {
case 'persistModel': {
save('model', payload);
break;
}
case 'clearDataAndReload': {
// Stop the evaluator first, otherwise it will save to local storage
// before the browser refreshes.
evaluator.stop();
localStorage.clear();
window.location.reload();
break;
}
case 'applySettings': {
editors.applySettings(payload);
evaluator.setRefreshDelay(payload.refreshDelay);
const classes = document.getElementsByTagName('body')[0].classList;
if (payload.scheme === 'dark') {
classes.add('dark');
} else {
classes.remove('dark');
}
const extraStyle = document.getElementById('ExtraStyle');
extraStyle!.innerHTML = `
.${payload.themeClass} .ace_marker-layer .ace_selection {
background: ${payload.selectionBackground};
}
`;
break;
}
case 'setMainTabEnabled': {
const elements = document
.getElementById('Main')
// Exclude .navbar-item since they always have tabindex -1 (we provide
// the keyboard shortcuts Ctrl-[ and Ctrl-] instead).
?.querySelectorAll('input,textarea,select,button:not(.navbar-item)');
const tabIndex = payload ? 0 : -1;
elements?.forEach(element => {
(element as HTMLElement).tabIndex = tabIndex;
});
break;
}
case 'updateEditors': {
editors.showSessions(payload.input, payload.output);
evaluator.setActive(payload.input, payload.output, payload.options);
evaluator.run();
break;
}
case 'resizeEditors': {
// Use requestAnimationFrame to give Elm time to render. The newly
// rendered DOM might affect the size of the container to which the Ace
// editors adjust when we call resize() on them.
requestAnimationFrame(() => {
editors.resize();
});
break;
}
case 'updateShareLink': {
app.ports.fromJS.send({
command: 'shareLinkUpdated',
payload: linkSharing.createShareLink(),
});
break;
}
case 'selectAndCopyShareLink': {
waitForElement('ShareLink').then(element => {
const input = element as HTMLInputElement;
input.select();
// Not all browsers will allow this, so only tell the user we copied
// the URL if in fact we were able to.
if (document.execCommand('copy')) {
app.ports.fromJS.send({
command: 'shareLinkCopied',
payload: null,
});
}
});
break;
}
}
});
evaluator.start();
}
// Load ace/mode/json first. Running main() eventually results in setting an
// editor mode to ace/mode/fidl, prompting Ace to execute the ace/mode/fidl
// definition function in ace-modes.js. This file imports some resources that
// are not exposed as separate files by the CDN, like ace/mode/behaviour/cstyle.
// By first loading ace/mode/json, these common resources get populated in Ace's
// internal map, so it won't attempt and fail to load them from the CDN.
(ace as any).config.loadModule('ace/mode/json', () => {
main();
});