sass-site/source/assets/js/playground.ts

258 lines
7.4 KiB
TypeScript
Raw Normal View History

2023-06-22 21:44:26 +02:00
/* eslint-disable node/no-extraneous-import */
import {setDiagnostics} from '@codemirror/lint';
import {Text} from '@codemirror/state';
import {EditorView} from 'codemirror';
2023-06-10 18:31:43 +02:00
import debounce from 'lodash/debounce';
2023-06-22 21:44:26 +02:00
import {compileString, info, Logger, OutputStyle, Syntax} from 'sass';
2023-06-22 21:44:26 +02:00
import {displayForConsoleLog} from './playground/console-utils.js';
import {editorSetup, outputSetup} from './playground/editor-setup.js';
import {
base64ToState,
errorToDiagnostic,
logsToDiagnostics,
ParseResult,
PlaygroundState,
stateToBase64,
} from './playground/utils.js';
2023-06-06 17:33:41 +02:00
function setupPlayground() {
2023-06-09 18:24:33 +02:00
const hashState = base64ToState(location.hash);
2023-06-07 15:46:39 +02:00
const initialState: PlaygroundState = {
2023-06-09 18:24:33 +02:00
inputFormat: hashState.inputFormat || 'scss',
outputFormat: hashState.outputFormat || 'expanded',
2023-06-06 22:02:26 +02:00
compilerHasError: false,
2023-06-09 18:24:33 +02:00
inputValue: hashState.inputValue || '',
2023-06-07 21:13:23 +02:00
debugOutput: [],
2023-06-06 22:02:26 +02:00
};
2023-06-07 15:46:39 +02:00
// Proxy intercepts setters and triggers side effects
2023-06-07 15:46:39 +02:00
const playgroundState = new Proxy(initialState, {
set(state: PlaygroundState, prop: keyof PlaygroundState, ...rest) {
// Set state first so called functions have access
const set = Reflect.set(state, prop, ...rest);
if (['inputFormat', 'outputFormat'].includes(prop)) {
updateButtonState();
debouncedUpdateCSS();
} else if (prop === 'compilerHasError') {
updateErrorState();
} else if (prop === 'inputValue') {
debouncedUpdateCSS();
}
2023-06-09 18:24:33 +02:00
if (['inputFormat', 'outputFormat', 'inputValue'].includes(prop)) {
updateURL();
}
2023-06-07 15:46:39 +02:00
return set;
},
});
2023-07-02 18:42:05 +02:00
// Setup input Sass view
const editor = new EditorView({
2023-06-09 18:24:33 +02:00
doc: playgroundState.inputValue,
extensions: [
...editorSetup,
2023-06-22 21:44:26 +02:00
EditorView.updateListener.of(v => {
if (v.docChanged) {
2023-06-06 22:02:26 +02:00
playgroundState.inputValue = editor.state.doc.toString();
}
}),
],
2023-07-02 18:42:05 +02:00
parent: document.querySelector('.sl-code-is-source') || undefined,
});
// Setup CSS view
const viewer = new EditorView({
extensions: [...outputSetup],
2023-06-15 21:19:37 +02:00
parent: document.querySelector('.sl-code-is-compiled') || undefined,
});
2023-06-09 18:24:33 +02:00
// Apply initial state to dom
function applyInitialState() {
updateButtonState();
debouncedUpdateCSS();
updateErrorState();
}
type TabbarItemDataset =
| {
value: Syntax;
setting: 'inputFormat';
}
| {
value: OutputStyle;
setting: 'outputFormat';
};
function attachListeners() {
// Settings buttons handlers
function clickHandler(event: Event) {
if (event.currentTarget instanceof HTMLElement) {
const settings = event.currentTarget.dataset as TabbarItemDataset;
if (settings.setting === 'inputFormat') {
playgroundState.inputFormat = settings.value;
} else {
playgroundState.outputFormat = settings.value;
}
}
}
2023-06-06 22:02:26 +02:00
const options = document.querySelectorAll('[data-value]');
2023-06-22 21:44:26 +02:00
Array.from(options).forEach(option => {
option.addEventListener('click', clickHandler);
});
2023-06-09 18:24:33 +02:00
// Copy URL handlers
2023-06-09 18:24:33 +02:00
const copyURLButton = document.getElementById('playground-copy-url');
const copiedAlert = document.getElementById('playground-copied-alert');
let timer: undefined | number;
copyURLButton?.addEventListener('click', () => {
void navigator.clipboard.writeText(location.href);
copiedAlert?.classList.add('show');
if (timer) clearTimeout(timer);
timer = window.setTimeout(() => {
copiedAlert?.classList.remove('show');
}, 3000);
});
}
2023-06-07 15:46:39 +02:00
/**
* updateButtonState
* Applies playgroundState to the buttons
* Called by state's proxy setter
*/
2023-06-06 22:02:26 +02:00
function updateButtonState() {
const inputFormatTab = document.querySelector(
2023-06-22 21:44:26 +02:00
'[data-setting="inputFormat"]'
2023-06-06 22:02:26 +02:00
) as HTMLDivElement;
2023-06-15 20:50:34 +02:00
const inputButtons = inputFormatTab.querySelectorAll('[data-value]');
2023-06-22 21:44:26 +02:00
inputButtons.forEach(button => {
2023-06-15 20:50:34 +02:00
if (!(button instanceof HTMLButtonElement)) return;
button.dataset.active = String(
2023-06-22 21:44:26 +02:00
button.dataset.value === playgroundState.inputFormat
2023-06-15 20:50:34 +02:00
);
});
2023-06-06 22:02:26 +02:00
const outputFormatTab = document.querySelector(
2023-06-22 21:44:26 +02:00
'[data-setting="outputFormat"]'
2023-06-06 22:02:26 +02:00
) as HTMLDivElement;
2023-06-15 20:50:34 +02:00
const outputButtons = outputFormatTab.querySelectorAll('[data-value]');
2023-06-22 21:44:26 +02:00
outputButtons.forEach(button => {
2023-06-15 20:50:34 +02:00
if (!(button instanceof HTMLButtonElement)) return;
button.dataset.active = String(
2023-06-22 21:44:26 +02:00
button.dataset.value === playgroundState.outputFormat
2023-06-15 20:50:34 +02:00
);
});
2023-06-06 22:02:26 +02:00
}
2023-06-07 15:46:39 +02:00
/**
* updateErrorState
* Applies error state
* Called by state's proxy setter
*/
2023-06-06 22:02:26 +02:00
function updateErrorState() {
const editorWrapper = document.querySelector(
2023-06-22 21:44:26 +02:00
'[data-compiler-has-error]'
2023-06-06 22:02:26 +02:00
) as HTMLDivElement;
editorWrapper.dataset.compilerHasError =
playgroundState.compilerHasError.toString();
}
/**
* updateDebugOutput
* Applies debug output state
* Called at end of updateCSS, and not by a debugOutput setter
* debugOutput may be updated multiple times during the sass compilation,
* so the output is collected through the compilation and the display updated just once.
*/
2023-06-07 21:13:23 +02:00
function updateDebugOutput() {
2023-06-15 21:19:37 +02:00
const console = document.querySelector(
2023-06-22 21:44:26 +02:00
'.sl-c-playground__console'
2023-06-15 21:19:37 +02:00
) as HTMLDivElement;
2023-06-07 21:13:23 +02:00
console.innerHTML = playgroundState.debugOutput
.map(displayForConsoleLog)
.join('\n');
}
function updateDiagnostics() {
const diagnostics = logsToDiagnostics(playgroundState.debugOutput);
const transaction = setDiagnostics(editor.state, diagnostics);
editor.dispatch(transaction);
}
function updateCSS() {
2023-06-07 21:13:23 +02:00
playgroundState.debugOutput = [];
2023-06-06 22:02:26 +02:00
const result = parse(playgroundState.inputValue);
if ('css' in result) {
2023-06-05 19:55:56 +02:00
const text = Text.of(result.css.split('\n'));
viewer.dispatch({
changes: {
from: 0,
to: viewer.state.doc.toString().length,
insert: text,
},
});
2023-06-06 22:02:26 +02:00
playgroundState.compilerHasError = false;
2023-06-05 19:55:56 +02:00
} else {
2023-06-06 22:02:26 +02:00
playgroundState.compilerHasError = true;
2023-06-07 21:13:23 +02:00
playgroundState.debugOutput = [
...playgroundState.debugOutput,
2023-06-22 21:44:26 +02:00
{type: 'error', error: result.error},
2023-06-07 21:13:23 +02:00
];
2023-06-05 19:55:56 +02:00
}
2023-06-07 21:13:23 +02:00
updateDebugOutput();
updateDiagnostics();
}
2023-06-08 20:40:42 +02:00
const debouncedUpdateCSS = debounce(updateCSS, 200);
2023-06-07 21:13:23 +02:00
const logger: Logger = {
warn(message, options) {
playgroundState.debugOutput = [
...playgroundState.debugOutput,
2023-06-22 21:44:26 +02:00
{message, options, type: 'warn'},
2023-06-07 21:13:23 +02:00
];
},
debug(message, options) {
playgroundState.debugOutput = [
...playgroundState.debugOutput,
2023-06-22 21:44:26 +02:00
{message, options, type: 'debug'},
2023-06-07 21:13:23 +02:00
];
},
};
2023-06-05 19:55:56 +02:00
function parse(css: string): ParseResult {
try {
2023-06-05 19:55:56 +02:00
const result = compileString(css, {
2023-06-06 22:02:26 +02:00
syntax: playgroundState.inputFormat,
style: playgroundState.outputFormat,
2023-06-07 21:13:23 +02:00
logger: logger,
2023-06-05 19:55:56 +02:00
});
2023-06-22 21:44:26 +02:00
return {css: result.css};
} catch (error) {
2023-06-22 21:44:26 +02:00
return {error};
}
2023-06-05 19:55:56 +02:00
}
2023-06-09 18:24:33 +02:00
function updateURL() {
const hash = stateToBase64(playgroundState);
2023-06-21 18:34:55 +02:00
history.replaceState('playground', '', `#${hash}`);
2023-06-09 18:24:33 +02:00
}
2023-06-21 16:33:05 +02:00
function updateSassVersion() {
const version = info.split('\t')[1];
const versionSpan = document.querySelector(
2023-06-22 21:44:26 +02:00
'.sl-c-playground__tabbar-version'
2023-06-21 16:33:05 +02:00
) as HTMLSpanElement;
versionSpan.innerText = `v${version}`;
}
attachListeners();
2023-06-09 18:24:33 +02:00
applyInitialState();
2023-06-21 16:33:05 +02:00
updateSassVersion();
}
2023-06-10 18:31:43 +02:00
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupPlayground);
} else {
setupPlayground();
}