editor.js 25 KB

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