(function() { // eslint-disable-line strict 'use strict'; // eslint-disable-line strict /* global monaco, require, lessonEditorSettings */ const { fixSourceLinks, fixJSForCodeSite, extraHTMLParsing, runOnResize, lessonSettings, } = lessonEditorSettings; const lessonHelperScriptRE = /'); html = obj.html; const fqURL = getFQUrl(url); /** @type Object */ const scriptInfos = {}; g.rootScriptInfo = { fqURL, deps: [], source: rootScript, }; scriptInfos[fqURL] = g.rootScriptInfo; const {text} = await getWorkerScripts(rootScript, fqURL, scriptInfos); g.rootScriptInfo.source = text; g.scriptInfos = scriptInfos; for (const [fqURL, scriptInfo] of Object.entries(scriptInfos)) { addSource('js', basename(fqURL), scriptInfo.source, scriptInfo); } const tm = titleRE.exec(html); if (tm) { g.title = tm[1]; } const scripts = []; html = html.replace(externalScriptRE, function(p0, p1, p2) { p1 = p1 || ''; scripts.push(`${p1}`); return ''; }); const dataScripts = []; html = html.replace(dataScriptRE, function(p0, p1, p2, p3) { p1 = p1 || ''; dataScripts.push(`${p1}`); return ''; }); htmlParts.html.sources[0].source += dataScripts.join('\n'); htmlParts.html.sources[0].source += scripts.join('\n'); // add style section if there is non if (html.indexOf('${css}') < 0) { html = html.replace('', '\n'); } // add hackedparams section. // We need a way to pass parameters to a blob. Normally they'd be passed as // query params but that only works in Firefox >:( html = html.replace('', '\n'); html = extraHTMLParsing(html, htmlParts); let links = ''; html = html.replace(cssLinkRE, function(p0, p1) { if (isCSSLinkRE.test(p1)) { const m = hrefRE.exec(p1); if (m) { links += `@import url("${m[1]}");\n`; } return ''; } else { return p0; } }); htmlParts.css.sources[0].source = links + htmlParts.css.sources[0].source; g.html = html; } function cantGetHTML(e) { // eslint-disable-line console.log(e); // eslint-disable-line console.log("TODO: don't run editor if can't get HTML"); // eslint-disable-line } async function main() { const query = getQuery(); g.url = getFQUrl(query.url); g.query = getSearch(g.url); let html; try { html = await getHTML(query.url); } catch (err) { console.log(err); // eslint-disable-line return; } await parseHTML(query.url, html); setupEditor(query.url); if (query.startPane) { const button = document.querySelector('.button-' + query.startPane); toggleSourcePane(button); } } function getJavaScriptBlob(source) { const blob = new Blob([source], {type: 'application/javascript'}); return URL.createObjectURL(blob); } let blobGeneration = 0; function makeBlobURLsForSources(scriptInfo) { ++blobGeneration; function makeBlobURLForSourcesImpl(scriptInfo) { if (scriptInfo.blobGenerationId !== blobGeneration) { scriptInfo.blobGenerationId = blobGeneration; if (scriptInfo.blobUrl) { URL.revokeObjectURL(scriptInfo.blobUrl); } scriptInfo.deps.forEach(makeBlobURLForSourcesImpl); let text = scriptInfo.source; scriptInfo.deps.forEach((depScriptInfo) => { text = text.split(depScriptInfo.fqURL).join(depScriptInfo.blobUrl); }); scriptInfo.numLinesBeforeScript = 0; if (scriptInfo.isWorker) { const extra = `self.lessonSettings = ${JSON.stringify(lessonSettings)}; importScripts('${dirname(scriptInfo.fqURL)}/resources/lessons-worker-helper.js')`; scriptInfo.numLinesBeforeScript = extra.split('\n').length; text = `${extra}\n${text}`; } scriptInfo.blobUrl = getJavaScriptBlob(text); scriptInfo.munged = text; } } makeBlobURLForSourcesImpl(scriptInfo); } function getSourceBlob(htmlParts) { g.rootScriptInfo.source = htmlParts.js; makeBlobURLsForSources(g.rootScriptInfo); const prefix = dirname(g.url); let source = g.html; source = source.replace('${hackedParams}', JSON.stringify(g.query)); source = source.replace('${html}', htmlParts.html); source = source.replace('${css}', htmlParts.css); source = source.replace('${js}', g.rootScriptInfo.munged); //htmlParts.js); source = source.replace('', ` `); source = source.replace('', ` `); const scriptNdx = source.indexOf('`; }).join('\n'); const init = ` // ------ // Creates Blobs for the Worker Scripts so things can be self contained for snippets/JSFiddle/Codepen // function getWorkerBlob() { const idsToUrls = []; const scriptElements = [...document.querySelectorAll('script[type=x-worker]')]; for (const scriptElement of scriptElements) { let text = scriptElement.text; for (const {id, url} of idsToUrls) { text = text.split(id).join(url); } const blob = new Blob([text], {type: 'application/javascript'}); const url = URL.createObjectURL(blob); const id = scriptElement.id; idsToUrls.push({id, url}); } return idsToUrls.pop().url; } `; return { js: mainScript.split(`'${workerName}'`).join('getWorkerBlob()') + init, html, }; } function openInCodepen() { const comment = `// ${g.title} // from ${g.url} `; getSourcesFromEditor(); const scripts = makeScriptsForWorkers(g.rootScriptInfo); const pen = { title : g.title, description : 'from: ' + g.url, tags : lessonEditorSettings.tags, editors : '101', html : scripts.html + htmlParts.html.sources[0].source.replace(lessonHelperScriptRE, ''), css : htmlParts.css.sources[0].source, js : comment + fixJSForCodeSite(scripts.js), }; const elem = document.createElement('div'); elem.innerHTML = ` " `; elem.querySelector('input[name=data]').value = JSON.stringify(pen); window.frameElement.ownerDocument.body.appendChild(elem); elem.querySelector('form').submit(); window.frameElement.ownerDocument.body.removeChild(elem); } function openInJSFiddle() { const comment = `// ${g.title} // from ${g.url} `; getSourcesFromEditor(); const scripts = makeScriptsForWorkers(g.rootScriptInfo); const elem = document.createElement('div'); elem.innerHTML = ` `; elem.querySelector('input[name=html]').value = scripts.html + htmlParts.html.sources[0].source.replace(lessonHelperScriptRE, ''); elem.querySelector('input[name=css]').value = htmlParts.css.sources[0].source; elem.querySelector('input[name=js]').value = comment + fixJSForCodeSite(scripts.js); elem.querySelector('input[name=title]').value = g.title; window.frameElement.ownerDocument.body.appendChild(elem); elem.querySelector('form').submit(); window.frameElement.ownerDocument.body.removeChild(elem); } function selectFile(info, ndx, fileDivs) { if (info.editors.length <= 1) { return; } info.editors.forEach((editorInfo, i) => { const selected = i === ndx; editorInfo.div.style.display = selected ? '' : 'none'; editorInfo.editor.layout(); addRemoveClass(fileDivs.children[i], 'fileSelected', selected); }); } function showEditorSubPane(type, ndx) { const info = htmlParts[type]; selectFile(info, ndx, info.files); } function setupEditor() { forEachHTMLPart(function(info, ndx, name) { info.pane = document.querySelector('.panes>.' + name); info.code = info.pane.querySelector('.code'); info.files = info.pane.querySelector('.files'); info.editors = info.sources.map((sourceInfo, ndx) => { if (info.sources.length > 1) { const div = document.createElement('div'); div.textContent = basename(sourceInfo.name); info.files.appendChild(div); div.addEventListener('click', () => { selectFile(info, ndx, info.files); }); } const div = document.createElement('div'); info.code.appendChild(div); const editor = runEditor(div, sourceInfo.source, info.language); sourceInfo.editor = editor; return { div, editor, }; }); info.button = document.querySelector('.button-' + name); info.button.addEventListener('click', function() { toggleSourcePane(info.button); runIfNeeded(); }); }); g.fullscreen = document.querySelector('.button-fullscreen'); g.fullscreen.addEventListener('click', toggleFullscreen); g.run = document.querySelector('.button-run'); g.run.addEventListener('click', run); g.iframe = document.querySelector('.result>iframe'); g.other = document.querySelector('.panes .other'); document.querySelector('.button-codepen').addEventListener('click', openInCodepen); document.querySelector('.button-jsfiddle').addEventListener('click', openInJSFiddle); g.result = document.querySelector('.panes .result'); g.resultButton = document.querySelector('.button-result'); g.resultButton.addEventListener('click', function() { toggleResultPane(); runIfNeeded(); }); g.result.style.display = 'none'; toggleResultPane(); if (window.innerWidth > 1200) { toggleSourcePane(htmlParts.js.button); } window.addEventListener('resize', resize); showEditorSubPane('js', 0); showOtherIfAllPanesOff(); document.querySelector('.other .loading').style.display = 'none'; resize(); run(); } function toggleFullscreen() { try { toggleIFrameFullscreen(window); resize(); runIfNeeded(); } catch (e) { console.error(e); // eslint-disable-line } } function runIfNeeded() { if (runOnResize) { run(); } } function run(options) { g.setPosition = false; const url = getSourceBlobFromEditor(options); g.iframe.src = url; } function addClass(elem, className) { const parts = elem.className.split(' '); if (parts.indexOf(className) < 0) { elem.className = elem.className + ' ' + className; } } function removeClass(elem, className) { const parts = elem.className.split(' '); const numParts = parts.length; for (;;) { const ndx = parts.indexOf(className); if (ndx < 0) { break; } parts.splice(ndx, 1); } if (parts.length !== numParts) { elem.className = parts.join(' '); return true; } return false; } function toggleClass(elem, className) { if (removeClass(elem, className)) { return false; } else { addClass(elem, className); return true; } } function toggleIFrameFullscreen(childWindow) { const frame = childWindow.frameElement; if (frame) { const isFullScreen = toggleClass(frame, 'fullscreen'); frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : ''; } } function addRemoveClass(elem, className, add) { if (add) { addClass(elem, className); } else { removeClass(elem, className); } } function toggleSourcePane(pressedButton) { forEachHTMLPart(function(info) { const pressed = pressedButton === info.button; if (pressed && !info.showing) { addClass(info.button, 'show'); info.pane.style.display = 'flex'; info.showing = true; } else { removeClass(info.button, 'show'); info.pane.style.display = 'none'; info.showing = false; } }); showOtherIfAllPanesOff(); resize(); } function showingResultPane() { return g.result.style.display !== 'none'; } function toggleResultPane() { const showing = showingResultPane(); g.result.style.display = showing ? 'none' : 'block'; addRemoveClass(g.resultButton, 'show', !showing); showOtherIfAllPanesOff(); resize(); } function showOtherIfAllPanesOff() { let paneOn = showingResultPane(); forEachHTMLPart(function(info) { paneOn = paneOn || info.showing; }); g.other.style.display = paneOn ? 'none' : 'block'; } // seems like we should probably store a map function getEditorNdxByBlobUrl(type, url) { return htmlParts[type].sources.findIndex(source => source.scriptInfo.blobUrl === url); } function getActualLineNumberAndMoveTo(url, lineNo, colNo) { let origUrl = url; let actualLineNo = lineNo; const scriptInfo = Object.values(g.scriptInfos).find(scriptInfo => scriptInfo.blobUrl === url); if (scriptInfo) { actualLineNo = lineNo - scriptInfo.numLinesBeforeScript; origUrl = basename(scriptInfo.fqURL); if (!g.setPosition) { // Only set the first position g.setPosition = true; const editorNdx = getEditorNdxByBlobUrl('js', url); if (editorNdx >= 0) { showEditorSubPane('js', editorNdx); const editor = htmlParts.js.editors[editorNdx].editor; editor.setPosition({ lineNumber: actualLineNo, column: colNo, }); editor.revealLineInCenterIfOutsideViewport(actualLineNo); editor.focus(); } } } return {origUrl, actualLineNo}; } window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo; function runEditor(parent, source, language) { return monaco.editor.create(parent, { value: source, language: language, //lineNumbers: false, theme: 'vs-dark', disableTranslate3d: true, // model: null, scrollBeyondLastLine: false, minimap: { enabled: false }, }); } async function runAsBlob() { const query = getQuery(); g.url = getFQUrl(query.url); g.query = getSearch(g.url); let html; try { html = await getHTML(query.url); } catch (err) { console.log(err); // eslint-disable-line return; } await parseHTML(query.url, html); window.location.href = getSourceBlobFromOrig(); } function applySubstitutions() { [...document.querySelectorAll('[data-subst]')].forEach((elem) => { elem.dataset.subst.split('&').forEach((pair) => { const [attr, key] = pair.split('|'); elem[attr] = lessonEditorSettings[key]; }); }); } function start() { const parentQuery = getQuery(window.parent.location.search); const isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i); const isEdge = window.navigator.userAgent.match(/Edge/i); if (isEdge || isSmallish || parentQuery.editor === 'false') { runAsBlob(); // var url = query.url; // window.location.href = url; } else { applySubstitutions(); require.config({ paths: { 'vs': '/monaco-editor/min/vs' }}); require(['vs/editor/editor.main'], main); } } start(); }());