editor.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. (function() { // eslint-disable-line strict
  2. 'use strict'; // eslint-disable-line strict
  3. /* global monaco, require, lessonEditorSettings */
  4. const {
  5. fixSourceLinks,
  6. fixJSForCodeSite,
  7. extraHTMLParsing,
  8. runOnResize,
  9. lessonSettings,
  10. } = lessonEditorSettings;
  11. const lessonHelperScriptRE = /<script src="[^"]+lessons-helper\.js"><\/script>/;
  12. const webglDebugHelperScriptRE = /<script src="[^"]+webgl-debug-helper\.js"><\/script>/;
  13. function getQuery(s) {
  14. s = s === undefined ? window.location.search : s;
  15. if (s[0] === '?' ) {
  16. s = s.substring(1);
  17. }
  18. const query = {};
  19. s.split('&').forEach(function(pair) {
  20. const parts = pair.split('=').map(decodeURIComponent);
  21. query[parts[0]] = parts[1];
  22. });
  23. return query;
  24. }
  25. function getSearch(url) {
  26. // yea I know this is not perfect but whatever
  27. const s = url.indexOf('?');
  28. return s < 0 ? {} : getQuery(url.substring(s));
  29. }
  30. function getFQUrl(path, baseUrl) {
  31. const url = new URL(path, baseUrl || window.location.href);
  32. return url.href;
  33. }
  34. async function getHTML(url) {
  35. const req = await fetch(url);
  36. return await req.text();
  37. }
  38. function getPrefix(url) {
  39. const u = new URL(url, window.location.href);
  40. const prefix = u.origin + dirname(u.pathname);
  41. return prefix;
  42. }
  43. function fixCSSLinks(url, source) {
  44. const cssUrlRE1 = /(url\(')(.*?)('\))/g;
  45. const cssUrlRE2 = /(url\()(.*?)(\))/g;
  46. const prefix = getPrefix(url);
  47. function addPrefix(url) {
  48. return url.indexOf('://') < 0 ? `${prefix}/${url}` : url;
  49. }
  50. function makeFQ(match, prefix, url, suffix) {
  51. return `${prefix}${addPrefix(url)}${suffix}`;
  52. }
  53. source = source.replace(cssUrlRE1, makeFQ);
  54. source = source.replace(cssUrlRE2, makeFQ);
  55. return source;
  56. }
  57. /**
  58. * @typedef {Object} Globals
  59. * @property {SourceInfo} rootScriptInfo
  60. * @property {Object<string, SourceInfo} scriptInfos
  61. */
  62. /** @type {Globals} */
  63. const g = {
  64. html: '',
  65. };
  66. /**
  67. * This is what's in the sources array
  68. * @typedef {Object} SourceInfo
  69. * @property {string} source The source text (html, css, js)
  70. * @property {string} name The filename or "main page"
  71. * @property {ScriptInfo} scriptInfo The associated ScriptInfo
  72. * @property {string} fqURL ??
  73. * @property {Editor} editor in instance of Monaco editor
  74. *
  75. */
  76. /**
  77. * @typedef {Object} EditorInfo
  78. * @property {HTMLElement} div The div holding the monaco editor
  79. * @property {Editor} editor an instance of a monaco editor
  80. */
  81. /**
  82. * What's under each language
  83. * @typedef {Object} HTMLPart
  84. * @property {string} language Name of language
  85. * @property {SourceInfo} sources array of SourceInfos. Usually 1 for HTML, 1 for CSS, N for JS
  86. * @property {HTMLElement} pane the pane for these editors
  87. * @property {HTMLElement} code the div holding the files
  88. * @property {HTMLElement} files the div holding the divs holding the monaco editors
  89. * @property {HTMLElement} button the element to click to show this pane
  90. * @property {EditorInfo} editors
  91. */
  92. /** @type {Object<string, HTMLPart>} */
  93. const htmlParts = {
  94. js: {
  95. language: 'javascript',
  96. sources: [],
  97. },
  98. css: {
  99. language: 'css',
  100. sources: [],
  101. },
  102. html: {
  103. language: 'html',
  104. sources: [],
  105. },
  106. };
  107. function forEachHTMLPart(fn) {
  108. Object.keys(htmlParts).forEach(function(name, ndx) {
  109. const info = htmlParts[name];
  110. fn(info, ndx, name);
  111. });
  112. }
  113. function getHTMLPart(re, obj, tag) {
  114. let part = '';
  115. obj.html = obj.html.replace(re, function(p0, p1) {
  116. part = p1;
  117. return tag;
  118. });
  119. return part.replace(/\s*/, '');
  120. }
  121. // doesn't handle multi-line comments or comments with { or } in them
  122. function formatCSS(css) {
  123. let indent = '';
  124. return css.split('\n').map((line) => {
  125. let currIndent = indent;
  126. if (line.includes('{')) {
  127. indent = indent + ' ';
  128. } else if (line.includes('}')) {
  129. indent = indent.substring(0, indent.length - 2);
  130. currIndent = indent;
  131. }
  132. return `${currIndent}${line.trim()}`;
  133. }).join('\n');
  134. }
  135. async function getScript(url, scriptInfos) {
  136. // check it's an example script, not some other lib
  137. if (!scriptInfos[url].source) {
  138. const source = await getHTML(url);
  139. const fixedSource = fixSourceLinks(url, source);
  140. const {text} = await getWorkerScripts(fixedSource, url, scriptInfos);
  141. scriptInfos[url].source = text;
  142. }
  143. }
  144. /**
  145. * @typedef {Object} ScriptInfo
  146. * @property {string} fqURL The original fully qualified URL
  147. * @property {ScriptInfo[]} deps Array of other ScriptInfos this is script dependant on
  148. * @property {boolean} isWorker True if this script came from `new Worker('someurl')` vs `import` or `importScripts`
  149. * @property {string} blobUrl The blobUrl for this script if one has been made
  150. * @property {number} blobGenerationId Used to not visit things twice while recursing.
  151. * @property {string} source The source as extracted. Updated from editor by getSourcesFromEditor
  152. * @property {string} munged The source after urls have been replaced with blob urls etc... (the text send to new Blob)
  153. */
  154. async function getWorkerScripts(text, baseUrl, scriptInfos = {}) {
  155. const parentScriptInfo = scriptInfos[baseUrl];
  156. const workerRE = /(new\s+Worker\s*\(\s*)('|")(.*?)('|")/g;
  157. const importScriptsRE = /(importScripts\s*\(\s*)('|")(.*?)('|")/g;
  158. const newScripts = [];
  159. const slashRE = /\/threejs\/[^/]+$/;
  160. function replaceWithUUID(match, prefix, quote, url) {
  161. const fqURL = getFQUrl(url, baseUrl);
  162. if (!slashRE.test(fqURL)) {
  163. return match.toString();
  164. }
  165. if (!scriptInfos[url]) {
  166. scriptInfos[fqURL] = {
  167. fqURL,
  168. deps: [],
  169. isWorker: prefix.indexOf('Worker') >= 0,
  170. };
  171. newScripts.push(fqURL);
  172. }
  173. parentScriptInfo.deps.push(scriptInfos[fqURL]);
  174. return `${prefix}${quote}${fqURL}${quote}`;
  175. }
  176. text = text.replace(workerRE, replaceWithUUID);
  177. text = text.replace(importScriptsRE, replaceWithUUID);
  178. await Promise.all(newScripts.map((url) => {
  179. return getScript(url, scriptInfos);
  180. }));
  181. return {text, scriptInfos};
  182. }
  183. // hack: scriptInfo is undefined for html and css
  184. // should try to include html and css in scriptInfos
  185. function addSource(type, name, source, scriptInfo) {
  186. htmlParts[type].sources.push({source, name, scriptInfo});
  187. }
  188. async function parseHTML(url, html) {
  189. html = fixSourceLinks(url, html);
  190. html = html.replace(/<div class="description">[^]*?<\/div>/, '');
  191. const styleRE = /<style>([^]*?)<\/style>/i;
  192. const titleRE = /<title>([^]*?)<\/title>/i;
  193. const bodyRE = /<body>([^]*?)<\/body>/i;
  194. const inlineScriptRE = /<script>([^]*?)<\/script>/i;
  195. const externalScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script\s*src\s*=\s*"(.*?)"\s*>\s*<\/script>/ig;
  196. const dataScriptRE = /(<!--(?:(?!-->)[\s\S])*?-->\n){0,1}<script (.*?)>([^]*?)<\/script>/ig;
  197. const cssLinkRE = /<link ([^>]+?)>/g;
  198. const isCSSLinkRE = /type="text\/css"|rel="stylesheet"/;
  199. const hrefRE = /href="([^"]+)"/;
  200. const obj = { html: html };
  201. addSource('css', 'css', formatCSS(fixCSSLinks(url, getHTMLPart(styleRE, obj, '<style>\n${css}</style>'))));
  202. addSource('html', 'html', getHTMLPart(bodyRE, obj, '<body>${html}</body>'));
  203. const rootScript = getHTMLPart(inlineScriptRE, obj, '<script>${js}</script>');
  204. html = obj.html;
  205. const fqURL = getFQUrl(url);
  206. /** @type Object<string, SourceInfo> */
  207. const scriptInfos = {};
  208. g.rootScriptInfo = {
  209. fqURL,
  210. deps: [],
  211. source: rootScript,
  212. };
  213. scriptInfos[fqURL] = g.rootScriptInfo;
  214. const {text} = await getWorkerScripts(rootScript, fqURL, scriptInfos);
  215. g.rootScriptInfo.source = text;
  216. g.scriptInfos = scriptInfos;
  217. for (const [fqURL, scriptInfo] of Object.entries(scriptInfos)) {
  218. addSource('js', basename(fqURL), scriptInfo.source, scriptInfo);
  219. }
  220. const tm = titleRE.exec(html);
  221. if (tm) {
  222. g.title = tm[1];
  223. }
  224. const scripts = [];
  225. html = html.replace(externalScriptRE, function(p0, p1, p2) {
  226. p1 = p1 || '';
  227. scripts.push(`${p1}<script src="${p2}"></script>`);
  228. return '';
  229. });
  230. const dataScripts = [];
  231. html = html.replace(dataScriptRE, function(p0, p1, p2, p3) {
  232. p1 = p1 || '';
  233. dataScripts.push(`${p1}<script ${p2}>${p3}</script>`);
  234. return '';
  235. });
  236. htmlParts.html.sources[0].source += dataScripts.join('\n');
  237. htmlParts.html.sources[0].source += scripts.join('\n');
  238. // add style section if there is non
  239. if (html.indexOf('${css}') < 0) {
  240. html = html.replace('</head>', '<style>\n${css}</style>\n</head>');
  241. }
  242. // add hackedparams section.
  243. // We need a way to pass parameters to a blob. Normally they'd be passed as
  244. // query params but that only works in Firefox >:(
  245. html = html.replace('</head>', '<script id="hackedparams">window.hackedParams = ${hackedParams}\n</script>\n</head>');
  246. html = extraHTMLParsing(html, htmlParts);
  247. let links = '';
  248. html = html.replace(cssLinkRE, function(p0, p1) {
  249. if (isCSSLinkRE.test(p1)) {
  250. const m = hrefRE.exec(p1);
  251. if (m) {
  252. links += `@import url("${m[1]}");\n`;
  253. }
  254. return '';
  255. } else {
  256. return p0;
  257. }
  258. });
  259. htmlParts.css.sources[0].source = links + htmlParts.css.sources[0].source;
  260. g.html = html;
  261. }
  262. function cantGetHTML(e) { // eslint-disable-line
  263. console.log(e); // eslint-disable-line
  264. console.log("TODO: don't run editor if can't get HTML"); // eslint-disable-line
  265. }
  266. async function main() {
  267. const query = getQuery();
  268. g.url = getFQUrl(query.url);
  269. g.query = getSearch(g.url);
  270. let html;
  271. try {
  272. html = await getHTML(query.url);
  273. } catch (err) {
  274. console.log(err); // eslint-disable-line
  275. return;
  276. }
  277. await parseHTML(query.url, html);
  278. setupEditor(query.url);
  279. if (query.startPane) {
  280. const button = document.querySelector('.button-' + query.startPane);
  281. toggleSourcePane(button);
  282. }
  283. }
  284. function getJavaScriptBlob(source) {
  285. const blob = new Blob([source], {type: 'application/javascript'});
  286. return URL.createObjectURL(blob);
  287. }
  288. let blobGeneration = 0;
  289. function makeBlobURLsForSources(scriptInfo) {
  290. ++blobGeneration;
  291. function makeBlobURLForSourcesImpl(scriptInfo) {
  292. if (scriptInfo.blobGenerationId !== blobGeneration) {
  293. scriptInfo.blobGenerationId = blobGeneration;
  294. if (scriptInfo.blobUrl) {
  295. URL.revokeObjectURL(scriptInfo.blobUrl);
  296. }
  297. scriptInfo.deps.forEach(makeBlobURLForSourcesImpl);
  298. let text = scriptInfo.source;
  299. scriptInfo.deps.forEach((depScriptInfo) => {
  300. text = text.split(depScriptInfo.fqURL).join(depScriptInfo.blobUrl);
  301. });
  302. scriptInfo.numLinesBeforeScript = 0;
  303. if (scriptInfo.isWorker) {
  304. const extra = `self.lessonSettings = ${JSON.stringify(lessonSettings)};
  305. importScripts('${dirname(scriptInfo.fqURL)}/resources/webgl-debug-helper.js');
  306. importScripts('${dirname(scriptInfo.fqURL)}/resources/lessons-worker-helper.js')`;
  307. scriptInfo.numLinesBeforeScript = extra.split('\n').length;
  308. text = `${extra}\n${text}`;
  309. }
  310. scriptInfo.blobUrl = getJavaScriptBlob(text);
  311. scriptInfo.munged = text;
  312. }
  313. }
  314. makeBlobURLForSourcesImpl(scriptInfo);
  315. }
  316. function getSourceBlob(htmlParts) {
  317. g.rootScriptInfo.source = htmlParts.js;
  318. makeBlobURLsForSources(g.rootScriptInfo);
  319. const prefix = dirname(g.url);
  320. let source = g.html;
  321. source = source.replace('${hackedParams}', JSON.stringify(g.query));
  322. source = source.replace('${html}', htmlParts.html);
  323. source = source.replace('${css}', htmlParts.css);
  324. source = source.replace('${js}', g.rootScriptInfo.munged); //htmlParts.js);
  325. source = source.replace('<head>', `<head>
  326. <link rel="stylesheet" href="${prefix}/resources/lesson-helper.css" type="text/css">
  327. <script match="false">self.lessonSettings = ${JSON.stringify(lessonSettings)}</script>`);
  328. source = source.replace('</head>', `<script src="${prefix}/resources/webgl-debug-helper.js"></script>
  329. <script src="${prefix}/resources/lessons-helper.js"></script>
  330. </head>`);
  331. const scriptNdx = source.indexOf('<script>');
  332. g.rootScriptInfo.numLinesBeforeScript = (source.substring(0, scriptNdx).match(/\n/g) || []).length;
  333. const blob = new Blob([source], {type: 'text/html'});
  334. // This seems hacky. We are combining html/css/js into one html blob but we already made
  335. // a blob for the JS so let's replace that blob. That means it will get auto-released when script blobs
  336. // are regenerated. It also means error reporting will work
  337. const blobUrl = URL.createObjectURL(blob);
  338. URL.revokeObjectURL(g.rootScriptInfo.blobUrl);
  339. g.rootScriptInfo.blobUrl = blobUrl;
  340. return blobUrl;
  341. }
  342. function getSourcesFromEditor() {
  343. for (const partTypeInfo of Object.values(htmlParts)) {
  344. for (const source of partTypeInfo.sources) {
  345. source.source = source.editor.getValue();
  346. // hack: shouldn't store this twice. Also see other comment,
  347. // should consolidate so scriptInfo is used for css and html
  348. if (source.scriptInfo) {
  349. source.scriptInfo.source = source.source;
  350. }
  351. }
  352. }
  353. }
  354. function getSourceBlobFromEditor() {
  355. getSourcesFromEditor();
  356. return getSourceBlob({
  357. html: htmlParts.html.sources[0].source,
  358. css: htmlParts.css.sources[0].source,
  359. js: htmlParts.js.sources[0].source,
  360. });
  361. }
  362. function getSourceBlobFromOrig() {
  363. return getSourceBlob({
  364. html: htmlParts.html.sources[0].source,
  365. css: htmlParts.css.sources[0].source,
  366. js: htmlParts.js.sources[0].source,
  367. });
  368. }
  369. function dirname(path) {
  370. const ndx = path.lastIndexOf('/');
  371. return path.substring(0, ndx);
  372. }
  373. function basename(path) {
  374. const ndx = path.lastIndexOf('/');
  375. return path.substring(ndx + 1);
  376. }
  377. function resize() {
  378. forEachHTMLPart(function(info) {
  379. info.editors.forEach((editorInfo) => {
  380. editorInfo.editor.layout();
  381. });
  382. });
  383. }
  384. function makeScriptsForWorkers(scriptInfo) {
  385. ++blobGeneration;
  386. function makeScriptsForWorkersImpl(scriptInfo) {
  387. const scripts = [];
  388. if (scriptInfo.blobGenerationId !== blobGeneration) {
  389. scriptInfo.blobGenerationId = blobGeneration;
  390. scripts.push(...scriptInfo.deps.map(makeScriptsForWorkersImpl).flat());
  391. let text = scriptInfo.source;
  392. scriptInfo.deps.forEach((depScriptInfo) => {
  393. text = text.split(depScriptInfo.fqURL).join(`worker-${basename(depScriptInfo.fqURL)}`);
  394. });
  395. scripts.push({
  396. name: `worker-${basename(scriptInfo.fqURL)}`,
  397. text,
  398. });
  399. }
  400. return scripts;
  401. }
  402. const scripts = makeScriptsForWorkersImpl(scriptInfo);
  403. const mainScript = scripts.pop().text;
  404. if (!scripts.length) {
  405. return {
  406. js: mainScript,
  407. html: '',
  408. };
  409. }
  410. const workerName = scripts[scripts.length - 1].name;
  411. const html = scripts.map((nameText) => {
  412. const {name, text} = nameText;
  413. return `<script id="${name}" type="x-worker">\n${text}\n</script>`;
  414. }).join('\n');
  415. const init = `
  416. // ------
  417. // Creates Blobs for the Worker Scripts so things can be self contained for snippets/JSFiddle/Codepen
  418. //
  419. function getWorkerBlob() {
  420. const idsToUrls = [];
  421. const scriptElements = [...document.querySelectorAll('script[type=x-worker]')];
  422. for (const scriptElement of scriptElements) {
  423. let text = scriptElement.text;
  424. for (const {id, url} of idsToUrls) {
  425. text = text.split(id).join(url);
  426. }
  427. const blob = new Blob([text], {type: 'application/javascript'});
  428. const url = URL.createObjectURL(blob);
  429. const id = scriptElement.id;
  430. idsToUrls.push({id, url});
  431. }
  432. return idsToUrls.pop().url;
  433. }
  434. `;
  435. return {
  436. js: mainScript.split(`'${workerName}'`).join('getWorkerBlob()') + init,
  437. html,
  438. };
  439. }
  440. function fixHTMLForCodeSite(html) {
  441. html = html.replace(lessonHelperScriptRE, '');
  442. html = html.replace(webglDebugHelperScriptRE, '');
  443. return html;
  444. }
  445. function openInCodepen() {
  446. const comment = `// ${g.title}
  447. // from ${g.url}
  448. `;
  449. getSourcesFromEditor();
  450. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  451. const pen = {
  452. title : g.title,
  453. description : 'from: ' + g.url,
  454. tags : lessonEditorSettings.tags,
  455. editors : '101',
  456. html : scripts.html + fixHTMLForCodeSite(htmlParts.html.sources[0].source),
  457. css : htmlParts.css.sources[0].source,
  458. js : comment + fixJSForCodeSite(scripts.js),
  459. };
  460. const elem = document.createElement('div');
  461. elem.innerHTML = `
  462. <form method="POST" target="_blank" action="https://codepen.io/pen/define" class="hidden">'
  463. <input type="hidden" name="data">
  464. <input type="submit" />
  465. "</form>"
  466. `;
  467. elem.querySelector('input[name=data]').value = JSON.stringify(pen);
  468. window.frameElement.ownerDocument.body.appendChild(elem);
  469. elem.querySelector('form').submit();
  470. window.frameElement.ownerDocument.body.removeChild(elem);
  471. }
  472. function openInJSFiddle() {
  473. const comment = `// ${g.title}
  474. // from ${g.url}
  475. `;
  476. getSourcesFromEditor();
  477. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  478. const elem = document.createElement('div');
  479. elem.innerHTML = `
  480. <form method="POST" target="_black" action="https://jsfiddle.net/api/mdn/" class="hidden">
  481. <input type="hidden" name="html" />
  482. <input type="hidden" name="css" />
  483. <input type="hidden" name="js" />
  484. <input type="hidden" name="title" />
  485. <input type="hidden" name="wrap" value="b" />
  486. <input type="submit" />
  487. </form>
  488. `;
  489. elem.querySelector('input[name=html]').value = scripts.html + fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  490. elem.querySelector('input[name=css]').value = htmlParts.css.sources[0].source;
  491. elem.querySelector('input[name=js]').value = comment + fixJSForCodeSite(scripts.js);
  492. elem.querySelector('input[name=title]').value = g.title;
  493. window.frameElement.ownerDocument.body.appendChild(elem);
  494. elem.querySelector('form').submit();
  495. window.frameElement.ownerDocument.body.removeChild(elem);
  496. }
  497. function selectFile(info, ndx, fileDivs) {
  498. if (info.editors.length <= 1) {
  499. return;
  500. }
  501. info.editors.forEach((editorInfo, i) => {
  502. const selected = i === ndx;
  503. editorInfo.div.style.display = selected ? '' : 'none';
  504. editorInfo.editor.layout();
  505. addRemoveClass(fileDivs.children[i], 'fileSelected', selected);
  506. });
  507. }
  508. function showEditorSubPane(type, ndx) {
  509. const info = htmlParts[type];
  510. selectFile(info, ndx, info.files);
  511. }
  512. function setupEditor() {
  513. forEachHTMLPart(function(info, ndx, name) {
  514. info.pane = document.querySelector('.panes>.' + name);
  515. info.code = info.pane.querySelector('.code');
  516. info.files = info.pane.querySelector('.files');
  517. info.editors = info.sources.map((sourceInfo, ndx) => {
  518. if (info.sources.length > 1) {
  519. const div = document.createElement('div');
  520. div.textContent = basename(sourceInfo.name);
  521. info.files.appendChild(div);
  522. div.addEventListener('click', () => {
  523. selectFile(info, ndx, info.files);
  524. });
  525. }
  526. const div = document.createElement('div');
  527. info.code.appendChild(div);
  528. const editor = runEditor(div, sourceInfo.source, info.language);
  529. sourceInfo.editor = editor;
  530. return {
  531. div,
  532. editor,
  533. };
  534. });
  535. info.button = document.querySelector('.button-' + name);
  536. info.button.addEventListener('click', function() {
  537. toggleSourcePane(info.button);
  538. runIfNeeded();
  539. });
  540. });
  541. g.fullscreen = document.querySelector('.button-fullscreen');
  542. g.fullscreen.addEventListener('click', toggleFullscreen);
  543. g.run = document.querySelector('.button-run');
  544. g.run.addEventListener('click', run);
  545. g.iframe = document.querySelector('.result>iframe');
  546. g.other = document.querySelector('.panes .other');
  547. document.querySelector('.button-codepen').addEventListener('click', openInCodepen);
  548. document.querySelector('.button-jsfiddle').addEventListener('click', openInJSFiddle);
  549. g.result = document.querySelector('.panes .result');
  550. g.resultButton = document.querySelector('.button-result');
  551. g.resultButton.addEventListener('click', function() {
  552. toggleResultPane();
  553. runIfNeeded();
  554. });
  555. g.result.style.display = 'none';
  556. toggleResultPane();
  557. if (window.innerWidth > 1200) {
  558. toggleSourcePane(htmlParts.js.button);
  559. }
  560. window.addEventListener('resize', resize);
  561. showEditorSubPane('js', 0);
  562. showOtherIfAllPanesOff();
  563. document.querySelector('.other .loading').style.display = 'none';
  564. resize();
  565. run();
  566. }
  567. function toggleFullscreen() {
  568. try {
  569. toggleIFrameFullscreen(window);
  570. resize();
  571. runIfNeeded();
  572. } catch (e) {
  573. console.error(e); // eslint-disable-line
  574. }
  575. }
  576. function runIfNeeded() {
  577. if (runOnResize) {
  578. run();
  579. }
  580. }
  581. function run(options) {
  582. g.setPosition = false;
  583. const url = getSourceBlobFromEditor(options);
  584. g.iframe.src = url;
  585. }
  586. function addClass(elem, className) {
  587. const parts = elem.className.split(' ');
  588. if (parts.indexOf(className) < 0) {
  589. elem.className = elem.className + ' ' + className;
  590. }
  591. }
  592. function removeClass(elem, className) {
  593. const parts = elem.className.split(' ');
  594. const numParts = parts.length;
  595. for (;;) {
  596. const ndx = parts.indexOf(className);
  597. if (ndx < 0) {
  598. break;
  599. }
  600. parts.splice(ndx, 1);
  601. }
  602. if (parts.length !== numParts) {
  603. elem.className = parts.join(' ');
  604. return true;
  605. }
  606. return false;
  607. }
  608. function toggleClass(elem, className) {
  609. if (removeClass(elem, className)) {
  610. return false;
  611. } else {
  612. addClass(elem, className);
  613. return true;
  614. }
  615. }
  616. function toggleIFrameFullscreen(childWindow) {
  617. const frame = childWindow.frameElement;
  618. if (frame) {
  619. const isFullScreen = toggleClass(frame, 'fullscreen');
  620. frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : '';
  621. }
  622. }
  623. function addRemoveClass(elem, className, add) {
  624. if (add) {
  625. addClass(elem, className);
  626. } else {
  627. removeClass(elem, className);
  628. }
  629. }
  630. function toggleSourcePane(pressedButton) {
  631. forEachHTMLPart(function(info) {
  632. const pressed = pressedButton === info.button;
  633. if (pressed && !info.showing) {
  634. addClass(info.button, 'show');
  635. info.pane.style.display = 'flex';
  636. info.showing = true;
  637. } else {
  638. removeClass(info.button, 'show');
  639. info.pane.style.display = 'none';
  640. info.showing = false;
  641. }
  642. });
  643. showOtherIfAllPanesOff();
  644. resize();
  645. }
  646. function showingResultPane() {
  647. return g.result.style.display !== 'none';
  648. }
  649. function toggleResultPane() {
  650. const showing = showingResultPane();
  651. g.result.style.display = showing ? 'none' : 'block';
  652. addRemoveClass(g.resultButton, 'show', !showing);
  653. showOtherIfAllPanesOff();
  654. resize();
  655. }
  656. function showOtherIfAllPanesOff() {
  657. let paneOn = showingResultPane();
  658. forEachHTMLPart(function(info) {
  659. paneOn = paneOn || info.showing;
  660. });
  661. g.other.style.display = paneOn ? 'none' : 'block';
  662. }
  663. // seems like we should probably store a map
  664. function getEditorNdxByBlobUrl(type, url) {
  665. return htmlParts[type].sources.findIndex(source => source.scriptInfo.blobUrl === url);
  666. }
  667. function getActualLineNumberAndMoveTo(url, lineNo, colNo) {
  668. let origUrl = url;
  669. let actualLineNo = lineNo;
  670. const scriptInfo = Object.values(g.scriptInfos).find(scriptInfo => scriptInfo.blobUrl === url);
  671. if (scriptInfo) {
  672. actualLineNo = lineNo - scriptInfo.numLinesBeforeScript;
  673. origUrl = basename(scriptInfo.fqURL);
  674. if (!g.setPosition) {
  675. // Only set the first position
  676. g.setPosition = true;
  677. const editorNdx = getEditorNdxByBlobUrl('js', url);
  678. if (editorNdx >= 0) {
  679. showEditorSubPane('js', editorNdx);
  680. const editor = htmlParts.js.editors[editorNdx].editor;
  681. editor.setPosition({
  682. lineNumber: actualLineNo,
  683. column: colNo,
  684. });
  685. editor.revealLineInCenterIfOutsideViewport(actualLineNo);
  686. editor.focus();
  687. }
  688. }
  689. }
  690. return {origUrl, actualLineNo};
  691. }
  692. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  693. function runEditor(parent, source, language) {
  694. return monaco.editor.create(parent, {
  695. value: source,
  696. language: language,
  697. //lineNumbers: false,
  698. theme: 'vs-dark',
  699. disableTranslate3d: true,
  700. // model: null,
  701. scrollBeyondLastLine: false,
  702. minimap: { enabled: false },
  703. });
  704. }
  705. async function runAsBlob() {
  706. const query = getQuery();
  707. g.url = getFQUrl(query.url);
  708. g.query = getSearch(g.url);
  709. let html;
  710. try {
  711. html = await getHTML(query.url);
  712. } catch (err) {
  713. console.log(err); // eslint-disable-line
  714. return;
  715. }
  716. await parseHTML(query.url, html);
  717. window.location.href = getSourceBlobFromOrig();
  718. }
  719. function applySubstitutions() {
  720. [...document.querySelectorAll('[data-subst]')].forEach((elem) => {
  721. elem.dataset.subst.split('&').forEach((pair) => {
  722. const [attr, key] = pair.split('|');
  723. elem[attr] = lessonEditorSettings[key];
  724. });
  725. });
  726. }
  727. function start() {
  728. const parentQuery = getQuery(window.parent.location.search);
  729. const isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i);
  730. const isEdge = window.navigator.userAgent.match(/Edge/i);
  731. if (isEdge || isSmallish || parentQuery.editor === 'false') {
  732. runAsBlob();
  733. // var url = query.url;
  734. // window.location.href = url;
  735. } else {
  736. applySubstitutions();
  737. require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
  738. require(['vs/editor/editor.main'], main);
  739. }
  740. }
  741. start();
  742. }());