editor.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861
  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 dname = dirname(g.url);
  320. // HACK! for webgl-2d-vs... those examples are not in /webgl they're in /webgl/resources
  321. // We basically assume url is https://foo/base/example.html so there will be 4 slashes
  322. // If the path is longer than then we need '../' to back up so prefix works below
  323. const prefix = `${dname}${dname.split('/').slice(4).map(() => '/..').join('')}`;
  324. let source = g.html;
  325. source = source.replace('${hackedParams}', JSON.stringify(g.query));
  326. source = source.replace('${html}', htmlParts.html);
  327. source = source.replace('${css}', htmlParts.css);
  328. source = source.replace('${js}', g.rootScriptInfo.munged); //htmlParts.js);
  329. source = source.replace('<head>', `<head>
  330. <link rel="stylesheet" href="${prefix}/resources/lesson-helper.css" type="text/css">
  331. <script match="false">self.lessonSettings = ${JSON.stringify(lessonSettings)}</script>`);
  332. source = source.replace('</head>', `<script src="${prefix}/resources/webgl-debug-helper.js"></script>
  333. <script src="${prefix}/resources/lessons-helper.js"></script>
  334. </head>`);
  335. const scriptNdx = source.indexOf('<script>');
  336. g.rootScriptInfo.numLinesBeforeScript = (source.substring(0, scriptNdx).match(/\n/g) || []).length;
  337. const blob = new Blob([source], {type: 'text/html'});
  338. // This seems hacky. We are combining html/css/js into one html blob but we already made
  339. // a blob for the JS so let's replace that blob. That means it will get auto-released when script blobs
  340. // are regenerated. It also means error reporting will work
  341. const blobUrl = URL.createObjectURL(blob);
  342. URL.revokeObjectURL(g.rootScriptInfo.blobUrl);
  343. g.rootScriptInfo.blobUrl = blobUrl;
  344. return blobUrl;
  345. }
  346. function getSourcesFromEditor() {
  347. for (const partTypeInfo of Object.values(htmlParts)) {
  348. for (const source of partTypeInfo.sources) {
  349. source.source = source.editor.getValue();
  350. // hack: shouldn't store this twice. Also see other comment,
  351. // should consolidate so scriptInfo is used for css and html
  352. if (source.scriptInfo) {
  353. source.scriptInfo.source = source.source;
  354. }
  355. }
  356. }
  357. }
  358. function getSourceBlobFromEditor() {
  359. getSourcesFromEditor();
  360. return getSourceBlob({
  361. html: htmlParts.html.sources[0].source,
  362. css: htmlParts.css.sources[0].source,
  363. js: htmlParts.js.sources[0].source,
  364. });
  365. }
  366. function getSourceBlobFromOrig() {
  367. return getSourceBlob({
  368. html: htmlParts.html.sources[0].source,
  369. css: htmlParts.css.sources[0].source,
  370. js: htmlParts.js.sources[0].source,
  371. });
  372. }
  373. function dirname(path) {
  374. const ndx = path.lastIndexOf('/');
  375. return path.substring(0, ndx);
  376. }
  377. function basename(path) {
  378. const ndx = path.lastIndexOf('/');
  379. return path.substring(ndx + 1);
  380. }
  381. function resize() {
  382. forEachHTMLPart(function(info) {
  383. info.editors.forEach((editorInfo) => {
  384. editorInfo.editor.layout();
  385. });
  386. });
  387. }
  388. function makeScriptsForWorkers(scriptInfo) {
  389. ++blobGeneration;
  390. function makeScriptsForWorkersImpl(scriptInfo) {
  391. const scripts = [];
  392. if (scriptInfo.blobGenerationId !== blobGeneration) {
  393. scriptInfo.blobGenerationId = blobGeneration;
  394. scripts.push(...scriptInfo.deps.map(makeScriptsForWorkersImpl).flat());
  395. let text = scriptInfo.source;
  396. scriptInfo.deps.forEach((depScriptInfo) => {
  397. text = text.split(depScriptInfo.fqURL).join(`worker-${basename(depScriptInfo.fqURL)}`);
  398. });
  399. scripts.push({
  400. name: `worker-${basename(scriptInfo.fqURL)}`,
  401. text,
  402. });
  403. }
  404. return scripts;
  405. }
  406. const scripts = makeScriptsForWorkersImpl(scriptInfo);
  407. const mainScript = scripts.pop().text;
  408. if (!scripts.length) {
  409. return {
  410. js: mainScript,
  411. html: '',
  412. };
  413. }
  414. const workerName = scripts[scripts.length - 1].name;
  415. const html = scripts.map((nameText) => {
  416. const {name, text} = nameText;
  417. return `<script id="${name}" type="x-worker">\n${text}\n</script>`;
  418. }).join('\n');
  419. const init = `
  420. // ------
  421. // Creates Blobs for the Worker Scripts so things can be self contained for snippets/JSFiddle/Codepen
  422. //
  423. function getWorkerBlob() {
  424. const idsToUrls = [];
  425. const scriptElements = [...document.querySelectorAll('script[type=x-worker]')];
  426. for (const scriptElement of scriptElements) {
  427. let text = scriptElement.text;
  428. for (const {id, url} of idsToUrls) {
  429. text = text.split(id).join(url);
  430. }
  431. const blob = new Blob([text], {type: 'application/javascript'});
  432. const url = URL.createObjectURL(blob);
  433. const id = scriptElement.id;
  434. idsToUrls.push({id, url});
  435. }
  436. return idsToUrls.pop().url;
  437. }
  438. `;
  439. return {
  440. js: mainScript.split(`'${workerName}'`).join('getWorkerBlob()') + init,
  441. html,
  442. };
  443. }
  444. function fixHTMLForCodeSite(html) {
  445. html = html.replace(lessonHelperScriptRE, '');
  446. html = html.replace(webglDebugHelperScriptRE, '');
  447. return html;
  448. }
  449. function openInCodepen() {
  450. const comment = `// ${g.title}
  451. // from ${g.url}
  452. `;
  453. getSourcesFromEditor();
  454. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  455. const pen = {
  456. title : g.title,
  457. description : 'from: ' + g.url,
  458. tags : lessonEditorSettings.tags,
  459. editors : '101',
  460. html : scripts.html + fixHTMLForCodeSite(htmlParts.html.sources[0].source),
  461. css : htmlParts.css.sources[0].source,
  462. js : comment + fixJSForCodeSite(scripts.js),
  463. };
  464. const elem = document.createElement('div');
  465. elem.innerHTML = `
  466. <form method="POST" target="_blank" action="https://codepen.io/pen/define" class="hidden">'
  467. <input type="hidden" name="data">
  468. <input type="submit" />
  469. "</form>"
  470. `;
  471. elem.querySelector('input[name=data]').value = JSON.stringify(pen);
  472. window.frameElement.ownerDocument.body.appendChild(elem);
  473. elem.querySelector('form').submit();
  474. window.frameElement.ownerDocument.body.removeChild(elem);
  475. }
  476. function openInJSFiddle() {
  477. const comment = `// ${g.title}
  478. // from ${g.url}
  479. `;
  480. getSourcesFromEditor();
  481. const scripts = makeScriptsForWorkers(g.rootScriptInfo);
  482. const elem = document.createElement('div');
  483. elem.innerHTML = `
  484. <form method="POST" target="_black" action="https://jsfiddle.net/api/mdn/" class="hidden">
  485. <input type="hidden" name="html" />
  486. <input type="hidden" name="css" />
  487. <input type="hidden" name="js" />
  488. <input type="hidden" name="title" />
  489. <input type="hidden" name="wrap" value="b" />
  490. <input type="submit" />
  491. </form>
  492. `;
  493. elem.querySelector('input[name=html]').value = scripts.html + fixHTMLForCodeSite(htmlParts.html.sources[0].source);
  494. elem.querySelector('input[name=css]').value = htmlParts.css.sources[0].source;
  495. elem.querySelector('input[name=js]').value = comment + fixJSForCodeSite(scripts.js);
  496. elem.querySelector('input[name=title]').value = g.title;
  497. window.frameElement.ownerDocument.body.appendChild(elem);
  498. elem.querySelector('form').submit();
  499. window.frameElement.ownerDocument.body.removeChild(elem);
  500. }
  501. function selectFile(info, ndx, fileDivs) {
  502. if (info.editors.length <= 1) {
  503. return;
  504. }
  505. info.editors.forEach((editorInfo, i) => {
  506. const selected = i === ndx;
  507. editorInfo.div.style.display = selected ? '' : 'none';
  508. editorInfo.editor.layout();
  509. addRemoveClass(fileDivs.children[i], 'fileSelected', selected);
  510. });
  511. }
  512. function showEditorSubPane(type, ndx) {
  513. const info = htmlParts[type];
  514. selectFile(info, ndx, info.files);
  515. }
  516. function setupEditor() {
  517. forEachHTMLPart(function(info, ndx, name) {
  518. info.pane = document.querySelector('.panes>.' + name);
  519. info.code = info.pane.querySelector('.code');
  520. info.files = info.pane.querySelector('.files');
  521. info.editors = info.sources.map((sourceInfo, ndx) => {
  522. if (info.sources.length > 1) {
  523. const div = document.createElement('div');
  524. div.textContent = basename(sourceInfo.name);
  525. info.files.appendChild(div);
  526. div.addEventListener('click', () => {
  527. selectFile(info, ndx, info.files);
  528. });
  529. }
  530. const div = document.createElement('div');
  531. info.code.appendChild(div);
  532. const editor = runEditor(div, sourceInfo.source, info.language);
  533. sourceInfo.editor = editor;
  534. return {
  535. div,
  536. editor,
  537. };
  538. });
  539. info.button = document.querySelector('.button-' + name);
  540. info.button.addEventListener('click', function() {
  541. toggleSourcePane(info.button);
  542. runIfNeeded();
  543. });
  544. });
  545. g.fullscreen = document.querySelector('.button-fullscreen');
  546. g.fullscreen.addEventListener('click', toggleFullscreen);
  547. g.run = document.querySelector('.button-run');
  548. g.run.addEventListener('click', run);
  549. g.iframe = document.querySelector('.result>iframe');
  550. g.other = document.querySelector('.panes .other');
  551. document.querySelector('.button-codepen').addEventListener('click', openInCodepen);
  552. document.querySelector('.button-jsfiddle').addEventListener('click', openInJSFiddle);
  553. g.result = document.querySelector('.panes .result');
  554. g.resultButton = document.querySelector('.button-result');
  555. g.resultButton.addEventListener('click', function() {
  556. toggleResultPane();
  557. runIfNeeded();
  558. });
  559. g.result.style.display = 'none';
  560. toggleResultPane();
  561. if (window.innerWidth > 1200) {
  562. toggleSourcePane(htmlParts.js.button);
  563. }
  564. window.addEventListener('resize', resize);
  565. showEditorSubPane('js', 0);
  566. showOtherIfAllPanesOff();
  567. document.querySelector('.other .loading').style.display = 'none';
  568. resize();
  569. run();
  570. }
  571. function toggleFullscreen() {
  572. try {
  573. toggleIFrameFullscreen(window);
  574. resize();
  575. runIfNeeded();
  576. } catch (e) {
  577. console.error(e); // eslint-disable-line
  578. }
  579. }
  580. function runIfNeeded() {
  581. if (runOnResize) {
  582. run();
  583. }
  584. }
  585. function run(options) {
  586. g.setPosition = false;
  587. const url = getSourceBlobFromEditor(options);
  588. g.iframe.src = url;
  589. }
  590. function addClass(elem, className) {
  591. const parts = elem.className.split(' ');
  592. if (parts.indexOf(className) < 0) {
  593. elem.className = elem.className + ' ' + className;
  594. }
  595. }
  596. function removeClass(elem, className) {
  597. const parts = elem.className.split(' ');
  598. const numParts = parts.length;
  599. for (;;) {
  600. const ndx = parts.indexOf(className);
  601. if (ndx < 0) {
  602. break;
  603. }
  604. parts.splice(ndx, 1);
  605. }
  606. if (parts.length !== numParts) {
  607. elem.className = parts.join(' ');
  608. return true;
  609. }
  610. return false;
  611. }
  612. function toggleClass(elem, className) {
  613. if (removeClass(elem, className)) {
  614. return false;
  615. } else {
  616. addClass(elem, className);
  617. return true;
  618. }
  619. }
  620. function toggleIFrameFullscreen(childWindow) {
  621. const frame = childWindow.frameElement;
  622. if (frame) {
  623. const isFullScreen = toggleClass(frame, 'fullscreen');
  624. frame.ownerDocument.body.style.overflow = isFullScreen ? 'hidden' : '';
  625. }
  626. }
  627. function addRemoveClass(elem, className, add) {
  628. if (add) {
  629. addClass(elem, className);
  630. } else {
  631. removeClass(elem, className);
  632. }
  633. }
  634. function toggleSourcePane(pressedButton) {
  635. forEachHTMLPart(function(info) {
  636. const pressed = pressedButton === info.button;
  637. if (pressed && !info.showing) {
  638. addClass(info.button, 'show');
  639. info.pane.style.display = 'flex';
  640. info.showing = true;
  641. } else {
  642. removeClass(info.button, 'show');
  643. info.pane.style.display = 'none';
  644. info.showing = false;
  645. }
  646. });
  647. showOtherIfAllPanesOff();
  648. resize();
  649. }
  650. function showingResultPane() {
  651. return g.result.style.display !== 'none';
  652. }
  653. function toggleResultPane() {
  654. const showing = showingResultPane();
  655. g.result.style.display = showing ? 'none' : 'block';
  656. addRemoveClass(g.resultButton, 'show', !showing);
  657. showOtherIfAllPanesOff();
  658. resize();
  659. }
  660. function showOtherIfAllPanesOff() {
  661. let paneOn = showingResultPane();
  662. forEachHTMLPart(function(info) {
  663. paneOn = paneOn || info.showing;
  664. });
  665. g.other.style.display = paneOn ? 'none' : 'block';
  666. }
  667. // seems like we should probably store a map
  668. function getEditorNdxByBlobUrl(type, url) {
  669. return htmlParts[type].sources.findIndex(source => source.scriptInfo.blobUrl === url);
  670. }
  671. function getActualLineNumberAndMoveTo(url, lineNo, colNo) {
  672. let origUrl = url;
  673. let actualLineNo = lineNo;
  674. const scriptInfo = Object.values(g.scriptInfos).find(scriptInfo => scriptInfo.blobUrl === url);
  675. if (scriptInfo) {
  676. actualLineNo = lineNo - scriptInfo.numLinesBeforeScript;
  677. origUrl = basename(scriptInfo.fqURL);
  678. if (!g.setPosition) {
  679. // Only set the first position
  680. g.setPosition = true;
  681. const editorNdx = getEditorNdxByBlobUrl('js', url);
  682. if (editorNdx >= 0) {
  683. showEditorSubPane('js', editorNdx);
  684. const editor = htmlParts.js.editors[editorNdx].editor;
  685. editor.setPosition({
  686. lineNumber: actualLineNo,
  687. column: colNo,
  688. });
  689. editor.revealLineInCenterIfOutsideViewport(actualLineNo);
  690. editor.focus();
  691. }
  692. }
  693. }
  694. return {origUrl, actualLineNo};
  695. }
  696. window.getActualLineNumberAndMoveTo = getActualLineNumberAndMoveTo;
  697. function runEditor(parent, source, language) {
  698. return monaco.editor.create(parent, {
  699. value: source,
  700. language: language,
  701. //lineNumbers: false,
  702. theme: 'vs-dark',
  703. disableTranslate3d: true,
  704. // model: null,
  705. scrollBeyondLastLine: false,
  706. minimap: { enabled: false },
  707. });
  708. }
  709. async function runAsBlob() {
  710. const query = getQuery();
  711. g.url = getFQUrl(query.url);
  712. g.query = getSearch(g.url);
  713. let html;
  714. try {
  715. html = await getHTML(query.url);
  716. } catch (err) {
  717. console.log(err); // eslint-disable-line
  718. return;
  719. }
  720. await parseHTML(query.url, html);
  721. window.location.href = getSourceBlobFromOrig();
  722. }
  723. function applySubstitutions() {
  724. [...document.querySelectorAll('[data-subst]')].forEach((elem) => {
  725. elem.dataset.subst.split('&').forEach((pair) => {
  726. const [attr, key] = pair.split('|');
  727. elem[attr] = lessonEditorSettings[key];
  728. });
  729. });
  730. }
  731. function start() {
  732. const parentQuery = getQuery(window.parent.location.search);
  733. const isSmallish = window.navigator.userAgent.match(/Android|iPhone|iPod|Windows Phone/i);
  734. const isEdge = window.navigator.userAgent.match(/Edge/i);
  735. if (isEdge || isSmallish || parentQuery.editor === 'false') {
  736. runAsBlob();
  737. // var url = query.url;
  738. // window.location.href = url;
  739. } else {
  740. applySubstitutions();
  741. require.config({ paths: { 'vs': '/monaco-editor/min/vs' }});
  742. require(['vs/editor/editor.main'], main);
  743. }
  744. }
  745. start();
  746. }());
粤ICP备19079148号