editor.js 24 KB

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